diff --git a/.gitignore b/.gitignore
index d09eb17..cb0ca4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@ local/*
# Root project metadata.
!/.gitignore
!/platforms.toml
+!/paseo.json
!/LICENSE
!/install.sh
!/README.md
diff --git a/README.md b/README.md
index 6c9821b..518ef29 100644
--- a/README.md
+++ b/README.md
@@ -187,26 +187,18 @@ memorph 支持 23 个 AI 编程 Agent 的会话管理。各 Provider 的能力
| Claude | ✅ | ✅ | ✅ |
| Codex | ✅ | ✅ | ✅ |
| OpenCode | ✅ | ✅ | ✅ |
-| DeepSeek | ✅ | ✅ | ✅ |
| Kimi | ✅ | ✅ | ✅ |
| Cursor | ✅ | ✅ | ✅ |
| Gemini CLI | ✅ | ❌ | ✅ |
| Kiro | ✅ | ❌ | ✅ |
-| Hermes | ✅ | ❌ | ✅ |
-| Cline | ✅ | ❌ | ❌ |
| Copilot | ✅ | ❌ | ❌ |
-| Droid | ✅ | ❌ | ❌ |
-| CodeBuddy | ✅ | ❌ | ❌ |
| Qoder | ✅ | ❌ | ❌ |
-| Trae | ✅ | ❌ | ❌ |
| WorkBuddy | ✅ | ❌ | ❌ |
| Pi | ✅ | ❌ | ❌ |
+| Oh My Pi | ✅ | ❌ | ❌ |
| Antigravity | ✅ | ❌ | ❌ |
-| OpenClaw | ✅ | ❌ | ❌ |
-| Augment | ✅ | ❌ | ❌ |
-| Windsurf | ✅ | ❌ | ❌ |
-| Amazon Q | ✅ | ❌ | ❌ |
-| Qwen | ✅ | ❌ | ❌ |
+| Grok Build | ✅ | ❌ | ❌ |
+| ZCode | ✅ | ❌ | ❌ |
> **导出**:将 OASF canonical Session 回写为 Provider 原生格式。
> **管理/恢复**:支持 delete / rename / resume 中至少一项。
@@ -218,7 +210,7 @@ memorph 基于 [OASF (Open Agent Session Format)](https://crates.io/crates/oasf)
- 所有会话文件(`.morph`、`.json`、`.md`、`.html`)在 meta 行携带 schema name/version,导入时校验,拒绝不兼容版本
- 导入侧:23 个 Provider 的原生格式统一转换为 OASF canonical Session
-- 导出侧:6 个 Provider 支持将 canonical Session 回写为原生格式(Claude、Codex、OpenCode、DeepSeek、Kimi、Cursor)
+- 导出侧:5 个 Provider 支持将 canonical Session 回写为原生格式(Claude、Codex、OpenCode、Kimi、Cursor)
- 保真度模型:每个 Provider × Block 类型声明 `Preserved` / `Normalized` / `Downgraded` / `Dropped` / `Unsupported`,通过 `export_report` 在导出时生成损失报告
---
diff --git a/README_en.md b/README_en.md
index 0e88546..1b32d1d 100644
--- a/README_en.md
+++ b/README_en.md
@@ -171,26 +171,18 @@ memorph supports session management for 23 AI coding agents. Capabilities differ
| Claude | ✅ | ✅ | ✅ |
| Codex | ✅ | ✅ | ✅ |
| OpenCode | ✅ | ✅ | ✅ |
-| DeepSeek | ✅ | ✅ | ✅ |
| Kimi | ✅ | ✅ | ✅ |
| Cursor | ✅ | ✅ | ✅ |
| Gemini CLI | ✅ | ❌ | ✅ |
| Kiro | ✅ | ❌ | ✅ |
-| Hermes | ✅ | ❌ | ✅ |
-| Cline | ✅ | ❌ | ❌ |
| Copilot | ✅ | ❌ | ❌ |
-| Droid | ✅ | ❌ | ❌ |
-| CodeBuddy | ✅ | ❌ | ❌ |
| Qoder | ✅ | ❌ | ❌ |
-| Trae | ✅ | ❌ | ❌ |
| WorkBuddy | ✅ | ❌ | ❌ |
| Pi | ✅ | ❌ | ❌ |
+| Oh My Pi | ✅ | ❌ | ❌ |
| Antigravity | ✅ | ❌ | ❌ |
-| OpenClaw | ✅ | ❌ | ❌ |
-| Augment | ✅ | ❌ | ❌ |
-| Windsurf | ✅ | ❌ | ❌ |
-| Amazon Q | ✅ | ❌ | ❌ |
-| Qwen | ✅ | ❌ | ❌ |
+| Grok Build | ✅ | ❌ | ❌ |
+| ZCode | ✅ | ❌ | ❌ |
> **Export**: write OASF canonical Session back as provider-native format.
> **Manage/Resume**: supports at least one of delete / rename / resume.
@@ -202,7 +194,7 @@ memorph is built on [OASF (Open Agent Session Format)](https://crates.io/crates/
- All session files (`.morph`, `.json`, `.md`, `.html`) carry schema name/version in the meta line; validated on import, rejecting incompatible versions
- Import: 23 providers' native formats are unified into OASF canonical Session
-- Export: 6 providers support writing canonical Session back to native format (Claude, Codex, OpenCode, DeepSeek, Kimi, Cursor)
+- Export: 5 providers support writing canonical Session back to native format (Claude, Codex, OpenCode, Kimi, Cursor)
- Fidelity model: each Provider × Block type declares `Preserved` / `Normalized` / `Downgraded` / `Dropped` / `Unsupported`, with loss reports generated at export time via `export_report`
---
diff --git a/README_zh.md b/README_zh.md
index 6c9821b..518ef29 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -187,26 +187,18 @@ memorph 支持 23 个 AI 编程 Agent 的会话管理。各 Provider 的能力
| Claude | ✅ | ✅ | ✅ |
| Codex | ✅ | ✅ | ✅ |
| OpenCode | ✅ | ✅ | ✅ |
-| DeepSeek | ✅ | ✅ | ✅ |
| Kimi | ✅ | ✅ | ✅ |
| Cursor | ✅ | ✅ | ✅ |
| Gemini CLI | ✅ | ❌ | ✅ |
| Kiro | ✅ | ❌ | ✅ |
-| Hermes | ✅ | ❌ | ✅ |
-| Cline | ✅ | ❌ | ❌ |
| Copilot | ✅ | ❌ | ❌ |
-| Droid | ✅ | ❌ | ❌ |
-| CodeBuddy | ✅ | ❌ | ❌ |
| Qoder | ✅ | ❌ | ❌ |
-| Trae | ✅ | ❌ | ❌ |
| WorkBuddy | ✅ | ❌ | ❌ |
| Pi | ✅ | ❌ | ❌ |
+| Oh My Pi | ✅ | ❌ | ❌ |
| Antigravity | ✅ | ❌ | ❌ |
-| OpenClaw | ✅ | ❌ | ❌ |
-| Augment | ✅ | ❌ | ❌ |
-| Windsurf | ✅ | ❌ | ❌ |
-| Amazon Q | ✅ | ❌ | ❌ |
-| Qwen | ✅ | ❌ | ❌ |
+| Grok Build | ✅ | ❌ | ❌ |
+| ZCode | ✅ | ❌ | ❌ |
> **导出**:将 OASF canonical Session 回写为 Provider 原生格式。
> **管理/恢复**:支持 delete / rename / resume 中至少一项。
@@ -218,7 +210,7 @@ memorph 基于 [OASF (Open Agent Session Format)](https://crates.io/crates/oasf)
- 所有会话文件(`.morph`、`.json`、`.md`、`.html`)在 meta 行携带 schema name/version,导入时校验,拒绝不兼容版本
- 导入侧:23 个 Provider 的原生格式统一转换为 OASF canonical Session
-- 导出侧:6 个 Provider 支持将 canonical Session 回写为原生格式(Claude、Codex、OpenCode、DeepSeek、Kimi、Cursor)
+- 导出侧:5 个 Provider 支持将 canonical Session 回写为原生格式(Claude、Codex、OpenCode、Kimi、Cursor)
- 保真度模型:每个 Provider × Block 类型声明 `Preserved` / `Normalized` / `Downgraded` / `Dropped` / `Unsupported`,通过 `export_report` 在导出时生成损失报告
---
diff --git a/apps/web/src/assets/provider-logos/alibaba.svg b/apps/web/src/assets/provider-logos/alibaba.svg
deleted file mode 100644
index b3a2edc..0000000
--- a/apps/web/src/assets/provider-logos/alibaba.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/web/src/assets/provider-logos/amazon-bedrock.svg b/apps/web/src/assets/provider-logos/amazon-bedrock.svg
deleted file mode 100644
index 1f185ef..0000000
--- a/apps/web/src/assets/provider-logos/amazon-bedrock.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/web/src/assets/provider-logos/augment.svg b/apps/web/src/assets/provider-logos/augment.svg
deleted file mode 100644
index 2438545..0000000
--- a/apps/web/src/assets/provider-logos/augment.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/cline.svg b/apps/web/src/assets/provider-logos/cline.svg
deleted file mode 100644
index 9488305..0000000
--- a/apps/web/src/assets/provider-logos/cline.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/codebuddy.svg b/apps/web/src/assets/provider-logos/codebuddy.svg
deleted file mode 100644
index ec353e2..0000000
--- a/apps/web/src/assets/provider-logos/codebuddy.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/deepseek.svg b/apps/web/src/assets/provider-logos/deepseek.svg
deleted file mode 100644
index 5d6efa9..0000000
--- a/apps/web/src/assets/provider-logos/deepseek.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/web/src/assets/provider-logos/droid.svg b/apps/web/src/assets/provider-logos/droid.svg
deleted file mode 100644
index 0b6b41b..0000000
--- a/apps/web/src/assets/provider-logos/droid.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/hermes.svg b/apps/web/src/assets/provider-logos/hermes.svg
deleted file mode 100644
index 2521a10..0000000
--- a/apps/web/src/assets/provider-logos/hermes.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/manifest.json b/apps/web/src/assets/provider-logos/manifest.json
index 4c29c2e..caff6cc 100644
--- a/apps/web/src/assets/provider-logos/manifest.json
+++ b/apps/web/src/assets/provider-logos/manifest.json
@@ -1,34 +1,23 @@
{
"source": "https://models.dev",
"note": "Subset aligned to memorph provider IDs; agent-specific logos sourced from official/lobe-icons/ACP where models.dev has no match.",
- "provider_count": 27,
+ "provider_count": 16,
"providers": [
- "alibaba",
- "amazon-bedrock",
"anthropic",
"antigravity",
- "augment",
"claude",
- "cline",
- "codebuddy",
"codex",
"cursor",
- "deepseek",
- "droid",
"gemini",
"github-copilot",
"google",
- "hermes",
"kimi-for-coding",
"kiro",
"openai",
- "openclaw",
"opencode",
"pi",
"qoder",
"synthetic",
- "trae",
- "windsurf",
"workbuddy"
],
"memorph_mapping": {
@@ -36,26 +25,14 @@
"codex": "codex",
"opencode": "opencode",
"kiro": "kiro",
- "deepseek": "deepseek",
"kimi": "kimi-for-coding",
"gemini": "gemini",
"antigravity": "antigravity",
"copilot": "github-copilot",
- "factory": "droid",
"cursor": "cursor",
- "cline": "cline",
- "windsurf": "windsurf",
- "trae": "trae",
"qoder": "qoder",
- "codebuddy": "codebuddy",
- "hermes": "hermes",
"pi": "pi",
- "droid": "droid",
- "workbuddy": "workbuddy",
- "augment": "augment",
- "amazonq": "amazon-bedrock",
- "qwen": "alibaba",
- "openclaw": "openclaw"
+ "workbuddy": "workbuddy"
},
"unmapped_memorph_providers": [],
"external_sources": {
@@ -64,17 +41,9 @@
"antigravity": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/antigravity.svg",
"codex": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/codex.svg",
"gemini": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/gemini-color.svg",
- "cline": "https://raw.githubusercontent.com/cline/cline/main/assets/icons/icon.png",
- "windsurf": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/windsurf.svg",
- "trae": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/trae-color.svg",
"qoder": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/qoder-color.svg",
- "codebuddy": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/codebuddy-color.svg",
- "hermes": "https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/hermesagent.svg",
"kiro": "https://kiro.dev/icon.svg",
"pi": "https://pi.dev/logo-auto.svg",
- "droid": "https://factory.ai/favicon.svg",
- "workbuddy": "https://codebuddy-1328495429.cos.accelerate.myqcloud.com/web/workbuddy/d1d0b9329b771f020a1025d11661a33296390d7c/assets/logo.svg",
- "augment": "https://www.augmentcode.com/favicon.svg",
- "openclaw": "https://openclaw.ai/favicon.svg"
+ "workbuddy": "https://codebuddy-1328495429.cos.accelerate.myqcloud.com/web/workbuddy/d1d0b9329b771f020a1025d11661a33296390d7c/assets/logo.svg"
}
}
diff --git a/apps/web/src/assets/provider-logos/openclaw.svg b/apps/web/src/assets/provider-logos/openclaw.svg
deleted file mode 100644
index bcbc1e1..0000000
--- a/apps/web/src/assets/provider-logos/openclaw.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-
diff --git a/apps/web/src/assets/provider-logos/trae.svg b/apps/web/src/assets/provider-logos/trae.svg
deleted file mode 100644
index 08b3ba0..0000000
--- a/apps/web/src/assets/provider-logos/trae.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/assets/provider-logos/windsurf.svg b/apps/web/src/assets/provider-logos/windsurf.svg
deleted file mode 100644
index 4df8f33..0000000
--- a/apps/web/src/assets/provider-logos/windsurf.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/web/src/components/shared/provider-logo-map.test.ts b/apps/web/src/components/shared/provider-logo-map.test.ts
index 86f973a..25208ef 100644
--- a/apps/web/src/components/shared/provider-logo-map.test.ts
+++ b/apps/web/src/components/shared/provider-logo-map.test.ts
@@ -10,12 +10,9 @@ describe("resolveProviderLogoAssetId", () => {
expect(resolveProviderLogoAssetId("codex")).toBe("codex");
expect(resolveProviderLogoAssetId("kimi")).toBe("kimi-for-coding");
expect(resolveProviderLogoAssetId("copilot")).toBe("github-copilot");
- expect(resolveProviderLogoAssetId("openclaw")).toBe("openclaw");
- expect(resolveProviderLogoAssetId("augment")).toBe("augment");
});
it("accepts direct asset ids", () => {
- expect(resolveProviderLogoAssetId("deepseek")).toBe("deepseek");
expect(resolveProviderLogoAssetId("opencode")).toBe("opencode");
});
@@ -26,12 +23,9 @@ describe("resolveProviderLogoAssetId", () => {
it("maps newly sourced agent logos by direct id", () => {
expect(resolveProviderLogoAssetId("cursor")).toBe("cursor");
expect(resolveProviderLogoAssetId("workbuddy")).toBe("workbuddy");
- expect(resolveProviderLogoAssetId("droid")).toBe("droid");
expect(resolveProviderLogoAssetId("kiro")).toBe("kiro");
expect(resolveProviderLogoAssetId("gemini")).toBe("gemini");
expect(resolveProviderLogoAssetId("antigravity")).toBe("antigravity");
- expect(resolveProviderLogoAssetId("factory")).toBe("droid");
- expect(resolveProviderLogoAssetId("cline")).toBe("cline");
});
});
diff --git a/apps/web/src/features/skills/skill-detail-panel.tsx b/apps/web/src/features/skills/skill-detail-panel.tsx
index 6bc06ef..d4de637 100644
--- a/apps/web/src/features/skills/skill-detail-panel.tsx
+++ b/apps/web/src/features/skills/skill-detail-panel.tsx
@@ -37,7 +37,7 @@ import { SectionHeading } from "@/components/shared/section-heading";
import type { I18nKey } from "@/lib/i18n-core";
import type { SkillAsset, SkillCatalogItem, SkillDetail, SkillFilePreview } from "@/lib/types";
-const AGENTS = ["claude", "codex", "gemini", "opencode", "hermes"] as const;
+const AGENTS = ["claude", "codex", "gemini", "opencode"] as const;
type DetailTab = "bundle" | "coverage" | "installations" | "consolidate";
diff --git a/paseo.json b/paseo.json
new file mode 100644
index 0000000..992f123
--- /dev/null
+++ b/paseo.json
@@ -0,0 +1,8 @@
+{
+ "worktree": {
+ "setup": [
+ "sh \"$PASEO_SOURCE_CHECKOUT_PATH/local/paseo-worktree-setup.sh\" || echo '[setup] 私有 setup 脚本缺失,跳过'",
+ "cargo fetch --manifest-path rust/Cargo.toml"
+ ]
+ }
+}
diff --git a/rust/apps/memorph-cli/src/api/tests.rs b/rust/apps/memorph-cli/src/api/tests.rs
index 2c2cee2..b12d5bf 100644
--- a/rust/apps/memorph-cli/src/api/tests.rs
+++ b/rust/apps/memorph-cli/src/api/tests.rs
@@ -1391,11 +1391,11 @@ async fn settings_route_lists_codeisland_gap_provider_hook_actions() {
#[tokio::test]
async fn provider_settings_route_accepts_provider_aliases() {
let canonical_request = Request::builder()
- .uri("/api/v1/providers/droid/settings")
+ .uri("/api/v1/providers/workbuddy/settings")
.body(Body::empty())
.unwrap();
let alias_request = Request::builder()
- .uri("/api/v1/providers/factory/settings")
+ .uri("/api/v1/providers/work-buddy/settings")
.body(Body::empty())
.unwrap();
@@ -1756,17 +1756,6 @@ async fn catalog_route_returns_classified_providers() {
.expect("missing opencode catalog entry");
assert_eq!(opencode["capability_set"]["page_strategy"], "native_page");
- let openclaw = providers
- .iter()
- .find(|provider| provider["provider_id"] == "openclaw")
- .expect("missing openclaw catalog entry");
- assert_eq!(openclaw["display_name"], "OpenClaw");
- assert_eq!(openclaw["capability_set"]["scan"], true);
- assert_eq!(openclaw["capability_set"]["import"], true);
- assert_eq!(openclaw["capability_set"]["storage_shape"], "sqlite");
- assert_eq!(openclaw["capability_set"]["resume"], false);
- assert_eq!(openclaw["capability_set"]["delete"], false);
-
let gemini = providers
.iter()
.find(|provider| provider["provider_id"] == "gemini")
@@ -1808,116 +1797,12 @@ async fn catalog_route_returns_classified_providers() {
gemini_capabilities["activity_support"]["session_activity"],
false
);
-
- let deepseek = providers
- .iter()
- .find(|provider| provider["provider_id"] == "deepseek")
- .expect("missing deepseek catalog entry");
- let deepseek_capabilities = &deepseek["capability_set"];
- assert_eq!(deepseek_capabilities["scan"], true);
- assert_eq!(deepseek_capabilities["import"], true);
- assert_eq!(deepseek_capabilities["export"], true);
- assert_eq!(deepseek_capabilities["delete"], true);
- assert_eq!(deepseek_capabilities["rename"], true);
- assert_eq!(deepseek_capabilities["resume"], true);
- assert_eq!(deepseek_capabilities["scan_strategy"], "full_scan");
- assert_eq!(deepseek_capabilities["page_strategy"], "full_import");
- assert_eq!(deepseek_capabilities["storage_shape"], "sqlite");
- assert_eq!(deepseek_capabilities["turn_quality"], "inferred");
- assert_eq!(deepseek_capabilities["resume_quality"], "native");
- assert_eq!(deepseek_capabilities["write_risk"]["level"], "high");
- assert_eq!(deepseek_capabilities["write_risk"]["sqlite"], true);
- assert_eq!(deepseek_capabilities["write_risk"]["sidecar_files"], true);
- assert_eq!(deepseek_capabilities["write_risk"]["index_repair"], true);
- assert_eq!(
- deepseek_capabilities["backup_support"]["before_write"],
- true
- );
- assert_eq!(deepseek_capabilities["backup_support"]["restore"], true);
- assert_eq!(
- deepseek_capabilities["activity_support"]["hook_events"],
- false
- );
- assert_eq!(
- deepseek_capabilities["activity_support"]["runtime_endpoint"],
- false
- );
- assert_eq!(
- deepseek_capabilities["activity_support"]["session_activity"],
- false
- );
- assert_eq!(
- deepseek_capabilities["export_fidelity"]["tool_call"],
- "preserved"
- );
- assert_eq!(
- deepseek_capabilities["export_fidelity"]["provider_payload"],
- "dropped"
- );
-}
-
-#[test]
-fn active_catalog_uses_projected_sessions_for_workspace_activity() {
- let ordered_ids = vec!["codex".to_string(), "claude".to_string()];
- let snapshots = vec![
- memorph::storage::snapshot_store::ProjectedSessionSnapshotRow {
- canonical_session_id: "codex:one".to_string(),
- provider_id: "codex".to_string(),
- provider_session_id: Some("one".to_string()),
- title: None,
- display_title: None,
- workspace_dir: Some("/tmp/current".to_string()),
- last_active_at_ms: Some(30),
- source_path: Some("/missing/codex.jsonl".to_string()),
- message_count: Some(0),
- event_count: 0,
- turn_count: 0,
- size_bytes: None,
- hidden: false,
- pinned: false,
- archived: false,
- preferred_targets: Vec::new(),
- stale: false,
- },
- memorph::storage::snapshot_store::ProjectedSessionSnapshotRow {
- canonical_session_id: "codex:two".to_string(),
- provider_id: "codex".to_string(),
- provider_session_id: Some("two".to_string()),
- title: None,
- display_title: None,
- workspace_dir: Some("/tmp/other".to_string()),
- last_active_at_ms: Some(50),
- source_path: Some("/missing/codex-other.jsonl".to_string()),
- message_count: Some(0),
- event_count: 0,
- turn_count: 0,
- size_bytes: None,
- hidden: false,
- pinned: false,
- archived: false,
- preferred_targets: Vec::new(),
- stale: false,
- },
- ];
-
- let catalog =
- provider_active_catalog_from_snapshots(&ordered_ids, Some("/tmp/current"), &snapshots);
-
- assert_eq!(catalog.providers.len(), 2);
- assert_eq!(catalog.providers[0].provider_id, "codex");
- assert!(catalog.providers[0].has_sessions);
- assert_eq!(catalog.providers[0].active_time.global, 50);
- assert_eq!(catalog.providers[0].active_time.workspace, 30);
- assert_eq!(catalog.providers[1].provider_id, "claude");
- assert!(!catalog.providers[1].has_sessions);
- assert_eq!(catalog.providers[1].active_time.global, 0);
- assert_eq!(catalog.providers[1].active_time.workspace, 0);
}
#[tokio::test]
-async fn agents_route_exposes_settings_field() {
+async fn agents_summary_route_includes_settings_and_environment_without_features() {
let request = Request::builder()
- .uri("/api/v1/agents")
+ .uri("/api/v1/agents/summary")
.body(Body::empty())
.unwrap();
let (status, value) = read_json(router(), request).await;
diff --git a/rust/apps/memorph-cli/src/bin/memorph.rs b/rust/apps/memorph-cli/src/bin/memorph.rs
index 84f9076..43e0ed2 100644
--- a/rust/apps/memorph-cli/src/bin/memorph.rs
+++ b/rust/apps/memorph-cli/src/bin/memorph.rs
@@ -1259,24 +1259,6 @@ mod tests {
assert!(output.contains(" provider_payload: dropped"));
}
- #[test]
- fn deepseek_cli_capability_detail_matches_sqlite_contract() {
- let capabilities = providers::find_provider("deepseek").unwrap().capabilities();
-
- let output = provider_capability_detail("deepseek", "DeepSeek", capabilities);
-
- assert!(output.contains("Provider: DeepSeek (deepseek)"));
- assert!(output.contains("Discovery: scan=full_scan page=full_import storage=sqlite"));
- assert!(output.contains("Turn quality: inferred"));
- assert!(output.contains("Resume quality: native"));
- assert!(output.contains("Operations: scan,import,export,delete,rename,resume"));
- assert!(output.contains("Write risk: level=high"));
- assert!(output.contains("Backup: before_write=true restore=true sync_only=false"));
- assert!(output
- .contains("Activity: hook_events=false runtime_endpoint=false session_activity=false"));
- assert!(output.contains(" provider_payload:"));
- }
-
#[test]
fn gemini_cli_capability_detail_matches_current_jsonl_contract() {
let capabilities = providers::find_provider("gemini").unwrap().capabilities();
diff --git a/rust/apps/memorph-cli/src/skills/server.rs b/rust/apps/memorph-cli/src/skills/server.rs
index e66dda4..64905bf 100644
--- a/rust/apps/memorph-cli/src/skills/server.rs
+++ b/rust/apps/memorph-cli/src/skills/server.rs
@@ -1587,7 +1587,7 @@ mod tests {
)
.await;
assert_eq!(status, StatusCode::OK);
- assert_eq!(scanned["data"]["roots_scanned"], 8);
+ assert_eq!(scanned["data"]["roots_scanned"], 7);
let listed = wait_for_skills(app, 2000).await;
assert_eq!(listed["data"]["total"], 1);
diff --git a/rust/crates/memorph/src/config.rs b/rust/crates/memorph/src/config.rs
index eb45fd5..0281f82 100644
--- a/rust/crates/memorph/src/config.rs
+++ b/rust/crates/memorph/src/config.rs
@@ -907,8 +907,8 @@ mod tests {
#[test]
fn normalize_provider_ids_canonicalizes_aliases() {
assert_eq!(
- normalize_provider_ids(vec!["factory".to_string(), "droid".to_string(),]),
- vec!["droid".to_string()]
+ normalize_provider_ids(vec!["work-buddy".to_string(), "workbuddy".to_string(),]),
+ vec!["workbuddy".to_string()]
);
}
}
diff --git a/rust/crates/memorph/src/core.rs b/rust/crates/memorph/src/core.rs
index c3dfc7b..49b622c 100644
--- a/rust/crates/memorph/src/core.rs
+++ b/rust/crates/memorph/src/core.rs
@@ -51,23 +51,18 @@ const PROJECTED_SESSION_PROVIDER_IDS: &[&str] = &[
"claude",
"codex",
"cursor",
- "deepseek",
"gemini",
"kimi",
"kiro",
"opencode",
- "openclaw",
- "cline",
"copilot",
- "droid",
- "hermes",
- "codebuddy",
"qoder",
"workbuddy",
"pi",
+ "omp",
+ "grok",
+ "zcode",
"antigravity",
- "windsurf",
- "trae",
];
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
diff --git a/rust/crates/memorph/src/core/active_compression.rs b/rust/crates/memorph/src/core/active_compression.rs
index f924960..545bda9 100644
--- a/rust/crates/memorph/src/core/active_compression.rs
+++ b/rust/crates/memorph/src/core/active_compression.rs
@@ -417,7 +417,6 @@ fn provider_chars_per_token_x100(provider_id: &str) -> usize {
"codex" | "openai" | "opencode" => 400,
"gemini" | "google" => 360,
"kimi" => 320,
- "deepseek" => 330,
_ => default_chars_per_token_x100(),
}
}
@@ -575,7 +574,7 @@ mod tests {
&sample_session(),
ActiveCompressionParams {
source_provider_id: "kimi".to_string(),
- target_provider_id: "deepseek".to_string(),
+ target_provider_id: "kimi".to_string(),
policy: ActiveCompressionPolicy::default(),
dry_run: true,
},
@@ -589,11 +588,11 @@ mod tests {
assert_eq!(value["token_estimator"]["strategy"], "provider_heuristic");
assert_eq!(
value["token_estimator"]["effective_provider_id"],
- "deepseek"
+ "kimi"
);
assert_eq!(
value["token_estimator"]["effective_chars_per_token_x100"],
- 330
+ 320
);
}
diff --git a/rust/crates/memorph/src/core/readiness.rs b/rust/crates/memorph/src/core/readiness.rs
index 92ac654..af551a0 100644
--- a/rust/crates/memorph/src/core/readiness.rs
+++ b/rust/crates/memorph/src/core/readiness.rs
@@ -1361,6 +1361,11 @@ mod tests {
let home = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
crate::config::set_test_home_dir(home.path().to_path_buf());
+ // cursor_data_dir() reads the real HOME on macOS, bypassing the test
+ // home; pin the state db to a missing path so the scan stays hermetic.
+ crate::providers::cursor::db::set_test_cursor_db_path(Some(
+ home.path().join("missing/state.vscdb"),
+ ));
let workspace = workspace.path().canonicalize().unwrap();
let report = projection::reconcile_workspace_session_projections_for(
@@ -1372,6 +1377,7 @@ mod tests {
let latest = HashMap::from([("cursor".to_string(), ActivityStatus::Success)]);
let readiness = assess_expected_workspace_checkpoints(&["cursor"], &latest);
+ crate::providers::cursor::db::set_test_cursor_db_path(None);
crate::config::reset_test_home_dir();
assert_eq!(report.discovered_sessions, 0);
assert_eq!(report.scanned_providers, 1);
diff --git a/rust/crates/memorph/src/core_tests.rs b/rust/crates/memorph/src/core_tests.rs
index b63191a..2ebea70 100644
--- a/rust/crates/memorph/src/core_tests.rs
+++ b/rust/crates/memorph/src/core_tests.rs
@@ -967,189 +967,6 @@ fn activity_bucket_selection_follows_span_thresholds() {
);
}
-#[test]
-fn bootstrap_projects_hermes_native_sqlite_source() {
- let dir = tempfile::tempdir().unwrap();
- let db = dir.path().join("state.db");
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute_batch(
- "CREATE TABLE sessions (id TEXT PRIMARY KEY, title TEXT, cwd TEXT, model TEXT, started_at REAL NOT NULL, ended_at REAL, message_count INTEGER, tool_call_count INTEGER, archived INTEGER NOT NULL DEFAULT 0);
- CREATE TABLE messages (id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT, tool_call_id TEXT, tool_calls TEXT, tool_name TEXT, effect_disposition TEXT, timestamp REAL NOT NULL, reasoning TEXT, reasoning_content TEXT, reasoning_details TEXT, compacted INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, api_content TEXT);
- INSERT INTO sessions VALUES ('hermes-1','Hermes fixture','/tmp/hermes-project','model-x',1000,NULL,1,0,0);
- INSERT INTO messages (id,session_id,role,content,tool_call_id,tool_calls,tool_name,effect_disposition,timestamp,reasoning,reasoning_content,reasoning_details,compacted,active,api_content) VALUES (1,'hermes-1','user','hello',NULL,NULL,NULL,NULL,1000,NULL,NULL,NULL,0,1,NULL);"
- ).unwrap();
- drop(conn);
-
- let provider = providers::find_provider("hermes").unwrap();
- assert!(provider.capabilities().scan);
- assert!(provider.capabilities().import);
-
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "hermes-1".into(),
- title: Some("Hermes fixture".into()),
- project_dir: Some("/tmp/hermes-project".into()),
- created_at: None,
- last_active_at: Some(1_000_000),
- source_path: Some(format!("{}#session=hermes-1", db.display())),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut report = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "hermes", &session, &mut report);
-
- assert_eq!(report.projected_sessions, 1);
- assert_eq!(report.failed_sessions, 0);
- assert_eq!(report.missing_sources, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'hermes' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "Hermes fixture");
-}
-
-#[test]
-fn hermes_projection_refreshes_after_source_change() {
- let dir = tempfile::tempdir().unwrap();
- let db = dir.path().join("state.db");
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute_batch(
- "CREATE TABLE sessions (id TEXT PRIMARY KEY, title TEXT, cwd TEXT, model TEXT, started_at REAL NOT NULL, ended_at REAL, message_count INTEGER, tool_call_count INTEGER, archived INTEGER NOT NULL DEFAULT 0);
- CREATE TABLE messages (id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT, tool_call_id TEXT, tool_calls TEXT, tool_name TEXT, effect_disposition TEXT, timestamp REAL NOT NULL, reasoning TEXT, reasoning_content TEXT, reasoning_details TEXT, compacted INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, api_content TEXT);
- INSERT INTO sessions VALUES ('hermes-1','Before','/tmp/hermes-project','model-x',1000,NULL,1,0,0);
- INSERT INTO messages (id,session_id,role,content,tool_call_id,tool_calls,tool_name,effect_disposition,timestamp,reasoning,reasoning_content,reasoning_details,compacted,active,api_content) VALUES (1,'hermes-1','user','before',NULL,NULL,NULL,NULL,1000,NULL,NULL,NULL,0,1,NULL);"
- ).unwrap();
- drop(conn);
-
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "hermes-1".into(),
- title: Some("Before".into()),
- project_dir: Some("/tmp/hermes-project".into()),
- created_at: None,
- last_active_at: Some(1_000_000),
- source_path: Some(format!("{}#session=hermes-1", db.display())),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "hermes", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
-
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute(
- "UPDATE sessions SET title = 'After' WHERE id = 'hermes-1'",
- [],
- )
- .unwrap();
- conn.execute("UPDATE messages SET content = 'after' WHERE id = 1", [])
- .unwrap();
- drop(conn);
-
- let session = ProviderSessionSummary {
- archived: false,
- title: Some("After".into()),
- ..session
- };
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "hermes", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'hermes' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "After");
-}
-
-#[test]
-fn bootstrap_projects_cline_task_source() {
- let dir = tempfile::tempdir().unwrap();
- let task = dir.path().join("task-cline-1");
- std::fs::create_dir_all(&task).unwrap();
- let history = task.join("api_conversation_history.json");
- std::fs::write(
- &history,
- r#"[{"role":"user","content":"hello"},{"role":"assistant","content":[{"type":"thinking","thinking":"reason"},{"type":"text","text":"done"}]}]"#,
- )
- .unwrap();
-
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "task-cline-1".into(),
- title: Some("hello".into()),
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(history.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut report = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "cline", &session, &mut report);
-
- assert_eq!(report.projected_sessions, 1);
- assert_eq!(report.failed_sessions, 0);
- assert_eq!(report.missing_sources, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'cline' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "hello");
-}
-
-#[test]
-fn cline_projection_refreshes_after_task_history_change() {
- let dir = tempfile::tempdir().unwrap();
- let task = dir.path().join("task-cline-1");
- std::fs::create_dir_all(&task).unwrap();
- let history = task.join("api_conversation_history.json");
- std::fs::write(&history, r#"[{"role":"user","content":"before"}]"#).unwrap();
-
- let mut session = ProviderSessionSummary {
- archived: false,
- session_id: "task-cline-1".into(),
- title: Some("before".into()),
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(history.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "cline", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
-
- std::fs::write(&history, r#"[{"role":"user","content":"after"}]"#).unwrap();
- session.title = Some("after".into());
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "cline", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'cline' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "after");
-}
-
#[test]
fn bootstrap_projects_copilot_event_source() {
let dir = tempfile::tempdir().unwrap();
@@ -1240,165 +1057,6 @@ fn copilot_projection_refreshes_after_event_source_change() {
assert_eq!(title, "after");
}
-#[test]
-fn bootstrap_projects_droid_session_source() {
- let dir = tempfile::tempdir().unwrap();
- let sessions = dir.path().join("encoded-cwd");
- std::fs::create_dir_all(&sessions).unwrap();
- let source = sessions.join("droid-session-1.jsonl");
- std::fs::write(
- &source,
- r#"{"role":"user","content":"before","cwd":"/tmp/droid"}
-{"role":"assistant","content":"done"}
-"#,
- )
- .unwrap();
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "droid-session-1".into(),
- title: Some("before".into()),
- project_dir: Some("/tmp/droid".into()),
- created_at: None,
- last_active_at: None,
- source_path: Some(source.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut report = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "droid", &session, &mut report);
- assert_eq!(report.projected_sessions, 1);
- assert_eq!(report.failed_sessions, 0);
- assert_eq!(report.missing_sources, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'droid' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "before");
-}
-
-#[test]
-fn droid_projection_refreshes_after_session_source_change() {
- let dir = tempfile::tempdir().unwrap();
- let sessions = dir.path().join("encoded-cwd");
- std::fs::create_dir_all(&sessions).unwrap();
- let source = sessions.join("droid-session-1.jsonl");
- std::fs::write(
- &source,
- r#"{"role":"user","content":"before"}
-"#,
- )
- .unwrap();
- let mut session = ProviderSessionSummary {
- archived: false,
- session_id: "droid-session-1".into(),
- title: Some("before".into()),
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(source.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "droid", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
- std::fs::write(
- &source,
- r#"{"role":"user","content":"after"}
-"#,
- )
- .unwrap();
- session.title = Some("after".into());
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "droid", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'droid' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "after");
-}
-
-#[test]
-fn bootstrap_projects_codebuddy_session_source() {
- let dir = tempfile::tempdir().unwrap();
- let source_dir = dir.path().join("project");
- std::fs::create_dir_all(&source_dir).unwrap();
- let source = source_dir.join("codebuddy-session-1.jsonl");
- std::fs::write(
- &source,
- r#"{"role":"user","content":"before","cwd":"/tmp/codebuddy"}
-"#,
- )
- .unwrap();
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "codebuddy-session-1".into(),
- title: Some("before".into()),
- project_dir: Some("/tmp/codebuddy".into()),
- created_at: None,
- last_active_at: None,
- source_path: Some(source.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut report = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "codebuddy", &session, &mut report);
- assert_eq!(report.projected_sessions, 1);
- assert_eq!(report.failed_sessions, 0);
- assert_eq!(report.missing_sources, 0);
-}
-
-#[test]
-fn codebuddy_projection_refreshes_after_session_source_change() {
- let dir = tempfile::tempdir().unwrap();
- let source_dir = dir.path().join("project");
- std::fs::create_dir_all(&source_dir).unwrap();
- let source = source_dir.join("codebuddy-session-1.jsonl");
- std::fs::write(
- &source,
- r#"{"role":"user","content":"before"}
-"#,
- )
- .unwrap();
- let mut session = ProviderSessionSummary {
- archived: false,
- session_id: "codebuddy-session-1".into(),
- title: Some("before".into()),
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(source.to_string_lossy().into_owned()),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "codebuddy", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
- std::fs::write(
- &source,
- r#"{"role":"user","content":"after"}
-"#,
- )
- .unwrap();
- session.title = Some("after".into());
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "codebuddy", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
-}
-
#[test]
fn bootstrap_projects_qoder_session_source() {
let dir = tempfile::tempdir().unwrap();
@@ -1672,142 +1330,6 @@ fn antigravity_projection_refreshes_after_session_source_change() {
assert_eq!(second.unchanged_sessions, 0);
}
-#[test]
-fn bootstrap_projects_windsurf_active_trajectory_source() {
- let dir = tempfile::tempdir().unwrap();
- let db = dir.path().join("state.vscdb");
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute(
- "CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
- [],
- )
- .unwrap();
- conn.execute(
- "INSERT INTO ItemTable(key,value) VALUES (?1,?2)",
- rusqlite::params![
- "windsurf.state.cachedActiveTrajectory:workspace-1",
- "CgZ3aW5kLTE="
- ],
- )
- .unwrap();
- drop(conn);
- let source = format!("{}#workspace=workspace-1", db.display());
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "wind-1".into(),
- title: None,
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(source),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut report = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "windsurf", &session, &mut report);
- assert_eq!(report.projected_sessions, 1);
- assert_eq!(report.failed_sessions, 0);
- assert_eq!(report.missing_sources, 0);
-}
-
-#[test]
-fn windsurf_projection_refreshes_after_active_trajectory_change() {
- let dir = tempfile::tempdir().unwrap();
- let db = dir.path().join("state.vscdb");
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute(
- "CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
- [],
- )
- .unwrap();
- conn.execute(
- "INSERT INTO ItemTable(key,value) VALUES (?1,?2)",
- rusqlite::params![
- "windsurf.state.cachedActiveTrajectory:workspace-1",
- "CgZ3aW5kLTE="
- ],
- )
- .unwrap();
- drop(conn);
- let source = format!("{}#workspace=workspace-1", db.display());
- let session = ProviderSessionSummary {
- archived: false,
- session_id: "wind-1".into(),
- title: None,
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(source),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "windsurf", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
- let conn = rusqlite::Connection::open(&db).unwrap();
- conn.execute(
- "UPDATE ItemTable SET value = ?1 WHERE key = ?2",
- rusqlite::params![
- "CgZ3aW5kLTEQYQ==",
- "windsurf.state.cachedActiveTrajectory:workspace-1"
- ],
- )
- .unwrap();
- drop(conn);
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "windsurf", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
-}
-
-#[test]
-fn windsurf_legacy_chat_projection_refreshes_after_source_change() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("history.pbtxt");
- std::fs::write(
- &path,
- r#"message: {source: CHAT_MESSAGE_SOURCE_USER conversation_id: "legacy-1" intent: {text: "before"}}
-"#,
- )
- .unwrap();
- let mut session = ProviderSessionSummary {
- archived: false,
- session_id: "legacy-1".into(),
- title: Some("before".into()),
- project_dir: None,
- created_at: None,
- last_active_at: None,
- source_path: Some(format!("{}#conversation=legacy-1", path.display())),
- };
- let mut projection = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&projection).unwrap();
- local_store::apply_schema(&mut projection).unwrap();
- let mut first = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "windsurf", &session, &mut first);
- assert_eq!(first.projected_sessions, 1);
- std::fs::write(
- &path,
- r#"message: {source: CHAT_MESSAGE_SOURCE_USER conversation_id: "legacy-1" intent: {text: "after"}}
-"#,
- )
- .unwrap();
- session.title = Some("after".into());
- let mut second = SessionProjectionBootstrapReport::default();
- bootstrap_provider_session(&mut projection, "windsurf", &session, &mut second);
- assert_eq!(second.projected_sessions, 1);
- assert_eq!(second.unchanged_sessions, 0);
- let title: String = projection
- .query_row(
- "SELECT title FROM session_snapshots WHERE provider_id = 'windsurf' LIMIT 1",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(title, "after");
-}
-
#[test]
fn bootstrap_discovers_projects_skips_unchanged_and_refreshes_changed_indexes() {
let opencode_dir = tempfile::tempdir().unwrap();
@@ -1996,22 +1518,6 @@ fn kiro_is_enabled_for_default_projection_bootstrap() {
assert!(provider_supports_session_projection("kiro"));
}
-#[test]
-fn bootstrap_scans_deepseek_without_source() {
- let home = tempfile::tempdir().unwrap();
- let _home_guard = TestConfigHomeGuard::new(home.path());
- let mut conn = rusqlite::Connection::open_in_memory().unwrap();
- local_store::configure_connection(&conn).unwrap();
- local_store::apply_schema(&mut conn).unwrap();
-
- let report = bootstrap_session_projections_in_connection(&mut conn, Some("deepseek")).unwrap();
-
- assert_eq!(report.scanned_providers, 1);
- assert_eq!(report.unsupported_providers, 0);
- assert_eq!(report.discovered_sessions, 0);
- assert!(report.failures.is_empty());
-}
-
#[test]
fn non_default_session_list_reads_index_without_provider_source() {
let home = tempfile::tempdir().unwrap();
diff --git a/rust/crates/memorph/src/hooks/discovery.rs b/rust/crates/memorph/src/hooks/discovery.rs
index d23eff7..5a0de3f 100644
--- a/rust/crates/memorph/src/hooks/discovery.rs
+++ b/rust/crates/memorph/src/hooks/discovery.rs
@@ -174,11 +174,8 @@ fn scan_target(provider: &str) -> Option<(&'static str, PathBuf)> {
"cursor" => home.join(".cursor/hooks.json"),
"gemini" => home.join(".gemini/settings.json"),
"qoder" => crate::providers::qoder::hook::settings_path(),
- "droid" => crate::providers::droid::hook::settings_path(),
- "codebuddy" => crate::providers::codebuddy::hook::settings_path(),
"antigravity" => crate::providers::antigravity::hook::settings_path(),
"workbuddy" => crate::providers::workbuddy::hook::settings_path(),
- "hermes" => crate::providers::hermes::hook::settings_path(),
_ => return None,
};
Some((profile.provider, path))
diff --git a/rust/crates/memorph/src/hooks/profiles.rs b/rust/crates/memorph/src/hooks/profiles.rs
index 4d43d74..5259dd5 100644
--- a/rust/crates/memorph/src/hooks/profiles.rs
+++ b/rust/crates/memorph/src/hooks/profiles.rs
@@ -17,15 +17,10 @@ pub enum HookFormat {
KimiToml,
KiroAgentJson,
OpenCodePlugin,
- TraeYaml,
QoderClaudeJson,
- FactoryClaudeJson,
- CodeBuddyClaudeJson,
AntiGravityClaudeJson,
WorkBuddyClaudeJson,
- HermesClaudeJson,
PiExtension,
- ClineFiles,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
diff --git a/rust/crates/memorph/src/hooks/strategies.rs b/rust/crates/memorph/src/hooks/strategies.rs
index 3def53f..8934781 100644
--- a/rust/crates/memorph/src/hooks/strategies.rs
+++ b/rust/crates/memorph/src/hooks/strategies.rs
@@ -13,7 +13,6 @@ use crate::hooks::model::{HookInstallStatus, HookOperationReport};
#[serde(rename_all = "snake_case")]
pub enum HookConfigStrategyKind {
ClaudeLikeJson,
- ClineFiles,
CodexJson,
CopilotJson,
FlatJson,
@@ -22,7 +21,6 @@ pub enum HookConfigStrategyKind {
KiroAgentJson,
OpenCodePlugin,
PiExtension,
- TraeYaml,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
diff --git a/rust/crates/memorph/src/provider_settings/cline.rs b/rust/crates/memorph/src/provider_settings/cline.rs
deleted file mode 100644
index a7425d9..0000000
--- a/rust/crates/memorph/src/provider_settings/cline.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use super::{ProviderSettingModule, SettingDefinition};
-
-pub struct ClineSettingModule;
-
-const SETTINGS: &[SettingDefinition] = &[];
-
-impl ProviderSettingModule for ClineSettingModule {
- fn provider_id(&self) -> &'static str {
- "cline"
- }
-
- fn settings(&self) -> &'static [SettingDefinition] {
- SETTINGS
- }
-}
diff --git a/rust/crates/memorph/src/provider_settings/codebuddy.rs b/rust/crates/memorph/src/provider_settings/codebuddy.rs
deleted file mode 100644
index d973305..0000000
--- a/rust/crates/memorph/src/provider_settings/codebuddy.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use super::{ProviderSettingModule, SettingDefinition};
-
-pub struct CodeBuddySettingModule;
-
-const SETTINGS: &[SettingDefinition] = &[];
-
-impl ProviderSettingModule for CodeBuddySettingModule {
- fn provider_id(&self) -> &'static str {
- "codebuddy"
- }
-
- fn settings(&self) -> &'static [SettingDefinition] {
- SETTINGS
- }
-}
diff --git a/rust/crates/memorph/src/provider_settings/droid.rs b/rust/crates/memorph/src/provider_settings/droid.rs
deleted file mode 100644
index 450f9c9..0000000
--- a/rust/crates/memorph/src/provider_settings/droid.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use super::{ProviderSettingModule, SettingDefinition};
-
-pub struct DroidSettingModule;
-
-const SETTINGS: &[SettingDefinition] = &[];
-
-impl ProviderSettingModule for DroidSettingModule {
- fn provider_id(&self) -> &'static str {
- "droid"
- }
-
- fn settings(&self) -> &'static [SettingDefinition] {
- SETTINGS
- }
-}
diff --git a/rust/crates/memorph/src/provider_settings/hermes.rs b/rust/crates/memorph/src/provider_settings/hermes.rs
deleted file mode 100644
index 06b4042..0000000
--- a/rust/crates/memorph/src/provider_settings/hermes.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use super::{ProviderSettingModule, SettingDefinition};
-
-pub struct HermesSettingModule;
-
-const SETTINGS: &[SettingDefinition] = &[];
-
-impl ProviderSettingModule for HermesSettingModule {
- fn provider_id(&self) -> &'static str {
- "hermes"
- }
-
- fn settings(&self) -> &'static [SettingDefinition] {
- SETTINGS
- }
-}
diff --git a/rust/crates/memorph/src/provider_settings/mod.rs b/rust/crates/memorph/src/provider_settings/mod.rs
index 2a9a889..14c6b8c 100644
--- a/rust/crates/memorph/src/provider_settings/mod.rs
+++ b/rust/crates/memorph/src/provider_settings/mod.rs
@@ -1,21 +1,16 @@
mod antigravity;
mod claude;
-mod cline;
-mod codebuddy;
mod codex;
mod common_hooks;
mod copilot;
mod cursor;
-mod droid;
mod gemini;
-mod hermes;
mod kimi;
mod kiro;
mod opencode;
mod pi;
mod qoder;
mod registry;
-mod trae;
mod workbuddy;
use anyhow::{Context as _, Result};
@@ -307,8 +302,8 @@ mod tests {
#[test]
fn alias_lists_same_provider_specific_settings_as_canonical_id() {
- let canonical = list_provider_settings("droid").unwrap();
- let alias = list_provider_settings("factory").unwrap();
+ let canonical = list_provider_settings("workbuddy").unwrap();
+ let alias = list_provider_settings("work-buddy").unwrap();
assert_eq!(alias.len(), canonical.len());
assert_eq!(
diff --git a/rust/crates/memorph/src/provider_settings/registry.rs b/rust/crates/memorph/src/provider_settings/registry.rs
index d0d5162..9e59a82 100644
--- a/rust/crates/memorph/src/provider_settings/registry.rs
+++ b/rust/crates/memorph/src/provider_settings/registry.rs
@@ -9,7 +9,6 @@ pub(super) fn find_provider_setting_module(
let provider_id = crate::providers::canonical_provider_id(provider_id);
match provider_id.as_str() {
"claude" => Some(&super::claude::ClaudeSettingModule),
- "cline" => Some(&super::cline::ClineSettingModule),
"codex" => Some(&super::codex::CodexSettingModule),
"copilot" => Some(&super::copilot::CopilotSettingModule),
"cursor" => Some(&super::cursor::CursorSettingModule),
@@ -19,12 +18,8 @@ pub(super) fn find_provider_setting_module(
"opencode" => Some(&super::opencode::OpenCodeSettingModule),
"pi" => Some(&super::pi::PiSettingModule),
"qoder" => Some(&super::qoder::QoderSettingModule),
- "droid" => Some(&super::droid::DroidSettingModule),
- "codebuddy" => Some(&super::codebuddy::CodeBuddySettingModule),
"antigravity" => Some(&super::antigravity::AntiGravitySettingModule),
"workbuddy" => Some(&super::workbuddy::WorkBuddySettingModule),
- "hermes" => Some(&super::hermes::HermesSettingModule),
- "trae" => Some(&super::trae::TraeSettingModule),
_ => None,
}
}
@@ -52,10 +47,10 @@ mod tests {
#[test]
fn registry_resolves_setting_aliases() {
assert_eq!(
- find_provider_setting_module("factory")
- .expect("factory alias module")
+ find_provider_setting_module("work-buddy")
+ .expect("work-buddy alias module")
.provider_id(),
- "droid"
+ "workbuddy"
);
}
}
diff --git a/rust/crates/memorph/src/provider_settings/trae.rs b/rust/crates/memorph/src/provider_settings/trae.rs
deleted file mode 100644
index 4af0072..0000000
--- a/rust/crates/memorph/src/provider_settings/trae.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use super::{ProviderSettingModule, SettingDefinition};
-
-pub struct TraeSettingModule;
-
-const SETTINGS: &[SettingDefinition] = &[];
-
-impl ProviderSettingModule for TraeSettingModule {
- fn provider_id(&self) -> &'static str {
- "trae"
- }
-
- fn settings(&self) -> &'static [SettingDefinition] {
- SETTINGS
- }
-}
diff --git a/rust/crates/memorph/src/providers/aliases.rs b/rust/crates/memorph/src/providers/aliases.rs
index 821954b..31e4784 100644
--- a/rust/crates/memorph/src/providers/aliases.rs
+++ b/rust/crates/memorph/src/providers/aliases.rs
@@ -1,8 +1,7 @@
pub(crate) fn canonical_provider_id(provider_id: &str) -> String {
match provider_id.trim().to_ascii_lowercase().as_str() {
- "amazon_q" | "amazon-q" => "amazonq".to_string(),
- "factory" => "droid".to_string(),
"work-buddy" | "work_buddy" => "workbuddy".to_string(),
+ "oh-my-pi" | "oh_my_pi" => "omp".to_string(),
other => other.to_string(),
}
}
@@ -11,7 +10,7 @@ pub(crate) fn canonical_provider_id(provider_id: &str) -> String {
mod tests {
#[test]
fn canonicalizes_known_provider_aliases() {
- assert_eq!(super::canonical_provider_id("factory"), "droid");
assert_eq!(super::canonical_provider_id("work-buddy"), "workbuddy");
+ assert_eq!(super::canonical_provider_id("oh-my-pi"), "omp");
}
}
diff --git a/rust/crates/memorph/src/providers/amazonq.rs b/rust/crates/memorph/src/providers/amazonq.rs
deleted file mode 100644
index 57ff141..0000000
--- a/rust/crates/memorph/src/providers/amazonq.rs
+++ /dev/null
@@ -1,235 +0,0 @@
-//! Amazon Q CLI provider backed by `data_local_dir()/amazon-q/data.sqlite3`.
-
-use crate::provider::{
- Provider, ProviderCapabilities, ProviderContentFidelity, ProviderSessionImportPage,
- ProviderSessionSummary, ProviderSourceFingerprint, ScanStrategy, StorageShape, TurnQuality,
-};
-use crate::session::{
- Context, Identity, ImportedSession, MappingDirection, MappingReport, Provenance, ProviderRef,
- Schema, Session,
-};
-use anyhow::{Context as _, Result};
-use chrono::Utc;
-use rusqlite::{Connection, OpenFlags, OptionalExtension};
-use sha2::{Digest, Sha256};
-use std::collections::BTreeMap;
-use std::path::{Path, PathBuf};
-
-pub struct AmazonQProvider;
-const PROVIDER_ID: &str = "amazonq";
-const SOURCE_PREFIX: &str = "amazonq://";
-
-impl Provider for AmazonQProvider {
- fn id(&self) -> &'static str {
- PROVIDER_ID
- }
- fn name(&self) -> &'static str {
- "Amazon Q"
- }
- fn capabilities(&self) -> ProviderCapabilities {
- ProviderCapabilities {
- scan: true,
- import: true,
- scan_strategy: ScanStrategy::Indexed,
- page_strategy: crate::provider::PageStrategy::FullImport,
- storage_shape: StorageShape::Sqlite,
- turn_quality: TurnQuality::Exact,
- import_fidelity: ProviderContentFidelity {
- text: Some(crate::session::Fidelity::Preserved),
- tool_call: Some(crate::session::Fidelity::Preserved),
- tool_result: Some(crate::session::Fidelity::Preserved),
- provider_payload: Some(crate::session::Fidelity::Preserved),
- ..ProviderContentFidelity::unknown()
- },
- ..ProviderCapabilities::default()
- }
- }
-
- fn scan_sessions(&self) -> Result> {
- let Some(path) = db_path() else {
- return Ok(Vec::new());
- };
- if !path.is_file() {
- return Ok(Vec::new());
- }
- let conn = open_db(&path)?;
- let mut stmt = conn.prepare("SELECT key, value FROM conversations")?;
- let rows = stmt.query_map([], |row| {
- Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
- })?;
- let mut out = Vec::new();
- for row in rows.flatten() {
- let (key, value) = row;
- let mut report = MappingReport::new(PROVIDER_ID, MappingDirection::Import);
- let events = crate::providers::q_conversation::parse_history(
- PROVIDER_ID,
- &value,
- &key,
- &mut report,
- );
- if events.is_empty() {
- continue;
- }
- let title = crate::providers::q_conversation::first_prompt_text_value(
- &serde_json::from_str(&value).unwrap_or_default(),
- 100,
- );
- let (_, last) = crate::providers::q_conversation::history_time_bounds(&value);
- out.push(ProviderSessionSummary {
- archived: false,
- session_id: key.clone(),
- title,
- project_dir: Some(key.clone()),
- created_at: last.map(|v| v.timestamp_millis()),
- last_active_at: last.map(|v| v.timestamp_millis()),
- source_path: Some(format!("{SOURCE_PREFIX}{key}")),
- });
- }
- Ok(out)
- }
-
- fn import_session(&self, source_path: &str) -> Result {
- let key = source_path
- .strip_prefix(SOURCE_PREFIX)
- .unwrap_or(source_path);
- let path = db_path().context("Amazon Q database path unavailable")?;
- let conn = open_db(&path)?;
- let value: String = conn
- .query_row(
- "SELECT value FROM conversations WHERE key = ?1",
- [key],
- |row| row.get(0),
- )
- .optional()?
- .context("Amazon Q conversation not found")?;
- let json: serde_json::Value = serde_json::from_str(&value)?;
- let mut report = MappingReport::new(PROVIDER_ID, MappingDirection::Import);
- let events = crate::providers::q_conversation::parse_history_value(
- PROVIDER_ID,
- &json,
- key,
- &mut report,
- );
- let (created_at, last_active_at) =
- crate::providers::q_conversation::history_time_bounds_value(&json);
- let event_meta = events
- .iter()
- .map(|_| crate::session::EventMeta::preserved(PROVIDER_ID))
- .collect();
- Ok(ImportedSession {
- session: Session {
- lineage: Vec::new(),
- schema: Schema::default(),
- identity: Identity {
- id: key.to_string(),
- title: crate::providers::q_conversation::first_prompt_text_value(&json, 100),
- },
- context: Context {
- workspace: Some(key.to_string()),
- created_at,
- last_active_at,
- tags: Vec::new(),
- },
- events,
- extensions: BTreeMap::new(),
- },
- provenance: Provenance {
- imported_at: Utc::now(),
- imported_by: Some("memorph-cli".into()),
- primary_source: ProviderRef {
- provider_id: PROVIDER_ID.into(),
- session_id: key.into(),
- source_path: Some(format!("{SOURCE_PREFIX}{key}")),
- },
- aliases: Vec::new(),
- },
- event_meta,
- report,
- })
- }
-
- fn import_session_page(
- &self,
- source_path: &str,
- event_offset: usize,
- event_limit: Option,
- ) -> Result {
- let imported = self.import_session(source_path)?;
- let all = imported.session.events.clone();
- let event_count = all.len();
- let message_count = all
- .iter()
- .filter(|e| crate::provider::event_is_visible_message(e))
- .count();
- let turns = crate::session_projection::project_session_turns(
- &imported.session.identity.id,
- &all,
- TurnQuality::Exact,
- );
- let page_events: Vec<_> = all
- .into_iter()
- .skip(event_offset)
- .take(event_limit.unwrap_or(usize::MAX))
- .collect();
- let page_turns = crate::session_projection::project_session_turns(
- &imported.session.identity.id,
- &page_events,
- TurnQuality::Inferred,
- );
- Ok(ProviderSessionImportPage {
- imported: ImportedSession {
- session: Session {
- events: page_events,
- ..imported.session
- },
- ..imported
- },
- event_count,
- message_count,
- turn_count: Some(turns.len()),
- turns: page_turns,
- })
- }
-
- fn session_source_fingerprint(
- &self,
- source_path: &str,
- ) -> Result