passalong 0.17.0 → 0.19.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 +71 -150
- package/package.json +1 -1
- package/skill/SKILL.md +6 -7
- package/src/api.js +18 -1
- package/src/guide.js +6 -4
- package/src/mcp.js +92 -49
- package/src/passalong.js +121 -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 → id + link, Ready; --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 Ready when shared (or a draft, with
|
|
77
|
+
`status: draft`, until a person runs `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
|
@@ -6,26 +6,25 @@
|
|
|
6
6
|
// handed to whoever pulls or starts that guide
|
|
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
|
-
// passalong task "<what needs doing>" write a task for an agent;
|
|
10
|
-
// passalong ready <id...|--all>
|
|
9
|
+
// passalong task "<what needs doing>" write a task for an agent; Ready at once
|
|
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
|
|
14
|
-
// passalong reject <id> <why> not done: back to Ready, and the next agent reads why
|
|
13
|
+
// passalong reopen <id> <why> finished work isn't right: back to To do, and whoever takes it next reads why
|
|
15
14
|
// passalong release <id> take work back from whoever holds it, of any kind
|
|
16
15
|
// passalong attach <file> upload a picture or a file and print the markdown that points at it
|
|
17
16
|
// 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>
|
|
17
|
+
// passalong pull <id|link> [--save] print a guide; --save also writes ./.passalong/<id>.md
|
|
18
|
+
// passalong start <id|link> [--save] the same as take, by link as well as id
|
|
20
19
|
// passalong inbox guides handed to you that you have not pulled yet
|
|
21
20
|
// passalong board waiting on you, in flight, landed, worth keeping
|
|
22
21
|
// passalong activity [--all] what happened while you were away; clears unless --all
|
|
23
22
|
// passalong log [repo] what you did, newest first, by month
|
|
24
|
-
// passalong list [query] your guides and your teams'
|
|
23
|
+
// passalong list [query] your guides and your teams'
|
|
25
24
|
// passalong open <id> [--print] view a guide in the browser (or the terminal)
|
|
26
25
|
// 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
|
|
26
|
+
// passalong take <id> [--save] you are doing it: nobody else can take it while you hold it
|
|
27
|
+
// passalong pass <id> <why> it is not yours, or you are stuck — hands it back with your reason
|
|
29
28
|
// passalong assign <id> <to> give it to @handle (or several: @ada @bola), a #group, or "team" — yours, or assigned to you
|
|
30
29
|
// passalong recall <id> take back something you sent: private again, gone from their inboxes
|
|
31
30
|
// passalong admin create <email> make an account that runs Passalong (owner only)
|
|
@@ -35,16 +34,15 @@
|
|
|
35
34
|
// give Solo to @handle, or Team to team/slug, until a date (operator only)
|
|
36
35
|
// passalong gifts what has been given away, and what is still running
|
|
37
36
|
// 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
37
|
// passalong archive <id> off your board and out of the free tier's count (was `done`)
|
|
41
38
|
// passalong rm <id> delete a guide locally and from sync
|
|
42
39
|
// passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
|
|
43
|
-
// passalong login
|
|
40
|
+
// passalong login sign in from the browser
|
|
41
|
+
// passalong login --with-token read a token from stdin instead (CI, scripts)
|
|
44
42
|
// passalong login --new add another account to this machine
|
|
45
43
|
// passalong accounts the accounts signed in here, and which is the default
|
|
46
44
|
// passalong use <name> make one of them the default
|
|
47
|
-
// passalong logout
|
|
45
|
+
// passalong logout [name] forget one of them (default: the current one)
|
|
48
46
|
// passalong me [--handle H] [--name N] [--email E] who you are to teammates
|
|
49
47
|
// passalong team current team and its members
|
|
50
48
|
// passalong team create <name> start a team (you become its owner)
|
|
@@ -65,7 +63,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
65
63
|
import { fileURLToPath } from "node:url";
|
|
66
64
|
import * as api from "../src/api.js";
|
|
67
65
|
import { scaffold, taskScaffold } from "../src/capture.js";
|
|
68
|
-
import { serialize } from "../src/guide.js";
|
|
66
|
+
import { parse, serialize } from "../src/guide.js";
|
|
69
67
|
import * as passalong from "../src/passalong.js";
|
|
70
68
|
import * as store from "../src/store.js";
|
|
71
69
|
|
|
@@ -167,75 +165,20 @@ async function browserLogin() {
|
|
|
167
165
|
const got = await api.devicePoll(start.id, verifier).catch((e) => die(e.message));
|
|
168
166
|
if (!got) continue;
|
|
169
167
|
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
168
|
console.error("Give teammates a name to address you by: passalong me --handle <you>");
|
|
174
169
|
return;
|
|
175
170
|
}
|
|
176
171
|
die("nobody approved it in time. Run `passalong login` again.");
|
|
177
172
|
}
|
|
178
173
|
|
|
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
|
-
}
|
|
174
|
+
/** A token piped in, for machines with no browser: `passalong login --with-token < token.txt`. */
|
|
175
|
+
async function stdinLogin() {
|
|
176
|
+
if (process.stdin.isTTY) die("--with-token reads the token from stdin: passalong login --with-token < token.txt");
|
|
177
|
+
let raw = "";
|
|
178
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
179
|
+
const token = raw.trim();
|
|
180
|
+
if (!token) die("no token on stdin");
|
|
181
|
+
await keepLogin(token);
|
|
239
182
|
}
|
|
240
183
|
|
|
241
184
|
function edit(path) {
|
|
@@ -360,7 +303,7 @@ const commands = {
|
|
|
360
303
|
if (tty && !f["no-edit"]) edit(path);
|
|
361
304
|
const to = typeof f.to === "string" ? f.to : undefined;
|
|
362
305
|
const follows = typeof f.follows === "string" ? f.follows : undefined;
|
|
363
|
-
const { guide, url,
|
|
306
|
+
const { guide, url, notified } = await passalong
|
|
364
307
|
.share(readFileSync(path, "utf8"), { to, follows })
|
|
365
308
|
.catch((e) => die(e.message));
|
|
366
309
|
// Any draft that went out is spent, however it was named — a task that failed validation is
|
|
@@ -368,14 +311,13 @@ const commands = {
|
|
|
368
311
|
// removeDraft() touches nothing outside the drafts directory.
|
|
369
312
|
store.removeDraft(resolve(path));
|
|
370
313
|
console.log(guide.meta.id);
|
|
371
|
-
console.error(`shared "${guide.meta.title}"
|
|
314
|
+
console.error(`shared "${guide.meta.title}"`);
|
|
372
315
|
if (guide.meta.parent) console.error(`follows ${guide.meta.parent}`);
|
|
373
316
|
if (guide.meta.team)
|
|
374
317
|
console.error(
|
|
375
318
|
`handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
|
|
376
319
|
);
|
|
377
|
-
|
|
378
|
-
else console.error("not synced (run `passalong login` to sync across machines)");
|
|
320
|
+
console.error(`link: ${url}`);
|
|
379
321
|
},
|
|
380
322
|
|
|
381
323
|
/**
|
|
@@ -387,17 +329,15 @@ const commands = {
|
|
|
387
329
|
const path = store.draftPath(`task-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
388
330
|
writeFileSync(path, taskScaffold(sentence));
|
|
389
331
|
if (tty && !f["no-edit"]) edit(path);
|
|
390
|
-
const { guide
|
|
332
|
+
const { guide } = await passalong
|
|
391
333
|
.share(readFileSync(path, "utf8"))
|
|
392
334
|
.catch((e) => die(`${e.message}\ndraft kept at ${path} — fix it and run \`passalong share ${path}\``));
|
|
393
335
|
store.removeDraft(path);
|
|
394
336
|
const id = guide.meta.id;
|
|
395
337
|
console.log(id);
|
|
396
338
|
const target = guide.meta.target_context;
|
|
397
|
-
console.error(`task "${guide.meta.title}" is
|
|
339
|
+
console.error(`task "${guide.meta.title}" is Ready${target ? ` for ${target}` : " (no repo)"}`);
|
|
398
340
|
if (guide.meta.team) console.error(`handed to ${guide.meta.team} (team inbox)`);
|
|
399
|
-
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
341
|
},
|
|
402
342
|
|
|
403
343
|
async tasks() {
|
|
@@ -442,16 +382,25 @@ const commands = {
|
|
|
442
382
|
if (n) console.error("review them with: passalong tasks");
|
|
443
383
|
},
|
|
444
384
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
console.error(
|
|
385
|
+
// Nothing waits for approval now (docs/REVIEW.md): finishing is final. Kept so a script that
|
|
386
|
+
// calls it is told, not refused.
|
|
387
|
+
async approve() {
|
|
388
|
+
console.error(
|
|
389
|
+
"nothing to approve: finished work is already Done. If it isn't right: passalong reopen <id> <why>",
|
|
390
|
+
);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/** Finished work back to To do, with why at its top; whoever finished it is told. */
|
|
394
|
+
async reopen(f) {
|
|
395
|
+
const id = f._[0] || die("usage: passalong reopen <id> <why>");
|
|
396
|
+
const why = f._.slice(1).join(" ");
|
|
397
|
+
if (!why) die("say why: passalong reopen <id> <what is wrong>");
|
|
398
|
+
await passalong.reopen(id, why).catch((e) => die(e.message));
|
|
399
|
+
console.error(`${id} is back in To do — whoever takes it next reads why`);
|
|
449
400
|
},
|
|
450
401
|
|
|
451
402
|
async reject(f) {
|
|
452
|
-
|
|
453
|
-
await passalong.rejectTask(id, f._.slice(1).join(" ")).catch((e) => die(e.message));
|
|
454
|
-
console.error(`${id} is back in Ready — the next agent reads why`);
|
|
403
|
+
return commands.reopen(f);
|
|
455
404
|
},
|
|
456
405
|
|
|
457
406
|
async release(f) {
|
|
@@ -480,31 +429,30 @@ const commands = {
|
|
|
480
429
|
|
|
481
430
|
async pull(f) {
|
|
482
431
|
const ref = f._[0] || die("usage: passalong pull <id|link>");
|
|
483
|
-
const { guide, markdown, path
|
|
432
|
+
const { guide, markdown, path } = await passalong
|
|
433
|
+
.pull(ref, { save: Boolean(f.save) })
|
|
434
|
+
.catch((e) => die(e.message));
|
|
484
435
|
process.stdout.write(markdown);
|
|
485
|
-
console.error(`\
|
|
436
|
+
if (path) console.error(`\nsaved to ${path}`);
|
|
486
437
|
await listFollowUps(guide.meta);
|
|
487
438
|
},
|
|
488
439
|
|
|
489
440
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
441
|
+
* Take it: you are doing it, and nobody else can take it while you hold it. The guide is printed,
|
|
442
|
+
* and `--save` also writes it to ./.passalong/. Every kind is taken the same way, and `start` is
|
|
443
|
+
* the same command by its older name, which also takes a share link.
|
|
493
444
|
*/
|
|
494
445
|
async start(f) {
|
|
495
|
-
const ref = f._[0] || die("usage: passalong
|
|
496
|
-
const r = await passalong.start(ref).catch((e) => die(e.message));
|
|
446
|
+
const ref = f._[0] || die("usage: passalong take <id|link>");
|
|
447
|
+
const r = await passalong.start(ref, { save: Boolean(f.save) }).catch((e) => die(e.message));
|
|
448
|
+
if (!r.guide?.markdown) return console.error(r.say || "nothing to take");
|
|
497
449
|
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);
|
|
450
|
+
if (r.path) console.error(`\nsaved to ${r.path}`);
|
|
451
|
+
console.error(`\nyou hold ${r.guide.id} — hand it in when it is done, or pass it with the reason`);
|
|
452
|
+
await listFollowUps(parse(r.markdown).meta);
|
|
506
453
|
},
|
|
507
454
|
|
|
455
|
+
|
|
508
456
|
/**
|
|
509
457
|
* Upload an image and print the markdown that points at it. Evidence goes *in* the document:
|
|
510
458
|
* a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
|
|
@@ -612,7 +560,6 @@ const commands = {
|
|
|
612
560
|
async list(f) {
|
|
613
561
|
const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
|
|
614
562
|
const rows = await passalong.list(f._.join(" "), { scope });
|
|
615
|
-
if (rows.warning) console.error(`warning: ${rows.warning}`);
|
|
616
563
|
if (!rows.length)
|
|
617
564
|
return console.error(
|
|
618
565
|
"no guides yet — run `passalong share` after your next finished piece of work",
|
|
@@ -644,17 +591,8 @@ const commands = {
|
|
|
644
591
|
} else console.log(url);
|
|
645
592
|
},
|
|
646
593
|
|
|
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
594
|
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`);
|
|
595
|
+
return commands.start(f);
|
|
658
596
|
},
|
|
659
597
|
|
|
660
598
|
async pass(f) {
|
|
@@ -663,10 +601,11 @@ const commands = {
|
|
|
663
601
|
// Required, and the reason is the whole point: "not me" without "why" leaves the sender where
|
|
664
602
|
// the silence did, still guessing whether to re-address it, wait, or go and ask.
|
|
665
603
|
if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
|
|
666
|
-
|
|
667
|
-
console.error(`${
|
|
604
|
+
await passalong.pass(id, why).catch((e) => die(e.message));
|
|
605
|
+
console.error(`${id} handed back with your reason: "${why}"`);
|
|
668
606
|
},
|
|
669
607
|
|
|
608
|
+
|
|
670
609
|
async assign(f) {
|
|
671
610
|
const id = f._[0] || die('usage: passalong assign <id> <@handle | #group | team>');
|
|
672
611
|
const to = f._.slice(1).join(",") || die('say who: @handle (or several), #group, or "team" for everyone');
|
|
@@ -701,7 +640,7 @@ const commands = {
|
|
|
701
640
|
console.log(`token ${r.token}`);
|
|
702
641
|
console.log(`password link ${r.password_url}`);
|
|
703
642
|
console.error(
|
|
704
|
-
`\n${r.email} is an admin. The token is for \`passalong login\`, the link sets their ` +
|
|
643
|
+
`\n${r.email} is an admin. The token is for \`passalong login --with-token\`, the link sets their ` +
|
|
705
644
|
`password for the hub, and it expires ${r.expires.slice(0, 10)}. Neither is shown again.`,
|
|
706
645
|
);
|
|
707
646
|
return;
|
|
@@ -755,23 +694,6 @@ const commands = {
|
|
|
755
694
|
console.error(`${id} taken back — that plan is lapsed as of now`);
|
|
756
695
|
},
|
|
757
696
|
|
|
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
697
|
/**
|
|
776
698
|
* Archive a guide: off your board, and out of what the free tier counts.
|
|
777
699
|
*
|
|
@@ -806,29 +728,26 @@ const commands = {
|
|
|
806
728
|
|
|
807
729
|
async export(f) {
|
|
808
730
|
const dir = resolve(f._[0] || "passalong-export");
|
|
809
|
-
const paths = passalong.exportAll(dir);
|
|
731
|
+
const paths = await passalong.exportAll(dir).catch((e) => die(e.message));
|
|
810
732
|
console.error(`exported ${paths.length} guide${paths.length === 1 ? "" : "s"} → ${dir}`);
|
|
811
733
|
},
|
|
812
734
|
|
|
813
735
|
async login(f) {
|
|
814
|
-
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
736
|
+
// A token on the command line lands in shell history, so it is only read from stdin.
|
|
737
|
+
if (f._[0]) die("pass a token on stdin instead: passalong login --with-token < token.txt");
|
|
738
|
+
if (f.email || f.password)
|
|
739
|
+
die("password sign-in is gone: run `passalong login` and approve it in the browser");
|
|
740
|
+
if (f["with-token"]) return stdinLogin();
|
|
819
741
|
if (api.loggedIn() && !f.new) {
|
|
820
742
|
const who = await api.me().catch(() => null);
|
|
821
743
|
if (who) {
|
|
822
|
-
console.error(`already logged in
|
|
823
|
-
console.error(` passalong login ${api.token()}`);
|
|
744
|
+
console.error(`already logged in as ${who.handle ? `@${who.handle}` : who.email || who.account}.`);
|
|
824
745
|
console.error("Add another account to this machine with `passalong login --new`.");
|
|
825
746
|
return;
|
|
826
747
|
}
|
|
827
748
|
}
|
|
828
|
-
//
|
|
829
|
-
// browser, which works with no terminal at all — an agent can start it, and the person it is
|
|
749
|
+
// The browser works with no terminal at all — an agent can start it, and the person it is
|
|
830
750
|
// working for clicks once.
|
|
831
|
-
if (f.email || f.password) return passwordLogin(f);
|
|
832
751
|
return browserLogin();
|
|
833
752
|
},
|
|
834
753
|
|
|
@@ -865,7 +784,7 @@ const commands = {
|
|
|
865
784
|
|
|
866
785
|
/** Forget one of them on this machine. The account itself is untouched: only its token here goes. */
|
|
867
786
|
async logout(f) {
|
|
868
|
-
const name = f._[0] || die("
|
|
787
|
+
const name = f._[0] || store.readConfig().active || die("not logged in (see `passalong accounts`)");
|
|
869
788
|
const r = store.removeAccount(name);
|
|
870
789
|
if (!r.removed) die(`no account called "${name}" here`);
|
|
871
790
|
console.error(
|
|
@@ -1135,6 +1054,8 @@ else if (!commands[cmd])
|
|
|
1135
1054
|
"if you expected this command, `npm i -g passalong` for the newest one.",
|
|
1136
1055
|
);
|
|
1137
1056
|
else {
|
|
1057
|
+
// Once per machine; a failure leaves it for the next command rather than failing this one.
|
|
1058
|
+
await passalong.tidy().catch(() => {});
|
|
1138
1059
|
await commands[cmd](argv).catch((e) => die(e.message));
|
|
1139
1060
|
// After the command, never before it: a version check is not worth a millisecond of somebody's
|
|
1140
1061
|
// 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,12 @@ 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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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). It is Ready and has reached whoever it is for. Then one line on how to use
|
|
71
|
+
it on the other side: `passalong take <id>` in the target repo, or "take passalong <id>" to an
|
|
72
|
+
agent with the Passalong MCP server. `take` rather than `pull` on purpose: it holds the work as
|
|
73
|
+
well as fetching it, and until somebody takes it the sender cannot tell a guide nobody noticed
|
|
74
|
+
from one somebody is deep in.
|
|
76
75
|
|
|
77
76
|
If the `passalong` command is missing, tell the user to run `npm i -g passalong && passalong setup` and
|
|
78
77
|
still write the draft file so nothing is lost.
|
package/src/api.js
CHANGED
|
@@ -337,10 +337,17 @@ export const renameFolderDocument = (folderId, documentId, name) =>
|
|
|
337
337
|
method: "PATCH",
|
|
338
338
|
body: { name },
|
|
339
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
|
+
});
|
|
340
345
|
export const folderDocumentHistory = (folderId, documentId, version = 0) =>
|
|
341
346
|
call(
|
|
342
347
|
`/v1/folders/${encodeURIComponent(folderId)}/documents/${encodeURIComponent(documentId)}/revisions${version ? `/${version}` : ""}`,
|
|
343
348
|
);
|
|
349
|
+
export const shareFolder = (folderId) =>
|
|
350
|
+
call(`/v1/folders/${encodeURIComponent(folderId)}/share`, { method: "POST" });
|
|
344
351
|
export const linkFolderGuide = (folderId, guide) =>
|
|
345
352
|
call(`/v1/folders/${encodeURIComponent(folderId)}/guides`, { method: "POST", body: { guide } });
|
|
346
353
|
export const unlinkFolderGuide = (folderId, guide) =>
|
|
@@ -372,7 +379,10 @@ export const inbox = () => call("/v1/inbox");
|
|
|
372
379
|
export const createReport = (body) => call("/v1/reports", { method: "POST", body });
|
|
373
380
|
/** One report and the issues filed under it, grouped by product area. */
|
|
374
381
|
export const report = (id) => call(`/v1/reports/${encodeURIComponent(id)}`);
|
|
375
|
-
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 })}`);
|
|
376
386
|
/**
|
|
377
387
|
* A guide's follow-ups, one level down. `markdown` adds each one's content, oldest first and
|
|
378
388
|
* capped server-side; neither form counts as opening them.
|
|
@@ -419,6 +429,13 @@ export const approveTask = (id) =>
|
|
|
419
429
|
call(`/v1/tasks/${encodeURIComponent(id)}/approve`, { method: "POST", body: {} });
|
|
420
430
|
export const rejectTask = (id, why) =>
|
|
421
431
|
call(`/v1/tasks/${encodeURIComponent(id)}/reject`, { method: "POST", body: { why } });
|
|
432
|
+
export const reopen = (id, why) =>
|
|
433
|
+
call(`/v1/guides/${encodeURIComponent(id)}/reopen`, { method: "POST", body: { why } });
|
|
434
|
+
/** A handoff's or bug's approve and send-back. No `place` names the only hand-in there is. */
|
|
435
|
+
export const closeGuide = (id) =>
|
|
436
|
+
call(`/v1/guides/${encodeURIComponent(id)}/close`, { method: "POST", body: {} });
|
|
437
|
+
export const sendBack = (id, why) =>
|
|
438
|
+
call(`/v1/guides/${encodeURIComponent(id)}/send_back`, { method: "POST", body: { why } });
|
|
422
439
|
/** Take work back from whoever holds it, of any kind. `/v1/tasks/…/release` is the old path. */
|
|
423
440
|
export const release = (id) =>
|
|
424
441
|
call(`/v1/guides/${encodeURIComponent(id)}/release`, { method: "POST", body: {} });
|
package/src/guide.js
CHANGED
|
@@ -497,7 +497,9 @@ export function stamp(guide, defaults = {}) {
|
|
|
497
497
|
const meta = { ...guide.meta };
|
|
498
498
|
if (!meta.id) meta.id = newId();
|
|
499
499
|
if (!meta.created) meta.created = new Date().toISOString();
|
|
500
|
-
|
|
500
|
+
// Ready unless whoever wrote it said draft: a guide sent to someone is meant to reach them, and
|
|
501
|
+
// drafts were where addressed work sat forgotten (every draft in production on 2026-10-15).
|
|
502
|
+
if (!meta.status) meta.status = "published";
|
|
501
503
|
for (const [k, v] of Object.entries(defaults))
|
|
502
504
|
if (meta[k] === undefined || meta[k] === "") meta[k] = v;
|
|
503
505
|
return { meta, body: guide.body };
|
|
@@ -616,7 +618,7 @@ export function template(meta = {}) {
|
|
|
616
618
|
summary: "",
|
|
617
619
|
author: "",
|
|
618
620
|
source_context: "",
|
|
619
|
-
status: "
|
|
621
|
+
status: "published",
|
|
620
622
|
stack_assumptions: [],
|
|
621
623
|
tags: [],
|
|
622
624
|
...meta,
|
|
@@ -654,7 +656,7 @@ function bugTemplate(meta = {}) {
|
|
|
654
656
|
kind: "bug",
|
|
655
657
|
author: "",
|
|
656
658
|
source_context: "",
|
|
657
|
-
status: "
|
|
659
|
+
status: "published",
|
|
658
660
|
severity: "s3",
|
|
659
661
|
stack_assumptions: [],
|
|
660
662
|
tags: ["bug"],
|
|
@@ -696,7 +698,7 @@ function taskTemplate(meta = {}) {
|
|
|
696
698
|
author: "",
|
|
697
699
|
source_context: "",
|
|
698
700
|
target_context: "",
|
|
699
|
-
status: "
|
|
701
|
+
status: "published",
|
|
700
702
|
stack_assumptions: [],
|
|
701
703
|
tags: [],
|
|
702
704
|
...meta,
|