memorix 1.4.4 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +14 -4
  4. package/README.zh-CN.md +9 -2
  5. package/dist/cli/index.js +1802 -1138
  6. package/dist/index.js +1298 -991
  7. package/dist/{maintenance-jobs-B1-W0ssz.d.ts → maintenance-jobs-DVRF-46r.d.ts} +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +1755 -829
  10. package/dist/media-audio-runner.js +1760 -835
  11. package/dist/media-video-runner.js +1755 -830
  12. package/dist/memcode-runtime/CHANGELOG.md +69 -0
  13. package/dist/sdk.js +1290 -990
  14. package/dist/types.d.ts +7 -1
  15. package/dist/vector-backfill-runner.d.ts +1 -1
  16. package/dist/vector-backfill-runner.js +1751 -826
  17. package/docs/1.4.5-AGENT-AUTO-MEMORY-RESEARCH.md +95 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -2
  19. package/docs/API_REFERENCE.md +23 -4
  20. package/docs/CONFIGURATION.md +17 -3
  21. package/docs/INTEGRATIONS.md +9 -2
  22. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -0
  23. package/docs/MEMCODE.md +1 -1
  24. package/docs/SETUP.md +14 -4
  25. package/llms-full.txt +5 -4
  26. package/llms.txt +1 -1
  27. package/package.json +1 -1
  28. package/plugins/antigravity/memorix/mcp_config.json +3 -1
  29. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  30. package/plugins/claude/memorix/.mcp.json +1 -1
  31. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  32. package/plugins/codebuddy/memorix/.mcp.json +1 -1
  33. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  34. package/plugins/codex/memorix/.mcp.json +1 -1
  35. package/plugins/copilot/memorix/.mcp.json +1 -1
  36. package/plugins/copilot/memorix/plugin.json +1 -1
  37. package/plugins/gemini/memorix/gemini-extension.json +4 -2
  38. package/plugins/omp/memorix/package.json +1 -1
  39. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  40. package/plugins/openclaw/memorix/.mcp.json +3 -1
  41. package/plugins/pi/memorix/package.json +1 -1
  42. package/server.json +2 -2
  43. package/src/cli/commands/agent-integrations.ts +7 -1
  44. package/src/cli/commands/dashboard.ts +14 -3
  45. package/src/cli/commands/hooks-install.ts +2 -0
  46. package/src/cli/commands/hooks-preview.ts +5 -0
  47. package/src/cli/commands/ingest-commit.ts +47 -3
  48. package/src/cli/commands/integrate.ts +2 -0
  49. package/src/cli/commands/memory.ts +17 -4
  50. package/src/cli/commands/purge.ts +120 -0
  51. package/src/cli/commands/serve-http.ts +39 -5
  52. package/src/cli/commands/setup.ts +60 -3
  53. package/src/cli/commands/uninstall.ts +20 -0
  54. package/src/cli/index.ts +3 -804
  55. package/src/codegraph/auto-context.ts +82 -1
  56. package/src/config/yaml-loader.ts +3 -8
  57. package/src/config.ts +0 -5
  58. package/src/evaluation/session-replay.ts +98 -0
  59. package/src/git/extractor.ts +5 -3
  60. package/src/hooks/installers/index.ts +58 -2
  61. package/src/hooks/types.ts +2 -0
  62. package/src/integrations/registry.ts +9 -0
  63. package/src/knowledge/workset.ts +58 -0
  64. package/src/memory/long-term.ts +97 -2
  65. package/src/runtime/control-plane-maintenance.ts +1 -0
  66. package/src/runtime/isolated-maintenance.ts +1 -0
  67. package/src/runtime/lifecycle.ts +19 -0
  68. package/src/runtime/maintenance-jobs.ts +1 -0
  69. package/src/runtime/project-maintenance.ts +8 -0
  70. package/src/search/surfaced-registry.ts +62 -0
  71. package/src/server.ts +74 -14
  72. package/src/store/orama-store.ts +17 -1
  73. package/src/types.ts +7 -1
  74. package/src/workspace/mcp-adapters/dsh.ts +147 -0
  75. package/src/cli/commands/audit-list.ts +0 -89
  76. package/src/cli/commands/purge-all-memory.ts +0 -85
  77. package/src/cli/commands/purge-project-memory.ts +0 -83
  78. package/src/cli/commands/uninstall-project-artifacts.ts +0 -86
  79. package/src/cli/tui/Sidebar.tsx +0 -179
  80. package/src/cli/tui/interactive-commands.ts +0 -261
  81. package/src/cli/tui/markdown-render.tsx +0 -371
  82. package/src/cli/tui/use-mouse.ts +0 -157
  83. package/src/cli/tui/useNavigation.ts +0 -56
  84. package/src/orchestrate/context-compact.ts +0 -188
  85. package/src/team/file-locks.ts +0 -173
  86. package/src/team/index.ts +0 -23
  87. package/src/team/messages.ts +0 -203
  88. package/src/team/persistence.ts +0 -96
  89. package/src/team/registry.ts +0 -158
  90. package/src/team/tasks.ts +0 -211
