setup-agent 0.0.9 → 0.1.1
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 +31 -0
- package/dist/browser.d.ts +40 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +91 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +23 -39
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -125
- package/dist/cli.js.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +13 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/needs.d.ts +62 -0
- package/dist/needs.d.ts.map +1 -0
- package/dist/needs.js +184 -0
- package/dist/needs.js.map +1 -0
- package/dist/presets.js +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/protocol.d.ts +16 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +8 -1
- package/dist/protocol.js.map +1 -1
- package/dist/sdk.d.ts +48 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +97 -0
- package/dist/sdk.js.map +1 -0
- package/dist/stream.d.ts +93 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +184 -0
- package/dist/stream.js.map +1 -0
- package/package.json +5 -1
- package/src/browser.test.ts +192 -0
- package/src/browser.ts +146 -0
- package/src/cli.test.ts +57 -1
- package/src/cli.ts +134 -186
- package/src/host.test.ts +25 -0
- package/src/host.ts +7 -0
- package/src/index.ts +1 -0
- package/src/needs.test.ts +180 -0
- package/src/needs.ts +273 -0
- package/src/presets.ts +1 -1
- package/src/protocol.ts +17 -0
- package/src/sdk.test.ts +111 -0
- package/src/sdk.ts +145 -0
- package/src/stream.test.ts +223 -0
- package/src/stream.ts +314 -0
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ npx setup-agent <url> add-step "Install the packages" --detail "pnpm add ..." --
|
|
|
17
17
|
npx setup-agent <url> step s1 done --note "installed"
|
|
18
18
|
npx setup-agent <url> env q2 OPENAI_API_KEY --file .env.local # write the key behind a model answer without seeing it
|
|
19
19
|
npx setup-agent <url> done --summary "installed the thread and a chat route" --preview http://localhost:3000
|
|
20
|
+
npx setup-agent <url> answer q3 pnpm # the user answered in the agent's own chat; also: answer plan approve, answer finish close
|
|
21
|
+
npx setup-agent <url> stream --setup-app-url https://setup.assistant-ui.com # also open the setup page when the setup waits on the user
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
## Phases
|
|
@@ -41,6 +43,35 @@ Two standard choices and a legacy project choice ship as presets (`setup-agent`
|
|
|
41
43
|
|
|
42
44
|
`--only` restricts a preset to some of its options and leaves out `--choices` (a single id locks the choice in) and `--default` preselects one.
|
|
43
45
|
|
|
46
|
+
## SDK
|
|
47
|
+
|
|
48
|
+
`setup-agent/sdk` is what a CLI embeds to offer `<tool> setup` to coding agents; `npx assistant-ui@latest setup` is built on it. The `setup-agent` bin is a thin wrapper around the same code.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { runAgentSetup } from "setup-agent/sdk";
|
|
52
|
+
|
|
53
|
+
const { stream, client } = await runAgentSetup({
|
|
54
|
+
server: "https://checkout.assistant-ui.com",
|
|
55
|
+
setupAppUrl: "https://setup.assistant-ui.com",
|
|
56
|
+
products: [{ slug: "assistant-ui", name: "assistant-ui" }],
|
|
57
|
+
command: "npx my-tool setup", // the agent runs `npx my-tool setup <url> <command>`
|
|
58
|
+
name: "my-tool setup",
|
|
59
|
+
});
|
|
60
|
+
process.on("SIGINT", () => stream.stop().finally(() => client.dispose()));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`runAgentSetup` starts a session, prints the briefing and keeps running as the stream. Route `<tool> setup <url> ...` to `runSetupCommand(argv, { command, browser: { setupAppUrl } })` from `setup-agent/cli` for the other steps.
|
|
64
|
+
|
|
65
|
+
The browser opens only when the setup waits on the user (an open input, a proposed plan, a proposal to close). The joined page sends `browser/heartbeat` every 5s and `browser/bye` on pagehide; it counts as present for 20s after a beat. Setup:
|
|
66
|
+
|
|
67
|
+
- opens the page for the first need, one tab for everything waiting;
|
|
68
|
+
- opens nothing while a joined page is present;
|
|
69
|
+
- never opens a second tab within 30s of the last, and never reopens a tab that has not joined (30s after opening one that never joined, it hands what waits to the agent); it prints `input.needed` (`reason: "recently-opened"`) so the agent can ask in its own chat;
|
|
70
|
+
- prints `input.needed` (`reason: "browser-closed"`) when the page leaves with something waiting, or a need arrives inside the 30s after it left; a later need reopens it once that 30s guard has expired;
|
|
71
|
+
- under `--no-open` (with `--setup-app-url` for the bin), prints `input.needed` (`reason: "no-browser"`) for every need.
|
|
72
|
+
|
|
73
|
+
Each `input.needed` carries the question, its options and the `answer` command. On a worker without browser presence, setup opens once and then leaves the rest to the agent.
|
|
74
|
+
|
|
44
75
|
## Workspace
|
|
45
76
|
|
|
46
77
|
The package lives in `packages/setup-agent`; its Cloudflare worker lives in `apps/checkout-worker`. Build the worker dependencies with `pnpm turbo build --filter="checkout-worker^..."`, then run `pnpm --filter checkout-worker dev`. The worker consumes the published `@statewire/cloudflare` adapter.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SetupNeed } from "./needs.js";
|
|
2
|
+
import type { Checkout } from "./protocol.js";
|
|
3
|
+
/** Within this long of opening a tab, setup never opens another. */
|
|
4
|
+
export declare const BROWSER_REOPEN_GUARD_MS = 30000;
|
|
5
|
+
/** A page silent this long after it was present counts as closed; one heartbeat gap is not enough. */
|
|
6
|
+
export declare const BROWSER_CLOSE_GRACE_MS: number;
|
|
7
|
+
/**
|
|
8
|
+
* Why the agent should ask the user itself:
|
|
9
|
+
* - recently-opened: a tab was opened moments ago, or opened and never joined;
|
|
10
|
+
* - browser-closed: the user closed the setup tab (and it may not reopen yet);
|
|
11
|
+
* - no-browser: setup may not open a browser (`--no-open`).
|
|
12
|
+
*/
|
|
13
|
+
export type AskReason = "recently-opened" | "browser-closed" | "no-browser";
|
|
14
|
+
export type BrowserGateAction = {
|
|
15
|
+
type: "open";
|
|
16
|
+
url: string;
|
|
17
|
+
need: SetupNeed;
|
|
18
|
+
} | {
|
|
19
|
+
type: "ask";
|
|
20
|
+
reason: AskReason;
|
|
21
|
+
need: SetupNeed;
|
|
22
|
+
openedAgoMs?: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Decides when setup opens the user's browser: only once the setup waits on
|
|
26
|
+
* the user, never while a joined tab is present, never twice within the
|
|
27
|
+
* guard window, and again for a later need once a joined tab went away.
|
|
28
|
+
* Presence is measured on this machine's clock from when each heartbeat
|
|
29
|
+
* arrived, so a skewed server clock does not matter.
|
|
30
|
+
*/
|
|
31
|
+
export declare const createBrowserGate: ({ url, canOpen, now, }: {
|
|
32
|
+
/** The setup page for a state; `undefined` when there is none to open. */
|
|
33
|
+
url: (state: Checkout.State) => string | undefined;
|
|
34
|
+
/** False for `--no-open`: the agent is told to ask instead. */
|
|
35
|
+
canOpen?: boolean;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
}) => {
|
|
38
|
+
update: (state: Checkout.State) => BrowserGateAction[];
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAgB;AAEzC,OAAO,KAAK,EAAE,QAAQ,EAAE,sBAAmB;AAE3C,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,YAAY,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,wBAI/B;IACD,0EAA0E;IAC1E,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;IACnD,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;oBAwCmB,QAAQ,CAAC,KAAK,KAAG,iBAAiB,EAAE;CAkEvD,CAAC"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { BROWSER_HEARTBEAT_MS, BROWSER_PRESENCE_MS } from "./protocol.js";
|
|
2
|
+
import { needKey, pendingNeeds } from "./needs.js";
|
|
3
|
+
// src/browser.ts
|
|
4
|
+
/** Within this long of opening a tab, setup never opens another. */
|
|
5
|
+
const BROWSER_REOPEN_GUARD_MS = 3e4;
|
|
6
|
+
/** A page silent this long after it was present counts as closed; one heartbeat gap is not enough. */
|
|
7
|
+
const BROWSER_CLOSE_GRACE_MS = BROWSER_HEARTBEAT_MS + 1e3;
|
|
8
|
+
/**
|
|
9
|
+
* Decides when setup opens the user's browser: only once the setup waits on
|
|
10
|
+
* the user, never while a joined tab is present, never twice within the
|
|
11
|
+
* guard window, and again for a later need once a joined tab went away.
|
|
12
|
+
* Presence is measured on this machine's clock from when each heartbeat
|
|
13
|
+
* arrived, so a skewed server clock does not matter.
|
|
14
|
+
*/
|
|
15
|
+
const createBrowserGate = ({ url, canOpen = true, now = Date.now }) => {
|
|
16
|
+
const handled = /* @__PURE__ */ new Set();
|
|
17
|
+
/** Needs the agent was already told to ask about. */
|
|
18
|
+
const told = /* @__PURE__ */ new Set();
|
|
19
|
+
let openedAt;
|
|
20
|
+
let joinedSinceOpen = false;
|
|
21
|
+
let lastBeat;
|
|
22
|
+
let beatArrivedAt;
|
|
23
|
+
let present = false;
|
|
24
|
+
let absentSince;
|
|
25
|
+
let closedReported = true;
|
|
26
|
+
let joinTimeoutReported = true;
|
|
27
|
+
const trackPresence = (browser) => {
|
|
28
|
+
if (browser === void 0) return;
|
|
29
|
+
const time = now();
|
|
30
|
+
if (browser.lastSeenAt !== lastBeat) {
|
|
31
|
+
beatArrivedAt = browser.lastSeenAt === null ? void 0 : time;
|
|
32
|
+
lastBeat = browser.lastSeenAt;
|
|
33
|
+
}
|
|
34
|
+
if (browser.connected && beatArrivedAt !== void 0 && time - beatArrivedAt < 2e4) {
|
|
35
|
+
present = joinedSinceOpen = true;
|
|
36
|
+
absentSince = void 0;
|
|
37
|
+
closedReported = false;
|
|
38
|
+
} else if (present || absentSince !== void 0) {
|
|
39
|
+
absentSince ??= browser.connected && beatArrivedAt !== void 0 ? Math.min(time, beatArrivedAt + BROWSER_PRESENCE_MS) : time;
|
|
40
|
+
present = false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return { update: (state) => {
|
|
44
|
+
trackPresence(state.browser);
|
|
45
|
+
const time = now();
|
|
46
|
+
const needs = pendingNeeds(state);
|
|
47
|
+
const keys = new Set(needs.map(needKey));
|
|
48
|
+
for (const key of handled) if (!keys.has(key)) handled.delete(key);
|
|
49
|
+
for (const key of told) if (!keys.has(key)) told.delete(key);
|
|
50
|
+
const looksPresent = present || absentSince !== void 0 && time - absentSince < BROWSER_CLOSE_GRACE_MS;
|
|
51
|
+
const closed = !looksPresent && absentSince !== void 0;
|
|
52
|
+
const ask = (need, reason) => {
|
|
53
|
+
told.add(needKey(need));
|
|
54
|
+
return {
|
|
55
|
+
type: "ask",
|
|
56
|
+
reason,
|
|
57
|
+
need,
|
|
58
|
+
...reason === "recently-opened" && { openedAgoMs: time - openedAt }
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const actions = [];
|
|
62
|
+
if (closed && !closedReported) {
|
|
63
|
+
closedReported = true;
|
|
64
|
+
for (const need of needs) if (handled.has(needKey(need))) actions.push(ask(need, "browser-closed"));
|
|
65
|
+
}
|
|
66
|
+
if (!joinTimeoutReported && !joinedSinceOpen && time - openedAt >= 3e4) {
|
|
67
|
+
joinTimeoutReported = true;
|
|
68
|
+
for (const need of needs) if (handled.has(needKey(need)) && !told.has(needKey(need))) actions.push(ask(need, "recently-opened"));
|
|
69
|
+
}
|
|
70
|
+
const fresh = needs.filter((need) => !handled.has(needKey(need)));
|
|
71
|
+
for (const need of fresh) handled.add(needKey(need));
|
|
72
|
+
if (fresh.length === 0 || looksPresent) return actions;
|
|
73
|
+
const target = canOpen ? url(state) : void 0;
|
|
74
|
+
if (target === void 0) return [...actions, ...fresh.map((need) => ask(need, "no-browser"))];
|
|
75
|
+
if (openedAt === void 0 || time - openedAt >= 3e4 && joinedSinceOpen) {
|
|
76
|
+
openedAt = time;
|
|
77
|
+
joinedSinceOpen = false;
|
|
78
|
+
joinTimeoutReported = false;
|
|
79
|
+
return [...actions, {
|
|
80
|
+
type: "open",
|
|
81
|
+
url: target,
|
|
82
|
+
need: fresh[0]
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
const reason = closed && joinedSinceOpen ? "browser-closed" : "recently-opened";
|
|
86
|
+
return [...actions, ...fresh.map((need) => ask(need, reason))];
|
|
87
|
+
} };
|
|
88
|
+
};
|
|
89
|
+
export { BROWSER_CLOSE_GRACE_MS, BROWSER_REOPEN_GUARD_MS, createBrowserGate };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","names":[],"sources":["../src/browser.ts"],"sourcesContent":["import { needKey, pendingNeeds } from \"./needs\";\nimport type { SetupNeed } from \"./needs\";\nimport { BROWSER_HEARTBEAT_MS, BROWSER_PRESENCE_MS } from \"./protocol\";\nimport type { Checkout } from \"./protocol\";\n\n/** Within this long of opening a tab, setup never opens another. */\nexport const BROWSER_REOPEN_GUARD_MS = 30_000;\n/** A page silent this long after it was present counts as closed; one heartbeat gap is not enough. */\nexport const BROWSER_CLOSE_GRACE_MS = BROWSER_HEARTBEAT_MS + 1_000;\n\n/**\n * Why the agent should ask the user itself:\n * - recently-opened: a tab was opened moments ago, or opened and never joined;\n * - browser-closed: the user closed the setup tab (and it may not reopen yet);\n * - no-browser: setup may not open a browser (`--no-open`).\n */\nexport type AskReason = \"recently-opened\" | \"browser-closed\" | \"no-browser\";\n\nexport type BrowserGateAction =\n | { type: \"open\"; url: string; need: SetupNeed }\n | { type: \"ask\"; reason: AskReason; need: SetupNeed; openedAgoMs?: number };\n\n/**\n * Decides when setup opens the user's browser: only once the setup waits on\n * the user, never while a joined tab is present, never twice within the\n * guard window, and again for a later need once a joined tab went away.\n * Presence is measured on this machine's clock from when each heartbeat\n * arrived, so a skewed server clock does not matter.\n */\nexport const createBrowserGate = ({\n url,\n canOpen = true,\n now = Date.now,\n}: {\n /** The setup page for a state; `undefined` when there is none to open. */\n url: (state: Checkout.State) => string | undefined;\n /** False for `--no-open`: the agent is told to ask instead. */\n canOpen?: boolean;\n now?: () => number;\n}) => {\n const handled = new Set<string>();\n /** Needs the agent was already told to ask about. */\n const told = new Set<string>();\n let openedAt: number | undefined;\n let joinedSinceOpen = false;\n let lastBeat: number | null | undefined;\n let beatArrivedAt: number | undefined;\n let present = false;\n let absentSince: number | undefined;\n let closedReported = true;\n let joinTimeoutReported = true;\n\n const trackPresence = (browser: Checkout.State[\"browser\"]) => {\n if (browser === undefined) return;\n const time = now();\n if (browser.lastSeenAt !== lastBeat) {\n // Every beat, the first one included, counts from when it arrived here.\n beatArrivedAt = browser.lastSeenAt === null ? undefined : time;\n lastBeat = browser.lastSeenAt;\n }\n if (\n browser.connected &&\n beatArrivedAt !== undefined &&\n time - beatArrivedAt < BROWSER_PRESENCE_MS\n ) {\n present = joinedSinceOpen = true;\n absentSince = undefined;\n closedReported = false;\n } else if (present || absentSince !== undefined) {\n // A page that went quiet left when its last beat expired, not when this noticed.\n absentSince ??=\n browser.connected && beatArrivedAt !== undefined\n ? Math.min(time, beatArrivedAt + BROWSER_PRESENCE_MS)\n : time;\n present = false;\n }\n };\n\n return {\n update: (state: Checkout.State): BrowserGateAction[] => {\n trackPresence(state.browser);\n const time = now();\n const needs = pendingNeeds(state);\n const keys = new Set(needs.map(needKey));\n for (const key of handled) if (!keys.has(key)) handled.delete(key);\n for (const key of told) if (!keys.has(key)) told.delete(key);\n // Present, or gone too briefly to call it closed.\n const looksPresent =\n present ||\n (absentSince !== undefined &&\n time - absentSince < BROWSER_CLOSE_GRACE_MS);\n const closed = !looksPresent && absentSince !== undefined;\n const ask = (need: SetupNeed, reason: AskReason): BrowserGateAction => {\n told.add(needKey(need));\n return {\n type: \"ask\",\n reason,\n need,\n ...(reason === \"recently-opened\" && {\n openedAgoMs: time - openedAt!,\n }),\n };\n };\n const actions: BrowserGateAction[] = [];\n if (closed && !closedReported) {\n // The tab closed while the user had something to answer there.\n closedReported = true;\n for (const need of needs)\n if (handled.has(needKey(need)))\n actions.push(ask(need, \"browser-closed\"));\n }\n\n if (\n !joinTimeoutReported &&\n !joinedSinceOpen &&\n time - openedAt! >= BROWSER_REOPEN_GUARD_MS\n ) {\n // The opened tab never joined: it did not open, or the user left it.\n joinTimeoutReported = true;\n for (const need of needs)\n if (handled.has(needKey(need)) && !told.has(needKey(need)))\n actions.push(ask(need, \"recently-opened\"));\n }\n\n const fresh = needs.filter((need) => !handled.has(needKey(need)));\n for (const need of fresh) handled.add(needKey(need));\n if (fresh.length === 0 || looksPresent) return actions;\n const target = canOpen ? url(state) : undefined;\n if (target === undefined)\n return [...actions, ...fresh.map((need) => ask(need, \"no-browser\"))];\n if (\n openedAt === undefined ||\n (time - openedAt >= BROWSER_REOPEN_GUARD_MS && joinedSinceOpen)\n ) {\n openedAt = time;\n joinedSinceOpen = false;\n joinTimeoutReported = false;\n // One tab shows every waiting question.\n return [...actions, { type: \"open\", url: target, need: fresh[0]! }];\n }\n const reason =\n closed && joinedSinceOpen ? \"browser-closed\" : \"recently-opened\";\n return [...actions, ...fresh.map((need) => ask(need, reason))];\n },\n };\n};\n"],"mappings":";;;;AAMA,MAAa,0BAA0B;;AAEvC,MAAa,yBAAyB,uBAAuB;;;;;;;;AAqB7D,MAAa,qBAAqB,EAChC,KACA,UAAU,MACV,MAAM,KAAK,UAOP;CACJ,MAAM,0BAAU,IAAI,IAAY;;CAEhC,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI;CACJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI;CACJ,IAAI,UAAU;CACd,IAAI;CACJ,IAAI,iBAAiB;CACrB,IAAI,sBAAsB;CAE1B,MAAM,iBAAiB,YAAuC;EAC5D,IAAI,YAAY,KAAA,GAAW;EAC3B,MAAM,OAAO,IAAI;EACjB,IAAI,QAAQ,eAAe,UAAU;GAEnC,gBAAgB,QAAQ,eAAe,OAAO,KAAA,IAAY;GAC1D,WAAW,QAAQ;EACrB;EACA,IACE,QAAQ,aACR,kBAAkB,KAAA,KAClB,OAAO,gBAAA,KACP;GACA,UAAU,kBAAkB;GAC5B,cAAc,KAAA;GACd,iBAAiB;EACnB,OAAO,IAAI,WAAW,gBAAgB,KAAA,GAAW;GAE/C,gBACE,QAAQ,aAAa,kBAAkB,KAAA,IACnC,KAAK,IAAI,MAAM,gBAAgB,mBAAmB,IAClD;GACN,UAAU;EACZ;CACF;CAEA,OAAO,EACL,SAAS,UAA+C;EACtD,cAAc,MAAM,OAAO;EAC3B,MAAM,OAAO,IAAI;EACjB,MAAM,QAAQ,aAAa,KAAK;EAChC,MAAM,OAAO,IAAI,IAAI,MAAM,IAAI,OAAO,CAAC;EACvC,KAAK,MAAM,OAAO,SAAS,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,QAAQ,OAAO,GAAG;EACjE,KAAK,MAAM,OAAO,MAAM,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,OAAO,GAAG;EAE3D,MAAM,eACJ,WACC,gBAAgB,KAAA,KACf,OAAO,cAAc;EACzB,MAAM,SAAS,CAAC,gBAAgB,gBAAgB,KAAA;EAChD,MAAM,OAAO,MAAiB,WAAyC;GACrE,KAAK,IAAI,QAAQ,IAAI,CAAC;GACtB,OAAO;IACL,MAAM;IACN;IACA;IACA,GAAI,WAAW,qBAAqB,EAClC,aAAa,OAAO,SACtB;GACF;EACF;EACA,MAAM,UAA+B,CAAC;EACtC,IAAI,UAAU,CAAC,gBAAgB;GAE7B,iBAAiB;GACjB,KAAK,MAAM,QAAQ,OACjB,IAAI,QAAQ,IAAI,QAAQ,IAAI,CAAC,GAC3B,QAAQ,KAAK,IAAI,MAAM,gBAAgB,CAAC;EAC9C;EAEA,IACE,CAAC,uBACD,CAAC,mBACD,OAAO,YAAA,KACP;GAEA,sBAAsB;GACtB,KAAK,MAAM,QAAQ,OACjB,IAAI,QAAQ,IAAI,QAAQ,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,QAAQ,IAAI,CAAC,GACvD,QAAQ,KAAK,IAAI,MAAM,iBAAiB,CAAC;EAC/C;EAEA,MAAM,QAAQ,MAAM,QAAQ,SAAS,CAAC,QAAQ,IAAI,QAAQ,IAAI,CAAC,CAAC;EAChE,KAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,MAAM,WAAW,KAAK,cAAc,OAAO;EAC/C,MAAM,SAAS,UAAU,IAAI,KAAK,IAAI,KAAA;EACtC,IAAI,WAAW,KAAA,GACb,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,KAAK,SAAS,IAAI,MAAM,YAAY,CAAC,CAAC;EACrE,IACE,aAAa,KAAA,KACZ,OAAO,YAAA,OAAuC,iBAC/C;GACA,WAAW;GACX,kBAAkB;GAClB,sBAAsB;GAEtB,OAAO,CAAC,GAAG,SAAS;IAAE,MAAM;IAAQ,KAAK;IAAQ,MAAM,MAAM;GAAI,CAAC;EACpE;EACA,MAAM,SACJ,UAAU,kBAAkB,mBAAmB;EACjD,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,KAAK,SAAS,IAAI,MAAM,MAAM,CAAC,CAAC;CAC/D,EACF;AACF"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CheckoutClient } from "./client.js";
|
|
2
2
|
import type { Checkout } from "./protocol.js";
|
|
3
|
+
export { diffEvents } from "./stream.js";
|
|
3
4
|
export declare const HELP: string;
|
|
4
5
|
/**
|
|
5
6
|
* The normalized setup URL, or an error saying why it is refused. A setup URL
|
|
@@ -9,15 +10,21 @@ export declare const HELP: string;
|
|
|
9
10
|
*/
|
|
10
11
|
export declare const parseSetupUrl: (value: string) => string;
|
|
11
12
|
export type AgentInstructionsOptions = {
|
|
13
|
+
/** How the agent invokes the setup commands, before the URL; `npx setup-agent` by default. */
|
|
14
|
+
command?: string;
|
|
12
15
|
/**
|
|
13
16
|
* The agent started this setup with `assistant-ui setup`, which already
|
|
14
|
-
* began planning
|
|
15
|
-
* no Begin plan click to wait for.
|
|
17
|
+
* began planning; there is no Begin plan click to wait for.
|
|
16
18
|
*/
|
|
17
19
|
startedByAgent?: {
|
|
18
20
|
browserUrl: string;
|
|
19
21
|
/** False when the browser was not opened for the user; the agent then hands them the link. */
|
|
20
22
|
opened?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The command that printed this briefing is the stream: it keeps running
|
|
25
|
+
* and opens the browser only when the setup waits on the user.
|
|
26
|
+
*/
|
|
27
|
+
streaming?: boolean;
|
|
21
28
|
};
|
|
22
29
|
};
|
|
23
30
|
export declare const agentInstructions: (setupUrl: string, options?: AgentInstructionsOptions) => string;
|
|
@@ -28,43 +35,6 @@ type Parsed = {
|
|
|
28
35
|
};
|
|
29
36
|
export { CODING_AGENTS, detectAgentKind, findCommand, isDirectInvocation, runCodingAgent, } from "./agents.js";
|
|
30
37
|
export type { CodingAgent } from "./agents.js";
|
|
31
|
-
type StreamEvent = {
|
|
32
|
-
event: "planning.started";
|
|
33
|
-
} | {
|
|
34
|
-
event: "message.created";
|
|
35
|
-
messageId: string;
|
|
36
|
-
text: string;
|
|
37
|
-
} | {
|
|
38
|
-
event: "input.answered";
|
|
39
|
-
inputId: string;
|
|
40
|
-
prompt: string;
|
|
41
|
-
/** The entries of a multiple choice answer, else the answer text. */
|
|
42
|
-
answer: string | string[];
|
|
43
|
-
note?: string;
|
|
44
|
-
/** True when a choice answer is, or a multiple one contains, the user's own text rather than an option. */
|
|
45
|
-
custom?: true;
|
|
46
|
-
} | {
|
|
47
|
-
event: "input.dismissed";
|
|
48
|
-
inputId: string;
|
|
49
|
-
prompt: string;
|
|
50
|
-
} | {
|
|
51
|
-
event: "plan.approved";
|
|
52
|
-
revision: number;
|
|
53
|
-
} | {
|
|
54
|
-
event: "plan.changes-requested";
|
|
55
|
-
revision: number;
|
|
56
|
-
feedback: string;
|
|
57
|
-
} | {
|
|
58
|
-
event: "finished";
|
|
59
|
-
next: string;
|
|
60
|
-
} | {
|
|
61
|
-
event: "cancelled";
|
|
62
|
-
next: string;
|
|
63
|
-
} | {
|
|
64
|
-
event: "setup.created";
|
|
65
|
-
products: string[];
|
|
66
|
-
};
|
|
67
|
-
export declare const diffEvents: (before: Checkout.State, after: Checkout.State) => StreamEvent[];
|
|
68
38
|
/** Resolves once a setup is planning or installing, or once the one being waited on is cancelled; a setup already closed when the wait begins is over, so the wait is for the next one. */
|
|
69
39
|
export declare const waitForStart: (client: Pick<CheckoutClient, "state" | "subscribe">) => Promise<Checkout.Status>;
|
|
70
40
|
export declare const askSeed: (rest: readonly string[], flags: Parsed["flags"]) => Checkout.InputSeed & {
|
|
@@ -79,5 +49,19 @@ export declare const upsertEnvLine: (content: string, key: string, value: string
|
|
|
79
49
|
* still deposited; it is read after, so lines written meanwhile survive.
|
|
80
50
|
*/
|
|
81
51
|
export declare const writeEnvSecret: (file: string, key: string, take: () => Promise<string>, projectRoot?: string) => Promise<void>;
|
|
52
|
+
export type RunSetupCommandOptions = {
|
|
53
|
+
/** How the agent invokes these commands, before the URL; `npx setup-agent` by default. */
|
|
54
|
+
command?: string;
|
|
55
|
+
/** Lets "stream" open the setup page when the setup waits on the user, as `--setup-app-url` does. */
|
|
56
|
+
browser?: {
|
|
57
|
+
setupAppUrl: string;
|
|
58
|
+
canOpen?: boolean;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
82
61
|
export declare const main: (argv: readonly string[]) => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Runs one setup command (`<url> <command> ...`) the way `npx setup-agent`
|
|
64
|
+
* does, for a CLI that embeds setup as its own command.
|
|
65
|
+
*/
|
|
66
|
+
export declare const runSetupCommand: (argv: readonly string[], options?: RunSetupCommandOptions) => Promise<void>;
|
|
83
67
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAiB;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAiB;AA2B/C,OAAO,KAAK,EAAE,QAAQ,EAAE,sBAAmB;AAI3C,OAAO,EAAE,UAAU,EAAE,oBAAiB;AAItC,eAAO,MAAM,IAAI,QAoChB,CAAC;AAMF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MA6B7C,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,8FAA8F;QAC9F,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB;;;WAGG;QACH,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC;CACH,CAAC;AAuCF,eAAO,MAAM,iBAAiB,GAC5B,UAAU,MAAM,EAChB,UAAS,wBAA6B,WA6CvC,CAAC;AAEF,eAAO,MAAM,YAAY,aAhDb,MAAM,YACP,wBAAwB,WA+CU,CAAC;AAE9C,KAAK,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;CAAE,CAAC;AAuB1E,OAAO,EACL,aAAa,EACb,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,cAAc,GACf,oBAAiB;AAClB,YAAY,EAAE,WAAW,EAAE,oBAAiB;AAuG5C,2LAA2L;AAC3L,eAAO,MAAM,YAAY,GACvB,QAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,WAAW,CAAC,6BAiBjD,CAAC;AAwDL,eAAO,MAAM,OAAO,GAClB,MAAM,SAAS,MAAM,EAAE,EACvB,OAAO,MAAM,CAAC,OAAO,CAAC,KACrB,QAAQ,CAAC,SAAS,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CA+HxC,CAAC;AAQF,+EAA+E;AAC/E,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,MAAM,WAqBxE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,KAAK,MAAM,EACX,MAAM,MAAM,OAAO,CAAC,MAAM,CAAC,EAC3B,oBAA2B,kBAsD5B,CAAC;AAgHF,MAAM,MAAM,sBAAsB,GAAG;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qGAAqG;IACrG,OAAO,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACtD,CAAC;AAkCF,eAAO,MAAM,IAAI,GAAI,MAAM,SAAS,MAAM,EAAE,kBAA0B,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,MAAM,SAAS,MAAM,EAAE,EACvB,UAAS,sBAA2B,kBAwRrC,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CODING_AGENTS, detectAgentKind, findCommand, isDirectInvocation, runCodingAgent } from "./agents.js";
|
|
2
|
+
import { OPTION_ICONS, currentPlan, isAgentPresent, isClosed, isLoopbackHostname, isOptionIcon, isValidModelAnswer, parseEntryPointOptions, stepProgress } from "./protocol.js";
|
|
3
|
+
import { answerNeed } from "./needs.js";
|
|
2
4
|
import { connectCheckout } from "./client.js";
|
|
3
5
|
import { INPUT_PRESETS, isPresetId, presetInput } from "./presets.js";
|
|
4
|
-
import {
|
|
6
|
+
import { answerFields, diffEvents, followSetup, shellWord } from "./stream.js";
|
|
5
7
|
import { constants } from "node:fs";
|
|
6
8
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -9,11 +11,16 @@ import { chmod, lstat, mkdir, open, readFile, realpath, unlink } from "node:fs/p
|
|
|
9
11
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
10
12
|
import { createInterface } from "node:readline/promises";
|
|
11
13
|
// src/cli.ts
|
|
14
|
+
const DEFAULT_COMMAND = "npx setup-agent";
|
|
12
15
|
const HELP = `setup-agent <url> [command]
|
|
13
16
|
|
|
14
17
|
Commands (the url always comes first):
|
|
15
18
|
status Print the setup as JSON.
|
|
16
|
-
stream
|
|
19
|
+
stream [--setup-app-url <url>] [--no-open]
|
|
20
|
+
Follow the setup: one JSON array of events per line. With --setup-app-url, open the setup page when the setup waits on the user.
|
|
21
|
+
answer <input-id> <answer> [--note <text>] [--dismiss]
|
|
22
|
+
answer plan approve | answer plan revise --feedback <text> | answer finish close
|
|
23
|
+
Answer for the user what they told you in your own conversation, as an "input.needed" event's "answer" shows.
|
|
17
24
|
log <text> Send a message to the user: a reply, something no question or step conveys, or one wrap-up when finished.
|
|
18
25
|
ack <message-id> Acknowledge a user message.
|
|
19
26
|
ask <prompt> [--placeholder <text>] [--optional] [--step <step-id>] [--wait]
|
|
@@ -64,54 +71,61 @@ const parseSetupUrl = (value) => {
|
|
|
64
71
|
if (!SETUP_URL_PATTERN.test(href)) throw new Error("the setup URL has characters a setup URL does not use");
|
|
65
72
|
return href;
|
|
66
73
|
};
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
npx setup-agent ${url} wait start
|
|
74
|
+
const EVENT_LIST = `"planning.started", "input.answered" ({inputId, answer, note?}; answer is an array for a choice question not asked with --single), "input.dismissed", "plan.approved", "plan.changes-requested" ({feedback}), "message.created" ({messageId, text}), "finished", "cancelled", "setup.created" ({products})`;
|
|
75
|
+
const beginPlanHandoff = (cmd, url) => `Before planning, check the current setup status. If it is "waiting", tell the user in your own conversation: "I'm connected. Go back to your browser and click Begin plan. You can watch my progress, answer questions, and steer me there." Then run:
|
|
76
|
+
${cmd} ${url} wait start
|
|
71
77
|
Keep the stream running while you wait. Do not inspect the project, fetch guides, ask setup questions, or propose a plan until this command returns {"ok":true}. If it returns {"ok":false,"status":"cancelled"}, the user ended this setup before planning: do not plan, tell the user you are connected and waiting for their next setup, and run "wait start" again. Only the user in the browser should start planning; never invoke the browser's start command yourself. "planning.started" also arrives on the stream. If the status is already "planning" or "installing", resume that phase without asking the user to begin again. If it is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.`;
|
|
72
|
-
const agentStartedHandoff = ({ browserUrl, opened = true }) => `You started this setup, and it is already planning: there is no Begin plan click to wait for. The browser asks the user to connect to you first; it shows your working directory once your stream is running. Check "status" before planning: if it is "planning" or "installing", go on with that phase. Tell the user in your own conversation, once: ${opened ? `"I opened the assistant-ui setup in your browser. I'll ask my questions there, and you can watch my progress and steer me from that page. If it didn't open, go to ${browserUrl} ."` : `"Open ${browserUrl} to follow the assistant-ui setup. I'll ask my questions there, and you can watch my progress and steer me from that page."`} If the status is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.`;
|
|
78
|
+
const agentStartedHandoff = ({ browserUrl, opened = true, streaming = false }) => streaming ? `You started this setup, and it is already planning: there is no Begin plan click to wait for. Check "status" before planning: if it is "planning" or "installing", go on with that phase. If the status is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.` : `You started this setup, and it is already planning: there is no Begin plan click to wait for. The browser asks the user to connect to you first; it shows your working directory once your stream is running. Check "status" before planning: if it is "planning" or "installing", go on with that phase. Tell the user in your own conversation, once: ${opened ? `"I opened the assistant-ui setup in your browser. I'll ask my questions there, and you can watch my progress and steer me from that page. If it didn't open, go to ${browserUrl} ."` : `"Open ${browserUrl} to follow the assistant-ui setup. I'll ask my questions there, and you can watch my progress and steer me from that page."`} If the status is "done" or "cancelled", that setup is over: tell the user you are connected and waiting for their next setup, and run "wait start", which returns once the next one begins.`;
|
|
79
|
+
const streamSection = (cmd, url, streaming, noOpen) => streaming ? `The command that printed this is the stream. Keep it running in the background until the setup ends: the browser shows you as connected only while it runs, and it is how answers reach you. Each new line of its output is a JSON array of events: ${EVENT_LIST}, plus:
|
|
80
|
+
- "browser.opened" ({id, message}): setup opened the setup page in the user's browser for a question. Tell the user once, in your own conversation, that it waits there.
|
|
81
|
+
- "input.needed" ({id, message, question, options?, answer}): the user may not see the browser right now. Do what its message says: wait for "input.answered", or ask the user in your own conversation with that question and options, then run its "answer" command with their reply. Answered either way, the setup goes on.
|
|
82
|
+
If this command stops, start it again in the background with
|
|
83
|
+
${cmd} ${url} stream${noOpen ? " --no-open" : ""}
|
|
84
|
+
The URL is the user's link to you from their browser, not one setup: it outlives this one, and their next setup arrives on the same stream.` : `First, start a persistent Monitor on
|
|
85
|
+
${cmd} ${url} stream
|
|
86
|
+
and keep it running: the browser shows you as connected only while it runs, and it is how answers reach you. The URL is the user's link to you from their browser, not one setup: it outlives this one, and their next setup arrives on the same stream. Each line is a JSON array of events: ${EVENT_LIST}.`;
|
|
73
87
|
const agentInstructions = (setupUrl, options = {}) => {
|
|
74
88
|
const url = shellWord(setupUrl);
|
|
89
|
+
const cmd = options.command ?? DEFAULT_COMMAND;
|
|
75
90
|
const started = options.startedByAgent;
|
|
76
|
-
|
|
91
|
+
const streaming = started?.streaming === true;
|
|
92
|
+
return `You are the coding agent for a setup ${started ? streaming ? started.opened === false ? "you started; setup opens no browser (--no-open), so the user follows it on the setup page if they open it, or answers you in your own conversation" : "you started; the user follows it in their browser, which setup opens when you ask the user something" : "you opened in the user's browser" : "the user opened in their browser"}. The browser shows this setup live; every command below updates it. The user watches the browser, not this terminal: once setup begins, send questions through "ask" so the user can answer in their browser.
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
npx setup-agent ${url} stream
|
|
80
|
-
and keep it running: the browser shows you as connected only while it runs, and it is how answers reach you. The URL is the user's link to you from their browser, not one setup: it outlives this one, and their next setup arrives on the same stream. Each line is a JSON array of events: "planning.started", "input.answered" ({inputId, answer, note?}; answer is an array for a choice question not asked with --single), "input.dismissed", "plan.approved", "plan.changes-requested" ({feedback}), "message.created" ({messageId, text}), "finished", "cancelled", "setup.created" ({products}).
|
|
94
|
+
${streamSection(cmd, url, streaming, started?.opened === false)}
|
|
81
95
|
|
|
82
|
-
The user can message you at any time, including while a question is waiting. Read "message.created" events, acknowledge each with "
|
|
96
|
+
The user can message you at any time, including while a question is waiting. Read "message.created" events, acknowledge each with "${cmd} ${url} ack <message-id>", and reply using "log" with what you will do. Pending messages are replayed when the stream starts and listed in "status". A message is not an answer or plan approval; keep those decisions explicit.
|
|
83
97
|
|
|
84
98
|
"log" is a message to the user, and the user reads every one: send it only to reply to their message, to tell them something no question or step conveys (a blocker, a deviation from the plan, a decision you made for them), or once as a wrap-up when the install is finished. Findings go in the plan, progress in step notes, questions in "ask"; do not narrate.
|
|
85
99
|
|
|
86
|
-
${started ? agentStartedHandoff(started) : beginPlanHandoff(url)}
|
|
100
|
+
${started ? agentStartedHandoff(started) : beginPlanHandoff(cmd, url)}
|
|
87
101
|
|
|
88
102
|
The remaining phases are plan, then install.
|
|
89
103
|
|
|
90
104
|
1. Plan. Investigate only; change nothing yet. If the current directory is not an app to install into (a monorepo root, an empty folder, a library), ask where to install first:
|
|
91
|
-
|
|
105
|
+
${cmd} ${url} ask "Which project should I install into?" --placeholder "An absolute path to the app" --wait
|
|
92
106
|
Read the project: its app framework and package manager, the agent framework and model provider it already uses, where components live, any existing chat route. Fetch each product's "guide" URL from the setup below; it is the install reference. Ask for what the project does not decide, using the standard questions so the browser can render them well:
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
${cmd} ${url} ask --preset framework
|
|
108
|
+
${cmd} ${url} ask --preset llm-provider
|
|
95
109
|
The setup's products are starting names and guide references. If the setup lists no products, first ask the user what to set up with a choice question whose options are slugs from https://www.assistant-ui.com/catalog.md, then fetch https://www.assistant-ui.com/install.md?items=<slugs> as the guide. Explore each product's setup instructions, then discover and plan any other relevant products or dependencies the app needs. Read /catalog.md on the guide URL’s origin to find related products and fetch their setup guides. The starting list does not restrict the implementation or require adding another product to the list. Include relevant additions in the plan for approval.
|
|
96
110
|
When the setup puts an assistant in the app, ask how users should access it after discovering the actual app and before proposing the plan. Skip this question when no product's guide installs an assistant (a standalone library or service setup, for example). Use an entry-point question with only 1–3 sensible alternatives. Describe the actual app's pages, workflow and component placement, not generic placeholder copy. Recommend one clear fit when there is one; two options may use the same form factor in different places. Each option needs a stable id, label, contextual description and entryPoint {"formFactor":"modal"|"sidebar"|"full-page","placement":"where it appears","trigger":"how users open it or navigate to it","recommended":true?}. Write the JSON array for this app and send it with:
|
|
97
|
-
|
|
111
|
+
${cmd} ${url} ask "Where should users access the assistant?" --entry-points '<JSON array>' --wait
|
|
98
112
|
Mark a clear fit with "recommended":true rather than --default, so the user makes the pick. When you ask it, do not submit the plan before the user picks an entry point. Respect the selected placement and trigger in the plan and implementation. Do not ask for a new-versus-existing app setting; discover the app from its files and ask only for an install path when needed.
|
|
99
113
|
Skip a preset the code already answers (a chat route on the AI SDK, a provider key in .env.local) and log why. Ask everything at once, keep investigating, and block with "wait <input-id>" only when nothing else can proceed. A framework answer is "<framework>:<language>", for example "ai-sdk:typescript". A model answer is JSON {"provider","model","reasoningEffort"?}. An answer may carry a "note" from the user; follow it. A choice answer that matches none of the options is the user's own text.
|
|
100
114
|
Then write the plan as markdown and submit it:
|
|
101
|
-
|
|
115
|
+
${cmd} ${url} plan --file <path> --wait
|
|
102
116
|
Use these headings, each with bullets: "## What I found" (facts as \`- **Label:** value\`: app framework, package manager, agent framework, model provider and model, where components live), "## What I will install" (packages and files, one per bullet), "## Steps" (a numbered list, one line each), and "## Open questions" only if any remain. Keep it under 30 lines; no preamble. "plan --wait" prints {"decision":"approve"} or {"decision":"revise","feedback":"..."}. On revise, address the feedback (ask again if you need to) and submit a new plan; repeat until approved. Do not change the project before approval: the setup refuses steps until then.
|
|
103
117
|
|
|
104
118
|
2. Install. Declare the steps from your plan up front, in order:
|
|
105
|
-
|
|
119
|
+
${cmd} ${url} add-step "<title>" --detail "<one line>"
|
|
106
120
|
which prints the step id. Then work through them: "step <id> active" before, "step <id> done --note '<one line on what you did>'" after; "skipped" when the step is already satisfied, "blocked" when you cannot continue, saying why in --note. Add steps as the work reveals them. Ask when you need another decision or value (--choices for a pick, plain for text; --optional when you can proceed without an answer; --wait only when the answer is the very next thing you need). A choice question is multi-select: the user checks any number of your options, and the answer is an array of the ids they checked, plus their own text as an entry when they typed one. Pass --single only when the options exclude each other (one framework, one model, yes or no); the answer is then one id. Every option must carry an icon, given as --icons "<id>=<icon>,..." with the closest name from this pack: ${OPTION_ICONS.join(", ")}. A question without an icon for each option is refused. Open inputs the user has not answered are listed under "inputs" in "status"; check them before asking again.
|
|
107
121
|
The API key behind a model answer is not in the answer and you never see it: pick the variable name the provider's SDK reads (OPENAI_API_KEY, ANTHROPIC_API_KEY, ...) and run
|
|
108
|
-
|
|
109
|
-
which writes ENV_VAR=<key> into that file. Use the answer's model in the chat route and map reasoningEffort onto the provider's setting when it is set. Never invent a key or a model name.
|
|
122
|
+
${cmd} ${url} env <input-id> <ENV_VAR> --file .env.local
|
|
123
|
+
which writes ENV_VAR=<key> into that file. Use the answer's model in the chat route and map reasoningEffort onto the provider's setting when it is set. If "env" says the key was never deposited (the model question was answered from your conversation), ask the user to add the key to that file themselves. Never invent a key or a model name.
|
|
110
124
|
|
|
111
125
|
When every product is installed and verified, run
|
|
112
|
-
|
|
126
|
+
${cmd} ${url} done --summary "<one or two lines on what you installed and how to try it>" --preview http://localhost:<port>
|
|
113
127
|
Before that, start the project's dev server in the background when the project has one, check that the page you changed loads, and pass the address as --preview (the page itself when it is not the root); leave the server running so the user can try the result. Omit --preview when there is nothing to open. This proposes closing the setup; only the user closes it. Do not stop the stream or exit: stay idle on it, because the user may follow up with a "message.created" event. Handle it, then run "done" again. The stream prints "finished" once the user closes the setup, and "cancelled" if they end it early. Keep the stream running after either: the user's next setup starts on this same URL and arrives as "setup.created" with its products. Run
|
|
114
|
-
|
|
128
|
+
${cmd} ${url} wait start
|
|
115
129
|
which returns once that setup begins, then run "status" for its products, guides and instructions and go through plan and install again. A "setup.created" event while you are still working means the user replaced this setup: drop what you were doing and start over the same way.
|
|
116
130
|
`;
|
|
117
131
|
};
|
|
@@ -208,99 +222,6 @@ const STEP_STATUSES = [
|
|
|
208
222
|
"skipped",
|
|
209
223
|
"blocked"
|
|
210
224
|
];
|
|
211
|
-
const STAY_CONNECTED = "This setup is over, not your connection. Keep the stream running, tell the user you are connected and waiting for their next setup, and run wait start.";
|
|
212
|
-
const diffEvents = (before, after) => {
|
|
213
|
-
const events = [];
|
|
214
|
-
if (before.status === "waiting" && after.status === "planning") events.push({ event: "planning.started" });
|
|
215
|
-
for (const entry of after.log) if (entry.role === "user" && entry.acknowledgedAt === void 0 && !before.log.some((previous) => previous.id === entry.id)) events.push({
|
|
216
|
-
event: "message.created",
|
|
217
|
-
messageId: entry.id,
|
|
218
|
-
text: entry.text
|
|
219
|
-
});
|
|
220
|
-
for (const input of after.inputs) {
|
|
221
|
-
if (before.inputs.find((candidate) => candidate.id === input.id)?.status === input.status) continue;
|
|
222
|
-
if (input.status === "answered") {
|
|
223
|
-
const answer = input.answer ?? "";
|
|
224
|
-
events.push({
|
|
225
|
-
event: "input.answered",
|
|
226
|
-
inputId: input.id,
|
|
227
|
-
prompt: input.prompt,
|
|
228
|
-
...answerFields(input, answer)
|
|
229
|
-
});
|
|
230
|
-
} else if (input.status === "dismissed" && after.status !== "cancelled") events.push({
|
|
231
|
-
event: "input.dismissed",
|
|
232
|
-
inputId: input.id,
|
|
233
|
-
prompt: input.prompt
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
for (const plan of after.plans) {
|
|
237
|
-
if (before.plans.find((candidate) => candidate.revision === plan.revision)?.status === plan.status || plan.status === "proposed") continue;
|
|
238
|
-
if (plan.status === "approved") events.push({
|
|
239
|
-
event: "plan.approved",
|
|
240
|
-
revision: plan.revision
|
|
241
|
-
});
|
|
242
|
-
else events.push({
|
|
243
|
-
event: "plan.changes-requested",
|
|
244
|
-
revision: plan.revision,
|
|
245
|
-
feedback: plan.feedback ?? ""
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
if (before.status !== "done" && after.status === "done") events.push({
|
|
249
|
-
event: "finished",
|
|
250
|
-
next: STAY_CONNECTED
|
|
251
|
-
});
|
|
252
|
-
if (before.status !== "cancelled" && after.status === "cancelled") events.push({
|
|
253
|
-
event: "cancelled",
|
|
254
|
-
next: STAY_CONNECTED
|
|
255
|
-
});
|
|
256
|
-
if (after.id !== null && after.id !== before.id) events.push({
|
|
257
|
-
event: "setup.created",
|
|
258
|
-
products: after.products.map((product) => product.slug)
|
|
259
|
-
});
|
|
260
|
-
return events;
|
|
261
|
-
};
|
|
262
|
-
const stream = async (client) => {
|
|
263
|
-
let previous = requireState(client);
|
|
264
|
-
let pending = diffEvents({
|
|
265
|
-
...previous,
|
|
266
|
-
log: []
|
|
267
|
-
}, previous);
|
|
268
|
-
let timer = null;
|
|
269
|
-
const flush = () => {
|
|
270
|
-
timer = null;
|
|
271
|
-
if (pending.length === 0) return;
|
|
272
|
-
print(pending);
|
|
273
|
-
pending = [];
|
|
274
|
-
};
|
|
275
|
-
client.subscribe(() => {
|
|
276
|
-
const next = client.state;
|
|
277
|
-
if (next === void 0) return;
|
|
278
|
-
pending.push(...diffEvents(previous, next));
|
|
279
|
-
previous = next;
|
|
280
|
-
if (pending.length > 0 && timer === null) timer = setTimeout(flush, 200);
|
|
281
|
-
});
|
|
282
|
-
flush();
|
|
283
|
-
const kind = detectAgentKind();
|
|
284
|
-
await client.commands["agent/hello"]({
|
|
285
|
-
cwd: process.cwd(),
|
|
286
|
-
...kind !== void 0 && { kind }
|
|
287
|
-
});
|
|
288
|
-
const heartbeat = setInterval(() => {
|
|
289
|
-
client.commands["agent/heartbeat"]().catch(() => {});
|
|
290
|
-
}, AGENT_HEARTBEAT_MS);
|
|
291
|
-
const stop = () => {
|
|
292
|
-
clearInterval(heartbeat);
|
|
293
|
-
flush();
|
|
294
|
-
client.commands["agent/bye"]().catch(() => {}).finally(() => {
|
|
295
|
-
client.dispose();
|
|
296
|
-
process.exit(0);
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
process.on("SIGHUP", stop);
|
|
300
|
-
process.on("SIGINT", stop);
|
|
301
|
-
process.on("SIGTERM", stop);
|
|
302
|
-
await new Promise(() => {});
|
|
303
|
-
};
|
|
304
225
|
/** Resolves once a setup is planning or installing, or once the one being waited on is cancelled; a setup already closed when the wait begins is over, so the wait is for the next one. */
|
|
305
226
|
const waitForStart = (client) => new Promise((resolve) => {
|
|
306
227
|
let previous;
|
|
@@ -579,18 +500,38 @@ const readRedeemKey = async (hash) => {
|
|
|
579
500
|
continue;
|
|
580
501
|
}
|
|
581
502
|
};
|
|
582
|
-
const answerFields = (input, answer) => ({
|
|
583
|
-
answer: input.multiple && parseMultipleAnswer(answer) || answer,
|
|
584
|
-
...input.note !== void 0 && { note: input.note },
|
|
585
|
-
...input.kind === "choice" && classifyChoiceAnswer(input, answer) === "custom" && { custom: true }
|
|
586
|
-
});
|
|
587
503
|
const answered = (input) => ({
|
|
588
504
|
ok: true,
|
|
589
505
|
inputId: input.id,
|
|
590
506
|
status: input.status,
|
|
591
507
|
...input.answer !== void 0 && answerFields(input, input.answer)
|
|
592
508
|
});
|
|
593
|
-
const
|
|
509
|
+
const runStream = async (client, url, flags, options) => {
|
|
510
|
+
const setupAppUrl = options.browser?.setupAppUrl ?? flagText(flags, "setup-app-url");
|
|
511
|
+
const linkId = new URL(url).pathname.split("/").filter(Boolean).at(-1);
|
|
512
|
+
const stream = await followSetup(client, { ...setupAppUrl !== void 0 && linkId !== void 0 && { browser: {
|
|
513
|
+
setupAppUrl: parseSetupUrl(setupAppUrl),
|
|
514
|
+
linkId,
|
|
515
|
+
answerCommand: `${options.command ?? DEFAULT_COMMAND} ${shellWord(url)} answer`,
|
|
516
|
+
canOpen: (options.browser?.canOpen ?? true) && !flags.has("no-open")
|
|
517
|
+
} } });
|
|
518
|
+
const stop = () => {
|
|
519
|
+
stream.stop().finally(() => {
|
|
520
|
+
client.dispose();
|
|
521
|
+
process.exit(0);
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
process.on("SIGHUP", stop);
|
|
525
|
+
process.on("SIGINT", stop);
|
|
526
|
+
process.on("SIGTERM", stop);
|
|
527
|
+
await new Promise(() => {});
|
|
528
|
+
};
|
|
529
|
+
const main = (argv) => runSetupCommand(argv);
|
|
530
|
+
/**
|
|
531
|
+
* Runs one setup command (`<url> <command> ...`) the way `npx setup-agent`
|
|
532
|
+
* does, for a CLI that embeds setup as its own command.
|
|
533
|
+
*/
|
|
534
|
+
const runSetupCommand = async (argv, options = {}) => {
|
|
594
535
|
const { positional, flags } = parseArgs(argv);
|
|
595
536
|
const [first, command = "intro", ...rest] = positional;
|
|
596
537
|
if (flags.has("help") || flags.has("h") || first === void 0) {
|
|
@@ -611,7 +552,7 @@ const main = async (argv) => {
|
|
|
611
552
|
case "intro": {
|
|
612
553
|
const kind = detectAgentKind();
|
|
613
554
|
await commands["agent/intro"]({ ...kind !== void 0 && { kind } }).catch(() => {});
|
|
614
|
-
process.stdout.write(
|
|
555
|
+
process.stdout.write(agentInstructions(url, { ...options.command !== void 0 && { command: options.command } }));
|
|
615
556
|
process.stdout.write("\nCurrent setup:\n");
|
|
616
557
|
print(summary(requireState(client)));
|
|
617
558
|
break;
|
|
@@ -620,8 +561,19 @@ const main = async (argv) => {
|
|
|
620
561
|
print(summary(requireState(client)));
|
|
621
562
|
break;
|
|
622
563
|
case "stream":
|
|
623
|
-
await
|
|
564
|
+
await runStream(client, url, flags, options);
|
|
624
565
|
break;
|
|
566
|
+
case "answer": {
|
|
567
|
+
const id = rest[0] ?? fail("usage: answer <input-id|plan|finish> <answer> [--note <text>]");
|
|
568
|
+
const note = flagText(flags, "note");
|
|
569
|
+
const feedback = flagText(flags, "feedback");
|
|
570
|
+
print(await answerNeed(client, id, rest[1], {
|
|
571
|
+
...note !== void 0 && { note },
|
|
572
|
+
...feedback !== void 0 && { feedback },
|
|
573
|
+
...flags.has("dismiss") && { dismiss: true }
|
|
574
|
+
}));
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
625
577
|
case "step": {
|
|
626
578
|
const usage = "usage: step <step-id> <pending|active|done|skipped|blocked>";
|
|
627
579
|
const stepId = rest[0] ?? fail(usage);
|
|
@@ -785,6 +737,6 @@ const main = async (argv) => {
|
|
|
785
737
|
client.dispose();
|
|
786
738
|
}
|
|
787
739
|
};
|
|
788
|
-
export { CODING_AGENTS, HELP, INSTRUCTIONS, agentInstructions, askSeed, detectAgentKind, diffEvents, findCommand, isDirectInvocation, main, parseSetupUrl, runCodingAgent, upsertEnvLine, waitForStart, writeEnvSecret };
|
|
740
|
+
export { CODING_AGENTS, HELP, INSTRUCTIONS, agentInstructions, askSeed, detectAgentKind, diffEvents, findCommand, isDirectInvocation, main, parseSetupUrl, runCodingAgent, runSetupCommand, upsertEnvLine, waitForStart, writeEnvSecret };
|
|
789
741
|
|
|
790
742
|
//# sourceMappingURL=cli.js.map
|