memorix 1.8.5 → 1.8.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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -2
  3. package/assets/star-history-dark.svg +5 -5
  4. package/assets/star-history-light.svg +5 -5
  5. package/assets/star-history.json +35 -5
  6. package/dist/cli/index.js +309 -131
  7. package/dist/index.js +75 -11
  8. package/dist/memcode-runtime/CHANGELOG.md +18 -0
  9. package/dist/sdk.js +74 -10
  10. package/dist/types.d.ts +1 -1
  11. package/docs/API_REFERENCE.md +1 -1
  12. package/docs/DEVELOPMENT.md +2 -2
  13. package/docs/INTEGRATIONS.md +1 -0
  14. package/docs/MODULES.md +1 -1
  15. package/docs/README.md +3 -3
  16. package/docs/SETUP.md +1 -0
  17. package/package.json +1 -1
  18. package/plugins/antigravity/memorix/plugin.json +1 -1
  19. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  20. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  21. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  22. package/plugins/copilot/memorix/plugin.json +1 -1
  23. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  24. package/plugins/hermes/memorix/plugin.yaml +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +2 -2
  29. package/src/cli/commands/agent-integrations.ts +1 -0
  30. package/src/cli/commands/hooks-install.ts +9 -2
  31. package/src/cli/commands/hooks-preview.ts +5 -0
  32. package/src/cli/commands/integrate.ts +2 -0
  33. package/src/cli/commands/setup.ts +11 -1
  34. package/src/cli/commands/uninstall.ts +24 -2
  35. package/src/codegraph/task-lens.ts +72 -7
  36. package/src/hooks/installers/index.ts +55 -0
  37. package/src/hooks/types.ts +2 -0
  38. package/src/integrations/registry.ts +9 -0
  39. package/src/types.ts +1 -1
  40. package/src/workspace/mcp-adapters/workbuddy.ts +71 -0
@@ -153,6 +153,7 @@ function estimateFileSize(agent: string, configPath: string): string {
153
153
  opencode: '~3KB',
154
154
  trae: '~1KB',
155
155
  dsh: '~1KB',
156
+ workbuddy: '~1KB',
156
157
  };
157
158
  return sizes[agent] || '~2KB';
158
159
  }
@@ -174,6 +175,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
174
175
  return path.join(home, '.trae', 'rules', 'project_rules.md');
175
176
  case 'dsh':
176
177
  return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
178
+ case 'workbuddy':
179
+ return '';
177
180
  default:
178
181
  return '';
179
182
  }
@@ -191,6 +194,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
191
194
  return path.join(cwd, '.trae', 'rules', 'project_rules.md');
192
195
  case 'dsh':
193
196
  return path.join(cwd, 'AGENTS.md');
197
+ case 'workbuddy':
198
+ return path.join(cwd, 'AGENTS.md');
194
199
  default:
195
200
  return '';
196
201
  }
@@ -115,6 +115,7 @@ function getAgentLabel(agent: string): string {
115
115
  'gemini-cli': 'Gemini CLI',
116
116
  trae: 'Trae',
117
117
  dsh: 'DeepSeek Harness',
118
+ workbuddy: 'WorkBuddy',
118
119
  };
119
120
  return labels[agent] || agent;
120
121
  }
@@ -132,6 +133,7 @@ function getAgentHint(agent: string): string {
132
133
  'gemini-cli': 'settings + GEMINI.md',
133
134
  trae: 'rules',
134
135
  dsh: 'MCP patch + AGENTS.md + skills',
136
+ workbuddy: 'MCP + AGENTS.md',
135
137
  };
136
138
  return hints[agent] || 'integration files';
137
139
  }
@@ -31,6 +31,7 @@ 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
33
  import { DshMCPAdapter } from '../../workspace/mcp-adapters/dsh.js';
34
+ import { WorkbuddyMCPAdapter } from '../../workspace/mcp-adapters/workbuddy.js';
34
35
  import { CodeBuddyMCPAdapter } from '../../workspace/mcp-adapters/codebuddy.js';
