setup-agent 0.0.8 → 0.1.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 +31 -0
- package/dist/agents.d.ts +33 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +221 -0
- package/dist/agents.js.map +1 -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 +38 -49
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -180
- 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/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 +16 -2
- package/src/agents.test.ts +137 -0
- package/src/agents.ts +243 -0
- package/src/browser.test.ts +192 -0
- package/src/browser.ts +146 -0
- package/src/cli.test.ts +78 -0
- package/src/cli.ts +189 -258
- 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/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.
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A coding agent a person can hand a setup to. `launch` is set when the
|
|
3
|
+
* agent has a terminal command that opens an interactive session with a
|
|
4
|
+
* first prompt; the others are handed the prompt to paste.
|
|
5
|
+
*/
|
|
6
|
+
export type CodingAgent = {
|
|
7
|
+
kind: string;
|
|
8
|
+
name: string;
|
|
9
|
+
launch?: {
|
|
10
|
+
/** Command names to try in order; the first one on PATH runs. */
|
|
11
|
+
commands: readonly string[];
|
|
12
|
+
args: (prompt: string) => string[];
|
|
13
|
+
install: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/** The coding agents people use most, in the order the picker lists them. */
|
|
17
|
+
export declare const CODING_AGENTS: readonly CodingAgent[];
|
|
18
|
+
/** The coding agent this process runs under, from the variables each one exports to its shell. */
|
|
19
|
+
export declare const detectAgentKind: (env?: NodeJS.ProcessEnv) => string | undefined;
|
|
20
|
+
/** True when a person typed the command into a terminal rather than an agent running it from its shell. */
|
|
21
|
+
export declare const isDirectInvocation: (env?: NodeJS.ProcessEnv, stdin?: {
|
|
22
|
+
isTTY?: boolean | undefined;
|
|
23
|
+
}, stdout?: {
|
|
24
|
+
isTTY?: boolean | undefined;
|
|
25
|
+
}) => boolean;
|
|
26
|
+
/** The first of `commands` found on PATH, or `undefined`. */
|
|
27
|
+
export declare const findCommand: (commands: readonly string[], env?: NodeJS.ProcessEnv) => string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Starts `agent` in this terminal with `prompt` as its first message and
|
|
30
|
+
* resolves with its exit code; `undefined` when its command is not installed.
|
|
31
|
+
*/
|
|
32
|
+
export declare const runCodingAgent: (agent: CodingAgent, prompt: string) => Promise<number | undefined>;
|
|
33
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE;QACP,iEAAiE;QACjE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAqG/C,CAAC;AA2CF,kGAAkG;AAClG,eAAO,MAAM,eAAe,GAC1B,MAAK,MAAM,CAAC,UAAwB,KACnC,MAAM,GAAG,SAaX,CAAC;AAEF,2GAA2G;AAC3G,eAAO,MAAM,kBAAkB,GAC7B,MAAK,MAAM,CAAC,UAAwB,EACpC,QAAO;IAAE,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAkB,EACtD,SAAQ;IAAE,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAmB,YAInC,CAAC;AAExB,6DAA6D;AAC7D,eAAO,MAAM,WAAW,GACtB,UAAU,SAAS,MAAM,EAAE,EAC3B,MAAK,MAAM,CAAC,UAAwB,KACnC,MAAM,GAAG,SAoBX,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,WAAW,EAClB,QAAQ,MAAM,KACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAgB5B,CAAC"}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
// src/agents.ts
|
|
5
|
+
/** The coding agents people use most, in the order the picker lists them. */
|
|
6
|
+
const CODING_AGENTS = [
|
|
7
|
+
{
|
|
8
|
+
kind: "claude",
|
|
9
|
+
name: "Claude Code",
|
|
10
|
+
launch: {
|
|
11
|
+
commands: ["claude"],
|
|
12
|
+
args: (prompt) => [prompt],
|
|
13
|
+
install: "npm install -g @anthropic-ai/claude-code"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
kind: "codex",
|
|
18
|
+
name: "Codex",
|
|
19
|
+
launch: {
|
|
20
|
+
commands: ["codex"],
|
|
21
|
+
args: (prompt) => [prompt],
|
|
22
|
+
install: "npm install -g @openai/codex"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
kind: "cursor",
|
|
27
|
+
name: "Cursor",
|
|
28
|
+
launch: {
|
|
29
|
+
commands: ["cursor-agent", "agent"],
|
|
30
|
+
args: (prompt) => [prompt],
|
|
31
|
+
install: "curl https://cursor.com/install -fsS | bash"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
kind: "gemini",
|
|
36
|
+
name: "Gemini CLI",
|
|
37
|
+
launch: {
|
|
38
|
+
commands: ["gemini"],
|
|
39
|
+
args: (prompt) => ["-i", prompt],
|
|
40
|
+
install: "npm install -g @google/gemini-cli"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
kind: "copilot",
|
|
45
|
+
name: "Copilot CLI",
|
|
46
|
+
launch: {
|
|
47
|
+
commands: ["copilot"],
|
|
48
|
+
args: (prompt) => ["-i", prompt],
|
|
49
|
+
install: "npm install -g @github/copilot"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: "amp",
|
|
54
|
+
name: "Amp"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
kind: "opencode",
|
|
58
|
+
name: "OpenCode",
|
|
59
|
+
launch: {
|
|
60
|
+
commands: ["opencode"],
|
|
61
|
+
args: (prompt) => ["--prompt", prompt],
|
|
62
|
+
install: "npm install -g opencode-ai"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: "cline",
|
|
67
|
+
name: "Cline"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
kind: "aider",
|
|
71
|
+
name: "Aider"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
kind: "goose",
|
|
75
|
+
name: "Goose"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: "warp",
|
|
79
|
+
name: "Warp"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
kind: "windsurf",
|
|
83
|
+
name: "Windsurf"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: "kiro",
|
|
87
|
+
name: "Kiro",
|
|
88
|
+
launch: {
|
|
89
|
+
commands: ["kiro-cli"],
|
|
90
|
+
args: (prompt) => ["chat", prompt],
|
|
91
|
+
install: "curl -fsSL https://cli.kiro.dev/install | bash"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: "junie",
|
|
96
|
+
name: "Junie"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
kind: "qwen",
|
|
100
|
+
name: "Qwen Code",
|
|
101
|
+
launch: {
|
|
102
|
+
commands: ["qwen"],
|
|
103
|
+
args: (prompt) => ["-i", prompt],
|
|
104
|
+
install: "npm install -g @qwen-code/qwen-code"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: "roo",
|
|
109
|
+
name: "Roo Code"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: "zed",
|
|
113
|
+
name: "Zed"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
kind: "augment",
|
|
117
|
+
name: "Augment",
|
|
118
|
+
launch: {
|
|
119
|
+
commands: ["auggie"],
|
|
120
|
+
args: (prompt) => [prompt],
|
|
121
|
+
install: "npm install -g @augmentcode/auggie"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
kind: "droid",
|
|
126
|
+
name: "Factory Droid",
|
|
127
|
+
launch: {
|
|
128
|
+
commands: ["droid"],
|
|
129
|
+
args: (prompt) => [prompt],
|
|
130
|
+
install: "curl -fsSL https://app.factory.ai/cli | sh"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
kind: "crush",
|
|
135
|
+
name: "Crush"
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
/** The variables each agent exports to the shells it runs commands in. */
|
|
139
|
+
const AGENT_ENV = [
|
|
140
|
+
["claude", "CLAUDECODE"],
|
|
141
|
+
["claude", "CLAUDE_CODE"],
|
|
142
|
+
["codex", "CODEX_SANDBOX"],
|
|
143
|
+
["codex", "CODEX_SANDBOX_NETWORK_DISABLED"],
|
|
144
|
+
["codex", "CODEX_CI"],
|
|
145
|
+
["codex", "CODEX_THREAD_ID"],
|
|
146
|
+
["cursor", "CURSOR_AGENT"],
|
|
147
|
+
["gemini", "GEMINI_CLI"],
|
|
148
|
+
["qwen", "QWEN_CODE"],
|
|
149
|
+
["copilot", "COPILOT_AGENT"],
|
|
150
|
+
["copilot", "COPILOT_CLI"],
|
|
151
|
+
["opencode", "OPENCODE_CLIENT"],
|
|
152
|
+
["augment", "AUGMENT_AGENT"],
|
|
153
|
+
["junie", "JUNIE_DATA"]
|
|
154
|
+
];
|
|
155
|
+
/** `AI_AGENT` values name the agent first, such as `claude-code_2-1-3_agent` or `github-copilot`. */
|
|
156
|
+
const AI_AGENT_PREFIXES = [
|
|
157
|
+
["claude", "claude"],
|
|
158
|
+
["codex", "codex"],
|
|
159
|
+
["cursor", "cursor"],
|
|
160
|
+
["gemini", "gemini"],
|
|
161
|
+
["github-copilot", "copilot"],
|
|
162
|
+
["github_copilot", "copilot"],
|
|
163
|
+
["copilot", "copilot"],
|
|
164
|
+
["opencode", "opencode"],
|
|
165
|
+
["augment", "augment"],
|
|
166
|
+
["auggie", "augment"],
|
|
167
|
+
["qwen", "qwen"],
|
|
168
|
+
["amp", "amp"],
|
|
169
|
+
["goose", "goose"],
|
|
170
|
+
["junie", "junie"],
|
|
171
|
+
["droid", "droid"],
|
|
172
|
+
["factory", "droid"],
|
|
173
|
+
["kiro", "kiro"],
|
|
174
|
+
["cline", "cline"],
|
|
175
|
+
["crush", "crush"]
|
|
176
|
+
];
|
|
177
|
+
/** The coding agent this process runs under, from the variables each one exports to its shell. */
|
|
178
|
+
const detectAgentKind = (env = process.env) => {
|
|
179
|
+
const named = env.AI_AGENT?.trim().toLowerCase();
|
|
180
|
+
if (named) return AI_AGENT_PREFIXES.find(([prefix]) => named.startsWith(prefix) && (named.length === prefix.length || /[-_ ./0-9]/.test(named[prefix.length])))?.[1] ?? "other";
|
|
181
|
+
return AGENT_ENV.find(([, variable]) => env[variable])?.[0];
|
|
182
|
+
};
|
|
183
|
+
/** True when a person typed the command into a terminal rather than an agent running it from its shell. */
|
|
184
|
+
const isDirectInvocation = (env = process.env, stdin = process.stdin, stdout = process.stdout) => detectAgentKind(env) === void 0 && stdin.isTTY === true && stdout.isTTY === true;
|
|
185
|
+
/** The first of `commands` found on PATH, or `undefined`. */
|
|
186
|
+
const findCommand = (commands, env = process.env) => {
|
|
187
|
+
const directories = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
188
|
+
const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
189
|
+
return commands.find((command) => directories.some((directory) => extensions.some((extension) => {
|
|
190
|
+
try {
|
|
191
|
+
accessSync(join(directory, command + extension), constants.X_OK);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
})));
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Starts `agent` in this terminal with `prompt` as its first message and
|
|
200
|
+
* resolves with its exit code; `undefined` when its command is not installed.
|
|
201
|
+
*/
|
|
202
|
+
const runCodingAgent = (agent, prompt) => {
|
|
203
|
+
const launch = agent.launch;
|
|
204
|
+
if (!launch) return Promise.resolve(void 0);
|
|
205
|
+
const command = findCommand(launch.commands);
|
|
206
|
+
if (command === void 0) return Promise.resolve(void 0);
|
|
207
|
+
const child = spawn(command, launch.args(prompt), { stdio: "inherit" });
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
child.on("error", (error) => {
|
|
210
|
+
if (error.code === "ENOENT") resolve(void 0);
|
|
211
|
+
else {
|
|
212
|
+
process.stderr.write(`${error.message}\n`);
|
|
213
|
+
resolve(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
child.on("exit", (status) => resolve(status ?? 1));
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
export { CODING_AGENTS, detectAgentKind, findCommand, isDirectInvocation, runCodingAgent };
|
|
220
|
+
|
|
221
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.js","names":[],"sources":["../src/agents.ts"],"sourcesContent":["import spawn from \"cross-spawn\";\nimport { accessSync, constants } from \"node:fs\";\nimport { delimiter, join } from \"node:path\";\n\n/**\n * A coding agent a person can hand a setup to. `launch` is set when the\n * agent has a terminal command that opens an interactive session with a\n * first prompt; the others are handed the prompt to paste.\n */\nexport type CodingAgent = {\n kind: string;\n name: string;\n launch?: {\n /** Command names to try in order; the first one on PATH runs. */\n commands: readonly string[];\n args: (prompt: string) => string[];\n install: string;\n };\n};\n\n/** The coding agents people use most, in the order the picker lists them. */\nexport const CODING_AGENTS: readonly CodingAgent[] = [\n {\n kind: \"claude\",\n name: \"Claude Code\",\n launch: {\n commands: [\"claude\"],\n args: (prompt) => [prompt],\n install: \"npm install -g @anthropic-ai/claude-code\",\n },\n },\n {\n kind: \"codex\",\n name: \"Codex\",\n launch: {\n commands: [\"codex\"],\n args: (prompt) => [prompt],\n install: \"npm install -g @openai/codex\",\n },\n },\n {\n kind: \"cursor\",\n name: \"Cursor\",\n launch: {\n commands: [\"cursor-agent\", \"agent\"],\n args: (prompt) => [prompt],\n install: \"curl https://cursor.com/install -fsS | bash\",\n },\n },\n {\n kind: \"gemini\",\n name: \"Gemini CLI\",\n launch: {\n commands: [\"gemini\"],\n args: (prompt) => [\"-i\", prompt],\n install: \"npm install -g @google/gemini-cli\",\n },\n },\n {\n kind: \"copilot\",\n name: \"Copilot CLI\",\n launch: {\n commands: [\"copilot\"],\n args: (prompt) => [\"-i\", prompt],\n install: \"npm install -g @github/copilot\",\n },\n },\n { kind: \"amp\", name: \"Amp\" },\n {\n kind: \"opencode\",\n name: \"OpenCode\",\n launch: {\n commands: [\"opencode\"],\n args: (prompt) => [\"--prompt\", prompt],\n install: \"npm install -g opencode-ai\",\n },\n },\n { kind: \"cline\", name: \"Cline\" },\n { kind: \"aider\", name: \"Aider\" },\n { kind: \"goose\", name: \"Goose\" },\n { kind: \"warp\", name: \"Warp\" },\n { kind: \"windsurf\", name: \"Windsurf\" },\n {\n kind: \"kiro\",\n name: \"Kiro\",\n launch: {\n commands: [\"kiro-cli\"],\n args: (prompt) => [\"chat\", prompt],\n install: \"curl -fsSL https://cli.kiro.dev/install | bash\",\n },\n },\n { kind: \"junie\", name: \"Junie\" },\n {\n kind: \"qwen\",\n name: \"Qwen Code\",\n launch: {\n commands: [\"qwen\"],\n args: (prompt) => [\"-i\", prompt],\n install: \"npm install -g @qwen-code/qwen-code\",\n },\n },\n { kind: \"roo\", name: \"Roo Code\" },\n { kind: \"zed\", name: \"Zed\" },\n {\n kind: \"augment\",\n name: \"Augment\",\n launch: {\n commands: [\"auggie\"],\n args: (prompt) => [prompt],\n install: \"npm install -g @augmentcode/auggie\",\n },\n },\n {\n kind: \"droid\",\n name: \"Factory Droid\",\n launch: {\n commands: [\"droid\"],\n args: (prompt) => [prompt],\n install: \"curl -fsSL https://app.factory.ai/cli | sh\",\n },\n },\n { kind: \"crush\", name: \"Crush\" },\n];\n\n/** The variables each agent exports to the shells it runs commands in. */\nconst AGENT_ENV: readonly [kind: string, variable: string][] = [\n [\"claude\", \"CLAUDECODE\"],\n [\"claude\", \"CLAUDE_CODE\"],\n [\"codex\", \"CODEX_SANDBOX\"],\n [\"codex\", \"CODEX_SANDBOX_NETWORK_DISABLED\"],\n [\"codex\", \"CODEX_CI\"],\n [\"codex\", \"CODEX_THREAD_ID\"],\n [\"cursor\", \"CURSOR_AGENT\"],\n [\"gemini\", \"GEMINI_CLI\"],\n [\"qwen\", \"QWEN_CODE\"],\n [\"copilot\", \"COPILOT_AGENT\"],\n [\"copilot\", \"COPILOT_CLI\"],\n [\"opencode\", \"OPENCODE_CLIENT\"],\n [\"augment\", \"AUGMENT_AGENT\"],\n [\"junie\", \"JUNIE_DATA\"],\n];\n\n/** `AI_AGENT` values name the agent first, such as `claude-code_2-1-3_agent` or `github-copilot`. */\nconst AI_AGENT_PREFIXES: readonly [prefix: string, kind: string][] = [\n [\"claude\", \"claude\"],\n [\"codex\", \"codex\"],\n [\"cursor\", \"cursor\"],\n [\"gemini\", \"gemini\"],\n [\"github-copilot\", \"copilot\"],\n [\"github_copilot\", \"copilot\"],\n [\"copilot\", \"copilot\"],\n [\"opencode\", \"opencode\"],\n [\"augment\", \"augment\"],\n [\"auggie\", \"augment\"],\n [\"qwen\", \"qwen\"],\n [\"amp\", \"amp\"],\n [\"goose\", \"goose\"],\n [\"junie\", \"junie\"],\n [\"droid\", \"droid\"],\n [\"factory\", \"droid\"],\n [\"kiro\", \"kiro\"],\n [\"cline\", \"cline\"],\n [\"crush\", \"crush\"],\n];\n\n/** The coding agent this process runs under, from the variables each one exports to its shell. */\nexport const detectAgentKind = (\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined => {\n const named = env.AI_AGENT?.trim().toLowerCase();\n if (named) {\n // The name, or the name followed by a separator and a version or role.\n const match = AI_AGENT_PREFIXES.find(\n ([prefix]) =>\n named.startsWith(prefix) &&\n (named.length === prefix.length ||\n /[-_ ./0-9]/.test(named[prefix.length]!)),\n );\n return match?.[1] ?? \"other\";\n }\n return AGENT_ENV.find(([, variable]) => env[variable])?.[0];\n};\n\n/** True when a person typed the command into a terminal rather than an agent running it from its shell. */\nexport const isDirectInvocation = (\n env: NodeJS.ProcessEnv = process.env,\n stdin: { isTTY?: boolean | undefined } = process.stdin,\n stdout: { isTTY?: boolean | undefined } = process.stdout,\n) =>\n detectAgentKind(env) === undefined &&\n stdin.isTTY === true &&\n stdout.isTTY === true;\n\n/** The first of `commands` found on PATH, or `undefined`. */\nexport const findCommand = (\n commands: readonly string[],\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined => {\n // Empty entries (the working directory) are skipped on purpose: a project\n // must not be able to plant its own `claude` next to the user.\n const directories = (env.PATH ?? \"\").split(delimiter).filter(Boolean);\n const extensions =\n process.platform === \"win32\"\n ? (env.PATHEXT ?? \".EXE;.CMD;.BAT\").split(\";\")\n : [\"\"];\n return commands.find((command) =>\n directories.some((directory) =>\n extensions.some((extension) => {\n try {\n accessSync(join(directory, command + extension), constants.X_OK);\n return true;\n } catch {\n return false;\n }\n }),\n ),\n );\n};\n\n/**\n * Starts `agent` in this terminal with `prompt` as its first message and\n * resolves with its exit code; `undefined` when its command is not installed.\n */\nexport const runCodingAgent = (\n agent: CodingAgent,\n prompt: string,\n): Promise<number | undefined> => {\n const launch = agent.launch;\n if (!launch) return Promise.resolve(undefined);\n const command = findCommand(launch.commands);\n if (command === undefined) return Promise.resolve(undefined);\n const child = spawn(command, launch.args(prompt), { stdio: \"inherit\" });\n return new Promise((resolve) => {\n child.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"ENOENT\") resolve(undefined);\n else {\n process.stderr.write(`${error.message}\\n`);\n resolve(1);\n }\n });\n child.on(\"exit\", (status) => resolve(status ?? 1));\n });\n};\n"],"mappings":"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"}
|
|
@@ -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":"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"}
|
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
|
|
@@ -8,57 +9,32 @@ export declare const HELP: string;
|
|
|
8
9
|
* no credentials, query or fragment, and hold only address characters.
|
|
9
10
|
*/
|
|
10
11
|
export declare const parseSetupUrl: (value: string) => string;
|
|
11
|
-
export
|
|
12
|
-
|
|
12
|
+
export type AgentInstructionsOptions = {
|
|
13
|
+
/** How the agent invokes the setup commands, before the URL; `npx setup-agent` by default. */
|
|
14
|
+
command?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The agent started this setup with `assistant-ui setup`, which already
|
|
17
|
+
* began planning; there is no Begin plan click to wait for.
|
|
18
|
+
*/
|
|
19
|
+
startedByAgent?: {
|
|
20
|
+
browserUrl: string;
|
|
21
|
+
/** False when the browser was not opened for the user; the agent then hands them the link. */
|
|
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;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export declare const agentInstructions: (setupUrl: string, options?: AgentInstructionsOptions) => string;
|
|
31
|
+
export declare const INSTRUCTIONS: (setupUrl: string, options?: AgentInstructionsOptions) => string;
|
|
13
32
|
type Parsed = {
|
|
14
33
|
positional: string[];
|
|
15
34
|
flags: Map<string, string | true>;
|
|
16
35
|
};
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
/** True when a person typed the command into a terminal rather than an agent running it from its shell. */
|
|
20
|
-
export declare const isDirectInvocation: (env?: NodeJS.ProcessEnv, stdin?: {
|
|
21
|
-
isTTY?: boolean | undefined;
|
|
22
|
-
}, stdout?: {
|
|
23
|
-
isTTY?: boolean | undefined;
|
|
24
|
-
}) => boolean;
|
|
25
|
-
type StreamEvent = {
|
|
26
|
-
event: "planning.started";
|
|
27
|
-
} | {
|
|
28
|
-
event: "message.created";
|
|
29
|
-
messageId: string;
|
|
30
|
-
text: string;
|
|
31
|
-
} | {
|
|
32
|
-
event: "input.answered";
|
|
33
|
-
inputId: string;
|
|
34
|
-
prompt: string;
|
|
35
|
-
/** The entries of a multiple choice answer, else the answer text. */
|
|
36
|
-
answer: string | string[];
|
|
37
|
-
note?: string;
|
|
38
|
-
/** True when a choice answer is, or a multiple one contains, the user's own text rather than an option. */
|
|
39
|
-
custom?: true;
|
|
40
|
-
} | {
|
|
41
|
-
event: "input.dismissed";
|
|
42
|
-
inputId: string;
|
|
43
|
-
prompt: string;
|
|
44
|
-
} | {
|
|
45
|
-
event: "plan.approved";
|
|
46
|
-
revision: number;
|
|
47
|
-
} | {
|
|
48
|
-
event: "plan.changes-requested";
|
|
49
|
-
revision: number;
|
|
50
|
-
feedback: string;
|
|
51
|
-
} | {
|
|
52
|
-
event: "finished";
|
|
53
|
-
next: string;
|
|
54
|
-
} | {
|
|
55
|
-
event: "cancelled";
|
|
56
|
-
next: string;
|
|
57
|
-
} | {
|
|
58
|
-
event: "setup.created";
|
|
59
|
-
products: string[];
|
|
60
|
-
};
|
|
61
|
-
export declare const diffEvents: (before: Checkout.State, after: Checkout.State) => StreamEvent[];
|
|
36
|
+
export { CODING_AGENTS, detectAgentKind, findCommand, isDirectInvocation, runCodingAgent, } from "./agents.js";
|
|
37
|
+
export type { CodingAgent } from "./agents.js";
|
|
62
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. */
|
|
63
39
|
export declare const waitForStart: (client: Pick<CheckoutClient, "state" | "subscribe">) => Promise<Checkout.Status>;
|
|
64
40
|
export declare const askSeed: (rest: readonly string[], flags: Parsed["flags"]) => Checkout.InputSeed & {
|
|
@@ -73,6 +49,19 @@ export declare const upsertEnvLine: (content: string, key: string, value: string
|
|
|
73
49
|
* still deposited; it is read after, so lines written meanwhile survive.
|
|
74
50
|
*/
|
|
75
51
|
export declare const writeEnvSecret: (file: string, key: string, take: () => Promise<string>, projectRoot?: string) => Promise<void>;
|
|
76
|
-
export
|
|
77
|
-
|
|
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
|
+
};
|
|
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>;
|
|
78
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":"
|
|
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"}
|