passalong 0.1.0 → 0.3.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 +54 -7
- package/bin/passalong +302 -22
- package/package.json +2 -2
- package/skill/SKILL.md +72 -7
- package/src/api.js +70 -3
- package/src/guide.js +264 -5
- package/src/mcp.js +444 -28
- package/src/passalong.js +290 -21
package/README.md
CHANGED
|
@@ -25,25 +25,72 @@ passalong login # optional: sync guides across machines and get share links
|
|
|
25
25
|
## Commands
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
passalong share [file]
|
|
29
|
-
passalong pull <id|link> fetch a guide into ./.passalong/ and print it
|
|
30
|
-
passalong
|
|
28
|
+
passalong share [file] [--to team[/handle]] publish a guide → id + link; --to hands it to a team or teammate
|
|
29
|
+
passalong pull <id|link> fetch a guide into ./.passalong/ (git-ignored) and print it
|
|
30
|
+
passalong inbox guides handed to you that you have not pulled yet
|
|
31
|
+
passalong board waiting on you, in flight, landed, worth keeping
|
|
32
|
+
passalong activity [--all] what happened while you were away; clears unless --all
|
|
33
|
+
passalong list [query] your guides and your teams', local and synced
|
|
31
34
|
passalong open <id> [--print] view a guide in the browser (or the terminal)
|
|
32
35
|
passalong hub open your synced guides in the browser
|
|
33
|
-
passalong
|
|
34
|
-
passalong
|
|
36
|
+
passalong works <id> you tried it and it holds up
|
|
37
|
+
passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
|
|
38
|
+
passalong done <id> deprecated: mark consumed
|
|
39
|
+
passalong promote <id> deprecated: mark promoted
|
|
35
40
|
passalong rm <id> delete a guide locally and from sync
|
|
36
41
|
passalong export [dir] dump every guide as plain markdown
|
|
37
|
-
passalong login [token] create an account, or attach this machine
|
|
42
|
+
passalong login [token] create an account, or attach this machine with a token from your hub
|
|
43
|
+
passalong me [--handle H] [--name N] [--email E] who you are to teammates
|
|
44
|
+
passalong team current team and its members
|
|
45
|
+
passalong team create <name> start a team (you become its owner)
|
|
46
|
+
passalong team invite [email] make an invite link (mailed when an email is given)
|
|
47
|
+
passalong team join <link> accept an invite
|
|
48
|
+
passalong team use <slug> switch the current team
|
|
38
49
|
passalong setup install the Claude Code capture skill + register the MCP server
|
|
39
50
|
passalong mcp run the MCP server over stdio
|
|
40
51
|
```
|
|
41
52
|
|
|
53
|
+
## Teams
|
|
54
|
+
|
|
55
|
+
A team is the unit of sharing. Members can find, pull, and mark consumed every guide shared to
|
|
56
|
+
the team; a guide can also be handed to one person, who sees it in their inbox (and by email
|
|
57
|
+
when the server has mail configured).
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
passalong me --handle lukman # how teammates address you
|
|
61
|
+
passalong team create Khaime # you become owner; "khaime" is now current
|
|
62
|
+
passalong team invite ada@example.com # or no email: prints a link to send yourself
|
|
63
|
+
passalong team join <link> # on Ada's machine — or she just opens the link
|
|
64
|
+
passalong share --to khaime/ada # hand this guide to Ada
|
|
65
|
+
passalong inbox # on Ada's side: what was handed to you
|
|
66
|
+
passalong activity # back on your side: it landed, and who pulled it
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A receiver who tries the work reports back with `passalong works <id>` or `passalong broken <id> <why>`.
|
|
70
|
+
That is separate from `done`, which means *implemented*: a verdict means it actually runs, a failing
|
|
71
|
+
one has to say why, and the author sees it on their board and in their inbox.
|
|
72
|
+
|
|
73
|
+
Sign in at [the hub](https://passalong.dev/hub) with an email and password, and mint
|
|
74
|
+
an API token there for the CLI and MCP servers — named, revocable, and shown once. Your password
|
|
75
|
+
never goes near the terminal. `passalong login` with no arguments still makes an account without
|
|
76
|
+
any of that; add an email and password later to be able to sign in from a browser and recover it.
|
|
77
|
+
|
|
78
|
+
Not everyone who receives a guide implements it. Every guide page has a **Verify** view
|
|
79
|
+
(`?view=verify`) that leads with Problem, Verification and Gotchas and folds the implementation
|
|
80
|
+
away — for a tester or anyone checking the work rather than doing it. Invites are accepted in the
|
|
81
|
+
browser, so a teammate needs no terminal to be part of a team.
|
|
82
|
+
|
|
83
|
+
`passalong activity` is the other half of the transfer: it tells you when someone pulled a guide
|
|
84
|
+
you handed over, when they marked it consumed, and when an invite was taken up. A handoff
|
|
85
|
+
addressed to a person is also emailed; a team-wide share is not, because mail nobody is
|
|
86
|
+
named in is the first thing people filter out. Guides that keep getting pulled get a nudge to
|
|
87
|
+
promote them into the team's small set of maintained references.
|
|
88
|
+
|
|
42
89
|
## MCP
|
|
43
90
|
|
|
44
91
|
`passalong setup` registers the server with Claude Code. For other clients, run `passalong mcp` over stdio.
|
|
45
92
|
|
|
46
|
-
Tools: `search_guides`, `get_guide`, `publish_guide
|
|
93
|
+
Tools: `search_guides`, `inbox`, `get_guide`, `publish_guide` (with `to`), `guide_template`, `set_guide_status`.
|
|
47
94
|
|
|
48
95
|
## Guides are files
|
|
49
96
|
|
package/bin/passalong
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Passalong CLI. Few flags on purpose: the defaults are the product.
|
|
3
3
|
//
|
|
4
|
-
// passalong share [file]
|
|
4
|
+
// passalong share [file] [--to TARGET] publish a guide → id + link
|
|
5
|
+
// TARGET is a team, team/@handle for one person, or "team/#group" for the people who do a
|
|
6
|
+
// thing — quote that one, since a shell may read the # as a comment
|
|
7
|
+
// passalong attach <image> upload evidence and print the markdown that points at it
|
|
5
8
|
// passalong pull <id|link> fetch a guide into ./.passalong/ and print it
|
|
6
|
-
// passalong
|
|
9
|
+
// passalong start <id|link> pull it and take it — one command for "I am doing this"
|
|
10
|
+
// passalong inbox guides handed to you that you have not pulled yet
|
|
11
|
+
// passalong board waiting on you, in flight, landed, worth keeping
|
|
12
|
+
// passalong activity [--all] what happened while you were away; clears unless --all
|
|
13
|
+
// passalong log [repo] what you did, newest first, by month
|
|
14
|
+
// passalong list [query] your guides and your teams', local and synced
|
|
7
15
|
// passalong open <id> [--print] view a guide in the browser (or the terminal)
|
|
8
16
|
// passalong hub open your synced guides in the browser
|
|
9
|
-
// passalong
|
|
10
|
-
// passalong
|
|
17
|
+
// passalong take <id> you are doing it — said before the work, so the sender stops guessing
|
|
18
|
+
// passalong pass <id> <why> it is not yours — hands it back to its author with your reason
|
|
19
|
+
// passalong works <id> you tried it and it holds up
|
|
20
|
+
// passalong broken <id> <why> you tried it and it does not — the author is told, with your reason
|
|
21
|
+
// passalong done <id> archive it: off your board and out of the free tier's count
|
|
22
|
+
// passalong promote <id> deprecated: mark promoted
|
|
11
23
|
// passalong rm <id> delete a guide locally and from sync
|
|
12
24
|
// passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
|
|
13
|
-
// passalong login [token] create an account, or attach this machine
|
|
25
|
+
// passalong login [token] create an account, or attach this machine with a token from your hub
|
|
26
|
+
// passalong me [--handle H] [--name N] [--email E] who you are to teammates
|
|
27
|
+
// passalong team current team and its members
|
|
28
|
+
// passalong team create <name> start a team (you become its owner)
|
|
29
|
+
// passalong team invite [email] make an invite link (mailed when an email is given)
|
|
30
|
+
// passalong team join <link|code> accept an invite
|
|
31
|
+
// passalong team use <slug> switch the current team
|
|
14
32
|
// passalong setup install the Claude Code capture skill + register the MCP server
|
|
15
33
|
// passalong mcp run the MCP server over stdio
|
|
16
34
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
@@ -53,18 +71,41 @@ function edit(path) {
|
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
function openInBrowser(url) {
|
|
56
|
-
const cmd =
|
|
74
|
+
const cmd =
|
|
75
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
57
76
|
spawnSync(cmd, [url], { stdio: "ignore", shell: process.platform === "win32" });
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function relDate(iso) {
|
|
61
80
|
if (!iso) return "";
|
|
62
81
|
const d = (Date.now() - new Date(iso).getTime()) / 864e5;
|
|
63
|
-
if (d < 1) return "
|
|
82
|
+
if (d < 1 / 24) return "just now";
|
|
83
|
+
if (d < 1) return `${Math.floor(d * 24)}h ago`;
|
|
64
84
|
if (d < 30) return `${Math.floor(d)}d ago`;
|
|
65
85
|
return iso.slice(0, 10);
|
|
66
86
|
}
|
|
67
87
|
|
|
88
|
+
function guideLine(r) {
|
|
89
|
+
const who = r.team ? ` ${r.team}${r.to ? `/@${r.to}` : ""}${r.mine ? "" : ` from @${r.from || "?"}`}` : "";
|
|
90
|
+
const tags = r.tags?.length ? ` #${r.tags.join(" #")}` : "";
|
|
91
|
+
let pulled = "";
|
|
92
|
+
if (r.mine && r.pulled_by?.length) {
|
|
93
|
+
const p = r.pulled_by[0];
|
|
94
|
+
pulled = ` ← ${p.handle ? `@${p.handle}` : "link"} ${relDate(p.at)}${r.pulled_by.length > 1 ? ` +${r.pulled_by.length - 1}` : ""}`;
|
|
95
|
+
}
|
|
96
|
+
const nudge = r.mine && r.status === "published" && r.pulls >= 3 ? " ★ promote?" : "";
|
|
97
|
+
return `${r.id} ${r.status.padEnd(9)} ${relDate(r.created).padEnd(10)} ${r.title}${who}${tags}${pulled}${nudge}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function activityLine(n) {
|
|
101
|
+
const dot = n.read ? " " : "•";
|
|
102
|
+
return `${dot} ${(n.guide || "").padEnd(8)} ${relDate(n.at).padEnd(10)} ${n.text}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MONTHS = "January February March April May June July August September October November December".split(" ");
|
|
106
|
+
/** "September 2026" from an ISO timestamp, without pulling in a date library for one heading. */
|
|
107
|
+
const monthOf = (iso) => `${MONTHS[Number(iso.slice(5, 7)) - 1]} ${iso.slice(0, 4)}`;
|
|
108
|
+
|
|
68
109
|
const commands = {
|
|
69
110
|
async share(f) {
|
|
70
111
|
let path = f._[0];
|
|
@@ -79,15 +120,23 @@ const commands = {
|
|
|
79
120
|
path = store.draftPath(new Date().toISOString().replace(/[:.]/g, "-"));
|
|
80
121
|
writeFileSync(path, scaffold());
|
|
81
122
|
fromDrafts = true;
|
|
82
|
-
if (!tty)
|
|
123
|
+
if (!tty)
|
|
124
|
+
die(`no draft to share; scaffolded ${path} — fill it in and run \`passalong share ${path}\``);
|
|
83
125
|
}
|
|
84
126
|
}
|
|
85
127
|
if (!existsSync(path)) die(`no such file: ${path}`);
|
|
86
128
|
if (tty && !f["no-edit"]) edit(path);
|
|
87
|
-
const
|
|
129
|
+
const to = typeof f.to === "string" ? f.to : undefined;
|
|
130
|
+
const { guide, url, synced, notified, path: saved } = await passalong
|
|
131
|
+
.share(readFileSync(path, "utf8"), { to })
|
|
132
|
+
.catch((e) => die(e.message));
|
|
88
133
|
if (fromDrafts) store.removeDraft(path);
|
|
89
134
|
console.log(guide.meta.id);
|
|
90
135
|
console.error(`shared "${guide.meta.title}" → ${saved}`);
|
|
136
|
+
if (guide.meta.team)
|
|
137
|
+
console.error(
|
|
138
|
+
`handed to ${guide.meta.team}${guide.meta.to ? `/@${guide.meta.to}${notified ? " (emailed)" : " (in their inbox)"}` : " (team inbox)"}`,
|
|
139
|
+
);
|
|
91
140
|
if (synced) console.error(`link: ${url}`);
|
|
92
141
|
else console.error("not synced (run `passalong login` to sync across machines)");
|
|
93
142
|
},
|
|
@@ -99,14 +148,116 @@ const commands = {
|
|
|
99
148
|
console.error(`\npulled from ${from} → ${path}`);
|
|
100
149
|
},
|
|
101
150
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Pull and take in one command. `pull` says it arrived, `take` says somebody is doing it, and
|
|
153
|
+
* they stay separate because reading a guide to decide it is not yours is what `pass` is for.
|
|
154
|
+
* This is the pair for the case where you already know: you are picking this up.
|
|
155
|
+
*/
|
|
156
|
+
async start(f) {
|
|
157
|
+
const ref = f._[0] || die("usage: passalong start <id|link>");
|
|
158
|
+
const r = await passalong.start(ref).catch((e) => die(e.message));
|
|
159
|
+
process.stdout.write(r.markdown);
|
|
160
|
+
console.error(`\npulled from ${r.from} → ${r.path}`);
|
|
161
|
+
if (r.took) console.error("you are on it, and the author has been told");
|
|
162
|
+
else if (r.own) console.error("your own guide — nothing to take");
|
|
163
|
+
else if (r.ack_error) console.error(`not taken (${r.ack_error}) — run \`passalong take ${r.guide.meta.id}\``);
|
|
164
|
+
// Saying nothing here would be the silence the whole hop exists to end: you would have the
|
|
165
|
+
// guide, believe you had answered for it, and the sender would still be waiting.
|
|
166
|
+
else console.error("not taken — sync is off, so nobody was told (run `passalong login`)");
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Upload an image and print the markdown that points at it. Evidence goes *in* the document:
|
|
171
|
+
* a guide travels as markdown to whoever holds its link, so a file beside it does not travel.
|
|
172
|
+
*/
|
|
173
|
+
async attach(f) {
|
|
174
|
+
const file = f._[0] || die("usage: passalong attach <image> [--name LABEL]");
|
|
175
|
+
const shot = await passalong.attach(file, { name: f.name || "" }).catch((e) => die(e.message));
|
|
176
|
+
process.stdout.write(`${shot.markdown}\n`);
|
|
177
|
+
console.error(`uploaded ${shot.bytes} bytes → ${shot.url}`);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async inbox() {
|
|
181
|
+
const rows = await passalong.inbox().catch((e) => die(e.message));
|
|
182
|
+
if (!rows.length) return console.error("inbox empty — nothing handed to you that you haven't pulled");
|
|
183
|
+
for (const r of rows) console.log(guideLine(r));
|
|
184
|
+
console.error(`\n${rows.length} waiting. Take one with: passalong start <id>`);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async board() {
|
|
188
|
+
const b = await passalong.board().catch((e) => die(e.message));
|
|
189
|
+
const queue = (title, note, rows, line) => {
|
|
190
|
+
if (!rows.length) return;
|
|
191
|
+
console.log(`\n${title} · ${rows.length} ${note}`);
|
|
192
|
+
for (const g of rows) console.log(` ${g.id} ${relDate(g.created).padEnd(10)} ${line(g)}`);
|
|
193
|
+
};
|
|
194
|
+
queue("Waiting on you", "handed to you, not pulled yet", b.waiting, (g) => `${g.title} from @${g.from || "?"}`);
|
|
195
|
+
queue("Not working", "someone tried it and it does not hold up", b.failing, (g) =>
|
|
196
|
+
`${g.title} ${g.verdict?.by ? `@${g.verdict.by}` : "someone"}: ${g.verdict?.note || "no reason given"}`);
|
|
197
|
+
queue("In flight", "handed over, nobody has taken it", b.in_flight, (g) =>
|
|
198
|
+
`${g.title} to ${g.team}${g.to ? `/@${g.to}` : ""}${g.stale ? " ← not picked up" : ""}`);
|
|
199
|
+
queue("Landed", "someone else has it", b.landed, (g) => `${g.title} ${g.pulls} pull${g.pulls === 1 ? "" : "s"}`);
|
|
200
|
+
const total = b.waiting.length + b.failing.length + b.in_flight.length + b.landed.length;
|
|
201
|
+
if (!total) console.error("nothing in flight — every transfer is closed");
|
|
202
|
+
else if (b.unread) console.error(`\n${b.unread} new: passalong activity`);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What you did, newest first, under a heading per month.
|
|
207
|
+
*
|
|
208
|
+
* The month headings are the whole point: every other list in this CLI is ordered by what needs
|
|
209
|
+
* you — the board's queues, `list`'s state rank — and none of them can answer "what did I get
|
|
210
|
+
* done in September". Time is the axis nothing else offers.
|
|
211
|
+
*
|
|
212
|
+
* A repo narrows it, positionally and without a flag, for the same reason `list` takes its query
|
|
213
|
+
* that way: it is the one thing you are likely to say, and a flag would make you name it twice.
|
|
214
|
+
*/
|
|
215
|
+
async log(f) {
|
|
216
|
+
const repo = f._.join(" ");
|
|
217
|
+
const rows = await passalong.log({ repo }).catch((e) => die(e.message));
|
|
218
|
+
if (!rows.length)
|
|
219
|
+
return console.error(
|
|
220
|
+
repo
|
|
221
|
+
? `nothing passed along from anything matching "${repo}"`
|
|
222
|
+
: "nothing yet — your log fills up as you share, pull and answer guides",
|
|
223
|
+
);
|
|
224
|
+
let month = "";
|
|
106
225
|
for (const r of rows) {
|
|
107
|
-
const
|
|
108
|
-
|
|
226
|
+
const m = monthOf(r.at);
|
|
227
|
+
if (m !== month) {
|
|
228
|
+
console.log(`\n${m}`);
|
|
229
|
+
month = m;
|
|
230
|
+
}
|
|
231
|
+
console.log(` ${r.guide} ${r.at.slice(0, 10)} ${r.text}${r.repo && !repo ? ` ${r.repo}` : ""}`);
|
|
109
232
|
}
|
|
233
|
+
// Said every time, because the list looks like a work log and is not one. A month you shipped
|
|
234
|
+
// without sharing looks identical here to a month you did nothing.
|
|
235
|
+
console.error(`\n${rows.length} thing${rows.length === 1 ? "" : "s"}. Work that never became a guide is not here.`);
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async activity(f) {
|
|
239
|
+
const { notifications, unread } = await passalong.activity({ all: f.all }).catch((e) => die(e.message));
|
|
240
|
+
if (!notifications.length)
|
|
241
|
+
return console.error(
|
|
242
|
+
f.all ? "nothing has happened yet" : "nothing new — your guides and handoffs are quiet",
|
|
243
|
+
);
|
|
244
|
+
for (const n of notifications) console.log(activityLine(n));
|
|
245
|
+
const waiting = notifications.filter((n) => n.kind === "handoff" && !n.read);
|
|
246
|
+
if (waiting.length) console.error(`\nPull one with: passalong pull ${waiting[0].guide}`);
|
|
247
|
+
// Reading the feed is what marks it read: an unread count you have to clear by hand is a
|
|
248
|
+
// chore, and `--all` is there for when you want to look without clearing.
|
|
249
|
+
if (!f.all && unread) await passalong.seen().catch(() => {});
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async list(f) {
|
|
253
|
+
const scope = typeof f.team === "string" ? f.team : f.mine ? "mine" : "";
|
|
254
|
+
const rows = await passalong.list(f._.join(" "), { scope });
|
|
255
|
+
if (rows.warning) console.error(`warning: ${rows.warning}`);
|
|
256
|
+
if (!rows.length)
|
|
257
|
+
return console.error(
|
|
258
|
+
"no guides yet — run `passalong share` after your next finished piece of work",
|
|
259
|
+
);
|
|
260
|
+
for (const r of rows) console.log(guideLine(r));
|
|
110
261
|
},
|
|
111
262
|
|
|
112
263
|
async open(f) {
|
|
@@ -122,7 +273,8 @@ const commands = {
|
|
|
122
273
|
},
|
|
123
274
|
|
|
124
275
|
async hub() {
|
|
125
|
-
if (!api.loggedIn())
|
|
276
|
+
if (!api.loggedIn())
|
|
277
|
+
die("not logged in — run `passalong login` first (the hub shows synced guides)");
|
|
126
278
|
// The token rides in the URL fragment: the browser never sends it to the server, and the
|
|
127
279
|
// page moves it into localStorage and scrubs the address bar on load.
|
|
128
280
|
const url = `${api.baseUrl()}/hub#token=${encodeURIComponent(api.token())}`;
|
|
@@ -132,16 +284,64 @@ const commands = {
|
|
|
132
284
|
} else console.log(url);
|
|
133
285
|
},
|
|
134
286
|
|
|
287
|
+
/**
|
|
288
|
+
* The first word back, before any of the work.
|
|
289
|
+
*
|
|
290
|
+
* Between a handoff and a pull the sender had no signal at all: an unanswered guide looks
|
|
291
|
+
* exactly like one nobody has noticed. These two are the hop the MCP tool and the hub already
|
|
292
|
+
* had and a terminal did not.
|
|
293
|
+
*/
|
|
294
|
+
async take(f) {
|
|
295
|
+
const id = f._[0] || die("usage: passalong take <id>");
|
|
296
|
+
const r = await passalong.ack(id, true).catch((e) => die(e.message));
|
|
297
|
+
console.error(`${r.id} — you are on it, and the author has been told`);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
async pass(f) {
|
|
301
|
+
const id = f._[0] || die("usage: passalong pass <id> <why it is not yours>");
|
|
302
|
+
const why = f._.slice(1).join(" ");
|
|
303
|
+
// Required, and the reason is the whole point: "not me" without "why" leaves the sender where
|
|
304
|
+
// the silence did, still guessing whether to re-address it, wait, or go and ask.
|
|
305
|
+
if (!why) die("say why you are passing: passalong pass <id> <why it is not yours>");
|
|
306
|
+
const r = await passalong.ack(id, false, why).catch((e) => die(e.message));
|
|
307
|
+
console.error(`${r.id} handed back — the author has been told: "${r.note}"`);
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
async works(f) {
|
|
311
|
+
const id = f._[0] || die("usage: passalong works <id>");
|
|
312
|
+
const r = await passalong.verdict(id, true, f._.slice(1).join(" ")).catch((e) => die(e.message));
|
|
313
|
+
console.error(`${r.id} verified — the author has been told`);
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
async broken(f) {
|
|
317
|
+
const id = f._[0] || die("usage: passalong broken <id> <what happened>");
|
|
318
|
+
const why = f._.slice(1).join(" ");
|
|
319
|
+
if (!why) die("say what went wrong: passalong broken <id> <what happened>");
|
|
320
|
+
const r = await passalong.verdict(id, false, why).catch((e) => die(e.message));
|
|
321
|
+
console.error(`${r.id} reported as not working — the author has been told: "${r.note}"`);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Archive a guide: off your board, and out of what the free tier counts.
|
|
326
|
+
*
|
|
327
|
+
* It was deprecated for meaning "I implemented it", which is the reader's judgement and belongs
|
|
328
|
+
* to the verdict. As shelf space it has a job nothing else does — it is the only way under the
|
|
329
|
+
* sync limit that does not delete the work, and the 402 says so by name.
|
|
330
|
+
*/
|
|
135
331
|
async done(f) {
|
|
136
332
|
const id = f._[0] || die("usage: passalong done <id>");
|
|
137
333
|
const g = await passalong.setStatus(id, "consumed").catch((e) => die(e.message));
|
|
138
|
-
console.error(`${g.meta.id}
|
|
334
|
+
console.error(`${g.meta.id} archived — off your board, and it no longer counts against sync`);
|
|
139
335
|
},
|
|
140
336
|
|
|
337
|
+
// Still on the way out: it sets an author lifecycle nothing reads, and the board reports pull
|
|
338
|
+
// counts on its own. It stays, and warns, because removing a command from a published package
|
|
339
|
+
// is a breaking change.
|
|
141
340
|
async promote(f) {
|
|
142
341
|
const id = f._[0] || die("usage: passalong promote <id>");
|
|
143
342
|
const g = await passalong.setStatus(id, "promoted").catch((e) => die(e.message));
|
|
144
343
|
console.error(`${g.meta.id} marked promoted`);
|
|
344
|
+
console.error("note: `promote` is deprecated — the board reports pull counts on their own");
|
|
145
345
|
},
|
|
146
346
|
|
|
147
347
|
async rm(f) {
|
|
@@ -177,7 +377,81 @@ const commands = {
|
|
|
177
377
|
console.error(`account ${account} created on ${api.baseUrl()}. Sync is on.`);
|
|
178
378
|
console.error("Attach other machines with:");
|
|
179
379
|
console.error(` passalong login ${token}`);
|
|
180
|
-
console.error("(
|
|
380
|
+
console.error("(keep it private — it is a credential, not a password)");
|
|
381
|
+
console.error("Give teammates a name to address you by: passalong me --handle <you>");
|
|
382
|
+
console.error(`Add an email and password so you can sign in and recover it: ${api.baseUrl()}/hub`);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async me(f) {
|
|
386
|
+
const patch = {};
|
|
387
|
+
for (const k of ["handle", "name", "email"]) if (typeof f[k] === "string") patch[k] = f[k];
|
|
388
|
+
const who = Object.keys(patch).length
|
|
389
|
+
? await api.updateMe(patch).then(api.me).catch((e) => die(e.message))
|
|
390
|
+
: await api.me().catch((e) => die(e.message));
|
|
391
|
+
console.log(`account ${who.account}`);
|
|
392
|
+
console.log(`handle ${who.handle ? `@${who.handle}` : "(none — set one: passalong me --handle <you>)"}`);
|
|
393
|
+
if (who.name) console.log(`name ${who.name}`);
|
|
394
|
+
if (who.email) console.log(`email ${who.email}`);
|
|
395
|
+
console.log(`guides ${who.guides} synced (${who.limit} active on the free tier)`);
|
|
396
|
+
const current = store.readConfig().team;
|
|
397
|
+
console.log(
|
|
398
|
+
`teams ${who.teams.length ? who.teams.map((t) => `${t.slug}${t.slug === current ? "*" : ""} (${t.role})`).join(", ") : "(none — passalong team create <name>)"}`,
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
async team(f) {
|
|
403
|
+
const [sub, ...rest] = f._;
|
|
404
|
+
const cfg = store.readConfig();
|
|
405
|
+
if (!sub) {
|
|
406
|
+
if (!cfg.team) {
|
|
407
|
+
const { teams } = await api.teams().catch((e) => die(e.message));
|
|
408
|
+
if (!teams.length) return console.error("no team yet — passalong team create <name>");
|
|
409
|
+
return console.error(`you are in ${teams.map((t) => t.slug).join(", ")}; pick one: passalong team use <slug>`);
|
|
410
|
+
}
|
|
411
|
+
const t = await api.team(cfg.team).catch((e) => die(e.message));
|
|
412
|
+
console.log(`${t.name} (${t.slug}) — ${t.members.length} member${t.members.length === 1 ? "" : "s"}, ${t.guides} guide${t.guides === 1 ? "" : "s"}`);
|
|
413
|
+
for (const m of t.members)
|
|
414
|
+
console.log(` ${m.handle ? `@${m.handle}` : "(no handle)"}${m.name ? ` ${m.name}` : ""} ${m.role} joined ${relDate(m.joined)}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (sub === "create") {
|
|
418
|
+
const name = rest.join(" ") || die("usage: passalong team create <name>");
|
|
419
|
+
const t = await api.createTeam(name).catch((e) => die(e.message));
|
|
420
|
+
store.writeConfig({ team: t.slug });
|
|
421
|
+
console.error(`team ${t.name} created as "${t.slug}" — now current. Invite people: passalong team invite [email]`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (sub === "invite") {
|
|
425
|
+
const slug = cfg.team || die("no current team — passalong team use <slug>");
|
|
426
|
+
const inv = await api.invite(slug, rest[0] || "").catch((e) => die(e.message));
|
|
427
|
+
console.log(inv.url);
|
|
428
|
+
console.error(
|
|
429
|
+
inv.email
|
|
430
|
+
? inv.mailed
|
|
431
|
+
? `emailed ${inv.email}`
|
|
432
|
+
: `not emailed (mail is not configured on the server) — send ${inv.email} the link above`
|
|
433
|
+
: "send this link; they run: passalong team join <link>",
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (sub === "join") {
|
|
438
|
+
const ref = rest[0] || die("usage: passalong team join <link|code>");
|
|
439
|
+
const code = ref.includes("/") ? ref.split("/").filter(Boolean).pop() : ref;
|
|
440
|
+
const r = await api.join(code).catch((e) => die(e.message));
|
|
441
|
+
store.writeConfig({ team: r.slug });
|
|
442
|
+
console.error(`${r.joined ? "joined" : "already in"} ${r.name} (${r.slug}) — now current`);
|
|
443
|
+
const who = await api.me().catch(() => null);
|
|
444
|
+
if (who && !who.handle) console.error("teammates address you by handle: passalong me --handle <you>");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (sub === "use") {
|
|
448
|
+
const slug = rest[0] || die("usage: passalong team use <slug>");
|
|
449
|
+
await api.team(slug).catch((e) => die(e.message));
|
|
450
|
+
store.writeConfig({ team: slug });
|
|
451
|
+
console.error(`current team: ${slug}`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
die("usage: passalong team [create <name> | invite [email] | join <link> | use <slug>]");
|
|
181
455
|
},
|
|
182
456
|
|
|
183
457
|
async setup() {
|
|
@@ -186,15 +460,21 @@ const commands = {
|
|
|
186
460
|
cpSync(join(PKG, "skill"), skillDir, { recursive: true });
|
|
187
461
|
console.error(`installed Claude Code skill → ${skillDir}`);
|
|
188
462
|
try {
|
|
189
|
-
execFileSync("claude", ["mcp", "add", "--scope", "user", "passalong", "--", "passalong", "mcp"], {
|
|
463
|
+
execFileSync("claude", ["mcp", "add", "--scope", "user", "passalong", "--", "passalong", "mcp"], {
|
|
464
|
+
stdio: "pipe",
|
|
465
|
+
});
|
|
190
466
|
console.error("registered MCP server: claude mcp add passalong -- passalong mcp");
|
|
191
467
|
} catch (err) {
|
|
192
468
|
const msg = String(err.stderr || err.message);
|
|
193
469
|
if (/already exists/i.test(msg)) console.error("MCP server already registered");
|
|
194
|
-
else
|
|
470
|
+
else
|
|
471
|
+
console.error(
|
|
472
|
+
`could not register MCP server automatically (${msg.trim()}).\n run: claude mcp add passalong -- passalong mcp`,
|
|
473
|
+
);
|
|
195
474
|
}
|
|
196
|
-
if (!api.loggedIn())
|
|
197
|
-
|
|
475
|
+
if (!api.loggedIn())
|
|
476
|
+
console.error("sync is off; run `passalong login` when you want guides to follow you across machines");
|
|
477
|
+
console.error('done. In Claude Code, say "pass this along" at the end of a task, or run /passalong-capture.');
|
|
198
478
|
},
|
|
199
479
|
|
|
200
480
|
async mcp() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passalong",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hand a finished implementation from one context to another, in a form an AI agent can act on.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"cli",
|
|
12
12
|
"markdown"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://passalong.
|
|
14
|
+
"homepage": "https://passalong.dev",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "https://github.com/codellyson/passalong.git",
|
package/skill/SKILL.md
CHANGED
|
@@ -11,27 +11,58 @@ session's history. Write for execution, not for permanence.
|
|
|
11
11
|
|
|
12
12
|
## Procedure
|
|
13
13
|
|
|
14
|
-
1. **
|
|
14
|
+
1. **Pick the one thing to transfer.** If the user named it (`/passalong-capture <what>` or
|
|
15
|
+
"pass along the webhook fix"), that is the scope. Otherwise take the most recent completed
|
|
16
|
+
piece of work. A long session usually holds several; do not merge them, and if it is
|
|
17
|
+
genuinely unclear which one the user means, ask before writing.
|
|
18
|
+
2. **Reconstruct the story from the session.** What was the problem, what was tried, what failed,
|
|
15
19
|
what was decided and why, what finally worked, and how it was verified. Read the diff
|
|
16
20
|
(`git diff`, `git log`) only to confirm details; the reasoning is in the transcript.
|
|
17
|
-
|
|
21
|
+
3. **Write the guide** with the structure below. Be concrete: file paths, commands, exact error
|
|
18
22
|
strings, version numbers. Prefer a short guide with every section filled over a long one.
|
|
19
|
-
|
|
23
|
+
4. **Mark context-specific parts** so the receiver can adapt rather than copy. Put
|
|
20
24
|
`ASSUMES: <thing>. If <alternative>, <what changes>.` at the start of any step that depends on
|
|
21
25
|
this repo's stack, layout, or conventions.
|
|
22
|
-
|
|
26
|
+
5. **Save the draft** to `~/.passalong/drafts/<slug>.md` (create the directory if needed), then run:
|
|
23
27
|
|
|
24
28
|
```bash
|
|
25
29
|
passalong share ~/.passalong/drafts/<slug>.md --no-edit
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
If the user named a person or team ("share this with Ada", "hand this to the platform
|
|
33
|
+
team"), add `--to <team>` or `--to <team>/<handle>`; `passalong team` lists the current
|
|
34
|
+
team's members and their handles. It prints the guide id on stdout and the share link (and
|
|
35
|
+
who it was handed to) on stderr.
|
|
36
|
+
6. **Report** the id (and link if synced) and one line on how to use it on the other side:
|
|
37
|
+
`passalong start <id>` in the target repo, or "start passalong <id>" to an agent with the
|
|
38
|
+
Passalong MCP server. `start` rather than `pull` on purpose: it takes the handoff as well as
|
|
39
|
+
fetching it, and until somebody says they are on it the sender cannot tell a guide nobody
|
|
40
|
+
noticed from one somebody is deep in.
|
|
31
41
|
|
|
32
42
|
If the `passalong` command is missing, tell the user to run `npm i -g passalong && passalong setup` and
|
|
33
43
|
still write the draft file so nothing is lost.
|
|
34
44
|
|
|
45
|
+
## Two kinds of guide
|
|
46
|
+
|
|
47
|
+
`kind:` in the frontmatter says what a guide is for, and the receiver behaves differently for
|
|
48
|
+
each. Absent means `transfer`, which is what every guide written before bug reports existed is.
|
|
49
|
+
|
|
50
|
+
| kind | what it is | what the receiver does |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `transfer` (default) | finished work to repeat somewhere else | follows the **Steps** |
|
|
53
|
+
| `bug` | a defect to fix where it is | fixes it — **Reproduce** shows the problem, it is not a procedure to apply |
|
|
54
|
+
|
|
55
|
+
To file bugs you found but are not fixing, call `file_bugs` with all of them in one call — it
|
|
56
|
+
opens a report and publishes each issue as its own guide, so any one of them can be handed to
|
|
57
|
+
whoever fixes it. `publish_guide` is for a single guide: work you finished and want repeated, or
|
|
58
|
+
one bug on its own.
|
|
59
|
+
|
|
60
|
+
A bug has no `## Steps` and publishing one with a Steps section is refused. That is the whole
|
|
61
|
+
reason the kinds are separate: "follow its Steps" is what the MCP server tells every agent that
|
|
62
|
+
pulls a guide, and steps that reproduce a defect are the one list that must never be run as a
|
|
63
|
+
remedy — an agent that follows them reproduces the bug, checks the Verification, finds it false
|
|
64
|
+
because the bug is real, and reports that the guide does not work.
|
|
65
|
+
|
|
35
66
|
## Guide structure
|
|
36
67
|
|
|
37
68
|
```markdown
|
|
@@ -64,12 +95,46 @@ Commands to run and what they should print. Test cases. What "done" looks like.
|
|
|
64
95
|
- What failed along the way, the exact error, and why. What looked right but wasn't.
|
|
65
96
|
```
|
|
66
97
|
|
|
98
|
+
## Bug structure
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
title: <what is broken, in one line: "Undo leaves section drag handles dead">
|
|
103
|
+
kind: bug
|
|
104
|
+
severity: <s1 blocker | s2 major | s3 minor | s4 cosmetic>
|
|
105
|
+
area: <which surface it is on>
|
|
106
|
+
report: <the report id, when it was filed with others>
|
|
107
|
+
status: published
|
|
108
|
+
tags: [bug, <2 to 4 more>]
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
> **Bug report.** The steps under Reproduce show the problem — they are not a fix to apply. Fix what Problem describes, then check Verification.
|
|
112
|
+
|
|
113
|
+
## Problem
|
|
114
|
+
What is broken and what it stops someone doing. The observable symptom, and the error if there is one.
|
|
115
|
+
|
|
116
|
+
## Reproduce
|
|
117
|
+
1. The steps that show the bug. These produce the defect — they are not a fix to apply.
|
|
118
|
+
|
|
119
|
+
## Verification
|
|
120
|
+
The behaviour that should have happened, as something the fixer can check.
|
|
121
|
+
|
|
122
|
+
## Gotchas
|
|
123
|
+
- Anything already ruled out, or that made it hard to pin down.
|
|
124
|
+
```
|
|
125
|
+
|
|
67
126
|
## Quality bar
|
|
68
127
|
|
|
69
128
|
- **Gotchas is the highest-value section.** If nothing went wrong, say what would have gone
|
|
70
129
|
wrong without a specific decision. Never leave it empty.
|
|
71
130
|
- **Verification must be runnable.** A command and an expected output, not "make sure it works".
|
|
72
131
|
- **Every ASSUMES has an alternative.** The point is adaptation on the other side.
|
|
132
|
+
- **One problem per guide.** A lesson from the same session that is not part of this fix goes in
|
|
133
|
+
its own guide (write and share it separately), not in this one's Gotchas.
|
|
73
134
|
- **No session narration.** "First I looked at..." is noise. State the finding.
|
|
74
135
|
- **No secrets.** Redact tokens, keys, and internal hostnames that would not apply elsewhere.
|
|
75
136
|
- Title, Problem, and Steps are required; the guide will not publish without them.
|
|
137
|
+
- For a bug: title, Problem and **Reproduce**. Never a Steps section — see the two kinds above.
|
|
138
|
+
- The line above Problem is written by `file_bugs` and the hub's form. It is in the document, not
|
|
139
|
+
added by whatever served it, so an agent that fetched the share link over plain HTTP — with no
|
|
140
|
+
MCP server and no knowledge of Passalong — still reads what the document is before acting on it.
|