35
36
  import { getSetupIntegrationRows as readSetupIntegrationRows } from '../../integrations/registry.js';
36
37
  import { OFFICIAL_MEMORIX_SKILLS } from '../../hooks/official-skills.js';
@@ -192,6 +193,7 @@ const SUPPORTED_SETUP_AGENTS: AgentName[] = [
192
193
  'kiro',
193
194
  'trae',
194
195
  'dsh',
196
+ 'workbuddy',
195
197
  'pi',
196
198
  ];
197
199
 
@@ -405,6 +407,7 @@ export function getMcpAdapter(agent: McpConfigAgent): MCPConfigAdapter {
405
407
  opencode: new OpenCodeMCPAdapter(),
406
408
  trae: new TraeMCPAdapter(),
407
409
  dsh: new DshMCPAdapter(),
410
+ workbuddy: new WorkbuddyMCPAdapter(),
408
411
  };
409
412
  return adapters[agent];
410
413
  }
@@ -1611,7 +1614,14 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1611
1614
  }
1612
1615
  } else if (plan.actions.includes('hooks')) {
1613
1616
  const result = await installHooks(agent, targetRoot, global);
1614
- p.log.success(`${agent}: integration files -> ${result.configPath}`);
1617
+ const note = (result.generated as Record<string, unknown>)?.note;
1618
+ if (result.configPath) {
1619
+ p.log.success(`${agent}: integration files -> ${result.configPath}`);
1620
+ } else if (note) {
1621
+ p.log.info(`${agent}: ${note}`);
1622
+ } else {
1623
+ p.log.info(`${agent}: no hooks or guidance are available at this scope`);
1624
+ }
1615
1625
  } else if (plan.actions.includes('project-guidance')) {
1616
1626
  const rulesPath = await installAgentGuidance(agent, targetRoot, global);
1617
1627
  p.log.success(`${agent}: guidance -> ${rulesPath}`);
@@ -24,7 +24,7 @@ import * as os from 'node:os';
24
24
 
25
25
  // ── MCP config paths (documented, not modified) ────────────────────
26
26
 
27
- interface MCPConfigEntry {
27
+ export interface MCPConfigEntry {
28
28
  agent: string;
29
29
  path: string;
30
30
  kind: 'project' | 'global';
@@ -66,7 +66,7 @@ function checkDshPatch(p: string): boolean {
66
66
  }
67
67
 
68
68
  /** Detects memorix MCP config entries in both global and project-level paths. */
69
- function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
69
+ export function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
70
70
  const entries: MCPConfigEntry[] = [];
71
71
 
72
72
  // ── Global paths ──
@@ -236,6 +236,28 @@ function getMCPConfigEntries(home: string, cwd: string): MCPConfigEntry[] {
236
236
  note: 'Remove the memory-memorix insert row from this patch file.',
237
237
  });
238
238
 
239
+ // WorkBuddy — global
240
+ const workbuddyCfg = path.join(home, '.workbuddy', 'mcp.json');
241
+ entries.push({
242
+ agent: 'WorkBuddy',
243
+ path: workbuddyCfg,
244
+ kind: 'global',
245
+ format: 'json',
246
+ detected: checkJSON(workbuddyCfg),
247
+ note: 'Remove the "memorix" key from mcpServers in this file.',
248
+ });
249
+
250
+ // WorkBuddy — project level (project-only installs must be discoverable too)
251
+ const workbuddyProjectCfg = path.join(cwd, '.workbuddy', 'mcp.json');
252
+ entries.push({
253
+ agent: 'WorkBuddy',
254
+ path: workbuddyProjectCfg,
255
+ kind: 'project',
256
+ format: 'json',
257
+ detected: checkJSON(workbuddyProjectCfg),
258
+ note: 'Remove the "memorix" key from mcpServers in this file.',
259
+ });
260
+
239
261
  return entries;
240
262
  }
241
263
 
@@ -143,19 +143,73 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
143
143
  // Continuation is a delivery intent, not a task lens. A request can both resume
144
144
  // prior work and be a bugfix, feature, or release task, so callers keep the
145
145
  // normal lens and add a bounded prior-work projection separately.
146
- const CONTINUATION_KEYWORDS = [
146
+ //
147
+ // Matching triggers buildHookProjectContext() and a full prior-work brief,
148
+ // which measurably costs 2-3x an ordinary prompt (~3s → ~7.5s locally, 15s+
149
+ // under load) — enough to blow the 10s UserPromptSubmit hook timeout, after
150
+ // which Claude Code discards the injection entirely.
151
+ //
152
+ // The split below is about *evidence of intent*, not about the words alone:
153
+ //
154
+ // EXPLICIT — handoff vocabulary is deliberate on its own. "交接" and
155
+ // "handoff" are not said casually, so a bare mention counts.
156
+ // AMBIGUOUS — "继续" / "continue" / "恢复" are everyday verbs. Bare, they
157
+ // are filler meaning "go on" (and "恢复" usually means "restore
158
+ // the service" in ops work). Carrying a real task they are a
159
+ // genuine resume request — `memorix resume "继续处理发布阻塞问题"`
160
+ // is documented usage — so they require substantive content.
161
+ const EXPLICIT_HANDOFF_KEYWORDS = [
162
+ 'handoff',
163
+ 'hand off',
164
+ 'hand over',
165
+ 'previous session',
166
+ 'last session',
167
+ 'pick up where',
168
+ '交接', // 覆盖「任务交接」「工作交接」
169
+ '接手',
170
+ '上次会话',
171
+ '上次的会话',
172
+ ];
173
+
174
+ const AMBIGUOUS_CONTINUATION_KEYWORDS = [
147
175
  'continue',
148
176
  'resume',
149
- 'pick up',
150
177
  'carry on',
151
- 'previous session',
178
+ 'pick up',
152
179
  '继续',
153
- '接手',
154
180
  '恢复',
155
181
  '延续',
156
- '上次会话',
157
182
  ];
158
183
 
184
+ /**
185
+ * Characters that carry no task meaning on their own: punctuation, whitespace,
186
+ * Chinese modal particles, and the generic scaffolding words users wrap around
187
+ * a bare "继续" ("任务继续", "继续一下").
188
+ */
189
+ const TASK_FILLER_WORDS = /\b(?:please|task|work|the|a|an)\b/giu;
190
+ const TASK_FILLER_PHRASES = /(?:吧|了|呢|啊|吗|嘛|呀|哦|噢|一下|一起|接着|请|帮我)/gu;
191
+
192
+ /** Minimum residual code points that count as a real task description. */
193
+ const MIN_TASK_SUBSTANCE = 2;
194
+
195
+ function hasSubstantiveTask(text: string, keyword: string): boolean {
196
+ const withoutKeyword = text.replace(
197
+ new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
198
+ ' ',
199
+ );
200
+ const residual = withoutKeyword
201
+ // English fillers are removed as whole words, so `a` in `API` and `work`
202
+ // in `workflow` remain meaningful task text.
203
+ .replace(TASK_FILLER_WORDS, ' ')
204
+ .replace(TASK_FILLER_PHRASES, ' ')
205
+ .replace(/[\s\p{P}]/gu, '');
206
+
207
+ // These are scaffolding-only phrases, not tasks. Keep `工作流` intact by
208
+ // checking the complete residual instead of removing `工作` as a prefix.
209
+ if (/^(?:任务|工作|一下|一起|接着)$/u.test(residual)) return false;
210
+ return Array.from(residual).length >= MIN_TASK_SUBSTANCE;
211
+ }
212
+
159
213
  const LENS_PRIORITY: Exclude<TaskLensId, 'general'>[] = [
160
214
  'bugfix',
161
215
  'release',
@@ -250,11 +304,22 @@ export function resolveTaskLens(task?: string): TaskLens {
250
304
  * Detect when the caller is asking to continue existing work. This stays
251
305
  * separate from task-lens routing so "continue fixing the timeout" remains a
252
306
  * bugfix, while still receiving a compact prior-work brief.
307
+ *
308
+ * Explicit handoff vocabulary counts on its own. Everyday continuation verbs
309
+ * additionally require a substantive task, so a bare "继续" / "continue" — the
310
+ * single most common thing a user types to mean "go on" — stays on the cheap
311
+ * path instead of paying for a full brief on every turn.
253
312
  */
254
313
  export function isContinuationTask(task?: string): boolean {
255
314
  const normalized = (task ?? '').trim().toLowerCase();
256
- return normalized.length > 0 && CONTINUATION_KEYWORDS.some((keyword) =>
257
- containsTaskKeyword(normalized, keyword),
315
+ if (normalized.length === 0) return false;
316
+
317
+ if (EXPLICIT_HANDOFF_KEYWORDS.some((keyword) => containsTaskKeyword(normalized, keyword))) {
318
+ return true;
319
+ }
320
+
321
+ return AMBIGUOUS_CONTINUATION_KEYWORDS.some((keyword) =>
322
+ containsTaskKeyword(normalized, keyword) && hasSubstantiveTask(normalized, keyword),
258
323
  );
259
324
  }
260
325
 
@@ -652,6 +652,12 @@ export function getProjectConfigPath(agent: AgentName, projectRoot: string): str
652
652
  return path.join(projectRoot, '.agents', 'hooks.json');
653
653
  case 'gemini-cli':
654
654
  return path.join(projectRoot, '.gemini', 'settings.json');
655
+ case 'workbuddy':
656
+ // WorkBuddy has no hook system at all — only MCP config and project
657
+ // AGENTS.md guidance. Return an empty path so install, detection and
658
+ // uninstall all treat hooks as a no-op instead of falling through to the
659
+ // `.memorix/hooks.json` default (which would report a path that never exists).
660
+ return '';
655
661
  default:
656
662
  return path.join(projectRoot, '.memorix', 'hooks.json');
657
663
  }
@@ -697,6 +703,9 @@ export function getGlobalConfigPath(agent: AgentName): string {
697
703
  case 'dsh':
698
704
  // DSH reads the user-global AGENTS.md from its harness home.
699
705
  return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
706
+ case 'workbuddy':
707
+ // WorkBuddy has no user-global instructions file — guidance is project-only.
708
+ return '';
700
709
  default:
701
710
  return path.join(home, '.memorix', 'hooks.json');
702
711
  }
@@ -725,6 +734,9 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
725
734
  case 'dsh':
726
735
  // The user-global AGENTS.md lives in the harness home DSH reads.
727
736
  return path.join(process.env.DSH_HOME?.trim() || path.join(root, '.dsh'), 'AGENTS.md');
737
+ case 'workbuddy':
738
+ // WorkBuddy has no user-global instructions file — guidance is project-only.
739
+ return '';
728
740
  default:
729
741
  return path.join(root, '.agent', 'rules', 'memorix.md');
730
742
  }
@@ -752,6 +764,8 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
752
764
  return path.join(root, '.trae', 'rules', 'project_rules.md');
753
765
  case 'dsh':
754
766
  return path.join(root, 'AGENTS.md');
767
+ case 'workbuddy':
768
+ return path.join(root, 'AGENTS.md');
755
769
  default:
756
770
  return path.join(root, '.agent', 'rules', 'memorix.md');
757
771
  }
@@ -867,6 +881,12 @@ export async function detectInstalledAgents(): Promise<AgentName[]> {
867
881
  agents.push('dsh');
868
882
  } catch { /* not installed */ }
869
883
 
884
+ // Check for WorkBuddy (its user-level home dir ~/.workbuddy)
885
+ try {
886
+ await fs.access(path.join(home, '.workbuddy'));
887
+ agents.push('workbuddy');
888
+ } catch { /* not installed */ }
889
+
870
890
  return agents;
871
891
  }
872
892
 
@@ -889,6 +909,19 @@ export async function installHooks(
889
909
  };
890
910
  }