@@ -159,6 +159,7 @@ function getAgentLabel(agent: string): string {
159
159
  hermes: 'Hermes Agent',
160
160
  omp: 'Oh-my-Pi',
161
161
  trae: 'Trae',
162
+ dsh: 'DeepSeek Harness',
162
163
  };
163
164
  return labels[agent] || agent;
164
165
  }
@@ -177,6 +178,7 @@ function getAgentHint(agent: string): string {
177
178
  hermes: 'Hermes plugin hooks; use `memorix setup --agent hermes`',
178
179
  omp: 'Oh-my-Pi package hooks; use `memorix setup --agent omp`',
179
180
  trae: '.trae/rules/project_rules.md (rules only, no hooks system)',
181
+ dsh: '~/.dsh/cordis.patch.yml MCP row via `memorix setup --agent dsh` (no hooks system)',
180
182
  codex: 'Codex plugin hooks; use `memorix setup --agent codex`',
181
183
  };
182
184
  return hints[agent] || '';
@@ -152,6 +152,7 @@ function estimateFileSize(agent: string, configPath: string): string {
152
152
  antigravity: '~2KB',
153
153
  opencode: '~3KB',
154
154
  trae: '~1KB',
155
+ dsh: '~1KB',
155
156
  };
156
157
  return sizes[agent] || '~2KB';
157
158
  }
@@ -171,6 +172,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
171
172
  return path.join(home, '.gemini', 'GEMINI.md');
172
173
  case 'trae':
173
174
  return path.join(home, '.trae', 'rules', 'project_rules.md');
175
+ case 'dsh':
176
+ return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
174
177
  default:
175
178
  return '';
176
179
  }
@@ -186,6 +189,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
186
189
  return path.join(cwd, 'GEMINI.md');
187
190
  case 'trae':
188
191
  return path.join(cwd, '.trae', 'rules', 'project_rules.md');
192
+ case 'dsh':
193
+ return path.join(cwd, 'AGENTS.md');
189
194
  default:
190
195
  return '';
191
196
  }
@@ -7,6 +7,37 @@
7
7
 
8
8
  import { defineCommand } from 'citty';
9
9
  import * as p from '@clack/prompts';
10
+ import type { CommitInfo } from '../../git/extractor.js';
11
+
12
+ export interface GitIngestPolicyInput {
13
+ ingestOnCommit?: boolean;
14
+ maxDiffSize?: number;
15
+ }
16
+
17
+ /**
18
+ * The documented git policy settings are real gates, not display-only facts:
19
+ * `ingest_on_commit = false` disables post-commit ingestion, and
20
+ * `max_diff_size` caps how much diff content is captured into the narrative.
21
+ */
22
+ export function applyGitIngestPolicy(
23
+ commit: CommitInfo,
24
+ policy: GitIngestPolicyInput,
25
+ ): { skip: boolean; reason?: string; diffSummary: string } {
26
+ if (policy.ingestOnCommit === false) {
27
+ return {
28
+ skip: true,
29
+ reason: 'ingest_on_commit is disabled',
30
+ diffSummary: commit.diffSummary,
31
+ };
32
+ }
33
+ const cap = policy.maxDiffSize;
34
+ return {
35
+ skip: false,
36
+ diffSummary: typeof cap === 'number' && Number.isFinite(cap)
37
+ ? commit.diffSummary.slice(0, Math.max(0, Math.floor(cap)))
38
+ : commit.diffSummary,
39
+ };
40
+ }
10
41
 
