@cr8rcho/alkahest 0.1.69 → 0.1.70

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.
@@ -4,7 +4,14 @@ export declare function findProjectRoot(start: string): string;
4
4
  export declare function localSlug(root: string): string | undefined;
5
5
  /** Code map this checkout publishes to, stored alongside the slug in .alkahest/project.json. */
6
6
  export declare function localMapSlug(root: string): string | undefined;
7
- /** Resolve { root, slug, mapSlug } from any path: explicit → local file → saved creds (by root/path). */
7
+ /**
8
+ * Resolve { root, slug, mapSlug } from any path:
9
+ * explicit → local file → saved creds (by root/path) → ALKAHEST_PROJECT.
10
+ *
11
+ * The env var is the LAST fallback on purpose (same shape as ALKAHEST_TOKEN in
12
+ * credentials.ts): it lets an MCP server be configured for one project — a personal wiki,
13
+ * say — without a checkout to bind, while a real linked checkout still wins over it.
14
+ */
8
15
  export declare function resolveProject(path: string, explicitSlug?: string): {
9
16
  root: string;
10
17
  slug?: string;
@@ -54,14 +54,23 @@ export function localMapSlug(root) {
54
54
  catch { /* missing or unparsable */ }
55
55
  return undefined;
56
56
  }
57
- /** Resolve { root, slug, mapSlug } from any path: explicit → local file → saved creds (by root/path). */
57
+ /**
58
+ * Resolve { root, slug, mapSlug } from any path:
59
+ * explicit → local file → saved creds (by root/path) → ALKAHEST_PROJECT.
60
+ *
61
+ * The env var is the LAST fallback on purpose (same shape as ALKAHEST_TOKEN in
62
+ * credentials.ts): it lets an MCP server be configured for one project — a personal wiki,
63
+ * say — without a checkout to bind, while a real linked checkout still wins over it.
64
+ */
58
65
  export function resolveProject(path, explicitSlug) {
59
66
  const root = findProjectRoot(path);
60
67
  const creds = loadCredentials();
61
68
  const slug = explicitSlug ||
62
69
  localSlug(root) ||
63
70
  creds.projects?.[root]?.slug ||
64
- creds.projects?.[resolve(path || ".")]?.slug;
71
+ creds.projects?.[resolve(path || ".")]?.slug ||
72
+ process.env.ALKAHEST_PROJECT?.trim() ||
73
+ undefined;
65
74
  const mapSlug = localMapSlug(root) ||
66
75
  creds.projects?.[root]?.mapSlug ||
67
76
  creds.projects?.[resolve(path || ".")]?.mapSlug;
@@ -1 +1 @@
1
- {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/core/project.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/core/project.ts"],"names":[],"mappings":"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"}
@@ -214,19 +214,20 @@ export function buildServer() {
214
214
  "node's source location (screen → sourceFile/route/title, resource → path/label) so you can open the right file " +
215
215
  "and address it. Use this to drive development from feedback: read open comments, edit the code, then call " +
216
216
  "resolve_comment. Needs an API token (ALKAHEST_TOKEN in this server's config, or a prior 'alkahest login') " +
217
- "and the project must have been published.",
217
+ "and the project named (see `project`).",
218
218
  inputSchema: {
219
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
219
220
  path: z.string().optional().describe("Project root (default: cwd)"),
220
221
  open: z.boolean().optional().describe("Only unresolved comments (default: false = all)"),
221
222
  },
222
- }, async ({ path, open }) => {
223
+ }, async ({ path, open, project }) => {
223
224
  const root = rootOf(path);
224
- const res = await pullComments(root, { open });
225
+ const res = await pullComments(root, { open, slug: project });
225
226
  if (!res.ok) {
226
227
  const hints = {
227
228
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
228
229
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
229
- no_slug: "This project hasn't been published yetrun the publish tool first.",
230
+ no_slug: "Pass `project` a slug from list_projects or set ALKAHEST_PROJECT in this MCP server's config.",
230
231
  invalid_token: "The API token is invalid or revoked — create a new one at alkahest.app → Account.",
231
232
  not_found: "No accessible project for this slug.",
232
233
  };
@@ -267,9 +268,10 @@ export function buildServer() {
267
268
  inputSchema: {
268
269
  node: z.string().describe("screen id/route/title, resource id/path/label, or 'map'"),
269
270
  body: z.string().describe("the comment text"),
271
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
270
272
  path: z.string().optional().describe("Project root (default: cwd)"),
271
273
  },
272
- }, async ({ node, body, path }) => {
274
+ }, async ({ node, body, path, project }) => {
273
275
  const root = findProjectRoot(rootOf(path));
274
276
  const map = loadOrScan(root);
275
277
  if (!map)
@@ -277,11 +279,11 @@ export function buildServer() {
277
279
  const n = resolveNode(map, node);
278
280
  if (!n)
279
281
  return text(`No node matches '${node}'. Use the overview tool to list screens/resources.`);
280
- const res = await postComment(root, { node_key: n.node_key, anchor_kind: n.anchor_kind, anchor_label: n.anchor_label, body });
282
+ const res = await postComment(root, { node_key: n.node_key, anchor_kind: n.anchor_kind, anchor_label: n.anchor_label, body, slug: project });
281
283
  if (!res.ok) {
282
284
  const hints = {
283
285
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
284
- no_slug: "Publish this project first (publish tool).",
286
+ no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
285
287
  forbidden: "Only the project owner or a collaborator can comment.",
286
288
  };
287
289
  return text(`Add comment failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
@@ -318,17 +320,18 @@ export function buildServer() {
318
320
  "already linked to an issue (creates a new one).",
319
321
  inputSchema: {
320
322
  ids: z.array(z.string()).min(1).describe("Comment ids to group into one issue (from the comments tool)"),
323
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
321
324
  path: z.string().optional().describe("Project root (default: cwd)"),
322
325
  title: z.string().optional().describe("Issue title (else derived from the comments)"),
323
326
  repo: z.string().optional().describe("Target GitHub repo owner/repo (else gh's default for the repo)"),
324
327
  force: z.boolean().optional().describe("File even if some selected comments are already tracked"),
325
328
  },
326
- }, async ({ ids, path, title, repo, force }) => {
327
- const res = await fileCommentsIssue(rootOf(path), ids, { title, repo, force });
329
+ }, async ({ ids, path, title, repo, force, project }) => {
330
+ const res = await fileCommentsIssue(rootOf(path), ids, { title, repo, force, slug: project });
328
331
  if (!res.ok) {
329
332
  const hints = {
330
333
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
331
- no_slug: "Publish this project first (publish tool).",
334
+ no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
332
335
  already_tracked: "Some comments already have an issue — pass force:true to file a new one.",
333
336
  gh_failed: "Install and authenticate the GitHub CLI (`gh auth login`) for this repo.",
334
337
  forbidden: "Only the project owner or a collaborator can file issues.",
@@ -342,7 +345,7 @@ export function buildServer() {
342
345
  const issueHints = {
343
346
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
344
347
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
345
- no_slug: "This project hasn't been published yetrun the publish tool first.",
348
+ no_slug: "Pass `project` a slug from list_projects or set ALKAHEST_PROJECT in this MCP server's config.",
346
349
  invalid_token: "The API token is invalid or revoked — create a new one at alkahest.app → Account.",
347
350
  forbidden: "Only the project owner or a collaborator can write issues.",
348
351
  not_found: "Not found — list ids with the issues tool, or the project's issue maps with the maps tool.",
@@ -360,15 +363,16 @@ export function buildServer() {
360
363
  "question awaits an answer), and awaitingDecision (open_questions > 0) — use actionable issues to decide what " +
361
364
  "to work on next. When you hit a decision you need the user to make mid-task, post it with ask_issue (the issue " +
362
365
  "stops being actionable until they answer and you resolve_issue_question). Read the thread with issue_comments. " +
363
- "Needs an API token and a published project.",
366
+ "Needs an API token and a named project — pass `project` (a slug from list_projects) unless the folder is a linked checkout or ALKAHEST_PROJECT is set. No publish required.",
364
367
  inputSchema: {
368
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
365
369
  path: z.string().optional().describe("Project root (default: cwd)"),
366
370
  open: z.boolean().optional().describe("Only issues that are not done (default: false = all)"),
367
371
  map: z.string().optional().describe("Restrict to one issue map (a project can hold several; omit when there's one). List them with the maps tool."),
368
372
  q: z.string().optional().describe("Filter issues by title/body substring (server-side). Edges/links stay unfiltered, so blockedBy may name issues outside the filtered list."),
369
373
  },
370
- }, async ({ path, open, map, q }) => {
371
- const res = await pullIssues(rootOf(path), { mapSlug: map, q });
374
+ }, async ({ path, open, map, q, project }) => {
375
+ const res = await pullIssues(rootOf(path), { mapSlug: map, q, slug: project });
372
376
  if (!res.ok || !res.graph)
373
377
  return issueFail("Read issues", res.code, res.message, res.maps);
374
378
  const states = deriveIssueStates(res.graph);
@@ -405,16 +409,17 @@ export function buildServer() {
405
409
  parent_id: z.string().optional().describe("Parent issue id — creates a contains edge (epic → task)"),
406
410
  target: z.string().optional().describe("Code-map target: 's:…'/'r:…' node key, '/route' (planned screen), or a resource label"),
407
411
  map: z.string().optional().describe("Which issue map to add to (a project can hold several; omit when there's one). List them with the maps tool, or create one with create_map."),
412
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
408
413
  path: z.string().optional().describe("Project root (default: cwd)"),
409
414
  },
410
- }, async ({ title, type, status, body, priority, due_on, assignee_id, parent_id, target, map, path }) => {
415
+ }, async ({ title, type, status, body, priority, due_on, assignee_id, parent_id, target, map, path, project }) => {
411
416
  const targetFields = target
412
417
  ? {
413
418
  target_kind: (target.startsWith("s:") || target.startsWith("r:") ? "node" : target.startsWith("/") ? "route" : "resource"),
414
419
  target_key: target,
415
420
  }
416
421
  : {};
417
- const res = await createIssue(rootOf(path), { title, type, status, body, priority, due_on, assignee_id, parent_id, mapSlug: map, ...targetFields });
422
+ const res = await createIssue(rootOf(path), { title, type, status, body, priority, due_on, assignee_id, parent_id, mapSlug: map, slug: project, ...targetFields });
418
423
  if (!res.ok || !res.issue)
419
424
  return issueFail("Add issue", res.code, res.message, res.maps);
420
425
  return json({ ok: true, issue: res.issue });
@@ -694,19 +699,20 @@ export function buildServer() {
694
699
  "Use it before creating anything (does a note/issue/task on this topic already exist?) and before linking " +
695
700
  "(which note should cite this issue?). Matching is a server-side substring over title + full body (and note " +
696
701
  "slug); results come back compact — note bodies as 240-char excerpts (read one in full with get_note), issues " +
697
- "with status/type, tasks with done state. Notes and issues need a published project (they fail soft with a " +
698
- "reason if there isn't one); tasks are yours and always searchable. For a deeper dive into one kind, use the " +
702
+ "with status/type, tasks with done state. Notes and issues need a named project (they fail soft with a " +
703
+ "reason if there isn't one — pass `project`); tasks are yours and always searchable. For a deeper dive into one kind, use the " +
699
704
  "notes / issues / list_tasks tools with their own `q`.",
700
705
  inputSchema: {
701
706
  q: z.string().describe("Text to find — substring match over titles and full bodies"),
707
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
702
708
  path: z.string().optional().describe("Project root (default: cwd)"),
703
709
  },
704
- }, async ({ q, path }) => {
710
+ }, async ({ q, path, project }) => {
705
711
  const root = rootOf(path);
706
712
  const [notesRes, issuesRes, tasksRes] = await Promise.all([
707
- pullNotes(root, { q, bodies: "excerpt" }),
708
- pullIssues(root, { q }),
709
- pullTasks(root, { q, status: "all" }),
713
+ pullNotes(root, { q, bodies: "excerpt", slug: project }),
714
+ pullIssues(root, { q, slug: project }),
715
+ pullTasks(root, { q, status: "all", project }),
710
716
  ]);
711
717
  const notes = notesRes.ok && notesRes.maps
712
718
  ? notesRes.maps.flatMap((m) => m.notes.map((n) => ({ slug: n.slug, title: n.title, map: m.slug, folder: n.folder ?? null, excerpt: n.body ?? null })))
@@ -732,15 +738,16 @@ export function buildServer() {
732
738
  "the notebook's property schema (key/type/options); notes carry their props values (reserved key `tags`). " +
733
739
  "ALWAYS check this before add_note when " +
734
740
  "recording knowledge: if a note on the topic exists, update_note it instead of adding a near-duplicate. " +
735
- "`q` searches title/slug/FULL body server-side. Needs an API token and a published project.",
741
+ "`q` searches title/slug/FULL body server-side. Needs an API token and a named project — pass `project` (a slug from list_projects) unless the folder is a linked checkout or ALKAHEST_PROJECT is set. No publish required.",
736
742
  inputSchema: {
743
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
737
744
  path: z.string().optional().describe("Project root (default: cwd)"),
738
745
  q: z.string().optional().describe("Filter notes by title/slug/body substring (matches the full body)"),
739
746
  map: z.string().optional().describe("Restrict to one note map (default: all readable). List them with the maps tool."),
740
747
  full_bodies: z.boolean().optional().describe("Return complete bodies instead of 240-char excerpts (heavy on a big wiki)"),
741
748
  },
742
- }, async ({ path, q, map, full_bodies }) => {
743
- const res = await pullNotes(rootOf(path), { mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt" });
749
+ }, async ({ path, q, map, full_bodies, project }) => {
750
+ const res = await pullNotes(rootOf(path), { mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt", slug: project });
744
751
  if (!res.ok || !res.maps)
745
752
  return issueFail("Read notes", res.code, res.message, res.mapList);
746
753
  return json({ ok: true, project: res.project, count: res.maps.reduce((n, m) => n + m.notes.length, 0), maps: res.maps });
@@ -752,10 +759,11 @@ export function buildServer() {
752
759
  inputSchema: {
753
760
  note: z.string().describe("Note slug (or id)"),
754
761
  map: z.string().optional().describe("Which note map (omit when the slug is unique across maps)"),
762
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
755
763
  path: z.string().optional().describe("Project root (default: cwd)"),
756
764
  },
757
- }, async ({ note, map, path }) => {
758
- const res = await getNote(rootOf(path), { note, mapSlug: map });
765
+ }, async ({ note, map, path, project }) => {
766
+ const res = await getNote(rootOf(path), { note, mapSlug: map, slug: project });
759
767
  if (!res.ok || !res.note)
760
768
  return issueFail("Get note", res.code, res.message, res.mapList);
761
769
  const { ok: _ok, code: _code, message: _message, mapList: _ml, ...rest } = res;
@@ -775,10 +783,11 @@ export function buildServer() {
775
783
  folder: z.string().optional().describe("Tree-sidebar path like 'raw/articles' (omit = unfiled) — the web viewer's Obsidian-style tree groups by it"),
776
784
  props: z.record(z.any()).optional().describe("Notebook properties (flat key→value): reserved key `tags` = string array; other keys should match the map's schema (see prop_defs in the notes tool) — unknown keys are kept but show as unregistered"),
777
785
  map: z.string().optional().describe("Which note map to add to (a project can hold several; omit when there's one). List them with the maps tool, or create one with create_map."),
786
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
778
787
  path: z.string().optional().describe("Project root (default: cwd)"),
779
788
  },
780
- }, async ({ title, body, note_slug, folder, props, map, path }) => {
781
- const res = await createNote(rootOf(path), { title, body, note_slug, folder, props, mapSlug: map });
789
+ }, async ({ title, body, note_slug, folder, props, map, path, project }) => {
790
+ const res = await createNote(rootOf(path), { title, body, note_slug, folder, props, mapSlug: map, slug: project });
782
791
  if (!res.ok || !res.note)
783
792
  return issueFail("Add note", res.code, res.message, res.maps);
784
793
  return json({ ok: true, note: res.note });
@@ -795,10 +804,11 @@ export function buildServer() {
795
804
  from: z.string().describe("Source note slug (or id)"),
796
805
  to: z.string().describe("Target: 'issue:<uuid>', or 'code:s:…' / 'code:r:…'"),
797
806
  remove: z.boolean().optional().describe("true → disconnect from→to instead"),
807
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
798
808
  path: z.string().optional().describe("Project root (default: cwd)"),
799
809
  },
800
- }, async ({ from, to, remove, path }) => {
801
- const res = await linkNotes(rootOf(path), { from, to, remove });
810
+ }, async ({ from, to, remove, path, project }) => {
811
+ const res = await linkNotes(rootOf(path), { from, to, remove, slug: project });
802
812
  if (!res.ok)
803
813
  return issueFail(remove ? "Unlink notes" : "Link notes", res.code, res.message, res.maps);
804
814
  return json({ ok: true, ...(remove ? { removed: `${from} → ${to}` } : { linked: `${from} → ${to}` }) });
@@ -812,10 +822,11 @@ export function buildServer() {
812
822
  inputSchema: {
813
823
  note: z.string().describe("Note slug (or id)"),
814
824
  map: z.string().optional().describe("Target note map (a project can hold several; omit when there's one). List them with the maps tool."),
825
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
815
826
  path: z.string().optional().describe("Project root (default: cwd)"),
816
827
  },
817
- }, async ({ note, map, path }) => {
818
- const res = await mapNote(rootOf(path), { noteRef: note, mapSlug: map });
828
+ }, async ({ note, map, path, project }) => {
829
+ const res = await mapNote(rootOf(path), { noteRef: note, mapSlug: map, slug: project });
819
830
  if (!res.ok)
820
831
  return issueFail("Move note", res.code, res.message, res.maps);
821
832
  return json({ ok: true, note: res.note, map: res.map });
@@ -841,10 +852,11 @@ export function buildServer() {
841
852
  reason: z.string().optional().describe("REQUIRED with delete (≤200 chars): a one-line reason the user sees in the Trash and the activity journal — say WHY the note should go, not just 'cleanup'"),
842
853
  restore: z.boolean().optional().describe("true → restore the note from the Trash (undoes a soft delete)"),
843
854
  map: z.string().optional().describe("Which note map (a project can hold several; omit when there's one)"),
855
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
844
856
  path: z.string().optional().describe("Project root (default: cwd)"),
845
857
  },
846
- }, async ({ note, title, body, new_slug, folder, props, delete: del, reason, restore, map, path }) => {
847
- const res = await updateNote(rootOf(path), { note, title, body, new_slug, folder, props, delete: del, reason, restore, mapSlug: map });
858
+ }, async ({ note, title, body, new_slug, folder, props, delete: del, reason, restore, map, path, project }) => {
859
+ const res = await updateNote(rootOf(path), { note, title, body, new_slug, folder, props, delete: del, reason, restore, mapSlug: map, slug: project });
848
860
  const what = del ? "Delete note" : restore ? "Restore note" : "Update note";
849
861
  if (!res.ok)
850
862
  return issueFail(what, res.code, res.message, res.maps);
@@ -877,13 +889,14 @@ export function buildServer() {
877
889
  })).optional().describe("Definitions to register/merge onto the note map's schema"),
878
890
  remove: z.array(z.string()).optional().describe("Property definition key(s) to unregister; note values are kept. Reserved key `tags` is refused."),
879
891
  map: z.string().optional().describe("Which note map (a project can hold several; omit when there's one)"),
892
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
880
893
  path: z.string().optional().describe("Project root (default: cwd)"),
881
894
  },
882
- }, async ({ define, remove, map, path }) => {
895
+ }, async ({ define, remove, map, path, project }) => {
883
896
  if (!define?.length && !remove?.length) {
884
897
  return json({ ok: false, error: "bad_request", message: "Pass `define` (definitions to register) and/or `remove` (keys to unregister)." });
885
898
  }
886
- const res = await editPropDefs(rootOf(path), { defs: define, remove, mapSlug: map });
899
+ const res = await editPropDefs(rootOf(path), { defs: define, remove, mapSlug: map, slug: project });
887
900
  if (!res.ok)
888
901
  return issueFail("Edit note props", res.code, res.message, res.maps);
889
902
  return json({ ok: true, added: res.added ?? 0, merged: res.merged ?? 0, removed: res.removed ?? 0, skipped: res.skipped ?? 0 });
@@ -893,21 +906,24 @@ export function buildServer() {
893
906
  description: "List the maps in this published project. A project is a container of many maps (ADR-011): code maps " +
894
907
  "(published from a scan) and issue maps — each with a per-project slug, addressed at /p/:project/:map. Maps " +
895
908
  "are equal (no default), so when a project has several of a type the publish / add_issue / issues tools return " +
896
- "'ambiguous_map' — call this to see the slugs, then pass `map`. Needs an API token and a published project.",
909
+ "'ambiguous_map' — call this to see the slugs, then pass `map`. Needs an API token and a named project — pass `project` (a slug from list_projects) unless the folder is a linked checkout or ALKAHEST_PROJECT is set. No publish required.",
897
910
  inputSchema: {
911
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
898
912
  path: z.string().optional().describe("Project root (default: cwd)"),
899
913
  type: z.enum(["code", "issue", "note"]).optional().describe("Restrict to one type (default: all)"),
900
914
  },
901
- }, async ({ path, type }) => {
902
- const res = await listMaps(rootOf(path), { type });
915
+ }, async ({ path, type, project }) => {
916
+ const res = await listMaps(rootOf(path), { type, slug: project });
903
917
  if (!res.ok || !res.maps)
904
918
  return issueFail("List maps", res.code, res.message);
905
919
  return json({ ok: true, slug: res.slug, count: res.maps.length, maps: res.maps });
906
920
  });
907
921
  server.registerTool("list_projects", {
908
922
  title: "List account projects & workspaces",
909
- description: "List every workspace and project this account's token can reach (ADR-022). Use it to find a project's slug — " +
910
- "e.g. to recover the right publish target after the local link was lost (a workspace move, a fresh clone, CI), " +
923
+ description: "List every workspace and project this account's token can reach (ADR-022). **This is how you get the `project` " +
924
+ "slug** the notes / issues / maps tools take call it whenever the user names a project in words ('my wiki') " +
925
+ "and you don't have its slug, or when a tool answers no_slug. Also useful " +
926
+ "to recover the right publish target after the local link was lost (a workspace move, a fresh clone, CI), " +
911
927
  "or before publishing to confirm which existing project to update instead of creating a duplicate. Each project " +
912
928
  "includes isOwner (only owned projects can be re-published/overwritten) and per-code-map fingerprints " +
913
929
  "(screens/resources counts) so you can match a local scan by structure. Needs an API token; no project context.",
@@ -938,14 +954,16 @@ export function buildServer() {
938
954
  description: "Show a code map's publish timeline (ADR-023) — when each publish happened, the screen/resource/" +
939
955
  "transition counts, and which nodes were added/removed since the previous publish. Use it to answer " +
940
956
  "'when did this last publish' and 'what changed' without diffing manually. Needs an API token and a " +
941
- "published project; `map` picks the code map when the project has several (else the oldest).",
957
+ "named project (see `project`); `map` picks the code map when the project has several (else the oldest). " +
958
+ "This one DOES read published versions — an unpublished project simply has no history yet.",
942
959
  inputSchema: {
960
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
943
961
  path: z.string().optional().describe("Project root (default: cwd)"),
944
962
  map: z.string().optional().describe("Which code map (default: this checkout's / the oldest)"),
945
963
  limit: z.number().optional().describe("Max versions (default 50)"),
946
964
  },
947
- }, async ({ path, map, limit }) => {
948
- const res = await listHistory(rootOf(path), { map, limit });
965
+ }, async ({ path, map, limit, project }) => {
966
+ const res = await listHistory(rootOf(path), { map, limit, slug: project });
949
967
  if (!res.ok || !res.versions)
950
968
  return issueFail("History", res.code, res.message);
951
969
  // Newest first; include count deltas vs the previous version so the agent needn't recompute.
@@ -975,10 +993,11 @@ export function buildServer() {
975
993
  slug: z.string().describe("The new map's slug (lowercase letters, numbers, dashes; the server slugifies)"),
976
994
  type: z.enum(["code", "issue", "note"]).optional().describe("Map type (default: issue)"),
977
995
  name: z.string().optional().describe("Display name (defaults to the slug)"),
996
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
978
997
  path: z.string().optional().describe("Project root (default: cwd)"),
979
998
  },
980
- }, async ({ slug, type, name, path }) => {
981
- const res = await createMap(rootOf(path), { mapSlug: slug, type, mapName: name });
999
+ }, async ({ slug, type, name, path, project }) => {
1000
+ const res = await createMap(rootOf(path), { mapSlug: slug, type, mapName: name, slug: project });
982
1001
  if (!res.ok || !res.map)
983
1002
  return issueFail("Create map", res.code, res.message);
984
1003
  return json({ ok: true, map: res.map });
@@ -1059,10 +1078,11 @@ export function buildServer() {
1059
1078
  issue: z.string().describe("Issue id (from the issues tool)"),
1060
1079
  map: z.string().optional().describe("Which issue map (a project can hold several; omit when there's one). List them with the maps tool."),
1061
1080
  remove: z.boolean().optional().describe("true → take the issue off the map (the issue itself is never deleted)"),
1081
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
1062
1082
  path: z.string().optional().describe("Project root (default: cwd)"),
1063
1083
  },
1064
- }, async ({ issue, map, remove, path }) => {
1065
- const res = await mapIssue(rootOf(path), { issueId: issue, mapSlug: map, remove });
1084
+ }, async ({ issue, map, remove, path, project }) => {
1085
+ const res = await mapIssue(rootOf(path), { issueId: issue, mapSlug: map, remove, slug: project });
1066
1086
  if (!res.ok)
1067
1087
  return issueFail(remove ? "Unmap issue" : "Map issue", res.code, res.message, res.maps);
1068
1088
  return json({ ok: true, issue: res.issue, map: res.map, member: res.member });
@@ -1078,10 +1098,11 @@ export function buildServer() {
1078
1098
  inputSchema: {
1079
1099
  issue: z.string().optional().describe("Restrict to one issue's thread (issue id from the issues tool)"),
1080
1100
  open: z.boolean().optional().describe("Only unresolved comments — the decisions still awaiting an answer (default: false)"),
1101
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
1081
1102
  path: z.string().optional().describe("Project root (default: cwd)"),
1082
1103
  },
1083
- }, async ({ issue, open, path }) => {
1084
- const res = await pullIssueComments(rootOf(path), { issue, open });
1104
+ }, async ({ issue, open, path, project }) => {
1105
+ const res = await pullIssueComments(rootOf(path), { issue, open, slug: project });
1085
1106
  if (!res.ok || !res.comments)
1086
1107
  return issueFail("Read issue comments", res.code, res.message);
1087
1108
  return json({ ok: true, count: res.comments.length, comments: res.comments });
@@ -1154,14 +1175,15 @@ export function buildServer() {
1154
1175
  id: z.string().describe("Issue id to complete (from the issues tool)"),
1155
1176
  result: z.string().describe("What you did / the outcome — recorded as a 'result' comment on the issue"),
1156
1177
  status: z.string().optional().describe("Terminal status id from issue_config (default: the first terminal status, usually 'done')"),
1178
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
1157
1179
  path: z.string().optional().describe("Project root (default: cwd)"),
1158
1180
  },
1159
- }, async ({ id, result, status, path }) => {
1181
+ }, async ({ id, result, status, path, project }) => {
1160
1182
  const root = rootOf(path);
1161
1183
  // Resolve the terminal status to move to (explicit, else the project's first terminal status).
1162
1184
  let target = status;
1163
1185
  if (!target) {
1164
- const g = await pullIssues(root, {});
1186
+ const g = await pullIssues(root, { slug: project });
1165
1187
  if (!g.ok || !g.graph)
1166
1188
  return issueFail("Complete issue", g.code, g.message, g.maps);
1167
1189
  const terminal = [...terminalStatuses(g.graph.issue_config)];