startup-builder 0.2.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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/chunk-ZR74VEI2.js +2322 -0
- package/dist/cli.js +338 -188
- package/dist/index.d.ts +660 -430
- package/dist/index.js +77 -17
- package/package.json +23 -16
- package/dist/chunk-NWM7TYW6.js +0 -1004
package/dist/cli.js
CHANGED
|
@@ -1,206 +1,356 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Api,
|
|
4
|
+
Auth,
|
|
5
|
+
DEFAULT_API,
|
|
6
|
+
DEFAULT_MODEL,
|
|
7
|
+
DEFAULT_WALL_MINUTES,
|
|
8
|
+
Leases,
|
|
9
|
+
Session,
|
|
10
|
+
agentRunner,
|
|
11
|
+
apiBase,
|
|
12
|
+
batchBodyPath,
|
|
13
|
+
batchText,
|
|
14
|
+
buildWorklist,
|
|
15
|
+
closeText,
|
|
16
|
+
configDir,
|
|
17
|
+
doVerb,
|
|
18
|
+
fetchWorklistMarkdown,
|
|
19
|
+
leasesDir,
|
|
20
|
+
parseBody,
|
|
21
|
+
parseInput,
|
|
22
|
+
parseWorker,
|
|
23
|
+
renderPrompt,
|
|
24
|
+
runCloud,
|
|
25
|
+
runnerBase,
|
|
26
|
+
serveStdio,
|
|
27
|
+
tokenFile,
|
|
28
|
+
work
|
|
29
|
+
} from "./chunk-ZR74VEI2.js";
|
|
8
30
|
|
|
9
31
|
// src/cli.ts
|
|
10
|
-
import {
|
|
32
|
+
import { createRequire } from "module";
|
|
33
|
+
import { readFileSync, realpathSync } from "fs";
|
|
34
|
+
import { join } from "path";
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
import { Command as Command2, Option as Option2 } from "commander";
|
|
11
37
|
import pc from "picocolors";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
|
|
39
|
+
// src/next/commands.ts
|
|
40
|
+
import { Option } from "commander";
|
|
41
|
+
var startupsOf = (v) => (v ?? []).flatMap((s) => s.split(",")).map((s) => s.trim().replace(/^https?:\/\/[^/]+\//, "").replace(/\/+$/, "")).filter(Boolean);
|
|
42
|
+
var worker = (target, flag) => flag ?? target;
|
|
43
|
+
var VERSIONS_USAGE = [
|
|
44
|
+
"usage: startup-builder versions <ref> a Thing\u2019s versions, newest first (GET <$id>?versions)",
|
|
45
|
+
" <ref>: a path (a FIX line\u2019s Thing), a $id, or a Task\u2019s number in the batch",
|
|
46
|
+
" e.g. startup-builder versions closers.deals/alternatives/alt_48RO5vekYCXl",
|
|
47
|
+
" startup-builder versions closers.deals/options",
|
|
48
|
+
" then: startup-builder show <path>?version=<n> for one version"
|
|
49
|
+
].join("\n");
|
|
50
|
+
function registerLoop(program2, d) {
|
|
51
|
+
const env = () => d.env ?? process.env;
|
|
52
|
+
const inBatch = () => !!env().SB_BATCH;
|
|
53
|
+
const readBody = (p) => d.readFile(inBatch() ? batchBodyPath(p, env().SB_WORKDIR || (d.cwd ?? process.cwd)()) : p);
|
|
54
|
+
const emit = (o, json) => {
|
|
55
|
+
d.out(json ? JSON.stringify(o.data, null, 2) : o.text);
|
|
56
|
+
if (o.exit) d.exit(o.exit);
|
|
57
|
+
};
|
|
58
|
+
program2.command("queue").description("Open batches on api.sb, highest priority first (GET /queue)").argument("[for:worker]", "only this Worker\u2019s batches, e.g. for:tom").option("--for <worker>", "the Worker (same as for:<worker>)").option("--limit <n>", "how many (default 5)").option("--json", "as api.sb serves it").action(async (target, o) => {
|
|
59
|
+
emit(await d.session().queue({ worker: worker(target, o.for), limit: o.limit ? Number(o.limit) : void 0 }), o.json);
|
|
60
|
+
});
|
|
61
|
+
program2.command("claim").description("Claim a batch: n from the last queue listing, a batch $id, or (default) the best free one, falling through a 409 to the next").argument("[batch]", "n, a batch $id or path, or next").option("--next", "the best free batch (the default)").option("--for <worker>", "only this Worker\u2019s batches").option("--lease <seconds>", "lease length (default 900)").option("--json", "the lease (never its token)").action(async (ref, o) => {
|
|
62
|
+
emit(await d.session().claim({ ref: o.next ? void 0 : ref, worker: o.for, leaseSeconds: o.lease ? Number(o.lease) : void 0 }), o.json);
|
|
63
|
+
});
|
|
64
|
+
program2.command("prompt").description("The claimed batch's prompt (its ?md), or a versioned Prompt at /prompts/{name}; --set writes its next version (the Owner)").argument("[name]", "a Prompt\u2019s name").option("--set <file|->", "new text for the named Prompt, from a file or - for stdin (POST /prompts/{name}:update, as the signed-in Owner)").option("--json", "with --set: the whole Action").action(async (name, o) => {
|
|
65
|
+
if (o.set !== void 0 && inBatch()) throw new Error("prompt --set is refused inside a batch (SB_BATCH is set): editing a Prompt is the Owner's, from their own sign-in");
|
|
66
|
+
const set = o.set === void 0 ? void 0 : o.set === "-" ? await d.readStdin() : d.readFile(o.set);
|
|
67
|
+
emit(await d.session().prompt({ name, set }), o.json);
|
|
68
|
+
});
|
|
69
|
+
program2.command("show").description("Read a Task or Thing, compact: an index (the claimed batch, else the last worklist), a path, or a $id").argument("<ref>", "1-5, a path, or a $id").allowExcessArguments(false).option("--md", "its ?md").option("--json", "the whole Thing").action(async (ref, o) => emit(await d.session().show({ ref, md: o.md }), o.json));
|
|
70
|
+
program2.command("close").description("Close a Task: invoke the Verb whose Event closes it. In a claimed batch: do Mode with the lease token; elsewhere a try preview unless --do/--send").argument("<task>", "an index (1-5), a path, or a $id").allowExcessArguments(false).option("--body <file|->", "the Verb input as JSON, from a file or - for stdin (checked against the closing Event\u2019s shape)").option("--data <json>", "the Verb input as inline JSON").option("--input <key=value...>", "the Verb input as pairs").option("--do", "run it (Prefer: mode=do)").option("--send", "run it durably (Prefer: mode=send)").option("--try", "preview it (Prefer: mode=try): nothing stored").addOption(new Option("--mode <mode>", "the Mode").choices(["try", "send", "do"])).option("--as <worker|role>", "act as (default the Task's assignee)").option("--force", "send even when api.sb says it would be blocked or refused").option("--idempotency-key <key>", "reuse a key (a retry)").option("--json", "the whole result").action(async (ref, o) => {
|
|
71
|
+
let body;
|
|
72
|
+
if (o.body) body = parseBody(o.body === "-" ? await d.readStdin() : readBody(o.body));
|
|
73
|
+
else if (o.data || o.input) body = parseInput(o.input, o.data);
|
|
74
|
+
const mode = o.try ? "try" : o.do ? "do" : o.send ? "send" : o.mode;
|
|
75
|
+
emit(await d.session().close({ ref, body, mode, as: o.as, force: o.force, idempotencyKey: o.idempotencyKey }), o.json);
|
|
76
|
+
});
|
|
77
|
+
program2.command("renew").description("Extend the claimed batch\u2019s lease").option("--lease <seconds>", "new length (default: as claimed)").action(async (o) => {
|
|
78
|
+
if (inBatch()) throw new Error("renew is refused inside a batch (SB_BATCH is set): the loop renews this lease");
|
|
79
|
+
emit(await d.session().renew({ leaseSeconds: o.lease ? Number(o.lease) : void 0 }));
|
|
80
|
+
});
|
|
81
|
+
program2.command("release").description("Give the claimed batch back: its unclosed Tasks return to the queue").action(async () => emit(await d.session().release()));
|
|
82
|
+
program2.command("worklist").description("A Worker's open Tasks (GET /workers/{name}/worklist), one line each, by Startup and step, best first").argument("[for:worker]", "the Worker, e.g. for:tom").option("--for <worker>", "the Worker (same as for:<worker>)").option("--startup <domain...>", "only these Startups (comma or repeat)").option("--limit <n>", "Tasks on the page (default 5; at most 50)").option("--page <n>", "a later page").option("--all", "every page (the long view)").option("--md", "api.sb's ready-to-run Markdown, verbatim").option("--prompt", "the CLI's own ready-to-run prompt").option("--long", "each Task with its $id, closing Verb and Permission").option("--stub", "OFFLINE FALLBACK only: assemble one from api.sb's other endpoints").option("--no-previews", "with --stub: don't read each Task's Verb preview").option("--json", "as api.sb serves it").action(async (target, o) => {
|
|
83
|
+
if (o.md || o.prompt || o.stub || o.all || o.long) return d.worklistMore(target, o);
|
|
84
|
+
const w = worker(target, o.for);
|
|
85
|
+
if (!w) throw new Error("name the Worker: worklist for:<worker> (e.g. for:tom)");
|
|
86
|
+
const page = o.page ? Number(o.page) : void 0;
|
|
87
|
+
if (page !== void 0 && !(Number.isInteger(page) && page >= 1)) throw new Error(`--page takes a page number (1, 2, \u2026), not "${o.page}"`);
|
|
88
|
+
const startups = startupsOf(o.startup ?? (process.env.STARTUP_BUILDER_STARTUP ? [process.env.STARTUP_BUILDER_STARTUP] : []));
|
|
89
|
+
emit(await d.session().worklist({ worker: w, startups, limit: o.limit ? Number(o.limit) : void 0, page }), o.json);
|
|
90
|
+
});
|
|
91
|
+
program2.command("versions").description("A Thing's versions, newest first (GET <$id>?versions)").argument("[ref]", "an index, a path, or a $id").allowExcessArguments(false).option("--json", "as api.sb serves them").action(async (ref, o) => {
|
|
92
|
+
if (!ref) return emit({ text: VERSIONS_USAGE, data: { usage: VERSIONS_USAGE }, exit: 2 }, false);
|
|
93
|
+
emit(await d.session().versions({ ref }), o.json);
|
|
94
|
+
});
|
|
95
|
+
program2.command("revert").description("Put a Thing back to an earlier version (POST <$id>:revert {to}): a try preview unless --do").argument("<ref>", "an index, a path, or a $id").requiredOption("--to <version>", "the version, e.g. 3 or v3").option("--do", "run it").option("--json", "the whole result").action(async (ref, o) => emit(await d.session().revert({ ref, to: o.to, mode: o.do ? "do" : "try" }), o.json));
|
|
96
|
+
program2.command("changes").description("Recent writes across the portfolio (GET /versions)").option("--for <worker>", "by this Worker").option("--worker <worker>", "same as --for").option("--startup <domain>", "in this Startup").option("--limit <n>", "how many (default 20)").option("--json", "as api.sb serves them").action(
|
|
97
|
+
async (o) => emit(await d.session().changes({ worker: o.for ?? o.worker, startup: o.startup, limit: o.limit ? Number(o.limit) : void 0 }), o.json)
|
|
98
|
+
);
|
|
99
|
+
program2.command("work").description("Loop: claim the next batch, hand its prompt to an agent, renew the lease meanwhile, release when it ends; then the next").option("--for <worker>", "only this Worker\u2019s batches").option("--worker <worker>", "same as --for").option("--once", "one batch, then stop").option("--max-batches <n>", "stop after n batches").option("--wait", "poll an empty queue instead of stopping").option("--poll <seconds>", "with --wait: how often (default 30)").option("--lease <seconds>", "lease length (default 900)").option("--agent <agent>", "claude: the local Claude Code (its logged-in Max account); codex; print: print the prompt and wait for its Tasks to close", "print").option("--model <model>", `claude/codex model (default ${DEFAULT_MODEL})`).option("--max-minutes <n>", `a batch's wall-clock cap: the agent is stopped and the lease never renewed past it (default ${DEFAULT_WALL_MINUTES})`).action(async (o) => {
|
|
100
|
+
if (inBatch()) throw new Error("work is refused inside a batch (SB_BATCH is set)");
|
|
101
|
+
const wallMinutes = o.maxMinutes ? Number(o.maxMinutes) : void 0;
|
|
102
|
+
if (wallMinutes !== void 0 && !(wallMinutes > 0)) throw new Error(`--max-minutes takes a positive number, not "${o.maxMinutes}"`);
|
|
103
|
+
const agent = agentRunner(o.agent, { out: d.out, model: o.model });
|
|
104
|
+
const s = d.session();
|
|
105
|
+
const ac = new AbortController();
|
|
106
|
+
const stop = () => ac.abort();
|
|
107
|
+
process.once("SIGINT", stop);
|
|
108
|
+
process.once("SIGTERM", stop);
|
|
109
|
+
try {
|
|
110
|
+
const sum = await work(s, {
|
|
111
|
+
agent,
|
|
112
|
+
agentName: o.agent,
|
|
113
|
+
worker: o.for ?? o.worker,
|
|
114
|
+
maxBatches: o.once ? 1 : o.maxBatches ? Number(o.maxBatches) : void 0,
|
|
115
|
+
wait: o.wait,
|
|
116
|
+
pollMs: o.poll ? Number(o.poll) * 1e3 : void 0,
|
|
117
|
+
logDir: d.logDir(),
|
|
118
|
+
line: d.err,
|
|
119
|
+
signal: ac.signal,
|
|
120
|
+
selfCli: d.selfCli(),
|
|
121
|
+
wallMinutes
|
|
122
|
+
});
|
|
123
|
+
if (sum.stopped === "signal") d.exit(130);
|
|
124
|
+
else if (sum.errors) d.exit(1);
|
|
125
|
+
} finally {
|
|
126
|
+
process.off("SIGINT", stop);
|
|
127
|
+
process.off("SIGTERM", stop);
|
|
52
128
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
${
|
|
129
|
+
});
|
|
130
|
+
program2.command("mcp").description("Serve these verbs as MCP tools over stdio (claude mcp add startup-builder -- npx -y startup-builder mcp)").option("--owner", "also the Owner's tools: prompt with set (a Prompt's next version) and revert, with the stored sign-in").action(async (o) => d.serveMcp(d.session(), { owner: !!o.owner }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/cli.ts
|
|
134
|
+
var pkg = createRequire(import.meta.url)("../package.json");
|
|
135
|
+
var err = (s) => process.stderr.write(s + "\n");
|
|
136
|
+
var out = (s) => process.stdout.write(s.endsWith("\n") ? s : s + "\n");
|
|
137
|
+
var shq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
138
|
+
function selfBin() {
|
|
139
|
+
if (process.env.STARTUP_BUILDER_BIN) return process.env.STARTUP_BUILDER_BIN;
|
|
140
|
+
let self;
|
|
141
|
+
try {
|
|
142
|
+
self = realpathSync(process.argv[1]);
|
|
143
|
+
} catch {
|
|
144
|
+
self = realpathSync(fileURLToPath(import.meta.url));
|
|
62
145
|
}
|
|
63
|
-
return
|
|
146
|
+
return `${shq(process.execPath)} ${shq(self)}`;
|
|
64
147
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
148
|
+
function auth(g) {
|
|
149
|
+
return new Auth({ api: apiBase(g.api), file: tokenFile() });
|
|
150
|
+
}
|
|
151
|
+
var envToken = () => process.env.SB_TOKEN || void 0;
|
|
152
|
+
async function client(g, quiet = false) {
|
|
153
|
+
const env = envToken();
|
|
154
|
+
if (env) return { api: new Api({ base: apiBase(g.api), token: env, userAgent: `startup-builder/${pkg.version}` }), signedIn: true };
|
|
155
|
+
const a = auth(g);
|
|
156
|
+
const had = await a.stored();
|
|
157
|
+
const tok = await a.current().catch(() => void 0);
|
|
158
|
+
if (had && !tok && !quiet) err(pc.dim(`(the stored api.sb token expired and did not refresh: going anonymous; run \`startup-builder login\`)`));
|
|
159
|
+
const api = new Api({ base: apiBase(g.api), token: tok?.access_token, bind: tok ? tok.api || DEFAULT_API : void 0, userAgent: `startup-builder/${pkg.version}` });
|
|
160
|
+
return { api, signedIn: !!api.token };
|
|
161
|
+
}
|
|
162
|
+
async function storedCred(g) {
|
|
163
|
+
const s = await auth(g).current().catch(() => void 0);
|
|
164
|
+
return s ? { token: s.access_token, api: s.api || DEFAULT_API } : void 0;
|
|
165
|
+
}
|
|
166
|
+
async function signedInClient(g, force = false) {
|
|
167
|
+
const c = force ? void 0 : await client(g, true);
|
|
168
|
+
if (c?.signedIn) return c.api;
|
|
169
|
+
if (envToken()) throw new Error("api.sb refused SB_TOKEN: unset it, or set a valid bearer");
|
|
170
|
+
err(pc.dim("Invoking a Verb needs a signed-in caller: signing in with id.org.ai\u2026"));
|
|
171
|
+
await auth(g).login({ force });
|
|
172
|
+
return (await client(g, true)).api;
|
|
173
|
+
}
|
|
174
|
+
function session(g, interactive = true) {
|
|
175
|
+
return new Session({
|
|
176
|
+
base: apiBase(g.api),
|
|
177
|
+
leases: new Leases(leasesDir()),
|
|
178
|
+
token: async () => envToken() ?? await storedCred(g),
|
|
179
|
+
...interactive ? {
|
|
180
|
+
login: async () => {
|
|
181
|
+
if (envToken()) throw new Error("api.sb refused SB_TOKEN: unset it, or set a valid bearer");
|
|
182
|
+
err(pc.dim("This needs a signed-in caller: signing in with id.org.ai\u2026"));
|
|
183
|
+
await auth(g).login({});
|
|
184
|
+
return storedCred(g);
|
|
185
|
+
}
|
|
186
|
+
} : {},
|
|
187
|
+
userAgent: `startup-builder/${pkg.version}`,
|
|
188
|
+
session: process.env.SB_SESSION
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function readStdin() {
|
|
192
|
+
const chunks = [];
|
|
193
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
194
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
195
|
+
}
|
|
196
|
+
var USER_KEYS = ["name", "email", "id", "role", "level", "authenticated", "person", "plan", "org", "roles", "error"];
|
|
197
|
+
async function whoami(g, json) {
|
|
198
|
+
const { api, signedIn } = await client(g, true);
|
|
199
|
+
const r = await api.get("/?proto=0");
|
|
200
|
+
const user = r.json?.user ?? {};
|
|
201
|
+
const who = Object.fromEntries(USER_KEYS.filter((k) => k in user).map((k) => [k, user[k]]));
|
|
202
|
+
const agent = user.agent?.name && user.agent.name !== "unknown" ? `${user.agent.name} ${user.agent.version ?? ""}`.trim() : void 0;
|
|
203
|
+
const where = [user.city, user.region, user.country].filter(Boolean).join(", ") || void 0;
|
|
204
|
+
const res = { api: api.base, signedIn: signedIn && !user.error && !!(user.name || user.email || user.id), tokenFile: tokenFile(), user: who, ...agent ? { agent } : {}, ...where ? { where } : {} };
|
|
205
|
+
if (json) {
|
|
206
|
+
out(JSON.stringify(res, null, 2));
|
|
207
|
+
return;
|
|
76
208
|
}
|
|
77
|
-
|
|
209
|
+
if (res.signedIn) out(`${pc.green("\u25CF")} signed in to ${api.base} as ${pc.bold(String(who.name ?? who.email ?? who.id))}${who.email && who.name ? ` <${who.email}>` : ""}`);
|
|
210
|
+
else out(`${pc.yellow("\u25CB")} anonymous on ${api.base}${who.error ? ` (${who.error})` : ""}: run \`startup-builder login\``);
|
|
211
|
+
for (const k of ["id", "role", "level", "plan", "org"]) if (who[k] !== void 0) out(` ${k}: ${typeof who[k] === "string" ? who[k] : JSON.stringify(who[k])}`);
|
|
212
|
+
if (agent) out(pc.dim(` agent: ${agent}`));
|
|
213
|
+
if (where) out(pc.dim(` where: ${where}`));
|
|
214
|
+
}
|
|
215
|
+
var startupsOf2 = (v) => (v ?? []).flatMap((s) => s.split(",")).map((s) => s.trim().replace(/^https?:\/\/[^/]+\//, "").replace(/\/+$/, "")).filter(Boolean);
|
|
216
|
+
function worklistArgs(target, o) {
|
|
217
|
+
const worker2 = parseWorker(o.for ?? target);
|
|
218
|
+
if (!worker2) throw new Error("name the Worker: worklist for:<worker> (e.g. for:tom)");
|
|
219
|
+
const startups = startupsOf2(o.startup ?? (process.env.STARTUP_BUILDER_STARTUP ? [process.env.STARTUP_BUILDER_STARTUP] : []));
|
|
220
|
+
if (o.page && o.all) throw new Error("--page or --all, not both");
|
|
221
|
+
const page = o.page ? Number(o.page) : void 0;
|
|
222
|
+
if (page !== void 0 && !(Number.isInteger(page) && page >= 1)) throw new Error(`--page takes a page number (1, 2, \u2026), not "${o.page}"`);
|
|
223
|
+
return { worker: worker2, startups, limit: o.limit ? Number(o.limit) : o.all ? 50 : 5, ...page ? { page } : {}, ...o.all ? { all: true } : {} };
|
|
224
|
+
}
|
|
225
|
+
async function worklistFrom(g, target, o) {
|
|
226
|
+
const { api } = await client(g);
|
|
227
|
+
return buildWorklist(api, { ...worklistArgs(target, o), stubOnly: o.stub, previews: o.previews });
|
|
228
|
+
}
|
|
229
|
+
function summary(w) {
|
|
230
|
+
return batchText(w);
|
|
78
231
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
232
|
+
var program = new Command2();
|
|
233
|
+
program.name("startup-builder").description("The command line for api.sb: sign in, get a Worker's worklist as a ready-to-run prompt, run it, invoke Verbs.").version(pkg.version).addHelpText("after", "\nStart here:\n npx startup-builder login\n npx startup-builder whoami\n npx startup-builder show /headless.ly --json\n npx startup-builder help do\n\nHosted MCP: https://api.sb/mcp (HTTP with OAuth; search, fetch, do).\nThe local mcp command is an optional stdio interface with different tools.\nDocs: https://api.sb/docs\n").option("--api <url>", "api.sb base URL (default https://api.sb; env SB_API); before the command, and refused inside a batch (SB_BATCH)").enablePositionalOptions().showHelpAfterError();
|
|
234
|
+
program.command("login").description("Sign in to api.sb with id.org.ai: opens your default browser (loopback + PKCE); --device where no browser can open").option("--device", "use the device-code flow").option("--force", "sign in again even with a valid token").option("--timeout <seconds>", "how long to wait for the browser", "300").option("--json", "machine output").action(async (o) => {
|
|
235
|
+
const g = program.opts();
|
|
236
|
+
const a = new Auth({ api: apiBase(g.api), file: tokenFile(), timeoutMs: Number(o.timeout) * 1e3 });
|
|
237
|
+
const { fresh } = await a.login({ device: o.device, force: o.force });
|
|
238
|
+
err(fresh ? `Signed in. Token kept in ${a.file} (mode 600).` : `Already signed in (token in ${a.file}; --force to sign in again).`);
|
|
239
|
+
await whoami(g, !!o.json);
|
|
240
|
+
});
|
|
241
|
+
program.command("logout").description("Revoke and forget the stored api.sb token").action(async () => {
|
|
242
|
+
await auth(program.opts()).logout();
|
|
243
|
+
out("Signed out of api.sb.");
|
|
244
|
+
});
|
|
245
|
+
program.command("whoami").description("Who api.sb sees: the signed-in user, or anonymous").option("--json", "machine output").action(async (o) => whoami(program.opts(), !!o.json));
|
|
246
|
+
var worklistOpts = (c) => c.argument("[for:worker]", "the Worker, e.g. for:tom").option("--for <worker>", "the Worker (same as for:<worker>)").option("--startup <domain...>", "the Startup(s) whose Tasks to read, e.g. barcoding.dev (comma or repeat)").option("--limit <n>", "Tasks in the batch (default 5; api.sb serves at most 50 a page)").option("--page <n>", "a page of the worklist (?page=): 2 is the Tasks after the first batch").option("--all", "every page: follow api.sb's next link to the last page (50 a page unless --limit)").option("--stub", "OFFLINE FALLBACK only: skip api.sb's GET /workers/{name}/worklist and assemble one from its other endpoints").option("--no-previews", "with --stub: don't read each Task's Verb preview (its closing Event)");
|
|
247
|
+
async function worklistMore(target, o) {
|
|
248
|
+
const g = program.opts();
|
|
249
|
+
if (o.md && !o.stub) {
|
|
250
|
+
if (o.all) throw new Error("--md prints one page of api.sb's Markdown: use --page <n> (or --limit up to 50), not --all");
|
|
251
|
+
const { api } = await client(g);
|
|
252
|
+
out(await fetchWorklistMarkdown(api, worklistArgs(target, o)));
|
|
253
|
+
return;
|
|
89
254
|
}
|
|
90
|
-
|
|
255
|
+
const w = await worklistFrom(g, target, o);
|
|
256
|
+
if (o.json) {
|
|
257
|
+
out(JSON.stringify(w, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (o.prompt || o.md) {
|
|
261
|
+
err(summary(w));
|
|
262
|
+
out(renderPrompt(w, { bin: selfBin() }));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
session(g).leases.remember("worklist", w.tasks.map((t) => t.$id));
|
|
266
|
+
out(summary(w));
|
|
91
267
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
progress: progress.map((p) => ({ state: p.state, label: p.label, status: p.status })),
|
|
100
|
-
requiredArtifacts: artifacts,
|
|
101
|
-
commands: commands.map((c) => ({
|
|
102
|
-
name: c.name,
|
|
103
|
-
collection: c.collection,
|
|
104
|
-
operation: c.operation,
|
|
105
|
-
command: c.command,
|
|
106
|
-
schema: c.schema
|
|
107
|
-
}))
|
|
108
|
-
};
|
|
109
|
-
output(ok(result), "json");
|
|
268
|
+
worklistOpts(program.command("run")).description("Hand a Worker's worklist to claude-runner (--cloud). A local agent runs through `work --agent claude`").option("--cloud", "run on the deployed claude-runner, one session per (Startup, Worker)").option("--dry-run", "show what would run; run nothing").option("--yes", "with --cloud: really start the session(s) (without it, --cloud only shows the plan)").option("--runner <url>", "claude-runner base URL (env SB_RUNNER_URL)").option("--model <model>", "model for the cloud session").option("--json", "machine output").action(async (target, o) => {
|
|
269
|
+
if (!o.cloud) throw new Error("`run` runs on claude-runner only (--cloud). Locally, `startup-builder work --agent claude` claims a batch and runs an isolated claude on it (`--agent print` prints the prompt)");
|
|
270
|
+
const g = program.opts();
|
|
271
|
+
const w = await worklistFrom(g, target, o);
|
|
272
|
+
err(summary(w));
|
|
273
|
+
if (!w.tasks.length) {
|
|
274
|
+
err("Nothing to run: no open Tasks.");
|
|
110
275
|
return;
|
|
111
276
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
277
|
+
const { api } = await client(g, true);
|
|
278
|
+
const dryRun = o.dryRun || !o.yes;
|
|
279
|
+
const sessions = await runCloud(api, w, { runner: runnerBase(o.runner), runnerToken: process.env.SB_RUNNER_TOKEN, dryRun, bin: process.env.STARTUP_BUILDER_BIN ?? "npx startup-builder", model: o.model });
|
|
280
|
+
if (dryRun && !o.dryRun) err(pc.yellow(`--cloud only planned ${sessions.length} session(s): add --yes to start them on claude-runner`));
|
|
281
|
+
if (o.json) {
|
|
282
|
+
out(JSON.stringify(sessions.map((s) => ({ ...s, request: { url: s.request.url, body: { ...s.request.body, prompt: `(${String(s.request.body.prompt).length} chars)` } } })), null, 2));
|
|
283
|
+
return;
|
|
117
284
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
285
|
+
for (const s of sessions) out(`${s.startup}: ${s.via === "none" ? pc.yellow("not started") : pc.green(`started via ${s.via}`)}${s.status ? ` (${s.status})` : ""}${s.url ? ` ${s.url}` : ""}${s.note ? `
|
|
286
|
+
${s.note}` : ""}
|
|
287
|
+
POST ${s.request.url}`);
|
|
288
|
+
if (sessions.some((s) => s.via === "none") && !o.dryRun) process.exitCode = 2;
|
|
289
|
+
});
|
|
290
|
+
var modeOf = (o) => o.do ? "do" : o.send ? "send" : o.mode ?? "try";
|
|
291
|
+
var isTaskRef = (s) => /^https?:\/\//.test(s) || s.includes("/");
|
|
292
|
+
async function closeAction(ref, o) {
|
|
293
|
+
const s = session(program.opts());
|
|
294
|
+
const mode = o.do ? "do" : o.send ? "send" : o.mode;
|
|
295
|
+
const r = await s.close({ ref, mode, as: o.as, body: o.input || o.data ? parseInput(o.input, o.data) : void 0, force: o.force, idempotencyKey: o.idempotencyKey });
|
|
296
|
+
out(o.json ? JSON.stringify(r.data, null, 2) : r.data && !o.short ? closeText(r.data) : r.text);
|
|
297
|
+
if (r.exit) process.exitCode = r.exit;
|
|
121
298
|
}
|
|
122
|
-
var
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!startupId) {
|
|
144
|
-
const startupName = opts.name || jsonInput.name || description || "Untitled Startup";
|
|
145
|
-
const startupResult = await apiRequest(apiBaseUrl, "/api/startups", {
|
|
146
|
-
method: "POST",
|
|
147
|
-
body: {
|
|
148
|
-
name: startupName,
|
|
149
|
-
...ctx.studioSlug ? { studio: ctx.studioSlug } : {}
|
|
150
|
-
},
|
|
151
|
-
apiKey: auth.apiKey
|
|
152
|
-
});
|
|
153
|
-
if (!startupResult.ok) {
|
|
154
|
-
output(err(startupResult.error.code, `Failed to create startup: ${startupResult.error.message}`), format);
|
|
155
|
-
process.exitCode = 1;
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
startupId = startupResult.data.id;
|
|
159
|
-
if (format === "text") {
|
|
160
|
-
process.stdout.write(`
|
|
161
|
-
${pc.green("\u2713")} Created startup ${pc.bold(startupResult.data.name)} ${pc.dim(`(${startupId})`)}
|
|
162
|
-
`);
|
|
163
|
-
}
|
|
164
|
-
if (!ideaId) {
|
|
165
|
-
const ideaResult = await apiRequest(apiBaseUrl, "/api/ideas", {
|
|
166
|
-
method: "POST",
|
|
167
|
-
body: {
|
|
168
|
-
name: description || startupName,
|
|
169
|
-
startup: startupId,
|
|
170
|
-
description: description || void 0
|
|
171
|
-
},
|
|
172
|
-
apiKey: auth.apiKey
|
|
173
|
-
});
|
|
174
|
-
if (ideaResult.ok) {
|
|
175
|
-
ideaId = ideaResult.data.id;
|
|
176
|
-
if (format === "text") {
|
|
177
|
-
process.stdout.write(` ${pc.green("\u2713")} Created idea ${pc.dim(`(${ideaId})`)}
|
|
178
|
-
`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const sprintResult = await apiRequest(apiBaseUrl, "/api/sprints", {
|
|
183
|
-
method: "POST",
|
|
184
|
-
body: {
|
|
185
|
-
name: "Foundation Sprint",
|
|
186
|
-
startup: startupId,
|
|
187
|
-
idea: ideaId || void 0,
|
|
188
|
-
type: "foundation"
|
|
189
|
-
},
|
|
190
|
-
apiKey: auth.apiKey
|
|
191
|
-
});
|
|
192
|
-
if (sprintResult.ok) {
|
|
193
|
-
sprintId = sprintResult.data.id;
|
|
194
|
-
if (format === "text") {
|
|
195
|
-
process.stdout.write(` ${pc.green("\u2713")} Created Foundation Sprint ${pc.dim(`(${sprintId})`)}
|
|
196
|
-
`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const { currentState, counts } = await reconcileState(startupId, apiBaseUrl, auth.apiKey);
|
|
201
|
-
renderNextSteps(currentState, startupId, counts, format);
|
|
202
|
-
} catch (e) {
|
|
203
|
-
handleError(e, format);
|
|
299
|
+
var verbFlags = (c) => c.option("--send", "really run it, Prefer: mode=send (you get the Pending Action at once)").option("--do", "really run it, Prefer: mode=do (waits until the Action finishes)").addOption(new Option2("--mode <mode>", "the Mode, sent as Prefer: mode=<mode> (default try: runs once, stores nothing)").choices(["try", "send", "do"])).option("--as <worker|role>", "act as a Worker or Role (?as=); close defaults to the Task's assignee").option("--input <key=value...>", "the Verb's input").option("--data <json>", "the Verb's input as JSON").option("--idempotency-key <key>", "reuse a key (a retry); default a fresh UUID").option("--json", "machine output");
|
|
300
|
+
verbFlags(
|
|
301
|
+
program.command("do").description("Invoke a Verb. `do <task $id>` closes a Task (as `close`); `do <Noun.verb> [target]` previews it (GET <$id>:<verb>), then POSTs it with an Idempotency-Key. A try-Mode preview unless --do or --send").argument("<Noun.verb|task>", "the Verb, e.g. Offer.publish or Issue.fix; or a Task's $id").argument("[target]", "the Thing: its $id, a path, or a slug in the Startup's collection for the Noun").option("--startup <domain>", "the Startup (needed unless the target is a full $id)").option("--preview", "only the GET preview; invoke nothing").option("--force", "invoke even when the preview says it would be refused")
|
|
302
|
+
).action(async (spec, target, o) => {
|
|
303
|
+
if (isTaskRef(spec) && !target) return closeAction(spec, o);
|
|
304
|
+
const g = program.opts();
|
|
305
|
+
const api = o.preview ? (await client(g)).api : await signedInClient(g);
|
|
306
|
+
const r = await doVerb(api, {
|
|
307
|
+
spec,
|
|
308
|
+
target,
|
|
309
|
+
startup: o.startup,
|
|
310
|
+
input: parseInput(o.input, o.data),
|
|
311
|
+
mode: modeOf(o),
|
|
312
|
+
as: o.as,
|
|
313
|
+
previewOnly: o.preview,
|
|
314
|
+
force: o.force,
|
|
315
|
+
idempotencyKey: o.idempotencyKey
|
|
316
|
+
});
|
|
317
|
+
if (o.json) {
|
|
318
|
+
out(JSON.stringify(r, null, 2));
|
|
319
|
+
return;
|
|
204
320
|
}
|
|
321
|
+
const p = r.preview;
|
|
322
|
+
out(`${pc.bold(p.name ?? spec)} ${pc.dim(p.url)}`);
|
|
323
|
+
if (p.error) out(` ${pc.red("\u2717")} ${p.error} (${p.status})`);
|
|
324
|
+
if (p.does) out(` does: ${p.does}`);
|
|
325
|
+
if (p.event) out(` emits: ${p.event}${p.mode ? ` \xB7 mode ${p.mode}` : ""}`);
|
|
326
|
+
if (p.state) out(` state: ${p.state}`);
|
|
327
|
+
if (p.permission) out(` needs: ${p.permission.needs} (held by ${p.permission.heldBy.join(", ") || "no Role"})`);
|
|
328
|
+
if (p.approvalNeeded !== void 0) out(` approval: ${p.approvalNeeded ? "needed" : "not needed"}`);
|
|
329
|
+
if (p.inputs.length) out(` input: ${p.inputs.join(", ")}`);
|
|
330
|
+
if (p.refused) out(` ${pc.red("refused")}: ${p.refused}${o.force ? "" : " (not invoked; --force to try anyway)"}`);
|
|
331
|
+
if (r.post) {
|
|
332
|
+
out(` POST ${r.post.status} \xB7 Prefer: mode=${modeOf(o)} \xB7 Idempotency-Key ${r.post.idempotencyKey}${r.post.actionStatus ? ` \xB7 ${r.post.actionStatus}` : ""}${r.post.action ? ` \xB7 ${r.post.action}` : ""}`);
|
|
333
|
+
if (r.post.says) out(` ${r.post.says}`);
|
|
334
|
+
if (r.post.note) out(` ${pc.yellow(r.post.note)}`);
|
|
335
|
+
} else if (o.preview) out(pc.dim(" (preview only: nothing ran)"));
|
|
336
|
+
if (r.post && modeOf(o) === "try") out(pc.dim(" (a try-Mode preview: nothing stored. --do or --send to run it)"));
|
|
337
|
+
if (!r.invoked && !o.preview) process.exitCode = p.refused || p.error ? 1 : 2;
|
|
338
|
+
});
|
|
339
|
+
registerLoop(program, {
|
|
340
|
+
session: () => session(program.opts()),
|
|
341
|
+
out,
|
|
342
|
+
err,
|
|
343
|
+
exit: (code) => process.exitCode = code,
|
|
344
|
+
readStdin,
|
|
345
|
+
readFile: (p) => readFileSync(p, "utf8"),
|
|
346
|
+
selfCli: () => process.env.STARTUP_BUILDER_BIN ? process.env.STARTUP_BUILDER_BIN : selfBin(),
|
|
347
|
+
logDir: () => join(configDir(), "logs"),
|
|
348
|
+
version: pkg.version,
|
|
349
|
+
serveMcp: (s, o) => serveStdio(s, pkg.version, o),
|
|
350
|
+
worklistMore
|
|
351
|
+
});
|
|
352
|
+
var argv = process.argv;
|
|
353
|
+
program.parseAsync(argv).catch((e) => {
|
|
354
|
+
err(pc.red(String(e?.message ?? e)));
|
|
355
|
+
process.exit(1);
|
|
205
356
|
});
|
|
206
|
-
program.parse();
|