11
42
  export default defineCommand({
12
43
  meta: {
@@ -42,13 +73,26 @@ export default defineCommand({
42
73
  if (!auto) p.intro(`Ingest commit: ${ref}`);
43
74
 
44
75
  try {
76
+ const { getGitConfig } = await import('../../config.js');
77
+ const gitCfg = getGitConfig();
45
78
  const { getCommitInfo, ingestCommit } = await import('../../git/extractor.js');
46
- const commit = getCommitInfo(cwd, ref);
79
+ const commit = getCommitInfo(cwd, ref, gitCfg.maxDiffSize ?? 500);
80
+
81
+ // The documented ingest_on_commit gate: post-commit execution stops
82
+ // here when disabled. An explicit manual run always proceeds.
83
+ const policy = applyGitIngestPolicy(commit, {
84
+ ingestOnCommit: gitCfg.ingestOnCommit,
85
+ maxDiffSize: gitCfg.maxDiffSize,
86
+ });
87
+ if (policy.skip && auto) {
88
+ console.error(`[memorix] Skipped ${commit.shortHash}: ${policy.reason}`);
89
+ process.exit(0);
90
+ return;
91
+ }
92
+ commit.diffSummary = policy.diffSummary;
47
93
 
48
94
  // Noise filter: skip low-value commits (typo, format, lockfile, merge, etc.)
49
95
  const { shouldFilterCommit } = await import('../../git/noise-filter.js');
50
- const { getGitConfig } = await import('../../config.js');
51
- const gitCfg = getGitConfig();
52
96
  const filterResult = shouldFilterCommit(commit, {
53
97
  skipMergeCommits: gitCfg.skipMergeCommits,
54
98
  excludePatterns: gitCfg.excludePatterns,
@@ -114,6 +114,7 @@ function getAgentLabel(agent: string): string {
114
114
  antigravity: 'Antigravity',
115
115
  'gemini-cli': 'Gemini CLI',
116
116
  trae: 'Trae',
117
+ dsh: 'DeepSeek Harness',
117
118
  };
118
119
  return labels[agent] || agent;
119
120
  }
@@ -130,6 +131,7 @@ function getAgentHint(agent: string): string {
130
131
  antigravity: 'plugin + MCP + hooks + skills',
131
132
  'gemini-cli': 'settings + GEMINI.md',
132
133
  trae: 'rules',
134
+ dsh: 'MCP patch + AGENTS.md + skills',
133
135
  };
134
136
  return hints[agent] || 'integration files';
135
137
  }
@@ -467,6 +467,7 @@ export default defineCommand({
467
467
  createManualLongTermMemory,
468
468
  getLongTermMemoryDetail,
469
469
  listLongTermMemories,
470
+ maybeAutoQualifyLongTermMemory,
470
471
  promoteObservationToLongTermMemory,
471
472
  qualifyLongTermMemory,
472
473
  supersedeLongTermMemory,
@@ -545,9 +546,15 @@ export default defineCommand({
545
546
  portability: portability(),
546
547
  reader: longTermReader,
547
548
  });
549
+ // Operator-written records carry their own source evidence.
550
+ const qualified = await maybeAutoQualifyLongTermMemory({
551
+ dataDir,
552
+ id: result.memory.id,
553
+ source: 'manual',
554
+ });
548
555
  emitResult(
549
- { project, ...result },
550
- `Created long-term memory candidate ${result.memory.id}. Qualify it after checking its evidence.`,
556
+ { project, ...result, memory: qualified ?? result.memory },
557
+ `Created long-term memory ${result.memory.id}.`,
551
558
  asJson,
552
559
  );
553
560
  return;
@@ -576,9 +583,15 @@ export default defineCommand({
576
583
  applicability: asStringArg(args.applicability),
577
584
  reader: longTermReader,
578
585
  });
586
+ // An explicit promote request carries its own source evidence.
587
+ const qualified = await maybeAutoQualifyLongTermMemory({
588
+ dataDir,
589
+ id: result.memory.id,
590
+ sourceDetail: 'explicit',
591
+ });
579
592
  emitResult(
580
- { project, sourceObservationId: observation.id, ...result },
581
- `Promoted observation #${observation.id} to long-term memory candidate ${result.memory.id}.`,
593
+ { project, sourceObservationId: observation.id, ...result, memory: qualified ?? result.memory },
594
+ `Promoted observation #${observation.id} to long-term memory ${result.memory.id}.`,
582
595
  asJson,
583
596
  );
584
597
  return;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * CLI Command: memorix purge
3
+ *
4
+ * Retire memories from retrieval. Project-scoped by default; --all covers
5
+ * every project. Observations and long-term records are archived (never
6
+ * hard-deleted) so the audit trail survives. Refuses to run outside a
7
+ * terminal without --yes.
8
+ */
9
+
10
+ import { defineCommand } from 'citty';
11
+ import { detectProject } from '../../project/detector.js';
12
+ import { getProjectDataDir } from '../../store/persistence.js';
13
+
14
+ export default defineCommand({
15
+ meta: {
16
+ name: 'purge',
17
+ description: 'Retire memories from retrieval (current project by default, --all for every project)',
18
+ },
19
+ args: {
20
+ all: {
21
+ type: 'boolean',
22
+ description: 'Purge every project instead of the current one',
23
+ default: false,
24
+ },
25
+ yes: {
26
+ type: 'boolean',
27
+ description: 'Skip the interactive confirmation',
28
+ default: false,
29
+ },
30
+ json: {
31
+ type: 'boolean',
32
+ description: 'Emit a machine-readable result',
33
+ default: false,
34
+ },
35
+ },
36
+ async run({ args }) {
37
+ const purgeAll = Boolean(args.all);
38
+ const yes = Boolean(args.yes);
39
+ const asJson = Boolean(args.json);
40
+
41
+ // Confirmation gate first: a destructive wipe never proceeds in a
42
+ // non-interactive shell without an explicit --yes.
43
+ if (!yes && !process.stdin.isTTY) {
44
+ console.error('[ERROR] Non-interactive environment. Add --yes to confirm.');
45
+ console.error('[ERROR] Refusing to purge memories without confirmation.');
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ if (!yes) {
50
+ const readline = await import('node:readline');
51
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
52
+ const question = purgeAll
53
+ ? 'Retire ALL memories from every project? (y/N) '
54
+ : 'Retire all memories for this project? (y/N) ';
55
+ const answer = await new Promise<string>((resolve) => rl.question(question, resolve));
56
+ rl.close();
57
+ if (answer.trim().toLowerCase() !== 'y') {
58
+ console.log('Cancelled.');
59
+ return;
60
+ }
61
+ }
62
+
63
+ let projectId: string | null = null;
64
+ if (!purgeAll) {
65
+ const project = detectProject();
66
+ if (!project) {
67
+ console.error('[ERROR] No .git found — run from a project directory or pass --all.');
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ projectId = project.id;
72
+ }
73
+
74
+ const dataDir = await getProjectDataDir(projectId ?? '');
75
+ const { initObservationStore } = await import('../../store/obs-store.js');
76
+ const { getAllObservations, resolveObservations } = await import('../../memory/observations.js');
77
+ const { withFreshIndex } = await import('../../memory/freshness.js');
78
+ await initObservationStore(dataDir);
79
+
80
+ const observations = await withFreshIndex(() => getAllObservations());
81
+ const target = purgeAll ? observations : observations.filter((o) => o.projectId === projectId);
82
+ const ids = target
83
+ .map((o) => o.id)
84
+ .filter((id): id is number => typeof id === 'number');
85
+ const archived = ids.length > 0
86
+ ? (await resolveObservations(ids, 'archived')).resolved.length
87
+ : 0;
88
+
89
+ const { LongTermMemoryStore } = await import('../../memory/long-term-store.js');
90
+ const { archiveLongTermMemory } = await import('../../memory/long-term.js');
91
+ const longTermStore = new LongTermMemoryStore();
92
+ await longTermStore.init(dataDir);
93
+ let longTermArchived = 0;
94
+ for (const memory of longTermStore.list()) {
95
+ if (memory.state === 'archived' || memory.state === 'superseded') continue;
96
+ if (!purgeAll && memory.originProjectId !== projectId) continue;
97
+ try {
98
+ await archiveLongTermMemory({
99
+ dataDir,
100
+ id: memory.id,
101
+ reason: purgeAll ? 'purge --all' : 'purge (project) ' + (projectId ?? ''),
102
+ });
103
+ longTermArchived += 1;
104
+ } catch { /* already inactive */ }
105
+ }
106
+
107
+ const result = {
108
+ scope: purgeAll ? 'all' : projectId,
109
+ observationsArchived: archived,
110
+ longTermArchived,
111
+ };
112
+ if (asJson) {
113
+ console.log(JSON.stringify(result));
114
+ } else {
115
+ console.log(
116
+ `[OK] Purged ${purgeAll ? 'all projects' : result.scope}: archived ${archived} observations and ${longTermArchived} long-term memories.`,
117
+ );
118
+ }
119
+ },
120
+ });
@@ -39,6 +39,36 @@ export function parseSessionTimeoutMs(raw: string | undefined): number {
39
39
  return Math.floor(parsed);
40
40
  }
41
41
 
42
+ export interface ServeHttpConfiguredDefaults {
43
+ port?: number;
44
+ transport?: 'stdio' | 'http';
45
+ dashboard?: boolean;
46
+ dashboardPort?: number;
47
+ }
48
+
49
+ /**
50
+ * The [server] config section is a real default source, not display-only:
51
+ * `port` becomes the listener default and an explicit `transport: "stdio"`
52
+ * produces a notice (an explicit serve-http command still wins). CLI flags
53
+ * always beat config.
54
+ */
55
+ export function resolveServeHttpDefaults(
56
+ argsPort: string | undefined,
57
+ configured: ServeHttpConfiguredDefaults,
58
+ ): { port: number; transportNotice?: string } {
59
+ const configuredPort = typeof configured.port === 'number' && Number.isFinite(configured.port)
60
+ ? configured.port
61
+ : undefined;
62
+ const port = parseTcpPortOrReport(argsPort, configuredPort ?? 3211);
63
+ if (port === undefined) {
64
+ return { port: 3211 };
65
+ }
66
+ const transportNotice = configured.transport === 'stdio'
67
+ ? '[memorix] config server.transport = "stdio"; serve-http continues on HTTP (the explicit command wins).'
68
+ : undefined;
69
+ return { port, transportNotice };
70
+ }
71
+
42
72
  export default defineCommand({
43
73
  meta: {
44
74
  name: 'serve-http',
@@ -81,11 +111,15 @@ export default defineCommand({
81
111
  const { writeFileSync, mkdirSync } = await import('node:fs');
82
112
  const { homedir } = await import('node:os');
83
113
 
84
- const port = parseTcpPortOrReport(args.port, 3211);
85
- if (port === undefined) {
86
- process.exitCode = 1;
87
- return;
88
- }
114
+ // The [server] config section feeds startup defaults; CLI flags win.
115
+ let configuredDefaults: ServeHttpConfiguredDefaults = {};
116
+ try {
117
+ const { getServerConfig } = await import('../../config.js');
118
+ configuredDefaults = getServerConfig();
119
+ } catch { /* config files are optional */ }
120
+ const resolved = resolveServeHttpDefaults(args.port as string | undefined, configuredDefaults);
121
+ if (resolved.transportNotice) console.error(resolved.transportNotice);
122
+ const port = resolved.port;
89
123
  const host = args.host || '127.0.0.1';
90
124
  const toolProfile = resolveToolProfile({ explicit: args.mode, envValue: process.env.MEMORIX_MODE, fallback: 'team' });
91
125
 
@@ -30,6 +30,7 @@ import { OmpMCPAdapter } from '../../workspace/mcp-adapters/omp.js';
30
30
  import { KiroMCPAdapter } from '../../workspace/mcp-adapters/kiro.js';
31
31
  import { OpenCodeMCPAdapter } from '../../workspace/mcp-adapters/opencode.js';
32
32
  import { TraeMCPAdapter } from '../../workspace/mcp-adapters/trae.js';
33
+ import { DshMCPAdapter } from '../../workspace/mcp-adapters/dsh.js';
33
34
  import { CodeBuddyMCPAdapter } from '../../workspace/mcp-adapters/codebuddy.js';
34
35
  import { getSetupIntegrationRows as readSetupIntegrationRows } from '../../integrations/registry.js';
35
36
  import { OFFICIAL_MEMORIX_SKILLS } from '../../hooks/official-skills.js';
@@ -190,6 +191,7 @@ const SUPPORTED_SETUP_AGENTS: AgentName[] = [
190
191
  'omp',
191
192
  'kiro',
192
193
  'trae',
194
+ 'dsh',
193
195
  'pi',
194
196
  ];
195
197
 
@@ -402,6 +404,7 @@ export function getMcpAdapter(agent: McpConfigAgent): MCPConfigAdapter {
402
404
  kiro: new KiroMCPAdapter(),
403
405
  opencode: new OpenCodeMCPAdapter(),
404
406
  trae: new TraeMCPAdapter(),
407
+ dsh: new DshMCPAdapter(),
405
408
  };
406
409
  return adapters[agent];
407
410
  }
@@ -419,7 +422,9 @@ export function buildMemorixServer(mcp: Exclude<SetupMcpTransport, 'none'>): MCP
419
422
  return {
420
423
  name: 'memorix',
421
424
  command: 'memorix',
422
- args: ['serve'],
425
+ // The installed surface must expose every tool the generated guidance
426
+ // teaches. lite covers the full taught set; micro would hide half of it.
427
+ args: ['serve', '--mode', 'lite'],
423
428
  };
424
429
  }
425
430
 
@@ -699,6 +704,55 @@ function mergeYamlMcpConfig(existingContent: string | null, generatedContent: st
699
704
  return stringifyYaml(merged, { lineWidth: 0 });
700
705
  }
701
706
 
707
+ /**
708
+ * Merge the generated DeepSeek Harness patch rows into an existing
709
+ * `cordis.patch.yml` without clobbering unrelated user patches.
710
+ *
711
+ * DSH patch files are lists of Cordis patch operations. Memorix owns exactly
712
+ * the `@deepseek-ai/dsh-mcp-client` row whose config.serverName is `memorix`;
713
+ * every other operation is user-owned and must survive the merge. When the
714
+ * existing file is not a parseable list, the generated rows are appended to
715
+ * the raw content instead of overwriting it.
716
+ */
717
+ function mergeDshPatchConfig(existingContent: string | null, generatedContent: string): string {
718
+ const generatedTrimmed = generatedContent.trimEnd();
719
+ if (!existingContent?.trim()) {
720
+ return generatedTrimmed.endsWith('\n') ? generatedTrimmed : `${generatedTrimmed}\n`;
721
+ }
722
+
723
+ const isMemorixInsertRow = (row: unknown): boolean => {
724
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return false;
725
+ const record = row as Record<string, unknown>;
726
+ if (record.name !== '@deepseek-ai/dsh-mcp-client') return false;
727
+ const config = record.config;
728
+ return Boolean(
729
+ config && typeof config === 'object' && !Array.isArray(config)
730
+ && (config as Record<string, unknown>).serverName === 'memorix',
731
+ );
732
+ };
733
+
734
+ const opContainsMemorixRow = (op: unknown): boolean => {
735
+ if (!op || typeof op !== 'object' || Array.isArray(op)) return false;
736
+ const insert = (op as Record<string, unknown>).insert;
737
+ return Array.isArray(insert) && insert.some(isMemorixInsertRow);
738
+ };
739
+
740
+ let existingDocument: unknown;
741
+ try {
742
+ existingDocument = parseYaml(existingContent);
743
+ } catch {
744
+ return `${existingContent.trimEnd()}\n\n${generatedTrimmed}\n`;
745
+ }
746
+ if (!Array.isArray(existingDocument)) {
747
+ // A scalar or mapping document is not a patch list — never rewrite it.
748
+ return `${existingContent.trimEnd()}\n\n${generatedTrimmed}\n`;
749
+ }
750
+
751
+ const kept = existingDocument.filter((op) => !opContainsMemorixRow(op));
752
+ const merged = [...kept, ...(parseYaml(generatedContent) as unknown[])];
753
+ return stringifyYaml(merged, { lineWidth: 0 }) + '\n';
754
+ }
755
+
702
756
  export async function installMcpConfig(options: {
703
757
  agent: McpConfigAgent;
704
758
  projectRoot?: string;
@@ -725,7 +779,9 @@ export async function installMcpConfig(options: {
725
779
  ? mergeTomlMcpConfig(existingContent, generated)
726
780
  : options.agent === 'hermes'
727
781
  ? mergeYamlMcpConfig(existingContent, generated)
728
- : mergeJsonMcpConfig(existingContent, generated);
782
+ : options.agent === 'dsh'
783
+ ? mergeDshPatchConfig(existingContent, generated)
784
+ : mergeJsonMcpConfig(existingContent, generated);
729
785
 
730
786
  await mkdir(path.dirname(configPath), { recursive: true });
731
787
  await writeFile(configPath, content, 'utf-8');
@@ -1603,7 +1659,7 @@ export default defineCommand({
1603
1659
  args: {
1604
1660
  agent: {
1605
1661
  type: 'string',
1606
- description: 'Target agent (claude, codex, codebuddy, opencode, cursor, windsurf, copilot, gemini-cli, antigravity, openclaw, hermes, omp, kiro, trae, pi, all)',
1662
+ description: 'Target agent (claude, codex, codebuddy, opencode, cursor, windsurf, copilot, gemini-cli, antigravity, openclaw, hermes, omp, kiro, trae, dsh, pi, all)',
1607
1663
  required: false,
1608
1664
  },
1609
1665
  mcp: {
@@ -1677,6 +1733,7 @@ export default defineCommand({
1677
1733
  { value: 'hermes', label: 'Hermes Agent', hint: 'plugin + hooks + commands + skills + MCP' },
1678
1734
  { value: 'omp', label: 'Oh-my-Pi', hint: 'package + extension + command + MCP' },
1679
1735
  { value: 'pi', label: 'Pi coding agent', hint: 'package + extension + skill' },
1736
+ { value: 'dsh', label: 'DeepSeek Harness', hint: 'MCP patch + AGENTS.md + skills' },
1680
1737
  { value: 'opencode', label: 'OpenCode', hint: 'local plugin + AGENTS.md + MCP' },
1681
1738
  { value: 'windsurf', label: 'Windsurf', hint: 'rules + hooks + MCP' },
1682
1739
  { value: 'all', label: 'All supported agents', hint: 'install detected-compatible files' },
@@ -56,6 +56,15 @@ function checkTOML(p: string): boolean {
56
56
  } catch { return false; }
57
57
  }
58
58
 
59
+ function checkDshPatch(p: string): boolean {
60
+ if (!exists(p)) return false;
61
+ try {
62
+ const content = fs.readFileSync(p, 'utf-8');
63
+ if (!content.includes('memory-memorix')) return false;
64
+ return /serverName:\s*['"]?memorix['"]?/.test(content);
65
+ } catch { return false; }
66
+ }
67
+
59
68
  /** Detects memorix MCP config entries in both global and project-level paths. */
60
69
  function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
61
70
  const entries: MCPConfigEntry[] = [];
@@ -216,6 +225,17 @@ function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
216
225
  note: 'Remove the "memorix" key from mcpServers in this file.',
217
226
  });
218
227
 
228
+ // DeepSeek Harness
229
+ const dshPatchPath = path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'cordis.patch.yml');
230
+ entries.push({
231
+ agent: 'DeepSeek Harness',
232
+ path: dshPatchPath,
233
+ kind: 'global',
234
+ format: 'json',
235
+ detected: checkDshPatch(dshPatchPath),
236
+ note: 'Remove the memory-memorix insert row from this patch file.',
237
+ });
238
+
219
239
  return entries;
220
240
  }
221
241