@steerable/agent-cli 0.6.73 → 0.7.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/dist/approve.d.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  import type { ApprovalDecisionKind } from '@steerable/agent-client';
2
2
  export type ApprovePolicy = 'deny' | 'allow-read' | 'allow-all';
3
+ export type ApprovalSurface = 'run' | 'tui';
4
+ export interface ApprovalSpec {
5
+ policy: ApprovePolicy | 'ask';
6
+ answerAsk: boolean;
7
+ }
8
+ export declare function resolveApprovalRequest(raw: string | undefined, surface: ApprovalSurface): ApprovalSpec | null;
3
9
  export declare function parseApprovePolicy(raw: string | undefined): ApprovePolicy | null;
4
- /** Headless answer for one approval prompt. Denied read-policy calls still exit 3. */
5
- export declare function decisionFor(policy: ApprovePolicy, prompt: {
6
- toolName?: string;
7
- category?: string;
8
- }): ApprovalDecisionKind;
10
+ /**
11
+ * `mode` is the declared tool mode. A missing mode is not treated as read.
12
+ * `prompt` means the TUI should show the card.
13
+ */
14
+ export declare function decisionFor(spec: ApprovalSpec, prompt: {
15
+ mode?: string;
16
+ }, surface: ApprovalSurface): ApprovalDecisionKind | 'prompt';
17
+ export declare function unattendedAskText(): string;
18
+ export declare function autoAnswer(questions: {
19
+ id: string;
20
+ }[], text?: string): Record<string, string>;
9
21
  export declare function denied(kind: ApprovalDecisionKind): boolean;
