passalong 0.15.1 → 0.16.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.
package/bin/passalong CHANGED
@@ -26,7 +26,8 @@
26
26
  // passalong hub open your synced guides in the browser
27
27
  // passalong take <id> you are doing it — said before the work, so the sender stops guessing
28
28
  // passalong pass <id> <why> it is not yours — hands it back to its author with your reason
29
- // passalong assign <id> <to> give it to @handle, a #group, or "team" — yours, or assigned to you
29
+ // passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team" — yours, or assigned to you
30
+ // passalong recall <id> take back something you sent: private again, gone from their inboxes
30
31
  // passalong admin create <email> make an account that runs Passalong (owner only)
31
32
  // passalong admin list who runs it
32
33
  // passalong admin grant|revoke <@handle> make an existing account an admin, or stop it being one
@@ -50,6 +51,8 @@
50
51
  // passalong team invite [email] make an invite link (mailed when an email is given)
51
52
  // passalong team join <link|code> accept an invite
52
53
  // passalong team use <slug> switch the current team
54
+ // passalong team promote|demote <@handle> make a teammate an admin, or a member again
55
+ // passalong team remove <@handle> take a teammate out; `team leave` leaves yourself
53
56
  // passalong setup install the Claude Code capture skill, MCP server and hooks
54
57
  // passalong now what this worktree holds and what is waiting (the hooks call --hook)
55
58
  // passalong mcp run the MCP server over stdio
