passalong 0.16.0 → 0.18.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/README.md CHANGED
@@ -28,25 +28,24 @@ codex mcp add passalong -- passalong mcp # Codex: the same MCP server
28
28
  ## Commands
29
29
 
30
30
  ```
31
- passalong share [file] [--to team[/handle]] publish a guide → id + link; --to hands it to a team or teammate
32
- passalong pull <id|link> fetch a guide into ./.passalong/ (git-ignored) and print it
31
+ passalong share [file] [--to team[/handle]] publish a guide as a draft → id + link; --to addresses it
32
+ passalong pull <id|link> print a guide; --save also writes ./.passalong/<id>.md (git-ignored)
33
33
  passalong inbox guides handed to you that you have not pulled yet
34
34
  passalong board waiting on you, in flight, landed, worth keeping
35
35
  passalong activity [--all] what happened while you were away; clears unless --all
36
- passalong list [query] your guides and your teams', local and synced
36
+ passalong list [query] your guides and your teams'
37
37
  passalong open <id> [--print] view a guide in the browser (or the terminal)
38
38
  passalong hub open your synced guides in the browser
39
- passalong works <id> <shot> you tried it and it holds up, with a screenshot of it working
40
- passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
41
39
  passalong archive <id> off your board and out of the free tier's count (was `done`)
42
40
  passalong promote <id> deprecated: mark promoted
43
41
  passalong rm <id> delete a guide locally and from sync
44
42
  passalong export [dir] dump every guide as plain markdown
45
- passalong login [token] sign in from the browser, or attach this machine with a token
43
+ passalong login sign in from the browser
44
+ passalong login --with-token read a token from stdin instead (CI, scripts)
46
45
  passalong login --new add another account to this machine
47
46
  passalong accounts the accounts signed in here, and which is the default
48
47
  passalong use <name> make one of them the default
49
- passalong logout <name> forget one of them
48
+ passalong logout [name] forget one of them (default: the current one)
50
49
  passalong me [--handle H] [--name N] [--email E] who you are to teammates
51
50
  passalong team current team and its members
52
51
  passalong team create <name> start a team (you become its owner)
@@ -74,11 +73,10 @@ passalong inbox # on Ada's side: what was handed to you
74
73
  passalong activity # back on your side: it landed, and who pulled it
75
74
  ```
76
75
 
77
- A receiver who tries the work reports back with `passalong works <id> <screenshot.png>` or
78
- `passalong broken <id> <why>`. That is separate from `done`, which means *implemented*: a verdict
79
- means it actually runs. A passing one has to show it — at least one screenshot of it working, kept
80
- until 5 days after the guide is closed — a failing one has to say why, and the author sees it on
81
- their board and in their inbox.
76
+ Every guide, whatever its kind, moves the same way: it is a draft until a person marks it Ready
77
+ (`passalong ready <id>`), then whoever takes it (`passalong take <id>`) holds it, hands it in with
78
+ the evidence, and a person approves it or sends it back with the reason (`passalong approve` /
79
+ `passalong reject`).
82
80
 
