passalong 0.16.0 → 0.17.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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/api.js +27 -3
  3. package/src/mcp.js +164 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Hand tasks, bugs and finished work to AI coding agents, and get back a write-up with evidence.",
5
5
  "mcpName": "io.github.codellyson/passalong",
6
6
  "keywords": [
package/src/api.js CHANGED
@@ -301,10 +301,13 @@ export const join = (code) =>
301
301
  call(`/v1/invites/${encodeURIComponent(code)}/accept`, { method: "POST" });
302
302
 
303
303
  export const board = () => call("/v1/board");
304
- export const folders = (scope = "all") => call(`/v1/folders${q({ scope })}`);
305
- export const createFolder = (title, description = "", team = "") =>
306
- call("/v1/folders", { method: "POST", body: { title, description, team } });
304
+ export const folders = (scope = "all", { query = "", guide = "" } = {}) =>
305
+ call(`/v1/folders${q({ scope, q: query, guide })}`);
306
+ export const createFolder = (title, description = "", team = "", parent = "", color = "") =>
307
+ call("/v1/folders", { method: "POST", body: { title, description, team, parent, color } });
307
308
  export const folder = (id) => call(`/v1/folders/${encodeURIComponent(id)}`);
309
+ export const updateFolder = (id, changes) =>
310
+ call(`/v1/folders/${encodeURIComponent(id)}`, { method: "PATCH", body: changes });
308
311
  export const folderDocument = (folderId, documentId) =>
309
312
  call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`);
310
313
  export const folderAsset = (folderId, assetId) =>
@@ -329,8 +332,29 @@ export const saveFolderDocument = (folderId, documentId, version, body) =>
329
332
  method: "PUT",
330
333
  body: { version, body },
331
334
  });
335
+ export const renameFolderDocument = (folderId, documentId, name) =>
336
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
337
+ method: "PATCH",
338
+ body: { name },
339
+ });
340
+ export const folderDocumentHistory = (folderId, documentId, version = 0) =>
341
+ call(
342
+ `/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
343
+ );
332
344
  export const linkFolderGuide = (folderId, guide) =>
333
345
  call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
346
+ export const unlinkFolderGuide = (folderId, guide) =>
347
+ call(`/v1/folders/${encodeURIComponent(folderId)}/guides/${encodeURIComponent(guide)}`, {
348
+ method: "DELETE",
349
+ });
350
+ export const deleteFolderDocument = (folderId, documentId) =>
351
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
352
+ method: "DELETE",
353
+ });
354
+ export const deleteFolderAsset = (folderId, assetId) =>
355
+ call(`/v1/folders/${encodeURIComponent(folderId)}/assets/${encodeURIComponent(assetId)}`, {
356
+ method: "DELETE",
357
+ });
334
358
  /** What you did, newest first. `since` is a date prefix: 2026, 2026-09, 2026-09-11. */
335
359
  export const log = ({ repo = "", since = "", limit = 0 } = {}) =>
336
360
  call(`/v1/log${q({ repo, since, limit: limit || "" })}`);
package/src/mcp.js CHANGED
@@ -219,6 +219,8 @@ const passOut = openObject({ id: z.string(), passed: z.boolean() });
219
219
 
220
220
  const READS = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
221
221
  const ADDS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
222
+ // Removes something that cannot be brought back. Only a person's request should lead here.
223
+ const REMOVES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
222
224
  const fail = (err) => ({ content: [{ type: "text", text: err.message }], isError: true });
223
225
 