package/dist/approve.js CHANGED
@@ -1,25 +1,44 @@
1
- const READ_TOOLS = new Set([
2
- 'local_read_file',
3
- 'local_list_scripts',
4
- 'web_search',
5
- 'web_fetch',
6
- ]);
7
- export function parseApprovePolicy(raw) {
8
- if (raw === undefined || raw === 'deny' || raw === 'allow-read' || raw === 'allow-all') {
9
- return raw ?? 'deny';
1
+ import { cliText } from './copy.js';
2
+ export function resolveApprovalRequest(raw, surface) {
3
+ if (raw === undefined) {
4
+ return surface === 'tui'
5
+ ? { policy: 'ask', answerAsk: false }
6
+ : { policy: 'deny', answerAsk: false };
10
7
  }
11
- return null;
8
+ if (raw !== 'deny' && raw !== 'allow-read' && raw !== 'allow-all')
9
+ return null;
10
+ return { policy: raw, answerAsk: raw === 'allow-all' };
11
+ }
12
+ export function parseApprovePolicy(raw) {
13
+ const spec = resolveApprovalRequest(raw, 'run');
14
+ if (!spec || spec.policy === 'ask')
15
+ return null;
16
+ return spec.policy;
12
17
  }
13
- /** Headless answer for one approval prompt. Denied read-policy calls still exit 3. */
14
- export function decisionFor(policy, prompt) {
15
- if (policy === 'allow-all')
18
+ /**
19
+ * `mode` is the declared tool mode. A missing mode is not treated as read.
20
+ * `prompt` means the TUI should show the card.
21
+ */
22
+ export function decisionFor(spec, prompt, surface) {
23
+ if (spec.policy === 'allow-all')
16
24
  return 'allow_once';
17
- const toolName = prompt.toolName ?? '';
18
- const category = prompt.category ?? '';
19
- if (policy === 'allow-read' && (category === 'read' || READ_TOOLS.has(toolName)))
25
+ if (spec.policy === 'ask')
26
+ return 'prompt';
27
+ if (spec.policy === 'allow-read' && prompt.mode === 'read')
20
28
  return 'allow_once';
29
+ if (spec.policy === 'allow-read' && surface === 'tui')
30
+ return 'prompt';
21
31
  return 'deny_once';
22
32
  }
33
+ export function unattendedAskText() {
34
+ return cliText('ask.unattended');
35
+ }
36
+ export function autoAnswer(questions, text = unattendedAskText()) {
37
+ const answers = {};
38
+ for (const question of questions)
39
+ answers[question.id] = text;
40
+ return answers;
41
+ }
23
42
  export function denied(kind) {
24
43
  return kind === 'deny_once' || kind === 'deny_for_session' || kind === 'deny_always' || kind === 'abort';
25
44
  }
package/dist/bin.js CHANGED
@@ -1,3 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { createCli } from './cli.js';
3
- process.exit(await createCli({ installSignals: true }));
2
+ import { ensureSupportedNode } from './node-version.js';
3
+ const status = ensureSupportedNode(process.versions.node, process.stderr, 'steerable-cli');
4
+ if (status !== 0)
5
+ process.exit(status);
6
+ const { createCli } = await import('./cli.js');
7
+ process.exit(await createCli({ installSignals: true, commandName: 'steerable-cli' }));
package/dist/cli.d.ts CHANGED
@@ -10,5 +10,7 @@ export interface CliOptions {
10
10
  createClient?: (options: LocalClientOptions) => Promise<AgentClient>;
11
11
  installSignals?: boolean;
12
12
  commands?: readonly CliCommandSpec[];
13
+ /** Bin name written in place of `<product>` in help and protected-directory text. */
14
+ commandName?: string;
13
15
  }
14
16
  export declare function createCli(options?: CliOptions): Promise<number>;
package/dist/cli.js CHANGED
@@ -5,9 +5,11 @@ import { restoreLogConsole, silenceLogConsole } from '@steerable/agent-shell/log
5
5
  import { collectPackCliCommands, } from '@steerable/agent-shell/host/pack-cli';
6
6
  import { getUserDataDir } from '@steerable/agent-shell/runtime';
7
7
  import { applyHostRuntimeEnv, createLocalClient, listBusyChatIds, } from '@steerable/agent-client';
8
- import { parseApprovePolicy } from './approve.js';
8
+ import { nodeDoctorLine } from './node-version.js';
9
+ import { parseApprovePolicy, resolveApprovalRequest } from './approve.js';
10
+ import { defaultProjectFs, projectStatusLabel, protectedTuiMessage, resolveProjectRequest, } from './project-binding.js';
9
11
  import { chatCommand, configCommand, mcpCommand, skillsCommand } from './commands.js';
10
- import { chatHelp, configHelp, doctorHelp, mcpHelp, rootHelp, runHelp, skillsHelp, tuiHelp } from './help.js';
12
+ import { chatHelp, configHelp, doctorHelp, mcpHelp, renderCommandName, rootHelp, runHelp, skillsHelp, tuiHelp } from './help.js';
11
13
  import { runTurn } from './run.js';
12
14
  import { runTui } from './tui/run.js';
13
15
  export async function createCli(options = {}) {
@@ -47,6 +49,9 @@ async function runCli(options) {
47
49
  json: { type: 'boolean' },
48
50
  'stream-json': { type: 'boolean' },
49
51
  cwd: { type: 'string' },
52
+ project: { type: 'string' },
53
+ 'no-project': { type: 'boolean', default: false },
54
+ trust: { type: 'boolean', default: false },
50
55
  timeout: { type: 'string' },
51
56
  'data-dir': { type: 'string' },
52
57
  agent: { type: 'string' },
@@ -56,12 +61,12 @@ async function runCli(options) {
56
61
  });
57
62
  }
58
63
  catch (error) {
59
- stderr.write(`${error instanceof Error ? error.message : String(error)}\n${rootHelp(packCommands)}\n`);
64
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n${present(options, rootHelp(packCommands))}\n`);
60
65
  return 2;
61
66
  }
62
67
  const command = parsed.positionals[0];
63
68
  if (parsed.values.help && !command) {
64
- stdout.write(`${rootHelp(packCommands)}\n`);
69
+ stdout.write(`${present(options, rootHelp(packCommands))}\n`);
65
70
  return 0;
66
71
  }
67
72
  if (!command)
@@ -69,13 +74,13 @@ async function runCli(options) {
69
74
  if (command === 'run')
70
75
  return runCommand(parsed, options, stdout, stderr);
71
76
  if (command === 'chat')
72
- return dispatch(parsed, options, stdout, stderr, chatHelp(), chatCommand);
77
+ return dispatch(parsed, options, stdout, stderr, present(options, chatHelp()), chatCommand);
73
78
  if (command === 'skills')
74
- return dispatch(parsed, options, stdout, stderr, skillsHelp(), skillsCommand);
79
+ return dispatch(parsed, options, stdout, stderr, present(options, skillsHelp()), skillsCommand);
75
80
  if (command === 'mcp')
76
- return dispatch(parsed, options, stdout, stderr, mcpHelp(), mcpCommand);
81
+ return dispatch(parsed, options, stdout, stderr, present(options, mcpHelp()), mcpCommand);
77
82
  if (command === 'config')
78
- return dispatch(parsed, options, stdout, stderr, configHelp(), configCommand);
83
+ return dispatch(parsed, options, stdout, stderr, present(options, configHelp()), configCommand);
79
84
  if (command === 'doctor')
80
85
  return doctorCommand(parsed, options, stdout, stderr);
81
86
  if (command === 'tui')
@@ -83,56 +88,68 @@ async function runCli(options) {
83
88
  const pack = packCommands.find((entry) => entry.name === command);
84
89
  if (pack)
85
90
  return runPackCommand(pack, parsed, options, stdout, stderr);
86
- stderr.write(`unknown command: ${command}\n${rootHelp(packCommands)}\n`);
91
+ stderr.write(`unknown command: ${command}\n${present(options, rootHelp(packCommands))}\n`);
87
92
  return 2;
88
93
  }
89
94
  async function runPackCommand(command, parsed, options, stdout, stderr) {
90
95
  if (parsed.values.help) {
91
- stdout.write(`<product> ${command.name}\n${command.summary}\n`);
96
+ stdout.write(present(options, `<product> ${command.name}\n${command.summary}\n`));
92
97
  return 0;
93
98
  }
94
99
  return withClient(parsed, options, async (client) => command.run(parsed.positionals.slice(1), {
95
- write: (text) => stdout.write(text),
96
- writeError: (text) => stderr.write(text),
100
+ write: (text) => stdout.write(present(options, text)),
101
+ writeError: (text) => stderr.write(present(options, text)),
97
102
  request: (method, requestPath, body) => client.request(method, requestPath, body),
98
103
  }), stderr);
99
104
  }
100
105
  async function runCommand(parsed, options, stdout, stderr) {
101
106
  if (parsed.values.help) {
102
- stdout.write(`${runHelp()}\n`);
107
+ stdout.write(`${present(options, runHelp())}\n`);
103
108
  return 0;
104
109
  }
105
110
  const policy = parseApprovePolicy(parsed.values.approve);
106
111
  if (!policy) {
107
- stderr.write(`unknown --approve value\n${runHelp()}\n`);
112
+ stderr.write(`unknown --approve value\n${present(options, runHelp())}\n`);
108
113
  return 2;
109
114
  }
110
115
  let timeoutMs;
111
116
  if (typeof parsed.values.timeout === 'string') {
112
117
  const parsedTimeout = parseTimeout(parsed.values.timeout);
113
118
  if (parsedTimeout === null) {
114
- stderr.write(`invalid --timeout\n${runHelp()}\n`);
119
+ stderr.write(`invalid --timeout\n${present(options, runHelp())}\n`);
115
120
  return 2;
116
121
  }
117
122
  timeoutMs = parsedTimeout;
118
123
  }
119
124
  const task = await readTask(parsed.positionals.slice(1), options);
120
125
  if (!task.trim()) {
121
- stderr.write(`missing task\n${runHelp()}\n`);
126
+ stderr.write(`missing task\n${present(options, runHelp())}\n`);
122
127
  return 2;
123
128
  }
124
129
  const request = {
125
130
  task: task.trim(),
126
131
  ...(typeof parsed.values.chat === 'string' ? { chatId: parsed.values.chat } : {}),
127
132
  ...(typeof parsed.values.agent === 'string' ? { agentId: parsed.values.agent } : {}),
128
- ...(typeof parsed.values.cwd === 'string' ? { cwd: parsed.values.cwd } : {}),
129
133
  files: parsed.values.file ?? [],
130
134
  policy,
131
135
  json: parsed.values.json === true,
132
136
  streamJson: parsed.values['stream-json'] === true,
133
137
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
134
138
  };
135
- return withClient(parsed, options, async (client, signal) => runTurn(client, request, stdout, stderr, signal));
139
+ const spec = projectSpec(parsed);
140
+ if (spec.kind === 'error') {
141
+ stderr.write(`${present(options, spec.message)}\n`);
142
+ return spec.code;
143
+ }
144
+ return withClient(parsed, options, async (client, signal) => {
145
+ const bound = await bindAndTrust(client, parsed, spec, stderr);
146
+ if (typeof bound === 'number')
147
+ return bound;
148
+ return runTurn(client, {
149
+ ...request,
150
+ ...(bound ? { projectId: bound.id } : {}),
151
+ }, stdout, stderr, signal);
152
+ });
136
153
  }
137
154
  function commandIO(parsed) {
138
155
  return {
@@ -147,29 +164,54 @@ async function dispatch(parsed, options, stdout, stderr, help, command) {
147
164
  }
148
165
  async function tuiCommand(parsed, options, stdout, stderr) {
149
166
  if (parsed.values.help) {
150
- stdout.write(`${tuiHelp()}\n`);
167
+ stdout.write(`${present(options, tuiHelp())}\n`);
151
168
  return 0;
152
169
  }
170
+ const approval = resolveApprovalRequest(typeof parsed.values.approve === 'string' ? parsed.values.approve : undefined, 'tui');
171
+ if (!approval) {
172
+ stderr.write(`unknown --approve value\n${present(options, tuiHelp())}\n`);
173
+ return 2;
174
+ }
153
175
  const tty = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
154
176
  if (!tty) {
155
- stderr.write(`tui needs a terminal. Use run for piped input.\n${tuiHelp()}\n`);
177
+ stderr.write(`tui needs a terminal. Use run for piped input.\n${present(options, tuiHelp())}\n`);
156
178
  return 2;
157
179
  }
180
+ const spec = projectSpec(parsed);
181
+ if (spec.kind === 'error') {
182
+ if (spec.reason === 'protected' && spec.protectedKind) {
183
+ stderr.write(`${present(options, protectedTuiMessage(spec.protectedKind))}\n`);
184
+ stderr.write('q 退出\n');
185
+ await waitForQuit(options.stdin ?? process.stdin);
186
+ }
187
+ else {
188
+ stderr.write(`${present(options, spec.message)}\n`);
189
+ }
190
+ return spec.code;
191
+ }
158
192
  if ((options.stdout ?? process.stdout) === process.stdout)
159
193
  silenceLogConsole('all');
160
- return withClient(parsed, options, async (client) => runTui({
161
- client,
162
- product: getBrand().displayName,
163
- dataDir: dataDirFrom(parsed, options.env ?? process.env),
164
- }), stderr);
194
+ return withClient(parsed, options, async (client) => {
195
+ const bound = await bindAndTrust(client, parsed, spec, stderr);
196
+ if (typeof bound === 'number')
197
+ return bound;
198
+ return runTui({
199
+ client,
200
+ product: getBrand().displayName,
201
+ dataDir: dataDirFrom(parsed, options.env ?? process.env),
202
+ ...(bound ? { projectId: bound.id, projectLabel: bound.label } : { projectLabel: projectStatusLabel(null) }),
203
+ approvalPolicy: approval,
204
+ });
205
+ }, stderr);
165
206
  }
166
207
  async function doctorCommand(parsed, options, stdout, stderr) {
167
208
  if (parsed.values.help) {
168
- stdout.write(`${doctorHelp()}\n`);
209
+ stdout.write(`${present(options, doctorHelp())}\n`);
169
210
  return 0;
170
211
  }
171
212
  const dataDir = dataDirFrom(parsed, options.env ?? process.env);
172
213
  const report = applyHostRuntimeEnv(dataDir, options.env ?? process.env);
214
+ stdout.write(nodeDoctorLine(process.versions.node));
173
215
  stdout.write(`data dir: ${dataDir}\n`);
174
216
  stdout.write(`log file: ${path.join(dataDir, 'logs', 'main.log')}\n`);
175
217
  stdout.write(`runtime file: ${report.fileFound ? report.filePath : 'missing'}\n`);
@@ -230,6 +272,64 @@ async function withClient(parsed, options, body, stderr = options.stderr ?? proc
230
272
  await client?.close();
231
273
  }
232
274
  }
275
+ function projectSpec(parsed) {
276
+ const fs = defaultProjectFs();
277
+ return resolveProjectRequest({
278
+ cwd: process.cwd(),
279
+ ...(typeof parsed.values.project === 'string' ? { projectArg: parsed.values.project } : {}),
280
+ ...(typeof parsed.values.cwd === 'string' ? { cwdArg: parsed.values.cwd } : {}),
281
+ noProject: parsed.values['no-project'] === true,
282
+ ...fs,
283
+ });
284
+ }
285
+ async function bindProject(client, spec, stderr) {
286
+ if (spec.kind === 'none')
287
+ return null;
288
+ if (spec.kind === 'error')
289
+ return spec.code;
290
+ const response = await client.request('POST', '/api/v2/projects/resolve', {
291
+ folderPath: spec.dir,
292
+ origin: 'cli',
293
+ });
294
+ const data = response.data;
295
+ if (response.status === 403) {
296
+ stderr.write('该产品未启用项目模式,请使用 --no-project。\n');
297
+ return 2;
298
+ }
299
+ const project = data.project;
300
+ if (response.status !== 200 || !project?.id || !project.name || !project.folderPath) {
301
+ stderr.write(`${data.error ?? `failed to bind project (${response.status})`}\n`);
302
+ return 2;
303
+ }
304
+ if (data.created)
305
+ stderr.write(`已绑定项目 ${project.name}:${project.folderPath}\n`);
306
+ return {
307
+ id: project.id,
308
+ label: projectStatusLabel({ name: project.name, folderPath: project.folderPath }),
309
+ };
310
+ }
311
+ async function bindAndTrust(client, parsed, spec, stderr) {
312
+ if (parsed.values.trust === true && spec.kind === 'none') {
313
+ stderr.write('--trust 需要绑定项目,不能与 --no-project 同用。\n');
314
+ return 2;
315
+ }
316
+ const bound = await bindProject(client, spec, stderr);
317
+ if (typeof bound === 'number' || !bound || parsed.values.trust !== true)
318
+ return bound;
319
+ const trusted = await client.request('PUT', `/api/v2/projects/${encodeURIComponent(bound.id)}/trust`, { trusted: true });
320
+ if (trusted.status !== 200) {
321
+ const data = trusted.data;
322
+ stderr.write(`${data.error ?? `failed to trust project (${trusted.status})`}\n`);
323
+ return 1;
324
+ }
325
+ return bound;
326
+ }
327
+ async function waitForQuit(stdin) {
328
+ for await (const chunk of stdin) {
329
+ if (String(chunk).includes('q'))
330
+ return;
331
+ }
332
+ }
233
333
  function dataDirFrom(parsed, env) {
234
334
  if (typeof parsed.values['data-dir'] === 'string')
235
335
  return parsed.values['data-dir'];
@@ -237,6 +337,9 @@ function dataDirFrom(parsed, env) {
237
337
  return env.DEEPPATH_USER_DATA_DIR;
238
338
  return getUserDataDir();
239
339
  }
340
+ function present(options, text) {
341
+ return renderCommandName(text, options.commandName);
342
+ }
240
343
  function parseTimeout(raw) {
241
344
  const match = /^(\d+)(ms|s|m)?$/.exec(raw.trim());
242
345
  if (!match)
package/dist/copy.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Command-line copy. Product packs do not override these strings.
3
+ */
4
+ export declare const cliCopy: {
5
+ readonly 'ask.unattended': "无人可答,请按最合理的默认继续。";
6
+ readonly 'cli.approveAllWarning': "警告:--approve allow-all 会放行本次运行中的全部审批";
7
+ readonly 'tui.approvalClosed': "审批已关闭";
8
+ readonly 'tui.approvalRead': "只读自动放行";
9
+ readonly 'tui.toolAuto': "自动";
10
+ readonly 'tui.toolAutoAnswer': "自动回答";
11
+ readonly 'tui.toolSessionAllow': "本会话已允许";
12
+ };
13
+ export type CliCopyKey = keyof typeof cliCopy;
14
+ /** @param key Dictionary key. @returns The command-line string for that key. */
15
+ export declare function cliText(key: CliCopyKey): string;
package/dist/copy.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Command-line copy. Product packs do not override these strings.
3
+ */
4
+ export const cliCopy = {
5
+ 'ask.unattended': '无人可答,请按最合理的默认继续。',
6
+ 'cli.approveAllWarning': '警告:--approve allow-all 会放行本次运行中的全部审批',
7
+ 'tui.approvalClosed': '审批已关闭',
8
+ 'tui.approvalRead': '只读自动放行',
9
+ 'tui.toolAuto': '自动',
10
+ 'tui.toolAutoAnswer': '自动回答',
11
+ 'tui.toolSessionAllow': '本会话已允许',
12
+ };
13
+ /** @param key Dictionary key. @returns The command-line string for that key. */
14
+ export function cliText(key) {
15
+ return cliCopy[key];
16
+ }
package/dist/help.d.ts CHANGED
@@ -9,3 +9,14 @@ export declare function mcpHelp(): string;
9
9
  export declare function configHelp(): string;
10
10
  export declare function tuiHelp(): string;
11
11
  export declare function doctorHelp(): string;
12
+ /** Placeholder left in help text until a product bin supplies its command name. */
13
+ export declare const COMMAND_NAME_TOKEN = "<product>";
14
+ /**
15
+ * Replaces {@link COMMAND_NAME_TOKEN} with the running bin name.
16
+ * A blank name, the token itself, or a name that is not a command token is left unchanged.
17
+ *
18
+ * @param text Help or protected-directory text.
19
+ * @param commandName Bin name such as `aroli` or `steerable-cli`.
20
+ * @returns Text with the placeholder replaced when the name is usable.
21
+ */
22
+ export declare function renderCommandName(text: string, commandName: string | undefined): string;
package/dist/help.js CHANGED
@@ -19,8 +19,8 @@ export function rootHelp(commands = []) {
19
19
  return `
20
20
  Usage:
21
21
  <product> run "<task>" [--chat <id>] [--approve deny|allow-read|allow-all]
22
- [--json | --stream-json] [--cwd <dir>] [--timeout <dur>]
23
- [--data-dir <dir>] [--agent <id>] [--file <path>...]
22
+ [--json | --stream-json] [--project <dir> | --cwd <dir> | --no-project] [--trust]
23
+ [--timeout <dur>] [--data-dir <dir>] [--agent <id>] [--file <path>...]
24
24
  <product> chat list | show <id> | rm <id> | export <id> [--format md|json]
25
25
  <product> skills list
26
26
  <product> mcp list | add <name> -- <cmd...> | rm <name>
@@ -35,9 +35,15 @@ export function runHelp() {
35
35
  return `
36
36
  Usage:
37
37
  <product> run "<task>" [--chat <id>] [--approve deny|allow-read|allow-all]
38
- [--json | --stream-json] [--data-dir <dir>]
38
+ [--json | --stream-json] [--project <dir> | --cwd <dir> | --no-project] [--trust]
39
+ [--data-dir <dir>]
39
40
 
40
41
  --approve deny is the default. A denied approval exits 3. A busy chat exits 6.
42
+ run binds the current directory as a project. --project and --cwd name that directory.
43
+ --no-project keeps a workspace per chat.
44
+ --trust marks that project trusted and loads its rule files. It cannot be combined with --no-project.
45
+ STEERABLE_APPROVAL=0 turns the approval layer off for this process. A destructive tool
46
+ is then rejected by the tool router. Use --approve allow-all to allow those tools instead.
41
47
 
42
48
  ${EXAMPLES}
43
49
  `.trim();
@@ -96,9 +102,15 @@ Examples:
96
102
  export function tuiHelp() {
97
103
  return `
98
104
  Usage:
99
- <product> tui [--data-dir <dir>]
105
+ <product> tui [--project <dir> | --cwd <dir> | --no-project] [--trust]
106
+ [--approve deny|allow-read|allow-all] [--data-dir <dir>]
100
107
 
101
- Starts the terminal interface. When stdin is not a terminal, use run instead.
108
+ Starts the terminal interface in the current directory's project.
109
+ --no-project keeps a workspace per chat.
110
+ --trust marks that project trusted and loads its rule files. It cannot be combined with --no-project.
111
+ STEERABLE_APPROVAL=0 turns the approval layer off for this process. A destructive tool
112
+ is then rejected by the tool router. Use --approve allow-all to allow those tools instead.
113
+ When stdin is not a terminal, use run instead.
102
114
 
103
115
  /new /model /clear /help
104
116
  Ctrl+C interrupts the current turn. Ctrl+L lists chats.
@@ -116,3 +128,21 @@ Examples:
116
128
  <product> doctor
117
129
  `.trim();
118
130
  }
131
+ /** Placeholder left in help text until a product bin supplies its command name. */
132
+ export const COMMAND_NAME_TOKEN = '<product>';
133
+ /**
134
+ * Replaces {@link COMMAND_NAME_TOKEN} with the running bin name.
135
+ * A blank name, the token itself, or a name that is not a command token is left unchanged.
136
+ *
137
+ * @param text Help or protected-directory text.
138
+ * @param commandName Bin name such as `aroli` or `steerable-cli`.
139
+ * @returns Text with the placeholder replaced when the name is usable.
140
+ */
141
+ export function renderCommandName(text, commandName) {
142
+ const name = commandName?.trim() ?? '';
143
+ if (!name || name === COMMAND_NAME_TOKEN)
144
+ return text;
145
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
146
+ return text;
147
+ return text.replaceAll(COMMAND_NAME_TOKEN, name);
148
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Minimum Node for the command line.
3
+ *
4
+ * `@opentui/core` 0.5.12 declares `engines.node` `>=26.4.0`. Its FFI packages
5
+ * and the rest of the agent-cli dependency tree do not declare a higher
6
+ * minimum. This module imports nothing: the bin loads it before the CLI.
7
+ */
8
+ export declare const REQUIRED_NODE = "26.4.0";
9
+ export interface NodeVersionWriter {
10
+ write(chunk: string): unknown;
11
+ }
12
+ /** @param current Installed version, with or without a leading `v`. @param required `major.minor.patch`. @returns Whether `current` is at least `required`. */
13
+ export declare function satisfies(current: string, required: string): boolean;
14
+ /** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `aroli`. @returns `0` when the version is new enough, otherwise `1`. */
15
+ export declare function ensureSupportedNode(current: string, stderr: NodeVersionWriter, product: string): number;
16
+ /** @param current Installed version. @returns One doctor row, including whether it meets {@link REQUIRED_NODE}. */
17
+ export declare function nodeDoctorLine(current: string): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Minimum Node for the command line.
3
+ *
4
+ * `@opentui/core` 0.5.12 declares `engines.node` `>=26.4.0`. Its FFI packages
5
+ * and the rest of the agent-cli dependency tree do not declare a higher
6
+ * minimum. This module imports nothing: the bin loads it before the CLI.
7
+ */
8
+ export const REQUIRED_NODE = '26.4.0';
9
+ /** @param current Installed version, with or without a leading `v`. @param required `major.minor.patch`. @returns Whether `current` is at least `required`. */
10
+ export function satisfies(current, required) {
11
+ const left = readVersion(current);
12
+ const right = readVersion(required);
13
+ if (!left || !right)
14
+ return false;
15
+ for (let index = 0; index < 3; index += 1) {
16
+ if (left[index] !== right[index])
17
+ return left[index] > right[index];
18
+ }
19
+ return true;
20
+ }
21
+ /** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `aroli`. @returns `0` when the version is new enough, otherwise `1`. */
22
+ export function ensureSupportedNode(current, stderr, product) {
23
+ if (satisfies(current, REQUIRED_NODE))
24
+ return 0;
25
+ const shown = current.replace(/^v/, '');
26
+ stderr.write(`${product} 需要 Node.js ${REQUIRED_NODE} 或更高版本,当前为 ${shown}。\n升级方法:https://nodejs.org/\n`);
27
+ return 1;
28
+ }
29
+ /** @param current Installed version. @returns One doctor row, including whether it meets {@link REQUIRED_NODE}. */
30
+ export function nodeDoctorLine(current) {
31
+ const shown = current.replace(/^v/, '');
32
+ const mark = satisfies(current, REQUIRED_NODE) ? '✓' : '✗';
33
+ return `Node.js: ${shown}(需要 ≥ ${REQUIRED_NODE}) ${mark}\n`;
34
+ }
35
+ function readVersion(value) {
36
+ const match = /v?(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
37
+ if (!match)
38
+ return null;
39
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
40
+ }
@@ -0,0 +1,35 @@
1
+ export interface ProjectRequest {
2
+ cwd: string;
3
+ projectArg?: string;
4
+ cwdArg?: string;
5
+ noProject: boolean;
6
+ home: string;
7
+ platform: NodeJS.Platform;
8
+ /** Windows `%SystemRoot%`. Other platforms ignore it. */
9
+ systemRoot?: string;
10
+ realpath: (target: string) => string;
11
+ isDirectory: (target: string) => boolean;
12
+ }
13
+ export type ProtectedDir = 'home' | 'root' | 'system';
14
+ export type ProjectSpec = {
15
+ kind: 'none';
16
+ } | {
17
+ kind: 'bind';
18
+ dir: string;
19
+ explicit: boolean;
20
+ } | {
21
+ kind: 'error';
22
+ code: 2;
23
+ message: string;
24
+ reason?: 'protected';
25
+ protectedKind?: ProtectedDir;
26
+ };
27
+ export declare function resolveProjectRequest(request: ProjectRequest): ProjectSpec;
28
+ export declare function shortenProjectPath(folder: string): string;
29
+ export declare function projectStatusLabel(project: {
30
+ name: string;
31
+ folderPath: string;
32
+ } | null): string;
33
+ export declare function protectedRunMessage(kind: ProtectedDir): string;
34
+ export declare function protectedTuiMessage(kind: ProtectedDir): string;
35
+ export declare function defaultProjectFs(): Pick<ProjectRequest, 'realpath' | 'isDirectory' | 'home' | 'platform' | 'systemRoot'>;