83
81
  Sign in at [the hub](https://passalong.dev/hub) with an email and password, and mint
84
82
  an API token there for the CLI and MCP servers — named, revocable, and shown once. Your password
@@ -102,10 +100,10 @@ promote them into the team's small set of maintained references.
102
100
 
103
101
  Tools: `search_guides`, `inbox`, `get_guide`, `publish_guide` (with `to`), `guide_template`, `set_guide_status`.
104
102
 
105
- ## Guides are files
103
+ ## Guides are plain markdown
106
104
 
107
- Plain markdown with frontmatter in `~/.passalong/guides`. Yours to edit, grep, and commit.
108
- `passalong export` dumps everything. Works with no account; `passalong login` adds sync and share links.
105
+ Plain markdown with frontmatter, held on the server; nothing piles up on your machine. Ask for a
106
+ file when you want one: `passalong pull <id> --save`, or `passalong export` for every guide.
109
107
 
110
108
  Point at a self-hosted server with `PASSALONG_API=https://your-host` before `passalong login`.
111
109
 
package/bin/passalong CHANGED
@@ -7,25 +7,25 @@
7
7
  // TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
8
8
  // thing — quote that one, since a shell may read the # as a comment
9
9
  // passalong task "<what needs doing>" write a task for an agent; it waits in Draft
10
- // passalong ready <id...|--all> tasks you have read are ready — an agent may take them now
10
+ // passalong ready <id...|--all> guides you have read are ready — whoever they are for may take them now
11
11
  // passalong tasks every task and where it is: draft, ready, claimed, stalled, review
12
12
  // passalong work [--once] [--agent CMD] run an agent on each ready task here, one after another
13
- // passalong approve <id> the work in review is done
14
- // passalong reject <id> <why> not done: back to Ready, and the next agent reads why
13
+ // passalong approve <id> the work in review is done, of any kind
14
+ // passalong reject <id> <why> not done: back to Ready, and whoever takes it next reads why
15
15
  // passalong release <id> take work back from whoever holds it, of any kind
16
16
  // passalong attach <file> upload a picture or a file and print the markdown that points at it
17
17
  // passalong reply <id> <text> write to whoever holds it (--file PATH to attach); or leave a note
18
- // passalong pull <id|link> fetch a guide into ./.passalong/ and print it
19
- // passalong start <id|link> pull it and take it — one command for "I am doing this"
18
+ // passalong pull <id|link> [--save] print a guide; --save also writes ./.passalong/<id>.md
19
+ // passalong start <id|link> [--save] the same as take, by link as well as id
20
20
  // passalong inbox guides handed to you that you have not pulled yet
21
21
  // passalong board waiting on you, in flight, landed, worth keeping
22
22
  // passalong activity [--all] what happened while you were away; clears unless --all
23
23
  // passalong log [repo] what you did, newest first, by month
24
- // passalong list [query] your guides and your teams', local and synced
24
+ // passalong list [query] your guides and your teams'
25
25
  // passalong open <id> [--print] view a guide in the browser (or the terminal)
26
26
  // passalong hub open your synced guides in the browser
27
- // passalong take <id> you are doing it — said before the work, so the sender stops guessing
28
- // passalong pass <id> <why> it is not yours — hands it back to its author with your reason
27
+ // passalong take <id> [--save] you are doing it: nobody else can take it while you hold it
28
+ // passalong pass <id> <why> it is not yours, or you are stuck — hands it back with your reason
29
29
  // passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team" — yours, or assigned to you
30
30
  // passalong recall <id> take back something you sent: private again, gone from their inboxes
31
31
  // passalong admin create <email> make an account that runs Passalong (owner only)
@@ -35,16 +35,15 @@
35
35
  // give Solo to @handle, or Team to team/slug, until a date (operator only)
36
36
  // passalong gifts what has been given away, and what is still running
37
37
  // passalong ungift <gift id> take one back now
38
- // passalong works <id> <shot.png...> you tried it and it holds up — with a screenshot of it working
39
- // passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
40
38
  // passalong archive <id> off your board and out of the free tier's count (was `done`)
41
39
  // passalong rm <id> delete a guide locally and from sync
42
40
  // passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
43
- // passalong login [token] sign in from the browser, or attach this machine with a token
41
+ // passalong login sign in from the browser
42
+ // passalong login --with-token read a token from stdin instead (CI, scripts)
44
43
  // passalong login --new add another account to this machine
45
44
  // passalong accounts the accounts signed in here, and which is the default
46
45
  // passalong use <name> make one of them the default
47
- // passalong logout <name> forget one of them
46
+ // passalong logout [name] forget one of them (default: the current one)
48
47
  // passalong me [--handle H] [--name N] [--email E] who you are to teammates
49
48
  // passalong team current team and its members
50
49
  // passalong team create <name> start a team (you become its owner)
@@ -65,7 +64,7 @@ import { dirname, join, resolve } from "node:path";
65
64
  import { fileURLToPath } from "node:url";
66
65
  import * as api from "../src/api.js";
67
66
  import { scaffold, taskScaffold } from "../src/capture.js";
68
- import { serialize } from "../src/guide.js";
67
+ import { parse, serialize } from "../src/guide.js";
69
68
  import * as passalong from "../src/passalong.js";
70
69
  import * as store from "../src/store.js";
71
70
 
@@ -167,75 +166,20 @@ async function browserLogin() {
167
166
  const got = await api.devicePoll(start.id, verifier).catch((e) => die(e.message));
168
167
  if (!got) continue;
169
168
  await keepLogin(got.token);
170
- console.error("Attach other machines with:");
171
- console.error(` passalong login ${got.token}`);
172
- console.error("(keep it private — it is a credential, not a password)");
173
169
  console.error("Give teammates a name to address you by: passalong me --handle <you>");
174
170
  return;
175
171
  }
176
172
  die("nobody approved it in time. Run `passalong login` again.");
177
173
  }
