@chamba/mcp 0.2.1 → 0.3.0

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 (2) hide show
  1. package/dist/main.js +285 -118
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -74,28 +74,86 @@ function registerCleanupWorktree(server, logger, services) {
74
74
  );
75
75
  }
76
76
 
77
+ // src/tools/cleanup-worktrees.ts
78
+ import {
79
+ detectGitRepos,
80
+ joinPath,
81
+ loadConfig,
82
+ MultiRepoWorktreeManager,
83
+ planWorktrees,
84
+ WORKSPACE_DIR
85
+ } from "@chamba/core";
86
+ import { z as z2 } from "zod";
87
+ var TOOL_NAME2 = "chamba_cleanup_worktrees";
88
+ var DESCRIPTION2 = "Remove a ticket's git worktrees across its repos. Removes only the worktree directories (no --force), KEEPS every branch for you to review and merge by hand. Returns the suggested `git merge --no-ff` command per repo.";
89
+ var CONFIG_FILE = `${WORKSPACE_DIR}/config.json`;
90
+ async function resolveRepos(config, services, explicit) {
91
+ if (explicit && explicit.length > 0) return explicit;
92
+ if (config.repos && config.repos.length > 0) return config.repos;
93
+ return detectGitRepos(services.fs, services.cwd);
94
+ }
95
+ function registerCleanupWorktrees(server, logger, services) {
96
+ server.registerTool(
97
+ TOOL_NAME2,
98
+ {
99
+ title: "Cleanup worktrees (multi-repo)",
100
+ description: DESCRIPTION2,
101
+ inputSchema: {
102
+ ticket: z2.string().describe("Ticket id whose worktrees to remove."),
103
+ repos: z2.array(z2.string()).optional().describe("Repos to clean; defaults like create.")
104
+ }
105
+ },
106
+ async ({ ticket, repos }) => {
107
+ const globalPath = joinPath(services.homedir, CONFIG_FILE);
108
+ const projectPath = joinPath(services.cwd, CONFIG_FILE);
109
+ const { worktrees } = await loadConfig(services.fs, { globalPath, projectPath });
110
+ const repoList = await resolveRepos(worktrees, services, repos);
111
+ const items = planWorktrees({
112
+ workspaceRoot: services.cwd,
113
+ ticket,
114
+ repos: repoList,
115
+ config: worktrees
116
+ });
117
+ const results = await new MultiRepoWorktreeManager(services.process, services.fs).cleanup(
118
+ items
119
+ );
120
+ logger.info({ tool: TOOL_NAME2, ticket, repos: results.length }, "cleanup-worktrees");
121
+ const removed = results.filter((r) => r.removed);
122
+ const lines = results.map(
123
+ (r) => `- ${r.repo}: ${r.removed ? "removed" : "not found"} \u2014 merge: ${r.mergeSuggestion}`
124
+ );
125
+ const text = `Cleaned ${removed.length}/${results.length} worktree(s) for ${ticket}. Branches kept.
126
+ ${lines.join("\n")}`;
127
+ return {
128
+ content: [{ type: "text", text }],
129
+ structuredContent: { ticket, worktrees: results }
130
+ };
131
+ }
132
+ );
133
+ }
134
+
77
135
  // src/tools/create-worktree.ts
78
136
  import { GitDetector, WorktreeError as WorktreeError2, WorktreeManager as WorktreeManager2 } from "@chamba/core";
79
- import { z as z2 } from "zod";
80
- var TOOL_NAME2 = "chamba_create_worktree";
137
+ import { z as z3 } from "zod";
138
+ var TOOL_NAME3 = "chamba_create_worktree";
81
139
  var NOT_GIT = "Not a git repo, worktree skipped. Worker should use main cwd.";
