memorix 1.4.4 → 1.4.5
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.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -2
- package/README.zh-CN.md +9 -2
- package/dist/cli/index.js +348 -127
- package/dist/index.js +35 -5
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/sdk.js +34 -4
- package/dist/types.d.ts +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -2
- package/docs/INTEGRATIONS.md +9 -2
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -0
- package/docs/MEMCODE.md +1 -1
- package/docs/SETUP.md +9 -1
- package/llms-full.txt +5 -4
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/agent-integrations.ts +7 -1
- package/src/cli/commands/hooks-install.ts +2 -0
- package/src/cli/commands/hooks-preview.ts +5 -0
- package/src/cli/commands/integrate.ts +2 -0
- package/src/cli/commands/setup.ts +57 -2
- package/src/cli/commands/uninstall.ts +20 -0
- package/src/hooks/installers/index.ts +43 -2
- package/src/hooks/types.ts +2 -0
- package/src/integrations/registry.ts +9 -0
- package/src/types.ts +1 -1
- package/src/workspace/mcp-adapters/dsh.ts +147 -0
|
@@ -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
|
}
|
|
@@ -699,6 +702,55 @@ function mergeYamlMcpConfig(existingContent: string | null, generatedContent: st
|
|
|
699
702
|
return stringifyYaml(merged, { lineWidth: 0 });
|
|
700
703
|
}
|
|
701
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Merge the generated DeepSeek Harness patch rows into an existing
|
|
707
|
+
* `cordis.patch.yml` without clobbering unrelated user patches.
|
|
708
|
+
*
|
|
709
|
+
* DSH patch files are lists of Cordis patch operations. Memorix owns exactly
|
|
710
|
+
* the `@deepseek-ai/dsh-mcp-client` row whose config.serverName is `memorix`;
|
|
711
|
+
* every other operation is user-owned and must survive the merge. When the
|
|
712
|
+
* existing file is not a parseable list, the generated rows are appended to
|
|
713
|
+
* the raw content instead of overwriting it.
|
|
714
|
+
*/
|
|
715
|
+
function mergeDshPatchConfig(existingContent: string | null, generatedContent: string): string {
|
|
716
|
+
const generatedTrimmed = generatedContent.trimEnd();
|
|
717
|
+
if (!existingContent?.trim()) {
|
|
718
|
+
return generatedTrimmed.endsWith('\n') ? generatedTrimmed : `${generatedTrimmed}\n`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const isMemorixInsertRow = (row: unknown): boolean => {
|
|
722
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return false;
|
|
723
|
+
const record = row as Record<string, unknown>;
|
|
724
|
+
if (record.name !== '@deepseek-ai/dsh-mcp-client') return false;
|
|
725
|
+
const config = record.config;
|
|
726
|
+
return Boolean(
|
|
727
|
+
config && typeof config === 'object' && !Array.isArray(config)
|
|
728
|
+
&& (config as Record<string, unknown>).serverName === 'memorix',
|
|
729
|
+
);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const opContainsMemorixRow = (op: unknown): boolean => {
|
|
733
|
+
if (!op || typeof op !== 'object' || Array.isArray(op)) return false;
|
|
734
|
+
const insert = (op as Record<string, unknown>).insert;
|
|
735
|
+
return Array.isArray(insert) && insert.some(isMemorixInsertRow);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
let existingDocument: unknown;
|
|
739
|
+
try {
|
|
740
|
+
existingDocument = parseYaml(existingContent);
|
|
741
|
+
} catch {
|
|
742
|
+
return `${existingContent.trimEnd()}\n\n${generatedTrimmed}\n`;
|
|
743
|
+
}
|
|
744
|
+
if (!Array.isArray(existingDocument)) {
|
|
745
|
+
// A scalar or mapping document is not a patch list — never rewrite it.
|
|
746
|
+
return `${existingContent.trimEnd()}\n\n${generatedTrimmed}\n`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const kept = existingDocument.filter((op) => !opContainsMemorixRow(op));
|
|
750
|
+
const merged = [...kept, ...(parseYaml(generatedContent) as unknown[])];
|
|
751
|
+
return stringifyYaml(merged, { lineWidth: 0 }) + '\n';
|
|
752
|
+
}
|
|
753
|
+
|
|
702
754
|
export async function installMcpConfig(options: {
|
|
703
755
|
agent: McpConfigAgent;
|
|
704
756
|
projectRoot?: string;
|
|
@@ -725,7 +777,9 @@ export async function installMcpConfig(options: {
|
|
|
725
777
|
? mergeTomlMcpConfig(existingContent, generated)
|
|
726
778
|
: options.agent === 'hermes'
|
|
727
779
|
? mergeYamlMcpConfig(existingContent, generated)
|
|
728
|
-
|
|
780
|
+
: options.agent === 'dsh'
|
|
781
|
+
? mergeDshPatchConfig(existingContent, generated)
|
|
782
|
+
: mergeJsonMcpConfig(existingContent, generated);
|
|
729
783
|
|
|
730
784
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
731
785
|
await writeFile(configPath, content, 'utf-8');
|
|
@@ -1603,7 +1657,7 @@ export default defineCommand({
|
|
|
1603
1657
|
args: {
|
|
1604
1658
|
agent: {
|
|
1605
1659
|
type: 'string',
|
|
1606
|
-
description: 'Target agent (claude, codex, codebuddy, opencode, cursor, windsurf, copilot, gemini-cli, antigravity, openclaw, hermes, omp, kiro, trae, pi, all)',
|
|
1660
|
+
description: 'Target agent (claude, codex, codebuddy, opencode, cursor, windsurf, copilot, gemini-cli, antigravity, openclaw, hermes, omp, kiro, trae, dsh, pi, all)',
|
|
1607
1661
|
required: false,
|
|
1608
1662
|
},
|
|
1609
1663
|
mcp: {
|
|
@@ -1677,6 +1731,7 @@ export default defineCommand({
|
|
|
1677
1731
|
{ value: 'hermes', label: 'Hermes Agent', hint: 'plugin + hooks + commands + skills + MCP' },
|
|
1678
1732
|
{ value: 'omp', label: 'Oh-my-Pi', hint: 'package + extension + command + MCP' },
|
|
1679
1733
|
{ value: 'pi', label: 'Pi coding agent', hint: 'package + extension + skill' },
|
|
1734
|
+
{ value: 'dsh', label: 'DeepSeek Harness', hint: 'MCP patch + AGENTS.md + skills' },
|
|
1680
1735
|
{ value: 'opencode', label: 'OpenCode', hint: 'local plugin + AGENTS.md + MCP' },
|
|
1681
1736
|
{ value: 'windsurf', label: 'Windsurf', hint: 'rules + hooks + MCP' },
|
|
1682
1737
|
{ 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
|
|
|
@@ -360,6 +360,9 @@ const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: st
|
|
|
360
360
|
kiro: { project: path.join('.kiro', 'skills'), global: path.join('.kiro', 'skills') },
|
|
361
361
|
opencode: { project: path.join('.opencode', 'skills'), global: path.join('.config', 'opencode', 'skills') },
|
|
362
362
|
trae: { project: path.join('.trae', 'skills'), global: path.join('.trae', 'skills') },
|
|
363
|
+
// DSH's user skill root is <harness home>/skills; the global root below is
|
|
364
|
+
// resolved to $DSH_HOME (or ~/.dsh) for dsh by installOfficialSkillsForAgent.
|
|
365
|
+
dsh: { project: path.join('.dsh', 'skills'), global: 'skills' },
|
|
363
366
|
};
|
|
364
367
|
|
|
365
368
|
const PACKAGE_OWNED_HOOK_AGENTS = new Set<AgentName>(['openclaw', 'hermes', 'omp']);
|
|
@@ -384,7 +387,11 @@ async function installOfficialSkillsForAgent(
|
|
|
384
387
|
const relativeDir = global ? dirs?.global : dirs?.project;
|
|
385
388
|
if (!relativeDir) return [];
|
|
386
389
|
|
|
387
|
-
const root = global
|
|
390
|
+
const root = global
|
|
391
|
+
? (agent === 'dsh'
|
|
392
|
+
? (process.env.DSH_HOME?.trim() || path.join(os.homedir(), '.dsh'))
|
|
393
|
+
: os.homedir())
|
|
394
|
+
: projectRoot;
|
|
388
395
|
const skillPaths: string[] = [];
|
|
389
396
|
for (const skillEntry of OFFICIAL_MEMORIX_SKILLS) {
|
|
390
397
|
const skillPath = path.join(root, relativeDir, skillEntry.name, 'SKILL.md');
|
|
@@ -626,6 +633,9 @@ export function getProjectConfigPath(agent: AgentName, projectRoot: string): str
|
|
|
626
633
|
case 'trae':
|
|
627
634
|
// Trae has no hooks system — only rules (.trae/rules/project_rules.md)
|
|
628
635
|
return path.join(projectRoot, '.trae', 'rules', 'project_rules.md');
|
|
636
|
+
case 'dsh':
|
|
637
|
+
// DeepSeek Harness has no hooks system — only guidance (AGENTS.md)
|
|
638
|
+
return path.join(projectRoot, 'AGENTS.md');
|
|
629
639
|
case 'opencode':
|
|
630
640
|
// OpenCode uses plugin files for hooks
|
|
631
641
|
return path.join(projectRoot, '.opencode', 'plugins', 'memorix.js');
|
|
@@ -684,6 +694,9 @@ export function getGlobalConfigPath(agent: AgentName): string {
|
|
|
684
694
|
return path.join(home, '.omp', 'agent', 'packages', 'memorix', 'extensions', 'memorix.js');
|
|
685
695
|
case 'trae':
|
|
686
696
|
return path.join(home, '.trae', 'rules', 'project_rules.md');
|
|
697
|
+
case 'dsh':
|
|
698
|
+
// DSH reads the user-global AGENTS.md from its harness home.
|
|
699
|
+
return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
|
|
687
700
|
default:
|
|
688
701
|
return path.join(home, '.memorix', 'hooks.json');
|
|
689
702
|
}
|
|
@@ -709,6 +722,9 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
709
722
|
return path.join(root, '.gemini', 'GEMINI.md');
|
|
710
723
|
case 'trae':
|
|
711
724
|
return path.join(root, '.trae', 'rules', 'project_rules.md');
|
|
725
|
+
case 'dsh':
|
|
726
|
+
// The user-global AGENTS.md lives in the harness home DSH reads.
|
|
727
|
+
return path.join(process.env.DSH_HOME?.trim() || path.join(root, '.dsh'), 'AGENTS.md');
|
|
712
728
|
default:
|
|
713
729
|
return path.join(root, '.agent', 'rules', 'memorix.md');
|
|
714
730
|
}
|
|
@@ -734,6 +750,8 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
734
750
|
return path.join(root, 'GEMINI.md');
|
|
735
751
|
case 'trae':
|
|
736
752
|
return path.join(root, '.trae', 'rules', 'project_rules.md');
|
|
753
|
+
case 'dsh':
|
|
754
|
+
return path.join(root, 'AGENTS.md');
|
|
737
755
|
default:
|
|
738
756
|
return path.join(root, '.agent', 'rules', 'memorix.md');
|
|
739
757
|
}
|
|
@@ -842,6 +860,13 @@ export async function detectInstalledAgents(): Promise<AgentName[]> {
|
|
|
842
860
|
agents.push('trae');
|
|
843
861
|
} catch { /* not installed */ }
|
|
844
862
|
|
|
863
|
+
// Check for DeepSeek Harness (its home dir or an explicit DSH_HOME)
|
|
864
|
+
const dshDir = process.env.DSH_HOME?.trim() || path.join(home, '.dsh');
|
|
865
|
+
try {
|
|
866
|
+
await fs.access(dshDir);
|
|
867
|
+
agents.push('dsh');
|
|
868
|
+
} catch { /* not installed */ }
|
|
869
|
+
|
|
845
870
|
return agents;
|
|
846
871
|
}
|
|
847
872
|
|
|
@@ -947,6 +972,22 @@ export async function installHooks(
|
|
|
947
972
|
generated: { note: 'Trae has no hooks system, only rules (.trae/rules/project_rules.md) installed' },
|
|
948
973
|
};
|
|
949
974
|
}
|
|
975
|
+
case 'dsh':
|
|
976
|
+
// DeepSeek Harness has no hook system — install AGENTS.md guidance and
|
|
977
|
+
// skills; the MCP row is installed by `memorix setup --agent dsh`.
|
|
978
|
+
{
|
|
979
|
+
const rulesPath = await installAgentRules(agent, projectRoot, global);
|
|
980
|
+
const skillPaths = await installOfficialSkillsForAgent(agent, projectRoot, global);
|
|
981
|
+
return {
|
|
982
|
+
agent,
|
|
983
|
+
configPath: rulesPath,
|
|
984
|
+
events: [],
|
|
985
|
+
generated: {
|
|
986
|
+
note: 'DeepSeek Harness has no hook system — installed AGENTS.md guidance and skills; the MCP row is installed by `memorix setup --agent dsh`.',
|
|
987
|
+
...(skillPaths.length > 0 ? { skillPaths, skillPath: skillPaths[0] } : {}),
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
950
991
|
case 'opencode': {
|
|
951
992
|
// OpenCode uses JS plugin files for hooks
|
|
952
993
|
const pluginContent = generateOpenCodePlugin();
|
|
@@ -1127,7 +1168,7 @@ async function installAgentRules(agent: AgentName, projectRoot: string, global =
|
|
|
1127
1168
|
try {
|
|
1128
1169
|
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
1129
1170
|
|
|
1130
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli') {
|
|
1171
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli' || agent === 'dsh') {
|
|
1131
1172
|
// For shared context files (CLAUDE.md / AGENTS.md / GEMINI.md), append rather than overwrite.
|
|
1132
1173
|
try {
|
|
1133
1174
|
const existing = await fs.readFile(rulesPath, 'utf-8');
|
package/src/hooks/types.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type AgentName =
|
|
|
41
41
|
| 'hermes'
|
|
42
42
|
| 'opencode'
|
|
43
43
|
| 'trae'
|
|
44
|
+
| 'dsh'
|
|
44
45
|
| 'pi'
|
|
45
46
|
| 'omp';
|
|
46
47
|
|
|
@@ -61,6 +62,7 @@ export const AGENT_SUPPORT_TIER: Record<AgentName, SupportTier> = {
|
|
|
61
62
|
hermes: 'community',
|
|
62
63
|
opencode: 'community',
|
|
63
64
|
trae: 'community',
|
|
65
|
+
dsh: 'community',
|
|
64
66
|
pi: 'community',
|
|
65
67
|
omp: 'community',
|
|
66
68
|
antigravity: 'community',
|
|
@@ -150,6 +150,15 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
|
|
|
150
150
|
install: 'memorix setup --agent trae',
|
|
151
151
|
surfaces: ['MCP', 'rules'],
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
agent: 'dsh',
|
|
155
|
+
name: 'DeepSeek Harness',
|
|
156
|
+
entry: 'official-config',
|
|
157
|
+
status: 'ready',
|
|
158
|
+
install: 'memorix setup --agent dsh',
|
|
159
|
+
surfaces: ['MCP', 'AGENTS.md', 'skills'],
|
|
160
|
+
note: 'DeepSeek Harness support writes a @deepseek-ai/dsh-mcp-client row into $DSH_HOME/cordis.patch.yml (default ~/.dsh/cordis.patch.yml), guidance into the harness AGENTS.md, and skills under $DSH_HOME/skills.',
|
|
161
|
+
},
|
|
153
162
|
{
|
|
154
163
|
agent: 'memcode',
|
|
155
164
|
name: 'memcode',
|
package/src/types.ts
CHANGED
|
@@ -654,7 +654,7 @@ export interface MemoryRef {
|
|
|
654
654
|
/** MCP config format adapter interface */
|
|
655
655
|
export interface MCPConfigAdapter {
|
|
656
656
|
/** Setup-only adapters may support a host that does not participate in workspace sync. */
|
|
657
|
-
readonly source: AgentTarget | 'codebuddy';
|
|
657
|
+
readonly source: AgentTarget | 'codebuddy' | 'dsh';
|
|
658
658
|
/** Parse MCP server entries from a config file */
|
|
659
659
|
parse(content: string): MCPServerEntry[];
|
|
660
660
|
/** Generate config file content from MCP server entries */
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import type { MCPConfigAdapter, MCPServerEntry } from '../../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DeepSeek Harness (DSH) MCP Configuration Adapter.
|
|
8
|
+
*
|
|
9
|
+
* DSH composes its runtime from Cordis patch layers. External MCP servers are
|
|
10
|
+
* registered as rows of the `@deepseek-ai/dsh-mcp-client` plugin inside a
|
|
11
|
+
* patch list. The user-level layer that applies to every profile lives at:
|
|
12
|
+
*
|
|
13
|
+
* $DSH_HOME/cordis.patch.yml (default: ~/.dsh/cordis.patch.yml)
|
|
14
|
+
*
|
|
15
|
+
* Row shape follows DSH's own shipped Memorix reference
|
|
16
|
+
* (examples/mcp-memory/memorix.cordis.yml in deepseek-ai/deepseek-harness):
|
|
17
|
+
*
|
|
18
|
+
* - insert:
|
|
19
|
+
* - id: memory-memorix
|
|
20
|
+
* name: '@deepseek-ai/dsh-mcp-client'
|
|
21
|
+
* config:
|
|
22
|
+
* serverName: memorix
|
|
23
|
+
* transport: stdio
|
|
24
|
+
* command: memorix
|
|
25
|
+
* args: [serve]
|
|
26
|
+
*
|
|
27
|
+
* The `cwd` key is deliberately omitted so the MCP child inherits the DSH
|
|
28
|
+
* process working directory — the workspace DSH was launched from — which is
|
|
29
|
+
* what keeps Memorix bound to the current Git project.
|
|
30
|
+
*
|
|
31
|
+
* Source: https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/mcp/mcp-client
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const MCP_CLIENT_PLUGIN = '@deepseek-ai/dsh-mcp-client';
|
|
35
|
+
const MEMORIX_ROW_ID = 'memory-memorix';
|
|
36
|
+
|
|
37
|
+
function resolveDshHome(): string {
|
|
38
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
39
|
+
if (configured) return configured;
|
|
40
|
+
return join(homedir(), '.dsh');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isMemorixRow(row: unknown): row is Record<string, unknown> {
|
|
44
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return false;
|
|
45
|
+
const record = row as Record<string, unknown>;
|
|
46
|
+
if (record.name !== MCP_CLIENT_PLUGIN) return false;
|
|
47
|
+
const config = record.config;
|
|
48
|
+
return Boolean(
|
|
49
|
+
config && typeof config === 'object' && !Array.isArray(config)
|
|
50
|
+
&& (config as Record<string, unknown>).serverName === 'memorix',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rowToEntry(row: Record<string, unknown>): MCPServerEntry | null {
|
|
55
|
+
const config = row.config as Record<string, unknown>;
|
|
56
|
+
if (!config || typeof config !== 'object') return null;
|
|
57
|
+
const entry: MCPServerEntry = {
|
|
58
|
+
name: 'memorix',
|
|
59
|
+
command: typeof config.command === 'string' ? config.command : '',
|
|
60
|
+
args: Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === 'string') : [],
|
|
61
|
+
};
|
|
62
|
+
if (typeof config.url === 'string' && config.url) entry.url = config.url;
|
|
63
|
+
if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) {
|
|
64
|
+
const env: Record<string, string> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(config.env as Record<string, unknown>)) {
|
|
66
|
+
if (typeof value === 'string') env[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(env).length > 0) entry.env = env;
|
|
69
|
+
}
|
|
70
|
+
if (config.disabled === true) entry.disabled = true;
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Flatten one parsed patch document into its row list, tolerating both
|
|
75
|
+
* `- insert: [...]` operations and bare row lists. */
|
|
76
|
+
function patchRows(document: unknown): Record<string, unknown>[] {
|
|
77
|
+
const rows: Record<string, unknown>[] = [];
|
|
78
|
+
if (!Array.isArray(document)) return rows;
|
|
79
|
+
for (const item of document) {
|
|
80
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
81
|
+
const record = item as Record<string, unknown>;
|
|
82
|
+
if (Array.isArray(record.insert)) {
|
|
83
|
+
for (const row of record.insert) {
|
|
84
|
+
if (row && typeof row === 'object' && !Array.isArray(row)) {
|
|
85
|
+
rows.push(row as Record<string, unknown>);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
rows.push(record);
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class DshMCPAdapter implements MCPConfigAdapter {
|
|
96
|
+
readonly source = 'dsh' as const;
|
|
97
|
+
|
|
98
|
+
parse(content: string): MCPServerEntry[] {
|
|
99
|
+
if (!content.trim()) return [];
|
|
100
|
+
let document: unknown;
|
|
101
|
+
try {
|
|
102
|
+
document = parseYaml(content);
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return patchRows(document)
|
|
107
|
+
.filter(isMemorixRow)
|
|
108
|
+
.map(rowToEntry)
|
|
109
|
+
.filter((entry): entry is MCPServerEntry => entry !== null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
generate(servers: MCPServerEntry[]): string {
|
|
113
|
+
const rows = servers
|
|
114
|
+
.filter((server) => server.name === 'memorix')
|
|
115
|
+
.map((server) => {
|
|
116
|
+
const config: Record<string, unknown> = {
|
|
117
|
+
serverName: server.name,
|
|
118
|
+
};
|
|
119
|
+
if (server.url) {
|
|
120
|
+
config.transport = 'streamable-http';
|
|
121
|
+
config.url = server.url;
|
|
122
|
+
if (server.headers && Object.keys(server.headers).length > 0) config.headers = server.headers;
|
|
123
|
+
} else {
|
|
124
|
+
config.transport = 'stdio';
|
|
125
|
+
config.command = server.command;
|
|
126
|
+
if (server.args && server.args.length > 0) config.args = server.args;
|
|
127
|
+
if (server.env && Object.keys(server.env).length > 0) config.env = server.env;
|
|
128
|
+
}
|
|
129
|
+
const row: Record<string, unknown> = {
|
|
130
|
+
id: MEMORIX_ROW_ID,
|
|
131
|
+
name: MCP_CLIENT_PLUGIN,
|
|
132
|
+
config,
|
|
133
|
+
};
|
|
134
|
+
if (server.disabled === true) row.disabled = true;
|
|
135
|
+
return row;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (rows.length === 0) return '';
|
|
139
|
+
return stringifyYaml([{ insert: rows }], { lineWidth: 0 }) + '\n';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getConfigPath(_projectRoot?: string): string {
|
|
143
|
+
return join(resolveDshHome(), 'cordis.patch.yml');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { MEMORIX_ROW_ID };
|