passalong 0.13.1 → 0.14.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 +2 -1
- package/bin/passalong +87 -36
- package/package.json +2 -2
- package/src/api.js +26 -0
- package/src/guide.js +98 -0
- package/src/mcp.js +82 -34
package/README.md
CHANGED
|
@@ -5,13 +5,14 @@ Hand work to your AI coding agents, and get back what they did and how they chec
|
|
|
5
5
|
Write a task, a bug or a finished change once. The agent that takes it starts knowing what done
|
|
6
6
|
looks like and what was already decided, reports as it goes, and hands back a write-up with the
|
|
7
7
|
evidence for each thing you asked for, which you approve or send back. One agent per job, and
|
|
8
|
-
everyone can see who has what. Works with Claude Code,
|
|
8
|
+
everyone can see who has what. Works with Claude Code, Codex, ChatGPT, Claude, or anything that
|
|
9
9
|
speaks MCP or HTTP. See [passalong.dev](https://passalong.dev).
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm i -g passalong
|
|
13
13
|
passalong setup # installs the Claude Code capture skill + registers the MCP server
|
|
14
14
|
passalong login # optional: sync guides across machines and get share links
|
|
15
|
+
codex mcp add passalong -- passalong mcp # Codex: the same MCP server
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
## The loop
|
package/bin/passalong
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// passalong archive <id> off your board and out of the free tier's count (was `done`)
|
|
40
40
|
// passalong rm <id> delete a guide locally and from sync
|
|
41
41
|
// passalong export [dir] dump every guide as plain markdown (default ./passalong-export)
|
|
42
|
-
// passalong login [token] sign in
|
|
42
|
+
// passalong login [token] sign in from the browser, or attach this machine with a token
|
|
43
43
|
// passalong me [--handle H] [--name N] [--email E] who you are to teammates
|
|
44
44
|
// passalong team current team and its members
|
|
45
45
|
// passalong team create <name> start a team (you become its owner)
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
// passalong mcp run the MCP server over stdio
|
|
52
52
|
// passalong version print the installed version (also -v, --version)
|
|
53
53
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
54
|
-
import { createHash } from "node:crypto";
|
|
54
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
55
55
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
56
56
|
import { homedir, hostname } from "node:os";
|
|
57
57
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -94,6 +94,86 @@ const tty = process.stdout.isTTY && process.stdin.isTTY;
|
|
|
94
94
|
* Written against readline rather than a dependency because this package has none beyond the MCP
|
|
95
95
|
* SDK and zod, and asking for a line of text is not where that changes.
|
|
96
96
|
*/
|
|
97
|
+
/** Open a page in the default browser, and say nothing if there is none to open. */
|
|
98
|
+
function openPage(url) {
|
|
99
|
+
const [cmd, args] =
|
|
100
|
+
process.platform === "darwin"
|
|
101
|
+
? ["open", [url]]
|
|
102
|
+
: process.platform === "win32"
|
|
103
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
104
|
+
: ["xdg-open", [url]];
|
|
105
|
+
try {
|
|
106
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => {}).unref();
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sign in by approving in the browser: show a code, wait, and be handed a token. No password is
|
|
112
|
+
* typed here and no token is pasted between windows. Needs no terminal to ask on, so an agent can
|
|
113
|
+
* run it and tell the person which page to open.
|
|
114
|
+
*/
|
|
115
|
+
async function browserLogin() {
|
|
116
|
+
const verifier = randomBytes(32).toString("hex");
|
|
117
|
+
const challenge = createHash("sha256").update(verifier).digest("hex");
|
|
118
|
+
const start = await api.deviceStart(challenge, `${hostname()} cli`).catch((e) => die(e.message));
|
|
119
|
+
console.error(`Open ${start.url}`);
|
|
120
|
+
console.error(`and check the page shows the code ${start.code}, then press Approve.`);
|
|
121
|
+
if (tty) openPage(start.url);
|
|
122
|
+
console.error("Waiting for you to approve it here… (Ctrl-C to stop)");
|
|
123
|
+
const until = Date.now() + start.expires_in * 1000;
|
|
124
|
+
while (Date.now() < until) {
|
|
125
|
+
await new Promise((r) => setTimeout(r, start.interval * 1000));
|
|
126
|
+
const got = await api.devicePoll(start.id, verifier).catch((e) => die(e.message));
|
|
127
|
+
if (!got) continue;
|
|
128
|
+
store.writeConfig({ token: got.token, api: api.baseUrl() });
|
|
129
|
+
console.error(`signed in (account ${got.account}) on ${api.baseUrl()}. Sync is on.`);
|
|
130
|
+
console.error("Attach other machines with:");
|
|
131
|
+
console.error(` passalong login ${got.token}`);
|
|
132
|
+
console.error("(keep it private — it is a credential, not a password)");
|
|
133
|
+
console.error("Give teammates a name to address you by: passalong me --handle <you>");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
die("nobody approved it in time. Run `passalong login` again.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The old way, kept for anyone who asks for it with `--email`: an email and a password, typed here. */
|
|
140
|
+
async function passwordLogin(f) {
|
|
141
|
+
// An account you cannot sign in to is one that dies with this file. This used to mint one
|
|
142
|
+
// silently and then suggest adding a password afterwards; it asks up front instead.
|
|
143
|
+
if (!tty)
|
|
144
|
+
die(
|
|
145
|
+
"no terminal to ask on. Sign in at " +
|
|
146
|
+
`${api.baseUrl()}/hub, then attach this machine with \`passalong login <token>\``,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const email = f.email || (await ask("Email: "));
|
|
150
|
+
if (!email) die("an email is needed to sign in");
|
|
151
|
+
const password = await ask("Password: ", { hidden: true });
|
|
152
|
+
if (!password) die("a password is needed to sign in");
|
|
153
|
+
|
|
154
|
+
const label = `${hostname()} cli`;
|
|
155
|
+
let who = await api.signIn(email, password, { label }).catch((e) => e);
|
|
156
|
+
if (who instanceof Error) {
|
|
157
|
+
// 401 is "no such account, or the wrong password", said the same way on purpose so the API
|
|
158
|
+
// does not confirm which addresses exist. The CLI has to ask rather than guess, because
|
|
159
|
+
// creating an account for a mistyped address is worse than being told to try again.
|
|
160
|
+
if (who.status !== 401) die(who.message);
|
|
161
|
+
const yes = (await ask(`No account signed in as ${email}. Create one? [y/N] `)).toLowerCase();
|
|
162
|
+
if (yes !== "y" && yes !== "yes")
|
|
163
|
+
die("not signed in. If the password was wrong, try again, or reset it at " + `${api.baseUrl()}/hub`);
|
|
164
|
+
const again = await ask("Choose a password: ", { hidden: true });
|
|
165
|
+
if (again !== password) die("those did not match");
|
|
166
|
+
who = await api.signIn(email, password, { make: true, label }).catch((e) => die(e.message));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
store.writeConfig({ token: who.token, api: api.baseUrl() });
|
|
170
|
+
console.error(`signed in as ${email} (account ${who.account}) on ${api.baseUrl()}. Sync is on.`);
|
|
171
|
+
console.error("Attach other machines with:");
|
|
172
|
+
console.error(` passalong login ${who.token}`);
|
|
173
|
+
console.error("(keep it private — it is a credential, not a password)");
|
|
174
|
+
console.error("Give teammates a name to address you by: passalong me --handle <you>");
|
|
175
|
+
}
|
|
176
|
+
|
|
97
177
|
async function ask(question, { hidden = false } = {}) {
|
|
98
178
|
const { createInterface } = await import("node:readline");
|
|
99
179
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
@@ -698,40 +778,11 @@ const commands = {
|
|
|
698
778
|
return;
|
|
699
779
|
}
|
|
700
780
|
}
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
`${api.baseUrl()}/hub, then attach this machine with \`passalong login <token>\``,
|
|
707
|
-
);
|
|
708
|
-
|
|
709
|
-
const email = f.email || (await ask("Email: "));
|
|
710
|
-
if (!email) die("an email is needed to sign in");
|
|
711
|
-
const password = await ask("Password: ", { hidden: true });
|
|
712
|
-
if (!password) die("a password is needed to sign in");
|
|
713
|
-
|
|
714
|
-
const label = `${hostname()} cli`;
|
|
715
|
-
let who = await api.signIn(email, password, { label }).catch((e) => e);
|
|
716
|
-
if (who instanceof Error) {
|
|
717
|
-
// 401 is "no such account, or the wrong password", said the same way on purpose so the API
|
|
718
|
-
// does not confirm which addresses exist. The CLI has to ask rather than guess, because
|
|
719
|
-
// creating an account for a mistyped address is worse than being told to try again.
|
|
720
|
-
if (who.status !== 401) die(who.message);
|
|
721
|
-
const yes = (await ask(`No account signed in as ${email}. Create one? [y/N] `)).toLowerCase();
|
|
722
|
-
if (yes !== "y" && yes !== "yes")
|
|
723
|
-
die("not signed in. If the password was wrong, try again, or reset it at " + `${api.baseUrl()}/hub`);
|
|
724
|
-
const again = await ask("Choose a password: ", { hidden: true });
|
|
725
|
-
if (again !== password) die("those did not match");
|
|
726
|
-
who = await api.signIn(email, password, { make: true, label }).catch((e) => die(e.message));
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
store.writeConfig({ token: who.token, api: api.baseUrl() });
|
|
730
|
-
console.error(`signed in as ${email} (account ${who.account}) on ${api.baseUrl()}. Sync is on.`);
|
|
731
|
-
console.error("Attach other machines with:");
|
|
732
|
-
console.error(` passalong login ${who.token}`);
|
|
733
|
-
console.error("(keep it private — it is a credential, not a password)");
|
|
734
|
-
console.error("Give teammates a name to address you by: passalong me --handle <you>");
|
|
781
|
+
// Passwords are for someone who asks: `--email` keeps the prompts. Everyone else is sent to the
|
|
782
|
+
// browser, which works with no terminal at all — an agent can start it, and the person it is
|
|
783
|
+
// working for clicks once.
|
|
784
|
+
if (f.email || f.password) return passwordLogin(f);
|
|
785
|
+
return browserLogin();
|
|
735
786
|
},
|
|
736
787
|
|
|
737
788
|
async me(f) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passalong",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Hand tasks, bugs and finished work to AI coding agents, and get back a write-up with evidence.",
|
|
5
5
|
"mcpName": "io.github.codellyson/passalong",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"mcp-server",
|
|
15
15
|
"coding-agents",
|
|
16
16
|
"task-management",
|
|
17
|
-
"
|
|
17
|
+
"codex"
|
|
18
18
|
],
|
|
19
19
|
"homepage": "https://passalong.dev",
|
|
20
20
|
"repository": {
|
package/src/api.js
CHANGED
|
@@ -126,6 +126,32 @@ export async function signIn(email, password, { make = false, label = "" } = {})
|
|
|
126
126
|
* still live and still right for the invite page, which mints before it claims, and because
|
|
127
127
|
* removing a published export is a breaking change for anyone who imported it.
|
|
128
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* Sign this machine in from the browser. `challenge` is the sha256, in hex, of a secret kept here;
|
|
131
|
+
* what comes back is a short code to show, the page to open, and how often to ask whether it was
|
|
132
|
+
* approved. Nothing in the answer is a credential.
|
|
133
|
+
*/
|
|
134
|
+
export const deviceStart = (challenge, label) =>
|
|
135
|
+
call("/v1/device/start", { method: "POST", auth: false, body: { challenge, label } });
|
|
136
|
+
|
|
137
|
+
/** `null` while nobody has said yes; the account and its token once somebody has. */
|
|
138
|
+
export async function devicePoll(id, verifier) {
|
|
139
|
+
let res;
|
|
140
|
+
try {
|
|
141
|
+
res = await fetch(`${baseUrl()}/v1/device/poll`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { ...CLIENT, "content-type": "application/json" },
|
|
144
|
+
body: JSON.stringify({ id, verifier }),
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
throw new ApiError(0, `could not reach ${baseUrl()} (${err.message})`);
|
|
148
|
+
}
|
|
149
|
+
if (res.status === 202) return null;
|
|
150
|
+
const said = await res.json().catch(() => ({}));
|
|
151
|
+
if (!res.ok) throw new ApiError(res.status, said.message || res.statusText);
|
|
152
|
+
return said;
|
|
153
|
+
}
|
|
154
|
+
|
|
129
155
|
export const createAccount = () => call("/v1/accounts", { method: "POST", auth: false });
|
|
130
156
|
export const me = () => call("/v1/me");
|
|
131
157
|
export const updateMe = (patch) => call("/v1/me", { method: "PATCH", body: patch });
|
package/src/guide.js
CHANGED
|
@@ -709,3 +709,101 @@ function taskTemplate(meta = {}) {
|
|
|
709
709
|
export function stripPlaceholders(body) {
|
|
710
710
|
return body.replace(/^<!-- passalong:.*?-->\n?/gm, "").trim();
|
|
711
711
|
}
|
|
712
|
+
|
|
713
|
+
// ---- reading a guide in parts, and how big one may be -----------------------------------------
|
|
714
|
+
// Mirrors apps/api/src/sections.ts — apps/api/test/sections.test.mjs holds the two to the same
|
|
715
|
+
// answers. Most of what is stored is a few very large guides (six of 123 held 72% of the text), and
|
|
716
|
+
// an agent that wants only the Acceptance of a long task should not read the rest first.
|
|
717
|
+
|
|
718
|
+
/** Over this a guide is large: its readers are offered the outline first, and its author a warning. */
|
|
719
|
+
export const GUIDE_WARN = 20_000;
|
|
720
|
+
/** Over this a new guide is refused by the server. A guide stored before it existed may keep its size. */
|
|
721
|
+
export const GUIDE_MAX = 60_000;
|
|
722
|
+
|
|
723
|
+
/** Where each heading starts, outside the frontmatter and outside code fences. */
|
|
724
|
+
function headingLines(markdown) {
|
|
725
|
+
const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(markdown);
|
|
726
|
+
const bodyStart = fm ? fm[0].length : 0;
|
|
727
|
+
const lines = [];
|
|
728
|
+
let fence = "";
|
|
729
|
+
let at = bodyStart;
|
|
730
|
+
for (const raw of markdown.slice(bodyStart).split("\n")) {
|
|
731
|
+
const mark = /^\s*(```|~~~)/.exec(raw)?.[1];
|
|
732
|
+
if (mark) fence = fence ? (fence === mark ? "" : fence) : mark;
|
|
733
|
+
const m = !fence && !mark ? /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(raw) : null;
|
|
734
|
+
if (m) lines.push({ heading: m[2].trim(), level: m[1].length, start: at });
|
|
735
|
+
at += raw.length + 1;
|
|
736
|
+
}
|
|
737
|
+
return lines;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** The end of the section that begins at lines[i]: the next heading at its level or above. */
|
|
741
|
+
function sectionEnd(lines, i, total) {
|
|
742
|
+
for (let j = i + 1; j < lines.length; j++)
|
|
743
|
+
if (lines[j].level <= lines[i].level) return lines[j].start;
|
|
744
|
+
return total;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Every heading, with the characters in its section (nested ones included). */
|
|
748
|
+
export function outline(markdown) {
|
|
749
|
+
const lines = headingLines(markdown);
|
|
750
|
+
return lines.map((l, i) => ({
|
|
751
|
+
heading: l.heading,
|
|
752
|
+
level: l.level,
|
|
753
|
+
chars: sectionEnd(lines, i, markdown.length) - l.start,
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** One section by its heading — exact, then by its start, then by what it contains. */
|
|
758
|
+
export function section(markdown, name) {
|
|
759
|
+
const lines = headingLines(markdown);
|
|
760
|
+
const want = String(name ?? "")
|
|
761
|
+
.trim()
|
|
762
|
+
.toLowerCase();
|
|
763
|
+
if (!want) return null;
|
|
764
|
+
const at = (test) => lines.findIndex((l) => test(l.heading.toLowerCase()));
|
|
765
|
+
const i = [
|
|
766
|
+
at((h) => h === want),
|
|
767
|
+
at((h) => h.startsWith(want)),
|
|
768
|
+
at((h) => h.includes(want)),
|
|
769
|
+
].find((n) => n >= 0);
|
|
770
|
+
if (i === undefined) return null;
|
|
771
|
+
return {
|
|
772
|
+
heading: lines[i].heading,
|
|
773
|
+
text: markdown.slice(lines[i].start, sectionEnd(lines, i, markdown.length)).trimEnd(),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** What a guide's author is told when it is on the large side. */
|
|
778
|
+
export function sizeWarning(chars) {
|
|
779
|
+
return chars > GUIDE_WARN
|
|
780
|
+
? `this guide is ${chars.toLocaleString("en")} characters, and over ${GUIDE_WARN.toLocaleString("en")} ` +
|
|
781
|
+
"it is read by outline first. Put logs, dumps and long output in a file (attach_file) and " +
|
|
782
|
+
"link it; keep the guide to what the next agent has to act on."
|
|
783
|
+
: null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* What an agent is shown instead of a large guide: who it is, what it is, and where its parts are.
|
|
788
|
+
* `path` is where the whole of it was written, for a search rather than a read.
|
|
789
|
+
*/
|
|
790
|
+
export function outlineText(markdown, { id = "", path = "" } = {}) {
|
|
791
|
+
const { meta } = parse(markdown);
|
|
792
|
+
const rows = outline(markdown)
|
|
793
|
+
.filter((h) => h.level <= 3)
|
|
794
|
+
.map(
|
|
795
|
+
(h) => `${" ".repeat(h.level - 1)}- ${h.heading} (${h.chars.toLocaleString("en")} chars)`,
|
|
796
|
+
);
|
|
797
|
+
return [
|
|
798
|
+
`${meta.title || id} — ${meta.kind || "guide"}, ${markdown.length.toLocaleString("en")} characters`,
|
|
799
|
+
meta.summary ? `Summary: ${meta.summary}` : "",
|
|
800
|
+
"",
|
|
801
|
+
"Sections:",
|
|
802
|
+
...rows,
|
|
803
|
+
"",
|
|
804
|
+
`Read one with get_guide ref=${id} section="<heading>", or the whole thing with full=true.` +
|
|
805
|
+
(path ? ` The full text is also on disk at ${path}, if you only need to search it.` : ""),
|
|
806
|
+
]
|
|
807
|
+
.filter((l, i, a) => l !== "" || a[i - 1] !== "")
|
|
808
|
+
.join("\n");
|
|
809
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -9,7 +9,18 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import * as api from "./api.js";
|
|
11
11
|
import { refusal, runChecks } from "./checks.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
AREAS,
|
|
14
|
+
BUG_SECTIONS,
|
|
15
|
+
GUIDE_WARN,
|
|
16
|
+
outlineText,
|
|
17
|
+
parse,
|
|
18
|
+
SECTIONS,
|
|
19
|
+
section as sectionOf,
|
|
20
|
+
sizeWarning,
|
|
21
|
+
TASK_SECTIONS,
|
|
22
|
+
template,
|
|
23
|
+
} from "./guide.js";
|
|
13
24
|
import * as passalong from "./passalong.js";
|
|
14
25
|
|
|
15
26
|
/**
|
|
@@ -396,7 +407,7 @@ export function buildServer() {
|
|
|
396
407
|
if (h)
|
|
397
408
|
return fail({
|
|
398
409
|
message:
|
|
399
|
-
`${err.message}. Somebody else is on it here${h.note ? ` (last said: "${h.note}")` : ""}. ` +
|
|
410
|
+
`${err.message}. Somebody else is on it here${h.agent ? ` (agent ${h.agent}${h.host ? ` on ${h.host}` : ""})` : ""}${h.note ? ` (last said: "${h.note}")` : ""}. ` +
|
|
400
411
|
"Tell the user before doing this work too; get_guide reads it without taking it.",
|
|
401
412
|
});
|
|
402
413
|
return failWith(err);
|
|
@@ -787,36 +798,44 @@ export function buildServer() {
|
|
|
787
798
|
),
|
|
788
799
|
checks: z
|
|
789
800
|
.array(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
"
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
801
|
+
// A bare string is a check line with nothing behind it. Let it through to the server's
|
|
802
|
+
// refusal, which says what to send, rather than a schema error that says only "expected
|
|
803
|
+
// object".
|
|
804
|
+
z.preprocess(
|
|
805
|
+
(v) => (typeof v === "string" ? { check: v } : v),
|
|
806
|
+
z.object({
|
|
807
|
+
check: z
|
|
808
|
+
.string()
|
|
809
|
+
.describe(
|
|
810
|
+
"the line this answers, in the guide's own words: an Acceptance line on a task, " +
|
|
811
|
+
"a Verification line on a handoff or a bug",
|
|
812
|
+
),
|
|
813
|
+
says: z
|
|
814
|
+
.string()
|
|
815
|
+
.optional()
|
|
816
|
+
.describe(
|
|
817
|
+
'what happened, in one plain sentence for a PERSON: "Swagger lists all six ' +
|
|
818
|
+
'endpoints, each with request and response schemas." It is the row they read ' +
|
|
819
|
+
"in the review table; the evidence stays folded behind it. Strongly " +
|
|
820
|
+
"recommended: a check without one shows only the line it answers",
|
|
821
|
+
),
|
|
822
|
+
ran: z
|
|
823
|
+
.string()
|
|
824
|
+
.optional()
|
|
825
|
+
.describe(
|
|
826
|
+
"what you ran for it, and what came back; not needed when `cmd` is set",
|
|
827
|
+
),
|
|
828
|
+
cmd: z
|
|
829
|
+
.string()
|
|
830
|
+
.optional()
|
|
831
|
+
.describe(
|
|
832
|
+
"the shell command that proves this line, run here before the hand-in lands — " +
|
|
833
|
+
"its exit code decides the check and its output is recorded as the evidence. " +
|
|
834
|
+
"Non-zero refuses the hand-in. Leave it out for a check nobody can run, like " +
|
|
835
|
+
'"the badge reads 3" — then SHOW it with attach_screenshot and put the line it returns in `ran`; a check is run or shown, and describing what you saw is refused',
|
|
836
|
+
),
|
|
837
|
+
}),
|
|
838
|
+
),
|
|
820
839
|
)
|
|
821
840
|
.optional()
|
|
822
841
|
.describe(
|
|
@@ -928,7 +947,9 @@ export function buildServer() {
|
|
|
928
947
|
title: "Get guide",
|
|
929
948
|
annotations: READS,
|
|
930
949
|
description:
|
|
931
|
-
"READ a transfer guide by passalong id or share link and return its
|
|
950
|
+
"READ a transfer guide by passalong id or share link and return its markdown. A guide over " +
|
|
951
|
+
"20,000 characters comes back as an outline (its headings and their sizes) unless you ask " +
|
|
952
|
+
"for `section`, for one part, or `full`, for all of it. Also " +
|
|
932
953
|
"writes it to .passalong/<id>.md in the working directory so it survives the session. " +
|
|
933
954
|
"Pulling a teammate's guide tells them the transfer landed. Use this to look at a guide " +
|
|
934
955
|
"you have not committed to. If you are about to DO the work, call take instead: " +
|
|
@@ -940,12 +961,38 @@ export function buildServer() {
|
|
|
940
961
|
.string()
|
|
941
962
|
.optional()
|
|
942
963
|
.describe("directory to write .passalong/<id>.md into; default is the server's cwd"),
|
|
964
|
+
section: z
|
|
965
|
+
.string()
|
|
966
|
+
.optional()
|
|
967
|
+
.describe("read only the part under this heading, e.g. Acceptance; see the outline"),
|
|
968
|
+
outline: z
|
|
969
|
+
.boolean()
|
|
970
|
+
.optional()
|
|
971
|
+
.describe("just the headings and their sizes, whatever the guide's length"),
|
|
972
|
+
full: z
|
|
973
|
+
.boolean()
|
|
974
|
+
.optional()
|
|
975
|
+
.describe("the whole document, even one over 20,000 characters"),
|
|
943
976
|
},
|
|
944
977
|
},
|
|
945
|
-
async ({ ref, cwd }) => {
|
|
978
|
+
async ({ ref, cwd, section, outline, full }) => {
|
|
946
979
|
try {
|
|
947
980
|
const { markdown, path, from } = await passalong.pull(ref, { cwd: cwd || process.cwd() });
|
|
948
981
|
const meta = parse(markdown).meta;
|
|
982
|
+
if (section) {
|
|
983
|
+
const part = sectionOf(markdown, section);
|
|
984
|
+
if (!part)
|
|
985
|
+
return text(
|
|
986
|
+
`No heading matching "${section}".\n\n${outlineText(markdown, { id: meta.id || ref, path })}`,
|
|
987
|
+
);
|
|
988
|
+
return text(
|
|
989
|
+
`${part.text}\n\n<!-- passalong: ${meta.id || ref}, "${part.heading}" only; the whole guide is at ${path} -->`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
if (outline || (!full && markdown.length > GUIDE_WARN))
|
|
993
|
+
return text(
|
|
994
|
+
`${outlineText(markdown, { id: meta.id || ref, path })}\n\n<!-- passalong: ${from}; written to ${path} -->`,
|
|
995
|
+
);
|
|
949
996
|
const lead = leadFor(meta);
|
|
950
997
|
const siblings = await related(meta);
|
|
951
998
|
// Follow-ups are more context for this guide, so they come with it — after the document,
|
|
@@ -1144,6 +1191,7 @@ export function buildServer() {
|
|
|
1144
1191
|
parent: guide.meta.parent || "",
|
|
1145
1192
|
notified,
|
|
1146
1193
|
path,
|
|
1194
|
+
...(sizeWarning(markdown.length) ? { warning: sizeWarning(markdown.length) } : {}),
|
|
1147
1195
|
// A task does not go live on publish, and an agent that reports "queued it" has told
|
|
1148
1196
|
// the user something untrue. Said in the result so it is said to them.
|
|
1149
1197
|
...(guide.meta.kind === "task" && guide.meta.status === "draft"
|