891
911
 
912
+ // WorkBuddy has no hook system and no user-global guidance file. Report an
913
+ // explicit no-op instead of a project path that is never written.
914
+ if (agent === 'workbuddy' && global) {
915
+ return {
916
+ agent,
917
+ configPath: '',
918
+ events: [],
919
+ generated: {
920
+ note: 'WorkBuddy has no hook system and no user-global guidance file — MCP only; no global guidance/hooks. Run `memorix setup --agent workbuddy` (without --global) to install project MCP config and AGENTS.md guidance.',
921
+ },
922
+ };
923
+ }
924
+
892
925
  // Guard: reject global install for agents that don't support it
893
926
  if (global && getGlobalConfigPath(agent) === '') {
894
927
  return {
@@ -988,6 +1021,20 @@ export async function installHooks(
988
1021
  },
989
1022
  };
990
1023
  }
1024
+ case 'workbuddy':
1025
+ // WorkBuddy has no hook system — install AGENTS.md guidance only;
1026
+ // the MCP row is installed by `memorix setup --agent workbuddy`.
1027
+ {
1028
+ const rulesPath = await installAgentRules(agent, projectRoot, global);
1029
+ return {
1030
+ agent,
1031
+ configPath: rulesPath,
1032
+ events: [],
1033
+ generated: {
1034
+ note: 'WorkBuddy has no hook system — installed AGENTS.md guidance only; the MCP row is installed by `memorix setup --agent workbuddy`.',
1035
+ },
1036
+ };
1037
+ }
991
1038
  case 'opencode': {
992
1039
  // OpenCode uses JS plugin files for hooks
993
1040
  const pluginContent = generateOpenCodePlugin();
@@ -1161,6 +1208,8 @@ export async function installHooks(
1161
1208
  * Rules instruct the agent to proactively use memorix for context continuity.
1162
1209
  */
1163
1210
  async function installAgentRules(agent: AgentName, projectRoot: string, global = false): Promise<string> {
1211
+ // WorkBuddy has no user-global instructions file — guidance is project-only.
1212
+ if (agent === 'workbuddy' && global) return '';
1164
1213
  const rulesContent = getAgentRulesContent(agent, global ? 'global' : 'project');
1165
1214
  const rulesRoot = global ? os.homedir() : projectRoot;
1166
1215
  const rulesPath = getAgentRulesPath(agent, rulesRoot, global);
@@ -1355,6 +1404,12 @@ export async function uninstallHooks(
1355
1404
  return false;
1356
1405
  }
1357
1406
 
1407
+ // WorkBuddy has no hook file at either scope — removing hooks is a no-op.
1408
+ // It must not touch the generic `.memorix/hooks.json` default path.
1409
+ if (agent === 'workbuddy') {
1410
+ return false;
1411
+ }
1412
+
1358
1413
  // Guard: reject global uninstall for agents that don't support it
1359
1414
  if (global && getGlobalConfigPath(agent) === '') {
1360
1415
  return false;
@@ -42,6 +42,7 @@ export type AgentName =
42
42
  | 'opencode'
43
43
  | 'trae'
44
44
  | 'dsh'
45
+ | 'workbuddy'
45
46
  | 'pi'
46
47
  | 'omp';
47
48
 
@@ -63,6 +64,7 @@ export const AGENT_SUPPORT_TIER: Record<AgentName, SupportTier> = {
63
64
  opencode: 'community',
64
65
  trae: 'community',
65
66
  dsh: 'community',
67
+ workbuddy: 'community',
66
68
  pi: 'community',
67
69
  omp: 'community',
68
70
  antigravity: 'community',
@@ -159,6 +159,15 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
159
159
  surfaces: ['MCP', 'AGENTS.md', 'skills'],
160
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
161
  },
162
+ {
163
+ agent: 'workbuddy',
164
+ name: 'WorkBuddy',
165
+ entry: 'official-config',
166
+ status: 'ready',
167
+ install: 'memorix setup --agent workbuddy',
168
+ surfaces: ['MCP', 'AGENTS.md'],
169
+ note: 'WorkBuddy support writes a memorix MCP server row into ~/.workbuddy/mcp.json (or <project>/.workbuddy/mcp.json for project installs) and guidance into AGENTS.md.',
170
+ },
162
171
  {
163
172
  agent: 'memcode',
164
173
  name: 'memcode',
package/src/types.ts CHANGED
@@ -670,7 +670,7 @@ export interface MemoryRef {
670
670
  /** MCP config format adapter interface */
671
671
  export interface MCPConfigAdapter {
672
672
  /** Setup-only adapters may support a host that does not participate in workspace sync. */
673
- readonly source: AgentTarget | 'codebuddy' | 'dsh';
673
+ readonly source: AgentTarget | 'codebuddy' | 'dsh' | 'workbuddy';
674
674
  /** Parse MCP server entries from a config file */
675
675
  parse(content: string): MCPServerEntry[];
676
676
  /** Generate config file content from MCP server entries */
@@ -0,0 +1,71 @@
1
+ import type { MCPConfigAdapter, MCPServerEntry } from '../../types.js';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * WorkBuddy MCP config adapter.
7
+ *
8
+ * WorkBuddy stores its MCP server list in a JSON file following the standard
9
+ * MCP client convention:
10
+ *
11
+ * { "mcpServers": { [name]: { command, args, env?, url?, headers? } } }
12
+ *
13
+ * Per the official WorkBuddy MCP guide, two scopes are supported:
14
+ * - user-level: ~/.workbuddy/mcp.json
15
+ * - project-level: <project>/.workbuddy/mcp.json
16
+ *
17
+ * getConfigPath returns the project-level path when a projectRoot is supplied,
18
+ * otherwise the user-level path — matching the existing adapter convention.
19
+ */
20
+ export class WorkbuddyMCPAdapter implements MCPConfigAdapter {
21
+ readonly source = 'workbuddy' as const;
22
+
23
+ parse(content: string): MCPServerEntry[] {
24
+ try {
25
+ const config = JSON.parse(content);
26
+ const servers = config.mcpServers ?? {};
27
+ return Object.entries(servers).map(([name, entry]: [string, any]) => ({
28
+ name,
29
+ command: entry.command ?? '',
30
+ args: entry.args ?? [],
31
+ ...(entry.env && Object.keys(entry.env).length > 0 ? { env: entry.env } : {}),
32
+ ...(entry.url ? { url: entry.url } : {}),
33
+ ...(entry.headers && Object.keys(entry.headers).length > 0 ? { headers: entry.headers } : {}),
34
+ ...(entry.disabled === true ? { disabled: true } : {}),
35
+ }));
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ generate(servers: MCPServerEntry[]): string {
42
+ const mcpServers: Record<string, any> = {};
43
+ for (const s of servers) {
44
+ const entry: Record<string, any> = {};
45
+ if (s.url) {
46
+ entry.url = s.url;
47
+ if (s.headers && Object.keys(s.headers).length > 0) {
48
+ entry.headers = s.headers;
49
+ }
50
+ } else {
51
+ entry.command = s.command;
52
+ entry.args = s.args;
53
+ }
54
+ if (s.env && Object.keys(s.env).length > 0) {
55
+ entry.env = s.env;
56
+ }
57
+ if (s.disabled === true) {
58
+ entry.disabled = true;
59
+ }
60
+ mcpServers[s.name] = entry;
61
+ }
62
+ return JSON.stringify({ mcpServers }, null, 2) + '\n';
63
+ }
64
+
65
+ getConfigPath(projectRoot?: string): string {
66
+ if (projectRoot) {
67
+ return join(projectRoot, '.workbuddy', 'mcp.json');
68
+ }
69
+ return join(homedir(), '.workbuddy', 'mcp.json');
70
+ }
71
+ }