224
226
  /**
@@ -347,6 +349,8 @@ export function buildServer() {
347
349
  "FOLDERS ARE REUSABLE PROJECT CONTEXT, NOT GUIDES. When the user asks for a folder for a " +
348
350
  "script, brief, assets or work spanning several guides, call list_folders first to avoid " +
349
351
  "making a duplicate. If none fits, call create_folder with a title and short description. " +
352
+ "Use its parent id to nest a subfolder at any depth; keep a related project together. " +
353
+ "A folder's color can mark an area, and update_folder can recolor or move it later. " +
350
354
  "A short request is enough: name it from the person's purpose and draft the first Markdown " +
351
355
  "document with create_folder_document. Do not leave an empty folder or make them fill a form. " +
352
356
  "Pass `team` only when the user wants it shared with that team; use the slug they named " +
@@ -355,8 +359,16 @@ export function buildServer() {
355
359
  "get_folder_document to read text and its version, and save_folder_document with that " +
356
360
  "version when editing. On a stale-version refusal, reread before retrying so another " +
357
361
  "person's work is not lost. Use add_folder_asset for a screenshot or file available on " +
358
- "this machine, get_folder_asset to read one already there, and link_folder_guide only " +
359
- "when an existing guide uses that folder.\n" +
362
+ "this machine, and get_folder_asset to read one already there. " +
363
+ "To put a guide in a folder — a task, bug or handoff, new or already published — publish it " +
364
+ "as usual (for a team folder, to that folder's team, or its members cannot see it), then call " +
365
+ "link_folder_guide with the folder id and the guide id. Linking leaves the guide unchanged; " +
366
+ "unlink_folder_guide takes it out again. Delete a document or file with " +
367
+ "delete_folder_document or delete_folder_asset only when the person asked for it to go: " +
368
+ "neither can be undone. No tool deletes a whole folder; a person does that in the hub. " +
369
+ "list_folders with query finds folders and documents by their words, and with guide the " +
370
+ "folders a guide is in. rename_folder_document renames a document; " +
371
+ "get_folder_document_history reads earlier versions, so a bad edit can be put back.\n" +
360
372
  "A FOLLOW-UP IS MORE CONTEXT FOR A GUIDE, WRITTEN AS ITS OWN GUIDE. When a guide needs " +
361
373
  "more context — a missing detail, a step that needed explaining, what changed since, what " +
362
374
  "you found doing it — publish that context with publish_guide `parent` set to the guide's " +
@@ -526,12 +538,16 @@ export function buildServer() {
526
538
  {
527
539
  title: "List project folders",
528
540
  annotations: READS,
529
- description: "Find project folders of documents, assets and guides. Requires sync sign-in.",
530
- inputSchema: { scope: z.string().optional().describe('"all", "mine", or a team slug') },
541
+ description: `Find project folders of documents, assets and guides. Pass query to find the folders whose name, description, or any document's name or text mention those words; each then names its matching documents. Pass guide to find the folders a guide is in. Requires sync sign-in.`,
542
+ inputSchema: {
543
+ scope: z.string().optional().describe('"all", "mine", or a team slug'),
544
+ query: z.string().optional().describe("Words to look for in folders and their documents"),
545
+ guide: z.string().optional().describe("A guide id: only the folders it is linked in"),
546
+ },
531
547
  },
532
- async ({ scope }) => {
548
+ async ({ scope, query, guide }) => {
533
549
  try {
534
- return json(await api.folders(scope || "all"));
550
+ return json(await api.folders(scope || "all", { query, guide }));
535
551
  } catch (err) {
536
552
  return fail(err);
537
553
  }
@@ -543,16 +559,57 @@ export function buildServer() {
543
559
  title: "Create project folder",
544
560
  annotations: ADDS,
545
561
  description:
546
- "Create a folder for project documents and assets after checking list_folders. Omit team for a private folder; pass a team slug only when sharing was requested.",
562
+ "Create a folder or nested subfolder after checking list_folders. Pass parent to nest it; omit team for a private root, and pass a team slug only when sharing was requested. A subfolder inherits its parent's space.",
547
563
  inputSchema: {
548
564
  title: z.string().describe("Name of the project or body of work"),
549
565
  description: z.string().optional().describe("Short purpose of the folder"),
550
566
  team: z.string().optional().describe("Team slug; omit for a private folder"),
567
+ parent: z
568
+ .string()
569
+ .optional()
570
+ .describe("Parent folder id for a nested subfolder; omit for a top-level folder"),
571
+ color: z
572
+ .enum(["coral", "amber", "green", "blue", "violet"])
573
+ .optional()
574
+ .describe("Optional grouping color"),
575
+ },
576
+ },
577
+ async ({ title, description, team, parent, color }) => {
578
+ try {
579
+ return json(
580
+ await api.createFolder(title, description || "", team || "", parent || "", color || ""),
581
+ );
582
+ } catch (err) {
583
+ return fail(err);
584
+ }
585
+ },
586
+ );
587
+ server.registerTool(
588
+ "update_folder",
589
+ {
590
+ title: "Organize project folder",
591
+ annotations: ADDS,
592
+ description:
593
+ "Rename, describe, recolor or move a folder. Pass parent as an empty string to move it to the top level. A folder cannot move inside itself or a descendant, or into another team/private space.",
594
+ inputSchema: {
595
+ folder: z.string(),
596
+ title: z.string().optional(),
597
+ description: z.string().optional(),
598
+ parent: z
599
+ .string()
600
+ .optional()
601
+ .describe("New parent folder id; empty string means top level"),
602
+ color: z.enum(["", "coral", "amber", "green", "blue", "violet"]).optional(),
551
603
  },
552
604
  },
553
- async ({ title, description, team }) => {
605
+ async ({ folder, title, description, parent, color }) => {
554
606
  try {
555
- return json(await api.createFolder(title, description || "", team || ""));
607
+ const changes = Object.fromEntries(
608
+ Object.entries({ title, description, parent, color }).filter(
609
+ ([, value]) => value !== undefined,
610
+ ),
611
+ );
612
+ return json(await api.updateFolder(folder, changes));
556
613
  } catch (err) {
557
614
  return fail(err);
558
615
  }
@@ -678,13 +735,51 @@ export function buildServer() {
678
735
  }
679
736
  },
680
737
  );
738
+ server.registerTool(
739
+ "rename_folder_document",
740
+ {
741
+ title: "Rename folder document",
742
+ annotations: ADDS,
743
+ description:
744
+ "Rename a folder document. Its id, text and history stay; no version is needed, because a rename cannot overwrite anyone's text.",
745
+ inputSchema: { folder: z.string(), document: z.string(), name: z.string() },
746
+ },
747
+ async ({ folder, document, name }) => {
748
+ try {
749
+ return json(await api.renameFolderDocument(folder, document, name));
750
+ } catch (err) {
751
+ return fail(err);
752
+ }
753
+ },
754
+ );
755
+ server.registerTool(
756
+ "get_folder_document_history",
757
+ {
758
+ title: "Read folder document history",
759
+ annotations: READS,
760
+ description:
761
+ "Read a folder document's earlier versions. Without version, lists them newest first; with version, returns that version's text. To bring one back, save it with save_folder_document over the current version.",
762
+ inputSchema: {
763
+ folder: z.string(),
764
+ document: z.string(),
765
+ version: z.number().int().positive().optional(),
766
+ },
767
+ },
768
+ async ({ folder, document, version }) => {
769
+ try {
770
+ return json(await api.folderDocumentHistory(folder, document, version || 0));
771
+ } catch (err) {
772
+ return fail(err);
773
+ }
774
+ },
775
+ );
681
776
  server.registerTool(
682
777
  "link_folder_guide",
683
778
  {
684
779
  title: "Link guide to folder",
685
780
  annotations: ADDS,
686
781
  description:
687
- "Put an existing task or handoff beside the folder documents and assets it uses.",
782
+ "Put a task, bug or handoff guide in the folder, beside the documents and assets it uses: one just published or one that already existed.",
688
783
  inputSchema: { folder: z.string(), guide: z.string() },
689
784
  },
690
785
  async ({ folder, guide }) => {
@@ -696,6 +791,60 @@ export function buildServer() {
696
791
  },
697
792
  );
698
793
 
794
+ server.registerTool(
795
+ "unlink_folder_guide",
796
+ {
797
+ title: "Unlink guide from folder",
798
+ annotations: ADDS,
799
+ description:
800
+ "Take a guide out of a folder. Only the link goes: the guide itself is unchanged, and link_folder_guide puts it back.",
801
+ inputSchema: { folder: z.string(), guide: z.string() },
802
+ },
803
+ async ({ folder, guide }) => {
804
+ try {
805
+ return json(await api.unlinkFolderGuide(folder, guide));
806
+ } catch (err) {
807
+ return fail(err);
808
+ }
809
+ },
810
+ );
811
+
812
+ server.registerTool(
813
+ "delete_folder_document",
814
+ {
815
+ title: "Delete folder document",
816
+ annotations: REMOVES,
817
+ description:
818
+ "Delete one document from a folder, with its earlier versions. It cannot be brought back, so do it only when the person asked for that document to go; to change what it says, use save_folder_document instead.",
819
+ inputSchema: { folder: z.string(), document: z.string() },
820
+ },
821
+ async ({ folder, document }) => {
822
+ try {
823
+ return json(await api.deleteFolderDocument(folder, document));
824
+ } catch (err) {
825
+ return fail(err);
826
+ }
827
+ },
828
+ );
829
+
830
+ server.registerTool(
831
+ "delete_folder_asset",
832
+ {
833
+ title: "Delete folder file",
834
+ annotations: REMOVES,
835
+ description:
836
+ "Delete one file from a folder. It cannot be brought back, so do it only when the person asked for that file to go.",
837
+ inputSchema: { folder: z.string(), asset: z.string() },
838
+ },
839
+ async ({ folder, asset }) => {
840
+ try {
841
+ return json(await api.deleteFolderAsset(folder, asset));
842
+ } catch (err) {
843
+ return fail(err);
844
+ }
845
+ },
846
+ );
847
+
699
848
  server.registerTool(
700
849
  "search_guides",
701
850
  {
@@ -769,8 +918,8 @@ export function buildServer() {
769
918
  "delivery of, and every verdict and ack they gave. Each item has a rendered `text` line " +
770
919
  "and the guide's repo. Use it to answer 'what have I been working on', to write a " +
771
920
  "standup or a weekly summary, or to find work from a repo by when it happened rather " +
772
- "than by what it was called. This is the opposite of `activity`, which is what other " +
773
- "people did. IMPORTANT: it records what was passed along, not what was worked on — work " +
921
+ "than by what it was called. The hub calls this page Activities, so 'my activities' means " +
922
+ "this tool. It is the opposite of `activity`, which is what other people did. IMPORTANT: it records what was passed along, not what was worked on — work " +
774
923
  "that never became a guide has no entry, so never present it as a complete record of " +
775
924
  "this user's work, and never infer that a quiet period was an idle one.",
776
925
  inputSchema: {
@@ -797,8 +946,9 @@ export function buildServer() {
797
946
  annotations: READS,
798
947
  description:
799
948
  "What has happened to this user's guides and handoffs: who pulled one, who archived one, " +
800
- "who was handed what, who joined a team. Each item has a ready-made `text` line. This only " +
801
- "reads; clear_activity is what marks the feed seen.",
949
+ "who was handed what, who joined a team. Each item has a ready-made `text` line. This is " +
950
+ "the hub's notifications, not its Activities page — for the user's own acts ('my " +
951
+ "activities'), use log. This only reads; clear_activity is what marks the feed seen.",
802
952
  inputSchema: {
803
953
  all: z.boolean().default(false).describe("include what the user has already seen"),
804
954
  },