@@ -666,12 +669,20 @@ const commands = {
666
669
 
667
670
  async assign(f) {
668
671
  const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
669
- const to = f._[1] || die('say who: @handle for one person, #group, or "team" for everyone');
672
+ const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
670
673
  const r = await passalong.assign(id, to).catch((e) => die(e.message));
671
674
  const back = r.taken_back ? ` — taken back from ${r.taken_back} who had it, and they were told` : "";
672
675
  console.error(`${r.id} is now for ${r.to || "everyone in the team"}${back}`);
673
676
  },
674
677
 
678
+ async recall(f) {
679
+ const id = f._[0] || die("usage: passalong recall <id>");
680
+ const r = await passalong.recall(id).catch((e) => die(e.message));
681
+ console.error(`${r.id} is private again, and gone from their inboxes`);
682
+ if (r.already_opened_by)
683
+ console.error(`${r.already_opened_by} ${r.already_opened_by === 1 ? "person" : "people"} had already opened it, and keep what they pulled`);
684
+ },
685
+
675
686
  // ---- who runs the product ---------------------------------------------------------------
676
687
  // `passalong admin <create|list|grant|revoke>`. An admin signs in like anybody else: the role is
677
688
  // a column, so there is no admin password and no admin token — only an account that has it.
@@ -901,7 +912,7 @@ const commands = {
901
912
  const t = await api.team(cfg.team).catch((e) => die(e.message));
902
913
  console.log(`${t.name} (${t.slug}) — ${t.members.length} member${t.members.length === 1 ? "" : "s"}, ${t.guides} guide${t.guides === 1 ? "" : "s"}`);
903
914
  for (const m of t.members)
904
- console.log(` ${m.handle ? `@${m.handle}` : "(no handle)"}${m.name ? ` ${m.name}` : ""} ${m.role} joined ${relDate(m.joined)}`);
915
+ console.log(` ${m.handle ? `@${m.handle}` : "(no handle)"}${m.name ? ` ${m.name}` : ""} ${m.role === "owner" ? "admin" : m.role} joined ${relDate(m.joined)}`);
905
916
  return;
906
917
  }
907
918
  if (sub === "create") {
@@ -934,6 +945,22 @@ const commands = {
934
945
  if (who && !who.handle) console.error("teammates address you by handle: passalong me --handle <you>");
935
946
  return;
936
947
  }
948
+ if (sub === "promote" || sub === "demote") {
949
+ const slug = cfg.team || die("no current team — passalong team use <slug>");
950
+ const who = rest[0] || die(`usage: passalong team ${sub} <@handle>`);
951
+ const r = await api.setMemberRole(slug, who, sub === "promote" ? "admin" : "member").catch((e) => die(e.message));
952
+ console.error(`${who.startsWith("@") ? who : `@${who}`} is ${r.role === "admin" ? "an admin" : "a member"} of ${slug}${sub === "promote" ? " — they are told" : ""}`);
953
+ return;
954
+ }
955
+ if (sub === "remove" || sub === "leave") {
956
+ const slug = cfg.team || die("no current team — passalong team use <slug>");
957
+ const me = sub === "leave" ? (await api.me().catch((e) => die(e.message))).account : "";
958
+ const who = sub === "leave" ? me : rest[0] || die("usage: passalong team remove <@handle>");
959
+ const r = await api.removeMember(slug, who).catch((e) => die(e.message));
960
+ console.error(sub === "leave" ? `you left ${slug}` : `${who.startsWith("@") ? who : `@${who}`} is out of ${slug}${r.released ? `; ${r.released} hold${r.released === 1 ? "" : "s"} of theirs went back to waiting` : ""}`);
961
+ if (sub === "leave") api.setTeam("");
962
+ return;
963
+ }
937
964
  if (sub === "use") {
938
965
  const slug = rest[0] || die("usage: passalong team use <slug>");
939
966
  await api.team(slug).catch((e) => die(e.message));
@@ -941,7 +968,7 @@ const commands = {
941
968
  console.error(`current team: ${slug}`);
942
969
  return;
943
970
  }
944
- die("usage: passalong team [create <name> | invite [email] | join <link> | use <slug>]");
971
+ die("usage: passalong team [create <name> | invite [email] | join <link> | use <slug> | promote <@handle> | demote <@handle> | remove <@handle> | leave]");
945
972
  },
946
973
 
947
974
  async setup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "0.15.1",
3
+ "version": "0.16.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/skill/SKILL.md CHANGED
@@ -14,7 +14,7 @@ session's history. Write for execution, not for permanence.
14
14
  **Check what is already open, and whether a guide is wanted at all.** Run `passalong now` (or
15
15
  `take` with no id over MCP) to see what this worktree holds and what was handed to you.
16
16
 
17
- - **The session answers something you hold** — hand it in (`passalong hand_in <id> …`) instead.
17
+ - **The session answers something you hold** — use the MCP `hand_in` tool instead.
18
18
  Publishing a second guide about it leaves two records of one piece of work. Every hand-in carries
19
19
  what you ran and what came back — the command and the lines that decided it, a test summary, a
20
20
  link to the change, or a screenshot url. Copy each one out of the session as you go; at the end
@@ -44,7 +44,8 @@ machine, another agent session, or a teammate without your branch.
44
44
  what was decided and why, what finally worked, and how it was verified. Read the diff
45
45
  (`git diff`, `git log`) only to confirm details; the reasoning is in the transcript.
46
46
  3. **Write the guide** with the structure below. Be concrete: file paths, commands, exact error
47
- strings, version numbers. Prefer a short guide with every section filled over a long one.
47
+ strings, version numbers. A transfer has no required body sections; keep only the context the
48
+ next agent needs.
48
49
  4. **Mark context-specific parts** so the receiver can adapt rather than copy. Put
49
50
  `ASSUMES: <thing>. If <alternative>, <what changes>.` at the start of any step that depends on
50
51
  this repo's stack, layout, or conventions.
@@ -60,14 +61,13 @@ machine, another agent session, or a teammate without your branch.
60
61
  who it was handed to) on stderr.
61
62
 
62
63
  **If what you are capturing is more context for an existing guide** — a missing detail, a step
63
- that needed explaining, what changed since, what you found doing it — add `--follows <that id>`.
64
+ that needed explaining, or what changed since — add `--follows <that id>`.
64
65
  That publishes it as a follow-up: its own guide, listed under the original, and handed to
65
66
  whoever opens the original, person or agent. It travels the other way too — whoever opens the
66
67
  follow-up gets the guide it came out of, and where that guide has got to — so write it as a
67
- note on that work rather than as a standalone brief. A session that started from a passalong guide
68
- (`passalong start` or `pull` on an id, or an agent opened one) usually has context worth adding
69
- this way. Whether the guide worked is still its own answer: `passalong works <id> <screenshot>` or
70
- `passalong broken <id> <why>`.
68
+ note on that work rather than as a standalone brief. What you did or found while working on a
69
+ guide belongs in its `hand_in`, not a follow-up. Whether a guide worked is its own answer:
70
+ `passalong works <id> <screenshot>` or `passalong broken <id> <why>`.
71
71
  6. **Report** the id (and link if synced) and one line on how to use it on the other side:
72
72
  `passalong start <id>` in the target repo, or "start passalong <id>" to an agent with the
73
73
  Passalong MCP server. `start` rather than `pull` on purpose: it takes the handoff as well as
@@ -77,26 +77,25 @@ machine, another agent session, or a teammate without your branch.
77
77
  If the `passalong` command is missing, tell the user to run `npm i -g passalong && passalong setup` and
78
78
  still write the draft file so nothing is lost.
79
79
 
80
- ## Two kinds of guide
80
+ ## Choose the right Passalong form
81
81
 
82
- `kind:` in the frontmatter says what a guide is for, and the receiver behaves differently for
83
- each. Absent means `transfer`, which is what every guide written before bug reports existed is.
82
+ Every guide must state `kind:`. An absent kind is refused; the server never guesses one.
84
83
 
85
- | kind | what it is | what the receiver does |
86
- | --- | --- | --- |
87
- | `transfer` (or no `kind:` line) | finished work to repeat somewhere else | follows the **Steps** |
88
- | `bug` | a defect to fix where it is | fixes it — **Reproduce** shows the problem, it is not a procedure to apply |
84
+ - `transfer` is context for a different repo, machine or agent session. It has no required
85
+ sections. Write only what the receiver needs to avoid starting cold.
86
+ - `bug` is a defect to fix. Put the reproduction under `## Reproduce`, never `## Steps`, so the
87
+ receiver does not mistake the steps that show the defect for a remedy. Use `file_bugs` when
88
+ filing several independent defects.
89
+ - `task` is work nobody has done yet. Use `plan_tasks` for a larger goal; its `## Acceptance`
90
+ gives the reviewer something to check. Do not write implementation steps for the taker.
89
91
 
90
- To file bugs you found but are not fixing, call `file_bugs` with all of them in one call — it
91
- opens a report and publishes each issue as its own guide, so any one of them can be handed to
92
- whoever fixes it. `publish_guide` is for a single guide: work you finished and want repeated, or
93
- one bug on its own.
94
-
95
- A bug has no `## Steps` and publishing one with a Steps section is refused. That is the whole
96
- reason the kinds are separate: "follow its Steps" is what the MCP server tells every agent that
97
- pulls a guide, and steps that reproduce a defect are the one list that must never be run as a
98
- remedy — an agent that follows them reproduces the bug, checks the Verification, finds it false
99
- because the bug is real, and reports that the guide does not work.
92
+ **A project folder is not a guide.** When asked to keep a script, brief, screenshots or other
93
+ assets together for continuing work, call `list_folders` first. Reuse a fitting folder, or call
94
+ `create_folder` with a title and short description drawn from the person's request. Create the
95
+ first Markdown document with `create_folder_document`; do not leave an empty folder or ask the
96
+ person to fill a form. Omit `team` unless they asked to share it. Read a document and its version
97
+ before `save_folder_document`; reread after a stale-version refusal. Add files with
98
+ `add_folder_asset` and link an existing guide with `link_folder_guide` only when it uses the folder.
100
99
 
101
100
  ## Say it to a person
102
101
 
@@ -116,6 +115,7 @@ billing.ts per the Paystack spec, see PR 412."
116
115
  ---
117
116
  title: <what this accomplishes, as a verb phrase: "Add Paystack webhook verification">
118
117
  summary: <one or two plain sentences for a PERSON, 400 characters at most, no ids or paths: what this is and whether anyone has to act>
118
+ kind: transfer
119
119
  author: <git user.name, if known>
120
120
  source_context: <repo or product this came from>
121
121
  status: published
@@ -123,26 +123,19 @@ stack_assumptions: [<runtime/framework/db/service versions the steps depend on>]
123
123
  tags: [<3 to 6 lowercase keywords>]
124
124
  ---
125
125
 
126
- ## Problem
127
- What was broken or needed. Two or three sentences. Include the observable symptom.
128
-
129
- ## Solution shape
130
- The approach at a high level, before any code. One short paragraph.
131
-
132
- ## Decisions and rationale
133
- - **Chose X over Y** because Z. (Each real decision, with what was rejected.)
126
+ ## What changed
127
+ The information the receiving agent needs, in the shape that makes it easiest to use.
134
128
 
135
- ## Steps
136
- 1. ASSUMES: <stack>. If <other>, <adjust>. Then the concrete step, with the file path and the code or command.
137
- 2. ...
129
+ ## Why
130
+ Decisions or constraints the receiver could not infer from the final code.
138
131
 
139
- ## Verification
140
- Commands to run and what they should print. Test cases. What "done" looks like.
141
-
142
- ## Gotchas
143
- - What failed along the way, the exact error, and why. What looked right but wasn't.
132
+ ## How to check
133
+ Commands and expected results, if the receiver needs to repeat or verify the work.
144
134
  ```
145
135
 
136
+ Those headings are examples, not a required template. A short transfer may be a paragraph.
137
+ Include paths, assumptions, verification or gotchas only when they help the next agent act.
138
+
146
139
  ## Bug structure
147
140
 
148
141
  ```markdown
@@ -174,16 +167,15 @@ The behaviour that should have happened, as something the fixer can check.
174
167
 
175
168
  ## Quality bar
176
169
 
177
- - **Gotchas is the highest-value section.** If nothing went wrong, say what would have gone
178
- wrong without a specific decision. Never leave it empty.
179
- - **Verification must be runnable.** A command and an expected output, not "make sure it works".
180
- - **Every ASSUMES has an alternative.** The point is adaptation on the other side.
170
+ - **Write for the crossing.** State what another agent cannot learn from the destination repo.
171
+ - **Verification, when relevant, must be checkable.** Give the command and expected result.
172
+ - **State assumptions where they matter.** Say how the next environment may differ.
181
173
  - **One problem per guide.** A lesson from the same session that is not part of this fix goes in
182
- its own guide (write and share it separately), not in this one's Gotchas.
174
+ its own guide only when it is separate work worth handing over.
183
175
  - **No session narration.** "First I looked at..." is noise. State the finding.
184
176
  - **No secrets.** Redact tokens, keys, and internal hostnames that would not apply elsewhere.
185
- - Title, Problem, and Steps are required; the guide will not publish without them.
186
- - For a bug: title, Problem and **Reproduce**. Never a Steps section — see the two kinds above.
177
+ - Every guide needs a title, summary and explicit kind. A transfer needs no fixed body section.
178
+ - A bug needs Problem and Reproduce. Never give a bug a Steps section.
187
179
  - The line above Problem is written by `file_bugs` and the hub's form. It is in the document, not
188
180
  added by whatever served it, so an agent that fetched the share link over plain HTTP — with no
189
181
  MCP server and no knowledge of Passalong — still reads what the document is before acting on it.
package/src/api.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Client for the hosted sync API (apps/api). Everything here is optional: with no token the
2
2
  // CLI is a purely local tool, and every function throws an ApiError the CLI turns into a hint.
3
3
  import { readFileSync } from "node:fs";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { basename } from "node:path";
4
6
  import { nameFor, readAccounts, readConfig, saveTeam } from "./store.js";
5
7
 
6
8
  /**
@@ -145,7 +147,10 @@ export async function meWith(tokenValue, base = baseUrl()) {
145
147
 
146
148
  export { nameFor };
147
149
 
148
- async function call(path, { method = "GET", body, auth = true, raw = false } = {}) {
150
+ async function call(
151
+ path,
152
+ { method = "GET", body, auth = true, raw = false, uploadName = "" } = {},
153
+ ) {
149
154
  const headers = { ...CLIENT };
150
155
  if (auth) {
151
156
  if (needsChoice()) throw new ApiError(409, choiceMessage());
@@ -154,14 +159,24 @@ async function call(path, { method = "GET", body, auth = true, raw = false } = {
154
159
  headers.authorization = `Bearer ${t}`;
155
160
  }
156
161
  if (body !== undefined) {
157
- headers["content-type"] = typeof body === "string" ? "text/markdown" : "application/json";
162
+ headers["content-type"] = uploadName
163
+ ? "application/octet-stream"
164
+ : typeof body === "string"
165
+ ? "text/markdown"
166
+ : "application/json";
158
167
  }
168
+ if (uploadName) headers["x-file-name"] = uploadName.replace(/[^\x20-\x7e]/g, "").slice(0, 120);
159
169
  let res;
160
170
  try {
161
171
  res = await fetch(`${baseUrl()}${path}`, {
162
172
  method,
163
173
  headers,
164
- body: body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body),
174
+ body:
175
+ body === undefined
176
+ ? undefined
177
+ : uploadName || typeof body === "string"
178
+ ? body
179
+ : JSON.stringify(body),
165
180
  });
166
181
  } catch (err) {
167
182
  throw new ApiError(0, `could not reach ${baseUrl()} (${err.message})`);
@@ -273,10 +288,49 @@ export const team = (slug) => call(`/v1/teams/${encodeURIComponent(slug)}`);
273
288
  export const createTeam = (name) => call("/v1/teams", { method: "POST", body: { name } });
274
289
  export const invite = (slug, email = "") =>
275
290
  call(`/v1/teams/${encodeURIComponent(slug)}/invites`, { method: "POST", body: { email } });
291
+ export const setMemberRole = (slug, who, role) =>
292
+ call(`/v1/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(who)}`, {
293
+ method: "PATCH",
294
+ body: { role },
295
+ });
296
+ export const removeMember = (slug, who) =>
297
+ call(`/v1/teams/${encodeURIComponent(slug)}/members/${encodeURIComponent(who)}`, {
298
+ method: "DELETE",
299
+ });
276
300
  export const join = (code) =>
277
301
  call(`/v1/invites/${encodeURIComponent(code)}/accept`, { method: "POST" });
278
302
 
279
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 } });
307
+ export const folder = (id) => call(`/v1/folders/${encodeURIComponent(id)}`);
308
+ export const folderDocument = (folderId, documentId) =>
309
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`);
310
+ export const folderAsset = (folderId, assetId) =>
311
+ call(`/v1/folders/${encodeURIComponent(folderId)}/assets/${encodeURIComponent(assetId)}/agent`);
312
+ export async function addFolderAsset(folderId, path, name = "") {
313
+ const file = await stat(path);
314
+ if (!file.isFile()) throw new ApiError(400, "Choose a file to add to the folder.");
315
+ if (file.size > 10 * 1024 * 1024) throw new ApiError(413, "That file is over 10 MB.");
316
+ return call(`/v1/folders/${encodeURIComponent(folderId)}/assets`, {
317
+ method: "POST",
318
+ body: new Uint8Array(await readFile(path)),
319
+ uploadName: name || basename(path),
320
+ });
321
+ }
322
+ export const createFolderDocument = (folderId, name, body) =>
323
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents`, {
324
+ method: "POST",
325
+ body: { name, body },
326
+ });
327
+ export const saveFolderDocument = (folderId, documentId, version, body) =>
328
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
329
+ method: "PUT",
330
+ body: { version, body },
331
+ });
332
+ export const linkFolderGuide = (folderId, guide) =>
333
+ call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
280
334
  /** What you did, newest first. `since` is a date prefix: 2026, 2026-09, 2026-09-11. */
281
335
  export const log = ({ repo = "", since = "", limit = 0 } = {}) =>
282
336
  call(`/v1/log${q({ repo, since, limit: limit || "" })}`);
@@ -347,6 +401,8 @@ export const release = (id) =>
347
401
  /** The four verbs every guide answers to. See docs/V2.md §11. */
348
402
  export const working = () => call("/v1/working");
349
403
  export const handedIn = () => call("/v1/handed_in");
404
+ export const recall = (id) =>
405
+ call(`/v1/guides/${encodeURIComponent(id)}/recall`, { method: "POST", body: {} });
350
406
  export const assign = (id, to) =>
351
407
  call(`/v1/guides/${encodeURIComponent(id)}/assign`, { method: "POST", body: { to } });
352
408
  export const take = (body) => call("/v1/take", { method: "POST", body });
package/src/mcp.js CHANGED
@@ -298,7 +298,8 @@ export function buildServer() {
298
298
  "is a spelling that is not one of the three. " +
299
299
  `kind: task is work nobody has done yet. Sections: ${TASK_SECTIONS.join(", ")}. ` +
300
300
  "It has no Steps: work out how to reach Goal within Constraints, leave Out of scope " +
301
- "alone, and treat Acceptance as the definition of done. hand_in with `markdown`: a " +
301
+ "alone, and treat Acceptance as the definition of done. New tasks say `status: draft`; " +
302
+ "a proposal awaiting a team decision stays Draft until a person makes it Ready. hand_in with `markdown`: a " +
302
303
  "transfer guide about what you did (guide_template kind transfer shows the shape) — its " +
303
304
  "author reviews it against Acceptance.\n" +
304
305
  `kind: transfer (or no kind: line) is a finished implementation to repeat here. Sections: ${SECTIONS.join(", ")}. ` +
@@ -343,6 +344,19 @@ export function buildServer() {
343
344
  "the user asks what they have been working on, or wants a standup or a summary of a " +
344
345
  "period, call log — but say that it holds what they passed along and not everything they " +
345
346
  "did. Gotchas are the highest-value section: record what failed and why.\n" +
347
+ "FOLDERS ARE REUSABLE PROJECT CONTEXT, NOT GUIDES. When the user asks for a folder for a " +
348
+ "script, brief, assets or work spanning several guides, call list_folders first to avoid " +
349
+ "making a duplicate. If none fits, call create_folder with a title and short description. " +
350
+ "A short request is enough: name it from the person's purpose and draft the first Markdown " +
351
+ "document with create_folder_document. Do not leave an empty folder or make them fill a form. " +
352
+ "Pass `team` only when the user wants it shared with that team; use the slug they named " +
353
+ "or one returned by list_folders, and omit it for a private folder. Use the returned " +
354
+ "folder id with create_folder_document to add Markdown, get_folder to see its contents, " +
355
+ "get_folder_document to read text and its version, and save_folder_document with that " +
356
+ "version when editing. On a stale-version refusal, reread before retrying so another " +
357
+ "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" +
346
360
  "A FOLLOW-UP IS MORE CONTEXT FOR A GUIDE, WRITTEN AS ITS OWN GUIDE. When a guide needs " +
347
361
  "more context — a missing detail, a step that needed explaining, what changed since, what " +
348
362
  "you found doing it — publish that context with publish_guide `parent` set to the guide's " +
@@ -490,7 +504,7 @@ export function buildServer() {
490
504
  annotations: ADDS,
491
505
  description:
492
506
  "Reassign a guide or task the user wrote, or one assigned to them, to someone else in its team, when the user asks: " +
493
- '`to` is @handle for one person, #group for the people who do a thing, or "team" for ' +
507
+ '`to` is @handle for one person, "@ada,@bola" for several named people, #group for the people who do a thing, or "team" for ' +
494
508
  "everyone. Whoever held it and is left out has it taken back and is told; a task then only " +
495
509
  "goes to the new assignee's agents. Its author, or whoever it is assigned to, can do this.",
496
510
  inputSchema: {
@@ -507,6 +521,181 @@ export function buildServer() {
507
521
  },
508
522
  );
509
523
 
524
+ server.registerTool(
525
+ "list_folders",
526
+ {
527
+ title: "List project folders",
528
+ 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') },
531
+ },
532
+ async ({ scope }) => {
533
+ try {
534
+ return json(await api.folders(scope || "all"));
535
+ } catch (err) {
536
+ return fail(err);
537
+ }
538
+ },
539
+ );
540
+ server.registerTool(
541
+ "create_folder",
542
+ {
543
+ title: "Create project folder",
544
+ annotations: ADDS,
545
+ 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.",
547
+ inputSchema: {
548
+ title: z.string().describe("Name of the project or body of work"),
549
+ description: z.string().optional().describe("Short purpose of the folder"),
550
+ team: z.string().optional().describe("Team slug; omit for a private folder"),
551
+ },
552
+ },
553
+ async ({ title, description, team }) => {
554
+ try {
555
+ return json(await api.createFolder(title, description || "", team || ""));
556
+ } catch (err) {
557
+ return fail(err);
558
+ }
559
+ },
560
+ );
561
+ server.registerTool(
562
+ "get_folder",
563
+ {
564
+ title: "Open project folder",
565
+ annotations: READS,
566
+ description: "List a folder's documents, assets and linked guides.",
567
+ inputSchema: { folder: z.string() },
568
+ },
569
+ async ({ folder }) => {
570
+ try {
571
+ return json(await api.folder(folder));
572
+ } catch (err) {
573
+ return fail(err);
574
+ }
575
+ },
576
+ );
577
+ server.registerTool(
578
+ "get_folder_document",
579
+ {
580
+ title: "Read folder document",
581
+ annotations: READS,
582
+ description: "Read the full Markdown and version before editing it.",
583
+ inputSchema: { folder: z.string(), document: z.string() },
584
+ },
585
+ async ({ folder, document }) => {
586
+ try {
587
+ return json(await api.folderDocument(folder, document));
588
+ } catch (err) {
589
+ return fail(err);
590
+ }
591
+ },
592
+ );
593
+ server.registerTool(
594
+ "get_folder_asset",
595
+ {
596
+ title: "Read folder asset",
597
+ annotations: READS,
598
+ description:
599
+ "Read an image or small text file from a folder into this agent's context. Other file types stay private downloads.",
600
+ inputSchema: { folder: z.string(), asset: z.string() },
601
+ },
602
+ async ({ folder, asset }) => {
603
+ try {
604
+ const file = await api.folderAsset(folder, asset);
605
+ const { data, text: body, ...meta } = file;
606
+ return {
607
+ content: data
608
+ ? [
609
+ { type: "text", text: file.name },
610
+ { type: "image", data, mimeType: file.type },
611
+ ]
612
+ : [{ type: "text", text: `${file.name}\n\n${body || ""}` }],
613
+ structuredContent: meta,
614
+ };
615
+ } catch (err) {
616
+ return fail(err);
617
+ }
618
+ },
619
+ );
620
+
621
+ server.registerTool(
622
+ "add_folder_asset",
623
+ {
624
+ title: "Add file to project folder",
625
+ annotations: { ...ADDS, openWorldHint: true },
626
+ description:
627
+ "Add a screenshot, reference file or other asset from this machine to a folder. Use a real file path you can read; the bytes are uploaded directly, not copied into a guide. Images, PDF, ZIP and text are accepted up to 10 MB.",
628
+ inputSchema: {
629
+ folder: z.string().describe("folder id returned by create_folder or list_folders"),
630
+ file: z.string().describe("path to the file on this machine"),
631
+ name: z.string().optional().describe("label in the folder; defaults to the filename"),
632
+ },
633
+ },
634
+ async ({ folder, file, name }) => {
635
+ try {
636
+ return json(await api.addFolderAsset(folder, file, name || ""));
637
+ } catch (err) {
638
+ return fail(err);
639
+ }
640
+ },
641
+ );
642
+
643
+ server.registerTool(
644
+ "create_folder_document",
645
+ {
646
+ title: "Write new folder document",
647
+ annotations: ADDS,
648
+ description: "Add Markdown such as a brief or script to a folder.",
649
+ inputSchema: { folder: z.string(), name: z.string(), body: z.string() },
650
+ },
651
+ async ({ folder, name, body }) => {
652
+ try {
653
+ return json(await api.createFolderDocument(folder, name, body));
654
+ } catch (err) {
655
+ return fail(err);
656
+ }
657
+ },
658
+ );
659
+ server.registerTool(
660
+ "save_folder_document",
661
+ {
662
+ title: "Save folder document",
663
+ annotations: ADDS,
664
+ description:
665
+ "Save a new revision. Send the version returned when you read it; a newer edit is refused.",
666
+ inputSchema: {
667
+ folder: z.string(),
668
+ document: z.string(),
669
+ version: z.number().int().positive(),
670
+ body: z.string(),
671
+ },
672
+ },
673
+ async ({ folder, document, version, body }) => {
674
+ try {
675
+ return json(await api.saveFolderDocument(folder, document, version, body));
676
+ } catch (err) {
677
+ return fail(err);
678
+ }
679
+ },
680
+ );
681
+ server.registerTool(
682
+ "link_folder_guide",
683
+ {
684
+ title: "Link guide to folder",
685
+ annotations: ADDS,
686
+ description:
687
+ "Put an existing task or handoff beside the folder documents and assets it uses.",
688
+ inputSchema: { folder: z.string(), guide: z.string() },
689
+ },
690
+ async ({ folder, guide }) => {
691
+ try {
692
+ return json(await api.linkFolderGuide(folder, guide));
693
+ } catch (err) {
694
+ return fail(err);
695
+ }
696
+ },
697
+ );
698
+
510
699
  server.registerTool(
511
700
  "search_guides",
512
701
  {
@@ -1160,7 +1349,9 @@ export function buildServer() {
1160
1349
  to: z
1161
1350
  .string()
1162
1351
  .optional()
1163
- .describe("team slug, team/@handle for one teammate, or team/#group for a set of them"),
1352
+ .describe(
1353
+ "team slug, team/@handle for one teammate, team/@ada,@bola for several named teammates, or team/#group for a set of them",
1354
+ ),
1164
1355
  cwd: z
1165
1356
  .string()
1166
1357
  .optional()
@@ -1233,7 +1424,9 @@ export function buildServer() {
1233
1424
  to: z
1234
1425
  .string()
1235
1426
  .optional()
1236
- .describe("team slug, team/@handle for one teammate, or team/#group for a set of them"),
1427
+ .describe(
1428
+ "team slug, team/@handle for one teammate, team/@ada,@bola for several named teammates, or team/#group for a set of them",
1429
+ ),
1237
1430
  issues: z
1238
1431
  .array(
1239
1432
  z.object({
package/src/passalong.js CHANGED
@@ -523,6 +523,19 @@ export async function assign(ref, to) {
523
523
  return r;
524
524
  }
525
525
 
526
+ /**
527
+ * Take back something you sent: it is private again and gone from every inbox it was in. Refused
528
+ * while somebody is on it. Whoever already opened it keeps what they pulled. The local copy is
529
+ * dropped so the next read shows it without the address.
530
+ */
531
+ export async function recall(ref) {
532
+ needsSync("taking a guide back");
533
+ const id = await resolveId(ref);
534
+ const r = await api.recall(id);
535
+ store.remove(id);
536
+ return r;
537
+ }
538
+
526
539
  /** Not this agent's to do: what it held is open again, and `why` goes to whoever is next. */
527
540
  export async function pass(id, why, { cwd = process.cwd() } = {}) {
528
541
  needsSync("passing work");