82
- var DESCRIPTION2 = "Create an isolated git worktree for a task/worker under `.chamba/worktrees/<task>/<worker>/` on branch `chamba/<date>-<task>/<worker>`. If the directory is not a git repo, returns an error and the worker should use the main cwd instead.";
140
+ var DESCRIPTION3 = "Create an isolated git worktree for a task/worker under `.chamba/worktrees/<task>/<worker>/` on branch `chamba/<date>-<task>/<worker>`. If the directory is not a git repo, returns an error and the worker should use the main cwd instead.";
83
141
  function registerCreateWorktree(server, logger, services) {
84
142
  server.registerTool(
85
- TOOL_NAME2,
143
+ TOOL_NAME3,
86
144
  {
87
145
  title: "Create worktree",
88
- description: DESCRIPTION2,
146
+ description: DESCRIPTION3,
89
147
  inputSchema: {
90
- taskSlug: z2.string().describe("Short task identifier (slugified for git)."),
91
- workerId: z2.string().describe('Worker identifier, e.g. "implementer".'),
92
- baseBranch: z2.string().optional().describe("Branch to base the worktree on.")
148
+ taskSlug: z3.string().describe("Short task identifier (slugified for git)."),
149
+ workerId: z3.string().describe('Worker identifier, e.g. "implementer".'),
150
+ baseBranch: z3.string().optional().describe("Branch to base the worktree on.")
93
151
  }
94
152
  },
95
153
  async ({ taskSlug, workerId, baseBranch }) => {
96
154
  const isGit = await new GitDetector(services.process).isGitRepo(services.cwd);
97
155
  if (!isGit) {
98
- logger.info({ tool: TOOL_NAME2 }, "not a git repo");
156
+ logger.info({ tool: TOOL_NAME3 }, "not a git repo");
99
157
  return { isError: true, content: [{ type: "text", text: NOT_GIT }] };
100
158
  }
101
159
  try {
@@ -106,7 +164,7 @@ function registerCreateWorktree(server, logger, services) {
106
164
  date: services.clock.today(),
107
165
  baseBranch
108
166
  });
109
- logger.info({ tool: TOOL_NAME2, branch: handle.branch }, "worktree created");
167
+ logger.info({ tool: TOOL_NAME3, branch: handle.branch }, "worktree created");
110
168
  return {
111
169
  content: [
112
170
  { type: "text", text: `Created worktree at ${handle.path} on branch ${handle.branch}` }
@@ -115,33 +173,140 @@ function registerCreateWorktree(server, logger, services) {
115
173
  };
116
174
  } catch (err) {
117
175
  const message = err instanceof WorktreeError2 ? err.message : String(err);
118
- logger.info({ tool: TOOL_NAME2, err: message }, "worktree create failed");
176
+ logger.info({ tool: TOOL_NAME3, err: message }, "worktree create failed");
119
177
  return { isError: true, content: [{ type: "text", text: message }] };
120
178
  }
121
179
  }
122
180
  );
123
181
  }
124
182
 
183
+ // src/tools/create-worktrees.ts
184
+ import {
185
+ buildTicketBranch,
186
+ detectGitRepos as detectGitRepos2,
187
+ editorWorkspaceDir,
188
+ joinPath as joinPath2,
189
+ loadConfig as loadConfig2,
190
+ MultiRepoWorktreeManager as MultiRepoWorktreeManager2,
191
+ planWorktrees as planWorktrees2,
192
+ WORKSPACE_DIR as WORKSPACE_DIR2,
193
+ writeEditorWorkspace
194
+ } from "@chamba/core";
195
+ import { z as z4 } from "zod";
196
+ var TOOL_NAME4 = "chamba_create_worktrees";
197
+ var DESCRIPTION4 = "Create git worktrees for a ticket across the repos it touches, driven by the `worktrees` block of .chamba/config.json (layout, branch prefix, base branch, env copy, editor workspace). Reuses an existing local/remote branch or forks a new one from the base. If `worktrees.command` is set, runs that command instead. Never merges, never pushes \u2014 branches are left open for you.";
198
+ var CONFIG_FILE2 = `${WORKSPACE_DIR2}/config.json`;
199
+ async function resolveRepos2(config, services, explicit) {
200
+ if (explicit && explicit.length > 0) return explicit;
201
+ if (config.repos && config.repos.length > 0) return config.repos;
202
+ return detectGitRepos2(services.fs, services.cwd);
203
+ }
204
+ function registerCreateWorktrees(server, logger, services) {
205
+ server.registerTool(
206
+ TOOL_NAME4,
207
+ {
208
+ title: "Create worktrees (multi-repo)",
209
+ description: DESCRIPTION4,
210
+ inputSchema: {
211
+ ticket: z4.string().describe("Ticket id, e.g. TICKET-123."),
212
+ repos: z4.array(z4.string()).optional().describe(
213
+ "Repos to act on; defaults to config.worktrees.repos or autodetected git repos."
214
+ )
215
+ }
216
+ },
217
+ async ({ ticket, repos }) => {
218
+ const globalPath = joinPath2(services.homedir, CONFIG_FILE2);
219
+ const projectPath = joinPath2(services.cwd, CONFIG_FILE2);
220
+ const { worktrees } = await loadConfig2(services.fs, { globalPath, projectPath });
221
+ if (worktrees.command) {
222
+ const cmd = worktrees.command.replaceAll("{ticket}", ticket).replaceAll("{repos}", (repos ?? []).join(" "));
223
+ const result = await services.process.exec("sh", ["-c", cmd], { cwd: services.cwd });
224
+ logger.info(
225
+ { tool: TOOL_NAME4, usedCommand: true, exitCode: result.exitCode },
226
+ "create-worktrees (command)"
227
+ );
228
+ const text2 = `Ran worktree command (exit ${result.exitCode}).
229
+ ${result.stdout || result.stderr}`;
230
+ return {
231
+ content: [{ type: "text", text: text2 }],
232
+ structuredContent: {
233
+ ticket,
234
+ usedCommand: true,
235
+ exitCode: result.exitCode,
236
+ stdout: result.stdout,
237
+ stderr: result.stderr
238
+ }
239
+ };
240
+ }
241
+ const repoList = await resolveRepos2(worktrees, services, repos);
242
+ const branch = buildTicketBranch(worktrees.branchPrefix, ticket);
243
+ if (repoList.length === 0) {
244
+ const text2 = `No git repos found in ${services.cwd}. Set worktrees.repos in .chamba/config.json or pass repos.`;
245
+ return {
246
+ content: [{ type: "text", text: text2 }],
247
+ structuredContent: { ticket, branch, worktrees: [] }
248
+ };
249
+ }
250
+ const items = planWorktrees2({
251
+ workspaceRoot: services.cwd,
252
+ ticket,
253
+ repos: repoList,
254
+ config: worktrees
255
+ });
256
+ const results = await new MultiRepoWorktreeManager2(services.process, services.fs).create({
257
+ items,
258
+ baseBranch: worktrees.baseBranch,
259
+ copyEnvFiles: worktrees.copyEnvFiles,
260
+ envPruneDirs: worktrees.envPruneDirs
261
+ });
262
+ let workspaceFile;
263
+ if (worktrees.editorWorkspace) {
264
+ const dir = editorWorkspaceDir(worktrees, services.cwd, ticket);
265
+ workspaceFile = await writeEditorWorkspace(services.fs, dir, ticket, items);
266
+ }
267
+ logger.info({ tool: TOOL_NAME4, ticket, repos: results.length }, "create-worktrees");
268
+ const lines = results.map(
269
+ (r) => `- ${r.repo}: ${r.status} \u2192 ${r.worktreePath}${r.envCopied > 0 ? ` (${r.envCopied} env)` : ""}`
270
+ );
271
+ const text = `Worktrees for ${ticket} on branch ${branch} (${worktrees.layout} layout):
272
+ ${lines.join("\n")}${workspaceFile ? `
273
+ Editor workspace: ${workspaceFile}` : ""}
274
+
275
+ Branches are left open \u2014 review, commit and merge by hand.`;
276
+ return {
277
+ content: [{ type: "text", text }],
278
+ structuredContent: {
279
+ ticket,
280
+ branch,
281
+ layout: worktrees.layout,
282
+ workspaceFile,
283
+ worktrees: results
284
+ }
285
+ };
286
+ }
287
+ );
288
+ }
289
+
125
290
  // src/tools/generate-plan.ts
126
291
  import { generatePlanTemplate, WorkspaceScanner } from "@chamba/core";
127
- import { z as z3 } from "zod";
128
- var TOOL_NAME3 = "chamba_generate_plan";
129
- var DESCRIPTION3 = "Generate a structured plan TEMPLATE (not a finished plan) for a task: goal, acceptance criteria, subtasks with suggested workers, risks, and files likely touched. The editor model fills the placeholders, then calls chamba_review_plan. No LLM is used here.";
292
+ import { z as z5 } from "zod";
293
+ var TOOL_NAME5 = "chamba_generate_plan";
294
+ var DESCRIPTION5 = "Generate a structured plan TEMPLATE (not a finished plan) for a task: goal, acceptance criteria, subtasks with suggested workers, risks, and files likely touched. The editor model fills the placeholders, then calls chamba_review_plan. No LLM is used here.";
130
295
  function registerGeneratePlan(server, logger, services) {
131
296
  server.registerTool(
132
- TOOL_NAME3,
297
+ TOOL_NAME5,
133
298
  {
134
299
  title: "Generate plan",
135
- description: DESCRIPTION3,
300
+ description: DESCRIPTION5,
136
301
  inputSchema: {
137
- task: z3.string().describe("What the plan should accomplish."),
138
- context: z3.string().optional().describe("Context from chamba_load_context, if any.")
302
+ task: z5.string().describe("What the plan should accomplish."),
303
+ context: z5.string().optional().describe("Context from chamba_load_context, if any.")
139
304
  }
140
305
  },
141
306
  async ({ task, context }) => {
142
307
  const workspace = await new WorkspaceScanner(services.fs).scan(services.cwd);
143
308
  const template = generatePlanTemplate({ task, context, workspace });
144
- logger.info({ tool: TOOL_NAME3 }, "plan template generated");
309
+ logger.info({ tool: TOOL_NAME5 }, "plan template generated");
145
310
  return { content: [{ type: "text", text: template }] };
146
311
  }
147
312
  );
@@ -152,36 +317,36 @@ import {
152
317
  AGENT_ROLES,
153
318
  buildHint,
154
319
  getModel,
155
- joinPath,
156
- loadConfig,
320
+ joinPath as joinPath3,
321
+ loadConfig as loadConfig3,
157
322
  resolveRole,
158
- WORKSPACE_DIR
323
+ WORKSPACE_DIR as WORKSPACE_DIR3
159
324
  } from "@chamba/core";
160
- import { z as z4 } from "zod";
161
- var TOOL_NAME4 = "chamba_get_agent_config";
162
- var DESCRIPTION4 = "Return the configured model + effort hint for a given agent role (orchestrator, planner, reviewer, implementer, tester, summarizer, researcher). chamba does not call any model \u2014 this is guidance the editor's own model can use to decide how to delegate. Reads ~/.chamba/config.json and ./.chamba/config.json (project overrides global); falls back to built-in defaults.";
163
- var CONFIG_FILE = `${WORKSPACE_DIR}/config.json`;
325
+ import { z as z6 } from "zod";
326
+ var TOOL_NAME6 = "chamba_get_agent_config";
327
+ var DESCRIPTION6 = "Return the configured model + effort hint for a given agent role (orchestrator, planner, reviewer, implementer, tester, summarizer, researcher). chamba does not call any model \u2014 this is guidance the editor's own model can use to decide how to delegate. Reads ~/.chamba/config.json and ./.chamba/config.json (project overrides global); falls back to built-in defaults.";
328
+ var CONFIG_FILE3 = `${WORKSPACE_DIR3}/config.json`;
164
329
  function registerGetAgentConfig(server, logger, services) {
165
330
  server.registerTool(
166
- TOOL_NAME4,
331
+ TOOL_NAME6,
167
332
  {
168
333
  title: "Get agent config",
169
- description: DESCRIPTION4,
334
+ description: DESCRIPTION6,
170
335
  inputSchema: {
171
- role: z4.enum([...AGENT_ROLES]).describe("The agent role to get a model + effort hint for.")
336
+ role: z6.enum([...AGENT_ROLES]).describe("The agent role to get a model + effort hint for.")
172
337
  }
173
338
  },
174
339
  async ({ role }) => {
175
- const globalPath = joinPath(services.homedir, CONFIG_FILE);
176
- const projectPath = joinPath(services.cwd, CONFIG_FILE);
177
- const { config, sources } = await loadConfig(services.fs, { globalPath, projectPath });
340
+ const globalPath = joinPath3(services.homedir, CONFIG_FILE3);
341
+ const projectPath = joinPath3(services.cwd, CONFIG_FILE3);
342
+ const { config, sources } = await loadConfig3(services.fs, { globalPath, projectPath });
178
343
  const agent = resolveRole(config, role);
179
344
  const model = getModel(agent.model);
180
345
  const hint = buildHint(role, agent);
181
346
  const invalid = sources.find((s) => s.status === "invalid");
182
347
  const warning = invalid ? `Ignored invalid config at ${invalid.path}: ${invalid.error}. Using defaults.` : void 0;
183
348
  logger.info(
184
- { tool: TOOL_NAME4, role, model: agent.model, effort: agent.effort },
349
+ { tool: TOOL_NAME6, role, model: agent.model, effort: agent.effort },
185
350
  "get-agent-config"
186
351
  );
187
352
  const structured = {
@@ -205,15 +370,15 @@ function registerGetAgentConfig(server, logger, services) {
205
370
 
206
371
  // src/tools/list-worktrees.ts
207
372
  import { WorktreeManager as WorktreeManager3 } from "@chamba/core";
208
- var TOOL_NAME5 = "chamba_list_worktrees";
209
- var DESCRIPTION5 = "List the git worktrees in the current repo (path, HEAD, branch).";
373
+ var TOOL_NAME7 = "chamba_list_worktrees";
374
+ var DESCRIPTION7 = "List the git worktrees in the current repo (path, HEAD, branch).";
210
375
  function registerListWorktrees(server, logger, services) {
211
376
  server.registerTool(
212
- TOOL_NAME5,
213
- { title: "List worktrees", description: DESCRIPTION5, inputSchema: {} },
377
+ TOOL_NAME7,
378
+ { title: "List worktrees", description: DESCRIPTION7, inputSchema: {} },
214
379
  async () => {
215
380
  const worktrees = await new WorktreeManager3(services.process).list(services.cwd);
216
- logger.info({ tool: TOOL_NAME5, count: worktrees.length }, "listed worktrees");
381
+ logger.info({ tool: TOOL_NAME7, count: worktrees.length }, "listed worktrees");
217
382
  const text = worktrees.length === 0 ? "No worktrees found (or not a git repo)." : worktrees.map((w) => `- ${w.path}${w.branch ? ` [${w.branch}]` : ""}`).join("\n");
218
383
  return {
219
384
  content: [{ type: "text", text }],
@@ -225,18 +390,18 @@ function registerListWorktrees(server, logger, services) {
225
390
 
226
391
  // src/tools/load-context.ts
227
392
  import { ContextBuilder, ObsidianDetector, WorkspaceScanner as WorkspaceScanner2 } from "@chamba/core";
228
- import { z as z5 } from "zod";
229
- var TOOL_NAME6 = "chamba_load_context";
230
- var DESCRIPTION6 = "Load context for a task: a summary of the workspace plus, when an Obsidian vault is available, the notes most relevant to the task (keyword search). Returns a markdown block the model can reason over before planning.";
393
+ import { z as z7 } from "zod";
394
+ var TOOL_NAME8 = "chamba_load_context";
395
+ var DESCRIPTION8 = "Load context for a task: a summary of the workspace plus, when an Obsidian vault is available, the notes most relevant to the task (keyword search). Returns a markdown block the model can reason over before planning.";
231
396
  function registerLoadContext(server, logger, services) {
232
397
  server.registerTool(
233
- TOOL_NAME6,
398
+ TOOL_NAME8,
234
399
  {
235
400
  title: "Load context",
236
- description: DESCRIPTION6,
401
+ description: DESCRIPTION8,
237
402
  inputSchema: {
238
- task: z5.string().describe("The task you are about to work on."),
239
- includeObsidian: z5.boolean().optional().describe("Search the Obsidian vault for relevant notes (default true).")
403
+ task: z7.string().describe("The task you are about to work on."),
404
+ includeObsidian: z7.boolean().optional().describe("Search the Obsidian vault for relevant notes (default true).")
240
405
  }
241
406
  },
242
407
  async ({ task, includeObsidian }) => {
@@ -251,7 +416,7 @@ function registerLoadContext(server, logger, services) {
251
416
  }
252
417
  const built = await new ContextBuilder(services.fs).build({ workspace, task, vaultPath });
253
418
  logger.info(
254
- { tool: TOOL_NAME6, vault: vaultPath ?? null, notes: built.relevantNotes.length },
419
+ { tool: TOOL_NAME8, vault: vaultPath ?? null, notes: built.relevantNotes.length },
255
420
  "context built"
256
421
  );
257
422
  return { content: [{ type: "text", text: built.context }] };
@@ -261,23 +426,23 @@ function registerLoadContext(server, logger, services) {
261
426
 
262
427
  // src/tools/recall.ts
263
428
  import { FilesystemMemoryStore } from "@chamba/core";
264
- import { z as z6 } from "zod";
265
- var TOOL_NAME7 = "chamba_recall";
266
- var DESCRIPTION7 = "Search persisted memories (case-insensitive substring over key, tags and content) and return the matches with their paths and content.";
429
+ import { z as z8 } from "zod";
430
+ var TOOL_NAME9 = "chamba_recall";
431
+ var DESCRIPTION9 = "Search persisted memories (case-insensitive substring over key, tags and content) and return the matches with their paths and content.";
267
432
  function registerRecall(server, logger, services) {
268
433
  server.registerTool(
269
- TOOL_NAME7,
434
+ TOOL_NAME9,
270
435
  {
271
436
  title: "Recall",
272
- description: DESCRIPTION7,
437
+ description: DESCRIPTION9,
273
438
  inputSchema: {
274
- query: z6.string().describe("Keywords to search for.")
439
+ query: z8.string().describe("Keywords to search for.")
275
440
  }
276
441
  },
277
442
  async ({ query }) => {
278
443
  const store = new FilesystemMemoryStore(services.fs, services.clock, services.cwd);
279
444
  const matches = await store.recall(query);
280
- logger.info({ tool: TOOL_NAME7, query, matches: matches.length }, "recall");
445
+ logger.info({ tool: TOOL_NAME9, query, matches: matches.length }, "recall");
281
446
  const text = matches.length === 0 ? `No memories matched "${query}".` : matches.map((m) => `### ${m.key} (\`${m.path}\`)
282
447
  ${m.content}`).join("\n\n");
283
448
  return {
@@ -297,25 +462,25 @@ ${m.content}`).join("\n\n");
297
462
 
298
463
  // src/tools/remember.ts
299
464
  import { FilesystemMemoryStore as FilesystemMemoryStore2 } from "@chamba/core";
300
- import { z as z7 } from "zod";
301
- var TOOL_NAME8 = "chamba_remember";
302
- var DESCRIPTION8 = "Persist a piece of knowledge across sessions as an editable markdown file under `.chamba/memory/<key>.md`. Re-remembering an existing key appends a timestamped section instead of overwriting.";
465
+ import { z as z9 } from "zod";
466
+ var TOOL_NAME10 = "chamba_remember";
467
+ var DESCRIPTION10 = "Persist a piece of knowledge across sessions as an editable markdown file under `.chamba/memory/<key>.md`. Re-remembering an existing key appends a timestamped section instead of overwriting.";
303
468
  function registerRemember(server, logger, services) {
304
469
  server.registerTool(
305
- TOOL_NAME8,
470
+ TOOL_NAME10,
306
471
  {
307
472
  title: "Remember",
308
- description: DESCRIPTION8,
473
+ description: DESCRIPTION10,
309
474
  inputSchema: {
310
- key: z7.string().describe('Short identifier, e.g. "auth-decisions".'),
311
- content: z7.string().describe("What to remember (markdown)."),
312
- tags: z7.array(z7.string()).optional().describe("Optional tags for search.")
475
+ key: z9.string().describe('Short identifier, e.g. "auth-decisions".'),
476
+ content: z9.string().describe("What to remember (markdown)."),
477
+ tags: z9.array(z9.string()).optional().describe("Optional tags for search.")
313
478
  }
314
479
  },
315
480
  async ({ key, content, tags }) => {
316
481
  const store = new FilesystemMemoryStore2(services.fs, services.clock, services.cwd);
317
482
  const memory = await store.remember({ key, content, tags });
318
- logger.info({ tool: TOOL_NAME8, key, path: memory.path }, "memory saved");
483
+ logger.info({ tool: TOOL_NAME10, key, path: memory.path }, "memory saved");
319
484
  return {
320
485
  content: [{ type: "text", text: `Saved memory '${key}' to ${memory.path}` }],
321
486
  structuredContent: { saved: true, path: memory.path }
@@ -326,38 +491,38 @@ function registerRemember(server, logger, services) {
326
491
 
327
492
  // src/tools/review-plan.ts
328
493
  import { Reviewer, WorkspaceScanner as WorkspaceScanner3 } from "@chamba/core";
329
- import { z as z8 } from "zod";
330
- var TOOL_NAME9 = "chamba_review_plan";
331
- var DESCRIPTION9 = "Review a plan with programmatic heuristics (NO LLM): checks for acceptance criteria, tests, subtasks with assigned workers, concrete descriptions, files outside the workspace, and risk assessment for sensitive areas. Returns { approved, issues, suggestions, riskFlags }.";
494
+ import { z as z10 } from "zod";
495
+ var TOOL_NAME11 = "chamba_review_plan";
496
+ var DESCRIPTION11 = "Review a plan with programmatic heuristics (NO LLM): checks for acceptance criteria, tests, subtasks with assigned workers, concrete descriptions, files outside the workspace, and risk assessment for sensitive areas. Returns { approved, issues, suggestions, riskFlags }.";
332
497
  function registerReviewPlan(server, logger, services) {
333
498
  server.registerTool(
334
- TOOL_NAME9,
499
+ TOOL_NAME11,
335
500
  {
336
501
  title: "Review plan",
337
- description: DESCRIPTION9,
502
+ description: DESCRIPTION11,
338
503
  inputSchema: {
339
- plan: z8.string().describe("The plan markdown to review."),
340
- task: z8.string().describe("The task the plan is for."),
341
- context: z8.string().optional().describe("Context from chamba_load_context, if any.")
504
+ plan: z10.string().describe("The plan markdown to review."),
505
+ task: z10.string().describe("The task the plan is for."),
506
+ context: z10.string().optional().describe("Context from chamba_load_context, if any.")
342
507
  },
343
508
  outputSchema: {
344
- approved: z8.boolean(),
345
- issues: z8.array(
346
- z8.object({
347
- code: z8.string(),
348
- severity: z8.enum(["error", "warning"]),
349
- message: z8.string()
509
+ approved: z10.boolean(),
510
+ issues: z10.array(
511
+ z10.object({
512
+ code: z10.string(),
513
+ severity: z10.enum(["error", "warning"]),
514
+ message: z10.string()
350
515
  })
351
516
  ),
352
- suggestions: z8.array(z8.string()),
353
- riskFlags: z8.array(z8.string())
517
+ suggestions: z10.array(z10.string()),
518
+ riskFlags: z10.array(z10.string())
354
519
  }
355
520
  },
356
521
  async ({ plan, task, context }) => {
357
522
  const workspace = await new WorkspaceScanner3(services.fs).scan(services.cwd);
358
523
  const review = new Reviewer().review({ plan, task, context, workspace });
359
524
  logger.info(
360
- { tool: TOOL_NAME9, approved: review.approved, issues: review.issues.length },
525
+ { tool: TOOL_NAME11, approved: review.approved, issues: review.issues.length },
361
526
  "plan reviewed"
362
527
  );
363
528
  const verdict = review.approved ? "\u2705 approved" : "\u274C changes requested";
@@ -382,20 +547,20 @@ ${review.suggestions.map((s) => `- ${s}`).join("\n")}` : ""
382
547
 
383
548
  // src/tools/summarize-to-vault.ts
384
549
  import { ObsidianDetector as ObsidianDetector2, VaultWriter } from "@chamba/core";
385
- import { z as z9 } from "zod";
386
- var TOOL_NAME10 = "chamba_summarize_to_vault";
550
+ import { z as z11 } from "zod";
551
+ var TOOL_NAME12 = "chamba_summarize_to_vault";
387
552
  var NO_VAULT_ERROR = "No Obsidian vault configured. Set CHAMBA_OBSIDIAN_VAULT_PATH or use the obsidian-mcp server";
388
- var DESCRIPTION10 = "Write a structured summary note to the Obsidian vault under `proyectos/<date>-<slug>.md` with valid YAML frontmatter. Fails clearly if no vault is configured.";
553
+ var DESCRIPTION12 = "Write a structured summary note to the Obsidian vault under `proyectos/<date>-<slug>.md` with valid YAML frontmatter. Fails clearly if no vault is configured.";
389
554
  function registerSummarizeToVault(server, logger, services) {
390
555
  server.registerTool(
391
- TOOL_NAME10,
556
+ TOOL_NAME12,
392
557
  {
393
558
  title: "Summarize to vault",
394
- description: DESCRIPTION10,
559
+ description: DESCRIPTION12,
395
560
  inputSchema: {
396
- title: z9.string().describe("Note title."),
397
- content: z9.string().describe("Markdown body (summary, plan, decisions, next steps)."),
398
- projectSlug: z9.string().optional().describe("Optional slug for the filename.")
561
+ title: z11.string().describe("Note title."),
562
+ content: z11.string().describe("Markdown body (summary, plan, decisions, next steps)."),
563
+ projectSlug: z11.string().optional().describe("Optional slug for the filename.")
399
564
  }
400
565
  },
401
566
  async ({ title, content, projectSlug }) => {
@@ -404,7 +569,7 @@ function registerSummarizeToVault(server, logger, services) {
404
569
  searchRoots: obsidianSearchRoots(services)
405
570
  });
406
571
  if (!detection.found || !detection.path) {
407
- logger.info({ tool: TOOL_NAME10 }, "no vault configured");
572
+ logger.info({ tool: TOOL_NAME12 }, "no vault configured");
408
573
  return { isError: true, content: [{ type: "text", text: NO_VAULT_ERROR }] };
409
574
  }
410
575
  const writer = new VaultWriter(services.fs, services.clock);
@@ -414,7 +579,7 @@ function registerSummarizeToVault(server, logger, services) {
414
579
  content,
415
580
  projectSlug
416
581
  });
417
- logger.info({ tool: TOOL_NAME10, notePath }, "note written to vault");
582
+ logger.info({ tool: TOOL_NAME12, notePath }, "note written to vault");
418
583
  return { content: [{ type: "text", text: `Wrote note to ${notePath}` }] };
419
584
  }
420
585
  );
@@ -422,31 +587,31 @@ function registerSummarizeToVault(server, logger, services) {
422
587
 
423
588
  // src/tools/workspace-init.ts
424
589
  import {
425
- joinPath as joinPath2,
590
+ joinPath as joinPath4,
426
591
  renderWorkspaceMarkdown,
427
- WORKSPACE_DIR as WORKSPACE_DIR2,
592
+ WORKSPACE_DIR as WORKSPACE_DIR4,
428
593
  WORKSPACE_RELATIVE_PATH,
429
594
  WorkspaceScanner as WorkspaceScanner4
430
595
  } from "@chamba/core";
431
- import { z as z10 } from "zod";
432
- var TOOL_NAME11 = "chamba_workspace_init";
433
- var DESCRIPTION11 = "Scan the workspace and generate `.chamba/workspace.md` (description, languages, framework, conventions, active projects, folder map). Respects .gitignore/.dockerignore and never reads node_modules or binaries. If the file already exists it is NOT overwritten \u2014 its current contents are returned so the model/user decides what to do.";
596
+ import { z as z12 } from "zod";
597
+ var TOOL_NAME13 = "chamba_workspace_init";
598
+ var DESCRIPTION13 = "Scan the workspace and generate `.chamba/workspace.md` (description, languages, framework, conventions, active projects, folder map). Respects .gitignore/.dockerignore and never reads node_modules or binaries. If the file already exists it is NOT overwritten \u2014 its current contents are returned so the model/user decides what to do.";
434
599
  function registerWorkspaceInit(server, logger, services) {
435
600
  server.registerTool(
436
- TOOL_NAME11,
601
+ TOOL_NAME13,
437
602
  {
438
603
  title: "Init workspace",
439
- description: DESCRIPTION11,
604
+ description: DESCRIPTION13,
440
605
  inputSchema: {
441
- root: z10.string().optional().describe("Workspace root to scan. Defaults to the directory chamba runs in.")
606
+ root: z12.string().optional().describe("Workspace root to scan. Defaults to the directory chamba runs in.")
442
607
  }
443
608
  },
444
609
  async ({ root }) => {
445
610
  const workspaceRoot = root ?? services.cwd;
446
- const wsPath = joinPath2(workspaceRoot, WORKSPACE_RELATIVE_PATH);
611
+ const wsPath = joinPath4(workspaceRoot, WORKSPACE_RELATIVE_PATH);
447
612
  if (await services.fs.exists(wsPath)) {
448
613
  const currentContents = await services.fs.readFile(wsPath);
449
- logger.info({ tool: TOOL_NAME11, wsPath }, "workspace.md already exists, not overwriting");
614
+ logger.info({ tool: TOOL_NAME13, wsPath }, "workspace.md already exists, not overwriting");
450
615
  return {
451
616
  content: [
452
617
  {
@@ -463,10 +628,10 @@ ${currentContents}`
463
628
  const scanner = new WorkspaceScanner4(services.fs);
464
629
  const workspace = await scanner.scan(workspaceRoot);
465
630
  const markdown = renderWorkspaceMarkdown(workspace);
466
- await services.fs.mkdir(joinPath2(workspaceRoot, WORKSPACE_DIR2));
631
+ await services.fs.mkdir(joinPath4(workspaceRoot, WORKSPACE_DIR4));
467
632
  await services.fs.writeFile(wsPath, markdown);
468
633
  logger.info(
469
- { tool: TOOL_NAME11, wsPath, projects: workspace.projects.length },
634
+ { tool: TOOL_NAME13, wsPath, projects: workspace.projects.length },
470
635
  "workspace.md created"
471
636
  );
472
637
  return {
@@ -486,26 +651,26 @@ ${markdown}`
486
651
  // src/tools/workspace-reload.ts
487
652
  import {
488
653
  diffLines,
489
- joinPath as joinPath3,
654
+ joinPath as joinPath5,
490
655
  renderWorkspaceMarkdown as renderWorkspaceMarkdown2,
491
656
  textsEqual,
492
657
  WORKSPACE_RELATIVE_PATH as WORKSPACE_RELATIVE_PATH2,
493
658
  WorkspaceScanner as WorkspaceScanner5
494
659
  } from "@chamba/core";
495
- var TOOL_NAME12 = "chamba_workspace_reload";
496
- var DESCRIPTION12 = "Re-scan the workspace and return a diff against the current `.chamba/workspace.md`. NEVER overwrites the file \u2014 the user may have hand-edited it. The model decides whether to apply changes.";
660
+ var TOOL_NAME14 = "chamba_workspace_reload";
661
+ var DESCRIPTION14 = "Re-scan the workspace and return a diff against the current `.chamba/workspace.md`. NEVER overwrites the file \u2014 the user may have hand-edited it. The model decides whether to apply changes.";
497
662
  function registerWorkspaceReload(server, logger, services) {
498
663
  server.registerTool(
499
- TOOL_NAME12,
664
+ TOOL_NAME14,
500
665
  {
501
666
  title: "Reload workspace",
502
- description: DESCRIPTION12,
667
+ description: DESCRIPTION14,
503
668
  inputSchema: {}
504
669
  },
505
670
  async () => {
506
- const wsPath = joinPath3(services.cwd, WORKSPACE_RELATIVE_PATH2);
671
+ const wsPath = joinPath5(services.cwd, WORKSPACE_RELATIVE_PATH2);
507
672
  if (!await services.fs.exists(wsPath)) {
508
- logger.info({ tool: TOOL_NAME12, wsPath }, "no workspace.md to reload");
673
+ logger.info({ tool: TOOL_NAME14, wsPath }, "no workspace.md to reload");
509
674
  return {
510
675
  content: [
511
676
  {
@@ -519,7 +684,7 @@ function registerWorkspaceReload(server, logger, services) {
519
684
  const scanner = new WorkspaceScanner5(services.fs);
520
685
  const rescanned = renderWorkspaceMarkdown2(await scanner.scan(services.cwd));
521
686
  if (textsEqual(current, rescanned)) {
522
- logger.info({ tool: TOOL_NAME12, wsPath }, "workspace.md up to date");
687
+ logger.info({ tool: TOOL_NAME14, wsPath }, "workspace.md up to date");
523
688
  return {
524
689
  content: [
525
690
  {
@@ -529,7 +694,7 @@ function registerWorkspaceReload(server, logger, services) {
529
694
  ]
530
695
  };
531
696
  }
532
- logger.info({ tool: TOOL_NAME12, wsPath }, "workspace.md differs from re-scan");
697
+ logger.info({ tool: TOOL_NAME14, wsPath }, "workspace.md differs from re-scan");
533
698
  return {
534
699
  content: [
535
700
  {
@@ -547,25 +712,25 @@ ${diffLines(current, rescanned)}
547
712
  }
548
713
 
549
714
  // src/tools/workspace-show.ts
550
- import { joinPath as joinPath4, WORKSPACE_RELATIVE_PATH as WORKSPACE_RELATIVE_PATH3 } from "@chamba/core";
551
- var TOOL_NAME13 = "chamba_workspace_show";
552
- var DESCRIPTION13 = "Show the current workspace map. Reads `.chamba/workspace.md` from the workspace root and returns its contents. If no workspace file exists yet, says so \u2014 the model can then run chamba_workspace_init to create one.";
715
+ import { joinPath as joinPath6, WORKSPACE_RELATIVE_PATH as WORKSPACE_RELATIVE_PATH3 } from "@chamba/core";
716
+ var TOOL_NAME15 = "chamba_workspace_show";
717
+ var DESCRIPTION15 = "Show the current workspace map. Reads `.chamba/workspace.md` from the workspace root and returns its contents. If no workspace file exists yet, says so \u2014 the model can then run chamba_workspace_init to create one.";
553
718
  function registerWorkspaceShow(server, logger, services) {
554
719
  server.registerTool(
555
- TOOL_NAME13,
720
+ TOOL_NAME15,
556
721
  {
557
722
  title: "Show workspace",
558
- description: DESCRIPTION13,
723
+ description: DESCRIPTION15,
559
724
  inputSchema: {}
560
725
  },
561
726
  async () => {
562
- const path = joinPath4(services.cwd, WORKSPACE_RELATIVE_PATH3);
727
+ const path = joinPath6(services.cwd, WORKSPACE_RELATIVE_PATH3);
563
728
  try {
564
729
  const contents = await services.fs.readFile(path);
565
- logger.info({ tool: TOOL_NAME13, path }, "workspace.md read");
730
+ logger.info({ tool: TOOL_NAME15, path }, "workspace.md read");
566
731
  return { content: [{ type: "text", text: contents }] };
567
732
  } catch {
568
- logger.info({ tool: TOOL_NAME13, path }, "no workspace.md found");
733
+ logger.info({ tool: TOOL_NAME15, path }, "no workspace.md found");
569
734
  return {
570
735
  content: [
571
736
  {
@@ -594,6 +759,8 @@ function createServer(logger, services = createNodeServices()) {
594
759
  registerCreateWorktree(server, logger, services);
595
760
  registerListWorktrees(server, logger, services);
596
761
  registerCleanupWorktree(server, logger, services);
762
+ registerCreateWorktrees(server, logger, services);
763
+ registerCleanupWorktrees(server, logger, services);
597
764
  registerRemember(server, logger, services);
598
765
  registerRecall(server, logger, services);
599
766
  registerGetAgentConfig(server, logger, services);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chamba/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "chamba MCP server — orchestration, workspace, worktree and Obsidian tools for any MCP-capable editor",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "@modelcontextprotocol/sdk": "^1.12.0",
37
37
  "pino": "^9.0.0",
38
38
  "zod": "^3.23.0",
39
- "@chamba/adapters": "0.2.1",
40
- "@chamba/core": "0.2.1"
39
+ "@chamba/adapters": "0.3.0",
40
+ "@chamba/core": "0.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.0.0",