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 +13 -15
- package/bin/passalong +54 -140
- package/package.json +1 -1
- package/skill/SKILL.md +7 -7
- package/src/api.js +43 -4
- package/src/guide.js +2 -1
- package/src/mcp.js +245 -49
- package/src/passalong.js +116 -182
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
|
|
32
|
-
passalong pull <id|link>
|
|
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'
|
|
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
|
|
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
|
|
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
|
-
|
|
78
|
-
`passalong
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
103
|
+
## Guides are plain markdown
|
|
106
104
|
|
|
107
|
-
Plain markdown with frontmatter
|
|
108
|
-
|
|
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>
|
|
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
|
|
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>
|
|
19
|
-
// passalong start <id|link>
|
|
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'
|
|
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>
|
|
28
|
-
// passalong pass <id> <why> it is not yours — hands it back
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
180
|
-
async function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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,
|
|
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}"
|
|
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
|
-
|
|
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
|
|
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
|
|
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(`\
|
|
429
|
+
if (path) console.error(`\nsaved to ${path}`);
|
|
486
430
|
await listFollowUps(guide.meta);
|
|
487
431
|
},
|
|
488
432
|
|
|
489
433
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
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
|
|
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(`\
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
console.error(`${
|
|
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
|
-
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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
|
-
//
|
|
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("
|
|
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
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.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`passalong
|
|
73
|
-
Passalong MCP server. `
|
|
74
|
-
fetching it, and until somebody
|
|
75
|
-
|
|
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"
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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
|
-
|
|
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
|
/**
|