178
174
 
179
- /** The old way, kept for anyone who asks for it with `--email`: an email and a password, typed here. */
180
- async function passwordLogin(f) {
181
- // An account you cannot sign in to is one that dies with this file. This used to mint one
182
- // silently and then suggest adding a password afterwards; it asks up front instead.
183
- if (!tty)
184
- die(
185
- "no terminal to ask on. Sign in at " +
186
- `${api.baseUrl()}/hub, then attach this machine with \`passalong login <token>\``,
187
- );
188
-
189
- const email = f.email || (await ask("Email: "));
190
- if (!email) die("an email is needed to sign in");
191
- const password = await ask("Password: ", { hidden: true });
192
- if (!password) die("a password is needed to sign in");
193
-
194
- const label = `${hostname()} cli`;
195
- let who = await api.signIn(email, password, { label }).catch((e) => e);
196
- if (who instanceof Error) {
197
- // 401 is "no such account, or the wrong password", said the same way on purpose so the API
198
- // does not confirm which addresses exist. The CLI has to ask rather than guess, because
199
- // creating an account for a mistyped address is worse than being told to try again.
200
- if (who.status !== 401) die(who.message);
201
- const yes = (await ask(`No account signed in as ${email}. Create one? [y/N] `)).toLowerCase();
202
- if (yes !== "y" && yes !== "yes")
203
- die("not signed in. If the password was wrong, try again, or reset it at " + `${api.baseUrl()}/hub`);
204
- const again = await ask("Choose a password: ", { hidden: true });
205
- if (again !== password) die("those did not match");
206
- who = await api.signIn(email, password, { make: true, label }).catch((e) => die(e.message));
207
- }
208
-
209
- await keepLogin(who.token);
210
- console.error("Attach other machines with:");
211
- console.error(` passalong login ${who.token}`);
212
- console.error("(keep it private — it is a credential, not a password)");
213
- console.error("Give teammates a name to address you by: passalong me --handle <you>");
214
- }
215
-
216
- async function ask(question, { hidden = false } = {}) {
217
- const { createInterface } = await import("node:readline");
218
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
219
- if (hidden) {
220
- // readline still writes the prompt itself; everything after it is swallowed. The newline the
221
- // user's Return would have echoed is put back by hand, or the next line starts mid-prompt.
222
- rl.output.write(question);
223
- rl._writeToOutput = () => {};
224
- }
225
- try {
226
- // `close` as well as an answer: stdin reaching EOF part-way through — a pipe, a closed pty, a
227
- // ^D — never calls the question's callback, and without this the promise is simply never
228
- // settled and the process exits on a warning about it rather than saying what happened.
229
- const answer = await new Promise((done) => {
230
- rl.on("close", () => done(null));
231
- rl.question(hidden ? "" : question, done);
232
- });
233
- if (hidden) process.stdout.write("\n");
234
- if (answer === null) die("nothing to read from — sign in at the terminal, or pass a token");
235
- return answer.trim();
236
- } finally {
237
- rl.close();
238
- }
175
+ /** A token piped in, for machines with no browser: `passalong login --with-token < token.txt`. */
176
+ async function stdinLogin() {
177
+ if (process.stdin.isTTY) die("--with-token reads the token from stdin: passalong login --with-token < token.txt");
178
+ let raw = "";
179
+ for await (const chunk of process.stdin) raw += chunk;
180
+ const token = raw.trim();
181
+ if (!token) die("no token on stdin");
182
+ await keepLogin(token);
239
183
  }
240
184
 
241
185
  function edit(path) {
@@ -360,7 +304,7 @@ const commands = {
360
304
  if (tty && !f["no-edit"]) edit(path);
361
305
  const to = typeof f.to === "string" ? f.to : undefined;
362
306
  const follows = typeof f.follows === "string" ? f.follows : undefined;
363
- const { guide, url, synced, notified, path: saved } = await passalong
307
+ const { guide, url, notified } = await passalong
364
308
  .share(readFileSync(path, "utf8"), { to, follows })
365
309
  .catch((e) => die(e.message));
366
310
  // Any draft that went out is spent, however it was named — a task that failed validation is
@@ -368,14 +312,13 @@ const commands = {
368
312
  // removeDraft() touches nothing outside the drafts directory.
369
313
  store.removeDraft(resolve(path));
370
314
  console.log(guide.meta.id);
371
- console.error(`shared "${guide.meta.title}" → ${saved}`);
315
+ console.error(`shared "${guide.meta.title}"`);
372
316
  if (guide.meta.parent) console.error(`follows ${guide.meta.parent}`);
373
317
  if (guide.meta.team)
374
318
  console.error(
375
319
  `handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
376
320
  );
377
- if (synced) console.error(`link: ${url}`);
378
- else console.error("not synced (run `passalong login` to sync across machines)");
321
+ console.error(`link: ${url}`);
379
322
  },
380
323
 
381
324
  /**
@@ -387,7 +330,7 @@ const commands = {
387
330
  const path = store.draftPath(`task-${new Date().toISOString().replace(/[:.]/g, "-")}`);
388
331
  writeFileSync(path, taskScaffold(sentence));
389
332
  if (tty && !f["no-edit"]) edit(path);
390
- const { guide, synced } = await passalong
333
+ const { guide } = await passalong
391
334
  .share(readFileSync(path, "utf8"))
392
335
  .catch((e) => die(`${e.message}\ndraft kept at ${path} — fix it and run \`passalong share ${path}\``));
393
336
  store.removeDraft(path);
@@ -397,7 +340,6 @@ const commands = {
397
340
  console.error(`task "${guide.meta.title}" is in Draft${target ? ` for ${target}` : " (no repo)"}`);
398
341
  if (guide.meta.team) console.error(`handed to ${guide.meta.team} (team inbox)`);
399
342
  console.error(`when an agent may take it: passalong ready ${id}`);
400
- if (!synced) console.error("not synced (run `passalong login` — agents elsewhere cannot see it)");
401
343
  },
402
344
 
403
345
  async tasks() {
@@ -480,31 +422,30 @@ const commands = {
480
422
 
481
423
  async pull(f) {
482
424
  const ref = f._[0] || die("usage: passalong pull <id|link>");
483
- const { guide, markdown, path, from } = await passalong.pull(ref).catch((e) => die(e.message));
425
+ const { guide, markdown, path } = await passalong
426
+ .pull(ref, { save: Boolean(f.save) })
427
+ .catch((e) => die(e.message));
484
428
  process.stdout.write(markdown);
485
- console.error(`\npulled from ${from} → ${path}`);
429
+ if (path) console.error(`\nsaved to ${path}`);
486
430
  await listFollowUps(guide.meta);
487
431
  },
488
432
 
489
433
  /**
490
- * Pull and take in one command. `pull` says it arrived, `take` says somebody is doing it, and
491
- * they stay separate because reading a guide to decide it is not yours is what `pass` is for.
492
- * This is the pair for the case where you already know: you are picking this up.
434
+ * Take it: you are doing it, and nobody else can take it while you hold it. The guide is printed,
435
+ * and `--save` also writes it to ./.passalong/. Every kind is taken the same way, and `start` is
436
+ * the same command by its older name, which also takes a share link.
493
437
  */
494
438
  async start(f) {
495
- const ref = f._[0] || die("usage: passalong start <id|link>");
496
- const r = await passalong.start(ref).catch((e) => die(e.message));
439
+ const ref = f._[0] || die("usage: passalong take <id|link>");
440
+ const r = await passalong.start(ref, { save: Boolean(f.save) }).catch((e) => die(e.message));
441
+ if (!r.guide?.markdown) return console.error(r.say || "nothing to take");
497
442
  process.stdout.write(r.markdown);
498
- console.error(`\npulled from ${r.from} → ${r.path}`);
499
- if (r.took) console.error("you are on it, and the author has been told");
500
- else if (r.own) console.error("your own guide — nothing to take");
501
- else if (r.ack_error) console.error(`not taken (${r.ack_error}) — run \`passalong take ${r.guide.meta.id}\``);
502
- // Saying nothing here would be the silence the whole hop exists to end: you would have the
503
- // guide, believe you had answered for it, and the sender would still be waiting.
504
- else console.error("not taken — sync is off, so nobody was told (run `passalong login`)");
505
- await listFollowUps(r.guide.meta);
443
+ if (r.path) console.error(`\nsaved to ${r.path}`);
444
+ console.error(`\nyou hold ${r.guide.id} — hand it in when it is done, or pass it with the reason`);
445
+ await listFollowUps(parse(r.markdown).meta);
506
446
  },
507
447
 
448
+
508
449
  /**
509
450
  * Upload an image and print the markdown that points at it. Evidence goes *in* the document:
510
451
  * a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
@@ -612,7 +553,6 @@ const commands = {
612
553
  async list(f) {
613
554
  const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
614
555
  const rows = await passalong.list(f._.join(" "), { scope });
615
- if (rows.warning) console.error(`warning: ${rows.warning}`);
616
556
  if (!rows.length)
617
557
  return console.error(
618
558
  "no guides yet — run `passalong share` after your next finished piece of work",
@@ -644,17 +584,8 @@ const commands = {
644
584
  } else console.log(url);
645
585
  },
646
586
 
647
- /**
648
- * The first word back, before any of the work.
649
- *
650
- * Between a handoff and a pull the sender had no signal at all: an unanswered guide looks
651
- * exactly like one nobody has noticed. These two are the hop the MCP tool and the hub already
652
- * had and a terminal did not.
653
- */
654
587
  async take(f) {
655
- const id = f._[0] || die("usage: passalong take <id>");
656
- const r = await passalong.ack(id, true).catch((e) => die(e.message));
657
- console.error(`${r.id} — you are on it, and the author has been told`);
588
+ return commands.start(f);
658
589
  },
659
590
 
660
591
  async pass(f) {
@@ -663,10 +594,11 @@ const commands = {
663
594
  // Required, and the reason is the whole point: "not me" without "why" leaves the sender where
664
595
  // the silence did, still guessing whether to re-address it, wait, or go and ask.
665
596
  if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
666
- const r = await passalong.ack(id, false, why).catch((e) => die(e.message));
667
- console.error(`${r.id} handed back — the author has been told: "${r.note}"`);
597
+ await passalong.pass(id, why).catch((e) => die(e.message));
598
+ console.error(`${id} handed back with your reason: "${why}"`);
668
599
  },
669
600
 
601
+
670
602
  async assign(f) {
671
603
  const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
672
604
  const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
@@ -701,7 +633,7 @@ const commands = {
701
633
  console.log(`token ${r.token}`);
702
634
  console.log(`password link ${r.password_url}`);
703
635
  console.error(
704
- `\n${r.email} is an admin. The token is for \`passalong login\`, the link sets their ` +
636
+ `\n${r.email} is an admin. The token is for \`passalong login --with-token\`, the link sets their ` +
705
637
  `password for the hub, and it expires ${r.expires.slice(0, 10)}. Neither is shown again.`,
706
638
  );
707
639
  return;
@@ -755,23 +687,6 @@ const commands = {
755
687
  console.error(`${id} taken back — that plan is lapsed as of now`);
756
688
  },
757
689
 
758
- async works(f) {
759
- const id = f._[0] || die("usage: passalong works <id> <screenshot.png...> [what you checked]");
760
- const { images, note } = passalong.proofArgs(f._.slice(1));
761
- const r = await passalong.verdict(id, true, note, { images }).catch((e) => die(e.message));
762
- console.error(
763
- `${r.id} verified with ${images.length || "your"} screenshot${images.length === 1 ? "" : "s"} — the author has been told`,
764
- );
765
- },
766
-
767
- async broken(f) {
768
- const id = f._[0] || die("usage: passalong broken <id> <what happened>");
769
- const why = f._.slice(1).join(" ");
770
- if (!why) die("say what went wrong: passalong broken <id> <what happened>");
771
- const r = await passalong.verdict(id, false, why).catch((e) => die(e.message));
772
- console.error(`${r.id} reported as not working — the author has been told: "${r.note}"`);
773
- },
774
-
775
690
  /**
776
691
  * Archive a guide: off your board, and out of what the free tier counts.
777
692
  *
@@ -806,29 +721,26 @@ const commands = {
806
721
 
807
722
  async export(f) {
808
723
  const dir = resolve(f._[0] || "passalong-export");
809
- const paths = passalong.exportAll(dir);
724
+ const paths = await passalong.exportAll(dir).catch((e) => die(e.message));
810
725
  console.error(`exported ${paths.length} guide${paths.length === 1 ? "" : "s"} → ${dir}`);
811
726
  },
812
727
 
813
728
  async login(f) {
814
- const given = f._[0];
815
- if (given) {
816
- await keepLogin(given);
817
- return;
818
- }
729
+ // A token on the command line lands in shell history, so it is only read from stdin.
730
+ if (f._[0]) die("pass a token on stdin instead: passalong login --with-token < token.txt");
731
+ if (f.email || f.password)
732
+ die("password sign-in is gone: run `passalong login` and approve it in the browser");
733
+ if (f["with-token"]) return stdinLogin();
819
734
  if (api.loggedIn() && !f.new) {
820
735
  const who = await api.me().catch(() => null);
821
736
  if (who) {
822
- console.error(`already logged in (account ${who.account}). Attach another machine with:`);
823
- console.error(` passalong login ${api.token()}`);
737
+ console.error(`already logged in as ${who.handle ? `@${who.handle}` : who.email || who.account}.`);
824
738
  console.error("Add another account to this machine with `passalong login --new`.");
825
739
  return;
826
740
  }
827
741
  }
828
- // Passwords are for someone who asks: `--email` keeps the prompts. Everyone else is sent to the
829
- // browser, which works with no terminal at all — an agent can start it, and the person it is
742
+ // The browser works with no terminal at all — an agent can start it, and the person it is
830
743
  // working for clicks once.
831
- if (f.email || f.password) return passwordLogin(f);
832
744
  return browserLogin();
833
745
  },
834
746
 
@@ -865,7 +777,7 @@ const commands = {
865
777
 
866
778
  /** Forget one of them on this machine. The account itself is untouched: only its token here goes. */
867
779
  async logout(f) {
868
- const name = f._[0] || die("usage: passalong logout <name> (see `passalong accounts`)");
780
+ const name = f._[0] || store.readConfig().active || die("not logged in (see `passalong accounts`)");
869
781
  const r = store.removeAccount(name);
870
782
  if (!r.removed) die(`no account called "${name}" here`);
871
783
  console.error(
@@ -1135,6 +1047,8 @@ else if (!commands[cmd])
1135
1047
  "if you expected this command, `npm i -g passalong` for the newest one.",
1136
1048
  );
1137
1049
  else {
1050
+ // Once per machine; a failure leaves it for the next command rather than failing this one.
1051
+ await passalong.tidy().catch(() => {});
1138
1052
  await commands[cmd](argv).catch((e) => die(e.message));
1139
1053
  // After the command, never before it: a version check is not worth a millisecond of somebody's
1140
1054
  // first impression, and it must never be the reason a command felt slow. `mcp` is a server that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passalong",
3
- "version": "0.16.0",
3
+ "version": "0.18.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
@@ -66,13 +66,13 @@ machine, another agent session, or a teammate without your branch.
66
66
  whoever opens the original, person or agent. It travels the other way too — whoever opens the
67
67
  follow-up gets the guide it came out of, and where that guide has got to — so write it as a
68
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
- 6. **Report** the id (and link if synced) and one line on how to use it on the other side:
72
- `passalong start <id>` in the target repo, or "start passalong <id>" to an agent with the
73
- Passalong MCP server. `start` rather than `pull` on purpose: it takes the handoff as well as
74
- fetching it, and until somebody says they are on it the sender cannot tell a guide nobody
75
- noticed from one somebody is deep in.
69
+ guide belongs in its `hand_in`, not a follow-up.
70
+ 6. **Report** the id (and link) and say it is a **draft**: it reaches nobody until the person reads
71
+ it and marks it Ready (`passalong ready <id>`, or Ready in the hub). Then one line on how to use
72
+ it on the other side: `passalong take <id>` in the target repo, or "take passalong <id>" to an
73
+ agent with the Passalong MCP server. `take` rather than `pull` on purpose: it holds the work as
74
+ well as fetching it, and until somebody takes it the sender cannot tell a guide nobody noticed
75
+ from one somebody is deep in.
76
76
 
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.
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,36 @@ 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 moveFolderDocument = (folderId, documentId, to, name = "") =>
341
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
342
+ method: "PATCH",
343
+ body: name ? { folder: to, name } : { folder: to },
344
+ });
345
+ export const folderDocumentHistory = (folderId, documentId, version = 0) =>
346
+ call(
347
+ `/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
348
+ );
349
+ export const shareFolder = (folderId) =>
350
+ call(`/v1/folders/${encodeURIComponent(folderId)}/share`, { method: "POST" });
332
351
  export const linkFolderGuide = (folderId, guide) =>
333
352
  call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
353
+ export const unlinkFolderGuide = (folderId, guide) =>
354
+ call(`/v1/folders/${encodeURIComponent(folderId)}/guides/${encodeURIComponent(guide)}`, {
355
+ method: "DELETE",
356
+ });
357
+ export const deleteFolderDocument = (folderId, documentId) =>
358
+ call(`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}`, {
359
+ method: "DELETE",
360
+ });
361
+ export const deleteFolderAsset = (folderId, assetId) =>
362
+ call(`/v1/folders/${encodeURIComponent(folderId)}/assets/${encodeURIComponent(assetId)}`, {
363
+ method: "DELETE",
364
+ });
334
365
  /** What you did, newest first. `since` is a date prefix: 2026, 2026-09, 2026-09-11. */
335
366
  export const log = ({ repo = "", since = "", limit = 0 } = {}) =>
336
367
  call(`/v1/log${q({ repo, since, limit: limit || "" })}`);
@@ -348,7 +379,10 @@ export const inbox = () => call("/v1/inbox");
348
379
  export const createReport = (body) => call("/v1/reports", { method: "POST", body });
349
380
  /** One report and the issues filed under it, grouped by product area. */
350
381
  export const report = (id) => call(`/v1/reports/${encodeURIComponent(id)}`);
351
- export const get = (id) => call(`/v1/guides/${id}`, { raw: true });
382
+ export const get = (id, { peek = false } = {}) =>
383
+ call(`/v1/guides/${id}${q({ peek: peek ? "1" : "" })}`, { raw: true });
384
+ /** One page of every guide this account can read, for `export`. Records no pull. */
385
+ export const exportPage = (after = "") => call(`/v1/export${q({ after })}`);
352
386
  /**
353
387
  * A guide's follow-ups, one level down. `markdown` adds each one's content, oldest first and
354
388
  * capped server-side; neither form counts as opening them.
@@ -395,6 +429,11 @@ export const approveTask = (id) =>
395
429
  call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
396
430
  export const rejectTask = (id, why) =>
397
431
  call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
432
+ /** A handoff's or bug's approve and send-back. No `place` names the only hand-in there is. */
433
+ export const closeGuide = (id) =>
434
+ call(`/v1/guides/${encodeURIComponent(id)}/close`, { method: "POST", body: {} });
435
+ export const sendBack = (id, why) =>
436
+ call(`/v1/guides/${encodeURIComponent(id)}/send_back`, { method: "POST", body: { why } });
398
437
  /** Take work back from whoever holds it, of any kind. `/v1/tasks/…/release` is the old path. */
399
438
  export const release = (id) =>
400
439
  call(`/v1/guides/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
package/src/guide.js CHANGED
@@ -525,7 +525,8 @@ export function bugGuide({
525
525
  area = "",
526
526
  report = "",
527
527
  environment = "",
528
- status = "published",
528
+ // A draft like every new guide: it reaches people when a person marks it Ready.
529
+ status = "draft",
529
530
  evidence = [],
530
531
  } = {}) {
531
532
  /**