@autopilot-agent/cli 0.0.0-stage → 0.2.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 +52 -2
- package/dist/activity-animation.d.ts +59 -0
- package/dist/activity-animation.js +148 -0
- package/dist/animation-clock.d.ts +22 -0
- package/dist/animation-clock.js +76 -0
- package/dist/composition-root.d.ts +105 -0
- package/dist/composition-root.js +600 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +173 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +741 -0
- package/dist/design-tokens.d.ts +38 -0
- package/dist/design-tokens.js +38 -0
- package/dist/error-diagnostics.d.ts +8 -0
- package/dist/error-diagnostics.js +62 -0
- package/dist/event-projector.d.ts +26 -0
- package/dist/event-projector.js +335 -0
- package/dist/file-mention-context.d.ts +34 -0
- package/dist/file-mention-context.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +40 -0
- package/dist/main.js +543 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.js +400 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +62 -0
- package/dist/scroll-viewport.d.ts +22 -0
- package/dist/scroll-viewport.js +62 -0
- package/dist/termination-format.d.ts +4 -0
- package/dist/termination-format.js +18 -0
- package/dist/timeline-lines.d.ts +38 -0
- package/dist/timeline-lines.js +104 -0
- package/dist/tui/app.d.ts +30 -0
- package/dist/tui/app.js +413 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +41 -0
- package/dist/tui/brand/brand-header.d.ts +20 -0
- package/dist/tui/brand/brand-header.js +22 -0
- package/dist/tui/brand/intro.d.ts +12 -0
- package/dist/tui/brand/intro.js +88 -0
- package/dist/tui/brand/pixel.d.ts +8 -0
- package/dist/tui/brand/pixel.js +50 -0
- package/dist/tui/constants.d.ts +56 -0
- package/dist/tui/constants.js +57 -0
- package/dist/tui/context.d.ts +11 -0
- package/dist/tui/context.js +12 -0
- package/dist/tui/diff-stats.d.ts +8 -0
- package/dist/tui/diff-stats.js +7 -0
- package/dist/tui/format.d.ts +75 -0
- package/dist/tui/format.js +178 -0
- package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
- package/dist/tui/hooks/use-file-mention-index.js +59 -0
- package/dist/tui/hooks/use-pickers.d.ts +47 -0
- package/dist/tui/hooks/use-pickers.js +288 -0
- package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
- package/dist/tui/hooks/use-prompt-input.js +258 -0
- package/dist/tui/hooks/use-status-view.d.ts +8 -0
- package/dist/tui/hooks/use-status-view.js +13 -0
- package/dist/tui/hooks/use-viewport.d.ts +22 -0
- package/dist/tui/hooks/use-viewport.js +117 -0
- package/dist/tui/input/file-mention.d.ts +33 -0
- package/dist/tui/input/file-mention.js +112 -0
- package/dist/tui/input/graphemes.d.ts +5 -0
- package/dist/tui/input/graphemes.js +10 -0
- package/dist/tui/input/keys.d.ts +16 -0
- package/dist/tui/input/keys.js +58 -0
- package/dist/tui/input/prompt-layout.d.ts +27 -0
- package/dist/tui/input/prompt-layout.js +84 -0
- package/dist/tui/input-area.d.ts +53 -0
- package/dist/tui/input-area.js +89 -0
- package/dist/tui/jobs.d.ts +10 -0
- package/dist/tui/jobs.js +24 -0
- package/dist/tui/lifecycle.d.ts +23 -0
- package/dist/tui/lifecycle.js +114 -0
- package/dist/tui/markdown/diff.d.ts +8 -0
- package/dist/tui/markdown/diff.js +19 -0
- package/dist/tui/markdown/inline.d.ts +4 -0
- package/dist/tui/markdown/inline.js +32 -0
- package/dist/tui/markdown/markdown.d.ts +17 -0
- package/dist/tui/markdown/markdown.js +117 -0
- package/dist/tui/markdown/table.d.ts +19 -0
- package/dist/tui/markdown/table.js +74 -0
- package/dist/tui/performance-diagnostics.d.ts +7 -0
- package/dist/tui/performance-diagnostics.js +57 -0
- package/dist/tui/pickers/effort.d.ts +10 -0
- package/dist/tui/pickers/effort.js +12 -0
- package/dist/tui/pickers/file-mention.d.ts +11 -0
- package/dist/tui/pickers/file-mention.js +22 -0
- package/dist/tui/pickers/model.d.ts +10 -0
- package/dist/tui/pickers/model.js +12 -0
- package/dist/tui/pickers/permission.d.ts +10 -0
- package/dist/tui/pickers/permission.js +13 -0
- package/dist/tui/pickers/skill.d.ts +13 -0
- package/dist/tui/pickers/skill.js +25 -0
- package/dist/tui/pickers/slash.d.ts +10 -0
- package/dist/tui/pickers/slash.js +11 -0
- package/dist/tui/prompt-input.d.ts +11 -0
- package/dist/tui/prompt-input.js +73 -0
- package/dist/tui/render-mode.d.ts +20 -0
- package/dist/tui/render-mode.js +31 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +237 -0
- package/dist/tui/status-line.d.ts +16 -0
- package/dist/tui/status-line.js +37 -0
- package/dist/tui/store.d.ts +4 -0
- package/dist/tui/store.js +380 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +153 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +15 -0
- package/dist/tui/timeline/presentation-timeline.js +128 -0
- package/dist/tui/timeline/progress.d.ts +12 -0
- package/dist/tui/timeline/progress.js +43 -0
- package/dist/tui/timeline/static-timeline.d.ts +13 -0
- package/dist/tui/timeline/static-timeline.js +131 -0
- package/dist/tui/timeline/tool-tree.d.ts +7 -0
- package/dist/tui/timeline/tool-tree.js +25 -0
- package/dist/tui/timeline/turn-items.d.ts +12 -0
- package/dist/tui/timeline/turn-items.js +28 -0
- package/dist/tui/timeline/turn.d.ts +19 -0
- package/dist/tui/timeline/turn.js +21 -0
- package/dist/tui/todo-list.d.ts +50 -0
- package/dist/tui/todo-list.js +120 -0
- package/dist/tui/transcript.d.ts +24 -0
- package/dist/tui/transcript.js +34 -0
- package/dist/tui/trust-dialog.d.ts +51 -0
- package/dist/tui/trust-dialog.js +127 -0
- package/dist/tui/types.d.ts +189 -0
- package/dist/tui/types.js +5 -0
- package/dist/tui/viewport/selection.d.ts +27 -0
- package/dist/tui/viewport/selection.js +79 -0
- package/dist/tui/viewport/sources.d.ts +29 -0
- package/dist/tui/viewport/sources.js +592 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +91 -0
- package/dist/tui-input-history.d.ts +23 -0
- package/dist/tui-input-history.js +42 -0
- package/dist/tui-input-router.d.ts +24 -0
- package/dist/tui-input-router.js +115 -0
- package/dist/tui.d.ts +15 -0
- package/dist/tui.js +13 -0
- package/dist/types.d.ts +467 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
package/dist/main.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent CLI 的 Step 5.3 运行入口。
|
|
4
|
+
*
|
|
5
|
+
* 入口只负责 argv、Composition Root、Plain Controller 和进程生命周期的编排;
|
|
6
|
+
* Session、Turn、审批、恢复和持久化规则全部委托给 Core Runtime。
|
|
7
|
+
*/
|
|
8
|
+
import { WEB_SEARCH_DATA_FLOW_NOTICE } from "@autopilot-agent/presentation";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import readline from "node:readline";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { AgentRuntimeError, WorkspaceTrustError, } from "@autopilot-agent/core";
|
|
14
|
+
import { createCliCompositionRoot } from "./composition-root.js";
|
|
15
|
+
import { createCliController } from "./controller.js";
|
|
16
|
+
import { classifyCliError } from "./error-diagnostics.js";
|
|
17
|
+
import { parseArgv, parseSlashAction, renderHelp } from "./parser.js";
|
|
18
|
+
import { createPlainRenderer } from "./renderer.js";
|
|
19
|
+
import { runCliTui, runCliTrustDialog } from "./tui.js";
|
|
20
|
+
/** 将宿主信号映射为固定 CLI 退出码。 */
|
|
21
|
+
export function exitCodeForSignal(signal) {
|
|
22
|
+
return signal === "SIGINT" ? 130 : 143;
|
|
23
|
+
}
|
|
24
|
+
/** 将控制器结果写入约定的 stdout/stderr 通道。 */
|
|
25
|
+
function writeControllerResult(result, streams) {
|
|
26
|
+
const target = result.kind === "error" || result.kind === "unavailable" ? streams.stderr : streams.stdout;
|
|
27
|
+
for (const line of result.lines)
|
|
28
|
+
target.write(`${line}\n`);
|
|
29
|
+
if (result.lines.length === 0 && result.message)
|
|
30
|
+
target.write(`${result.message}\n`);
|
|
31
|
+
}
|
|
32
|
+
/** 将 Runtime/Trust 异常收窄为不泄露绝对路径、凭据或原始输入的诊断。 */
|
|
33
|
+
function formatCliError(error) {
|
|
34
|
+
const classification = classifyCliError(error);
|
|
35
|
+
if (classification.category === "config_directory")
|
|
36
|
+
return classification.code;
|
|
37
|
+
if (error instanceof WorkspaceTrustError)
|
|
38
|
+
return `WORKSPACE_TRUST_REQUIRED: ${error.status}`;
|
|
39
|
+
if (error instanceof AgentRuntimeError)
|
|
40
|
+
return `${error.code}: ${sanitizeErrorMessage(error.message)}`;
|
|
41
|
+
return "CLI_RUNTIME_ERROR";
|
|
42
|
+
}
|
|
43
|
+
/** 对错误摘要做长度和常见敏感字段脱敏。 */
|
|
44
|
+
function sanitizeErrorMessage(message) {
|
|
45
|
+
return message
|
|
46
|
+
.slice(0, 500)
|
|
47
|
+
.replace(/(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|private|tmp|var|mnt|workspace|Volumes)\/)[^\s"'`]+/gu, "[path]")
|
|
48
|
+
.replace(/\b(api[_-]?key|access[_-]?token|password|secret|dsn)\s*[:=]\s*[^\s,;]+/giu, "$1=[redacted]");
|
|
49
|
+
}
|
|
50
|
+
/** 输出管理命令的单行安全结果;保留 JSON 可解析性并统一遮蔽凭据和绝对路径。 */
|
|
51
|
+
function writeManagementResult(streams, value) {
|
|
52
|
+
const safe = sanitizeManagementValue(value);
|
|
53
|
+
streams.stdout.write(`${typeof safe === "string" ? safe : JSON.stringify(safe)}\n`);
|
|
54
|
+
}
|
|
55
|
+
/** 在序列化前逐字段限制字符串并脱敏,保证长数组仍是完整有效 JSON。 */
|
|
56
|
+
function sanitizeManagementValue(value) {
|
|
57
|
+
if (typeof value === "string")
|
|
58
|
+
return sanitizeManagementString(value);
|
|
59
|
+
if (Array.isArray(value))
|
|
60
|
+
return value.map(sanitizeManagementValue);
|
|
61
|
+
if (typeof value === "object" && value !== null) {
|
|
62
|
+
return Object.fromEntries(Object.entries(value).map(function sanitizeEntry(entry) {
|
|
63
|
+
return [entry[0], sanitizeManagementValue(entry[1])];
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
/** 管理字段先做独立长度限制与二次脱敏,不截断已经序列化的 JSON。 */
|
|
69
|
+
function sanitizeManagementString(value) {
|
|
70
|
+
return value
|
|
71
|
+
.slice(0, 16 * 1024)
|
|
72
|
+
.replace(/(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|private|tmp|var|mnt|workspace|Volumes)\/)[^\s"'`]+/gu, "[path]")
|
|
73
|
+
.replace(/\b(api[_-]?key|access[_-]?token|password|secret|dsn)\s*[:=]\s*[^\s,;]+/giu, "$1=[redacted]");
|
|
74
|
+
}
|
|
75
|
+
/** 将 Core 安全 JobView 补充独立命令所需身份,不读取命令、路径、PID 或 owner。 */
|
|
76
|
+
function summarizeJob(job, view) {
|
|
77
|
+
return {
|
|
78
|
+
id: job.id,
|
|
79
|
+
sessionId: job.sessionId,
|
|
80
|
+
turnId: job.turnId,
|
|
81
|
+
...view,
|
|
82
|
+
snapshotKind: view.controlState === "owned" ? "live" : "stored",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** 通过 durable 身份定位 Job 后改走 Session 绑定 API,避免独立命令绕过 owner 与安全投影。 */
|
|
86
|
+
async function readIndependentJob(runtime, workspaceRoot, jobId) {
|
|
87
|
+
const job = await runtime.jobs.get(jobId);
|
|
88
|
+
if (!job || job.workspaceId !== workspaceRoot)
|
|
89
|
+
throw new AgentRuntimeError("JOB_NOT_FOUND", "JOB_NOT_FOUND");
|
|
90
|
+
const scoped = runtime.jobs.forSession({ workspaceId: workspaceRoot, sessionId: job.sessionId });
|
|
91
|
+
return { job, scoped, read: await scoped.read({ jobId, waitMs: 0 }) };
|
|
92
|
+
}
|
|
93
|
+
/** 将 MCP 服务收口为身份、授权与执行隔离事实,绝不输出命令、参数、cwd 或环境变量。 */
|
|
94
|
+
function summarizeMcpServer(server) {
|
|
95
|
+
return { serverName: server.serverName, approved: server.approved, executionMode: server.executionMode, isolation: server.isolation };
|
|
96
|
+
}
|
|
97
|
+
/** 读取交互删除确认;非 TTY 调用方必须使用 --yes,避免 stdin 被误消费。 */
|
|
98
|
+
async function confirmSessionDelete(streams, session) {
|
|
99
|
+
const input = streams.stdin ?? process.stdin;
|
|
100
|
+
if (streams.stdout.isTTY !== true || input.isTTY !== true)
|
|
101
|
+
return false;
|
|
102
|
+
streams.stdout.write(`delete session ${session.id} (${session.title ?? "untitled"})? [y/N] `);
|
|
103
|
+
const inputInterface = readline.createInterface({ input, output: streams.stdout });
|
|
104
|
+
try {
|
|
105
|
+
const answer = await new Promise(function readConfirmation(resolve) {
|
|
106
|
+
inputInterface.question("", resolve);
|
|
107
|
+
});
|
|
108
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
inputInterface.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 显示 MCP 的最小安全授权摘要并读取一次显式确认;非 TTY 只能使用 --yes。 */
|
|
115
|
+
async function confirmMcpApproval(streams, server) {
|
|
116
|
+
const input = streams.stdin ?? process.stdin;
|
|
117
|
+
if (streams.stdout.isTTY !== true || input.isTTY !== true)
|
|
118
|
+
return false;
|
|
119
|
+
streams.stdout.write(`approve MCP server ${server.serverName} for this workspace? [y/N] `);
|
|
120
|
+
const inputInterface = readline.createInterface({ input, output: streams.stdout });
|
|
121
|
+
try {
|
|
122
|
+
const answer = await new Promise(function readConfirmation(resolve) { inputInterface.question("", resolve); });
|
|
123
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
inputInterface.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** 将 Doctor DTO 投影为稳定的纯文本行,不暴露路径、凭据和 Provider 正文。 */
|
|
130
|
+
function writeDoctorResult(streams, checks) {
|
|
131
|
+
for (const check of checks)
|
|
132
|
+
streams.stdout.write(`${check.status} ${check.name}: ${sanitizeErrorMessage(check.detail)}\n`);
|
|
133
|
+
}
|
|
134
|
+
/** 按 workspaceRoot 找到既有 Project,否则通过 Runtime 创建最小 Project。 */
|
|
135
|
+
async function ensureProject(runtime, workspaceRoot) {
|
|
136
|
+
const projects = await runtime.projects.list();
|
|
137
|
+
const existing = projects.find(function findProject(project) { return project.workspaceRoot === workspaceRoot; });
|
|
138
|
+
if (existing)
|
|
139
|
+
return existing;
|
|
140
|
+
return runtime.projects.create({ name: path.basename(workspaceRoot) || "project", workspaceRoot });
|
|
141
|
+
}
|
|
142
|
+
/** 读取指定 Session 或创建当前 Project 下的新 Session。 */
|
|
143
|
+
async function ensureChatSession(runtime, project, sessionId, prompt) {
|
|
144
|
+
if (sessionId) {
|
|
145
|
+
const session = await runtime.sessions.get(sessionId);
|
|
146
|
+
if (!session)
|
|
147
|
+
throw new AgentRuntimeError("SESSION_NOT_FOUND", `Session ${sessionId} 不存在`);
|
|
148
|
+
if (session.projectId && session.projectId !== project.id)
|
|
149
|
+
throw new AgentRuntimeError("INVALID_INPUT", "Session 不属于当前受信任 Project");
|
|
150
|
+
return session;
|
|
151
|
+
}
|
|
152
|
+
return runtime.sessions.create({
|
|
153
|
+
projectId: project.id,
|
|
154
|
+
title: prompt?.trim().slice(0, 80),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** 读取指定或最近的可恢复 Session;没有可用 Session 时返回稳定错误。 */
|
|
158
|
+
async function selectResumeSession(runtime, project, sessionId) {
|
|
159
|
+
if (sessionId)
|
|
160
|
+
return ensureChatSession(runtime, project, sessionId, undefined);
|
|
161
|
+
const sessions = (await runtime.sessions.list())
|
|
162
|
+
.filter(function selectProjectSession(item) { return !item.projectId || item.projectId === project.id; })
|
|
163
|
+
.sort(function sortRecent(left, right) { return right.updatedAt - left.updatedAt; });
|
|
164
|
+
for (const session of sessions) {
|
|
165
|
+
const state = await runtime.turns.getRecoverableState({ sessionId: session.id });
|
|
166
|
+
if (state?.recoverable && (state.status === "awaiting_approval" || state.status === "interrupted_recoverable" || state.status === "limit_reached"))
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
throw new AgentRuntimeError("INVALID_INPUT", "没有可恢复的 Turn;已完成或失败的 Turn 不能 resume");
|
|
170
|
+
}
|
|
171
|
+
/** 在一次 Runtime 操作期间注册并清理 SIGINT/SIGTERM,活动 turn 统一走 cancel。 */
|
|
172
|
+
async function runWithSignals(controller, operation, signalSource) {
|
|
173
|
+
let signalExitCode;
|
|
174
|
+
let cancellation;
|
|
175
|
+
/** 收到宿主信号时请求 Core 取消当前 turn。 */
|
|
176
|
+
function handleSignal(signal) {
|
|
177
|
+
signalExitCode = exitCodeForSignal(signal);
|
|
178
|
+
cancellation = controller.dispatch({ type: "cancel_turn" }).then(function ignoreCancellationResult() { return undefined; }).catch(function ignoreCancellationError() { return undefined; });
|
|
179
|
+
}
|
|
180
|
+
function handleSigint() { handleSignal("SIGINT"); }
|
|
181
|
+
function handleSigterm() { handleSignal("SIGTERM"); }
|
|
182
|
+
signalSource.on("SIGINT", handleSigint);
|
|
183
|
+
signalSource.on("SIGTERM", handleSigterm);
|
|
184
|
+
try {
|
|
185
|
+
const result = await operation();
|
|
186
|
+
if (cancellation)
|
|
187
|
+
await cancellation;
|
|
188
|
+
return { result, exitCode: signalExitCode ?? result.exitCode };
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
signalSource.removeListener("SIGINT", handleSigint);
|
|
192
|
+
signalSource.removeListener("SIGTERM", handleSigterm);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** 将一行输入映射为安全 Action,并运行 Plain Controller。 */
|
|
196
|
+
async function runInputLoop(controller, format, streams, signalSource, showPrompt = format !== "jsonl") {
|
|
197
|
+
const input = streams.stdin ?? process.stdin;
|
|
198
|
+
const inputInterface = readline.createInterface({ input, crlfDelay: Infinity });
|
|
199
|
+
let requestedExitCode;
|
|
200
|
+
let cancellation;
|
|
201
|
+
const closeInput = function closeInput() { inputInterface.close(); };
|
|
202
|
+
/** 输入信号统一取消活动 turn,并关闭 EOF 读取器。 */
|
|
203
|
+
function handleSignal(signal) {
|
|
204
|
+
requestedExitCode = exitCodeForSignal(signal);
|
|
205
|
+
cancellation = controller.dispatch({ type: "cancel_turn" }).then(function ignoreCancellationResult() { return undefined; }).catch(function ignoreCancellationError() { return undefined; });
|
|
206
|
+
closeInput();
|
|
207
|
+
}
|
|
208
|
+
function handleSigint() { handleSignal("SIGINT"); }
|
|
209
|
+
function handleSigterm() { handleSignal("SIGTERM"); }
|
|
210
|
+
signalSource.on("SIGINT", handleSigint);
|
|
211
|
+
signalSource.on("SIGTERM", handleSigterm);
|
|
212
|
+
let lastExitCode = 0;
|
|
213
|
+
try {
|
|
214
|
+
if (showPrompt)
|
|
215
|
+
streams.stdout.write("> ");
|
|
216
|
+
for await (const rawLine of inputInterface) {
|
|
217
|
+
if (requestedExitCode)
|
|
218
|
+
break;
|
|
219
|
+
const parsed = parseSlashAction(String(rawLine));
|
|
220
|
+
if (parsed.kind === "error") {
|
|
221
|
+
streams.stderr.write(`${parsed.message}\n`);
|
|
222
|
+
lastExitCode = 2;
|
|
223
|
+
if (showPrompt)
|
|
224
|
+
streams.stdout.write("> ");
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const result = await controller.dispatch(parsed.action);
|
|
228
|
+
writeControllerResult(result, streams);
|
|
229
|
+
lastExitCode = result.exitCode;
|
|
230
|
+
if (result.kind === "exit")
|
|
231
|
+
break;
|
|
232
|
+
if (showPrompt)
|
|
233
|
+
streams.stdout.write("> ");
|
|
234
|
+
}
|
|
235
|
+
if (cancellation)
|
|
236
|
+
await cancellation;
|
|
237
|
+
return requestedExitCode ?? lastExitCode;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
inputInterface.close();
|
|
241
|
+
signalSource.removeListener("SIGINT", handleSigint);
|
|
242
|
+
signalSource.removeListener("SIGTERM", handleSigterm);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** 执行已经通过 parser 校验的 CLI 命令。 */
|
|
246
|
+
async function executeInvocation(invocation, streams, dependencies) {
|
|
247
|
+
const command = invocation.command;
|
|
248
|
+
const workspaceRoot = invocation.host.workspaceRoot ?? (command.type === "project_open" ? command.path : undefined);
|
|
249
|
+
const rootFactory = dependencies.createCompositionRoot ?? createCliCompositionRoot;
|
|
250
|
+
const root = rootFactory({
|
|
251
|
+
stateRoot: invocation.host.stateRoot,
|
|
252
|
+
trustRoot: invocation.host.trustRoot,
|
|
253
|
+
workspaceRoot,
|
|
254
|
+
cwd: dependencies.cwd ?? process.cwd(),
|
|
255
|
+
});
|
|
256
|
+
root.ensureUserConfigDirectory();
|
|
257
|
+
const signalSource = dependencies.signalSource ?? process;
|
|
258
|
+
const input = streams.stdin ?? process.stdin;
|
|
259
|
+
const hasTty = streams.stdout.isTTY === true && (streams.stdin?.isTTY ?? process.stdin.isTTY) === true;
|
|
260
|
+
// Registry 只管理宿主级索引,不需要也不允许为 list/remove 建立 Runtime 或读取工作区配置。
|
|
261
|
+
if (command.type === "project_list") {
|
|
262
|
+
const entries = root.listWorkspaceRegistry().map(function toProjectListEntry(entry) {
|
|
263
|
+
return { workspaceId: entry.workspaceId, displayName: entry.displayName, lastOpenedAt: entry.lastOpenedAt, available: fs.existsSync(entry.workspaceRoot) };
|
|
264
|
+
}).sort(function sortRecent(left, right) { return right.lastOpenedAt - left.lastOpenedAt; });
|
|
265
|
+
writeManagementResult(streams, entries);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
if (command.type === "project_remove") {
|
|
269
|
+
if (!root.removeWorkspaceRegistry(command.workspaceId)) {
|
|
270
|
+
streams.stderr.write("WORKSPACE_REGISTRY_ENTRY_NOT_FOUND\n");
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
writeManagementResult(streams, `project registry removed: ${command.workspaceId}; source files, sessions, and trust were not deleted`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
// Trust 引导:先解析 canonical 工作区并读取信任状态;未信任时不创建 Runtime/Project/Session。
|
|
277
|
+
const trust = await root.resolveTrust();
|
|
278
|
+
if (command.type === "doctor") {
|
|
279
|
+
writeDoctorResult(streams, await root.doctor(trust.workspaceRoot, command.connectivity));
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
if (command.type === "sandbox_status") {
|
|
283
|
+
writeManagementResult(streams, await root.sandboxStatus(trust.workspaceRoot));
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
if (command.type === "trust_status") {
|
|
287
|
+
writeManagementResult(streams, { status: trust.trustEntry.status });
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
if (command.type === "trust_revoke") {
|
|
291
|
+
const revoked = await root.revokeTrust(trust.workspaceRoot);
|
|
292
|
+
writeManagementResult(streams, `trust revoked: ${revoked.status}; future capability, Runtime, and Provider initialization is blocked`);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (command.type === "project_current") {
|
|
296
|
+
writeManagementResult(streams, { workspaceId: root.listWorkspaceRegistry().find(function isCurrent(entry) { return entry.workspaceRoot === trust.workspaceRoot; })?.workspaceId, trust: trust.trustEntry.status });
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
if (command.type === "config_show" || command.type === "config_get" || command.type === "config_set") {
|
|
300
|
+
if (command.scope !== "user" && trust.trustEntry.status !== "trusted") {
|
|
301
|
+
streams.stderr.write(`WORKSPACE_TRUST_REQUIRED: ${trust.trustEntry.status}\n`);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
if (command.type === "config_show")
|
|
305
|
+
writeManagementResult(streams, root.showConfig(command.scope, trust.workspaceRoot));
|
|
306
|
+
if (command.type === "config_get")
|
|
307
|
+
writeManagementResult(streams, { key: command.key, value: root.getConfig(command.scope, command.key, trust.workspaceRoot) });
|
|
308
|
+
if (command.type === "config_set")
|
|
309
|
+
writeManagementResult(streams, root.setConfig(command.scope, command.key, command.value, trust.workspaceRoot));
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
if (trust.trustEntry.status !== "trusted") {
|
|
313
|
+
const interactive = (command.type === "project_open" || ("output" in command && command.output === "interactive")) && hasTty;
|
|
314
|
+
if (!interactive) {
|
|
315
|
+
streams.stderr.write(`WORKSPACE_TRUST_REQUIRED: ${trust.trustEntry.status}\n`);
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
const decision = await runCliTrustDialog({
|
|
319
|
+
workspaceRoot: trust.workspaceRoot,
|
|
320
|
+
streams: { stdout: streams.stdout, stdin: input },
|
|
321
|
+
signalSource,
|
|
322
|
+
env: process.env,
|
|
323
|
+
});
|
|
324
|
+
if (decision !== "granted")
|
|
325
|
+
return 0;
|
|
326
|
+
await root.grantTrust(trust.workspaceRoot);
|
|
327
|
+
const confirmed = await root.getTrustStatus(trust.workspaceRoot);
|
|
328
|
+
if (confirmed.status !== "trusted") {
|
|
329
|
+
streams.stderr.write(`WORKSPACE_TRUST_REQUIRED: ${confirmed.status}\n`);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
root.ensureProjectAutopilotDirectory(trust.workspaceRoot);
|
|
334
|
+
if (command.type === "sandbox_setup") {
|
|
335
|
+
// Windows provision 属于系统级变更;UAC 结果经脱敏能力报告返回,不做自动重试。
|
|
336
|
+
writeManagementResult(streams, await root.sandboxSetup(trust.workspaceRoot));
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
if (command.type === "mcp_list" || command.type === "mcp_approve" || command.type === "mcp_revoke") {
|
|
340
|
+
const servers = await root.listMcp(trust.workspaceRoot);
|
|
341
|
+
if (command.type === "mcp_list") {
|
|
342
|
+
writeManagementResult(streams, servers.map(summarizeMcpServer));
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
const server = servers.find(function matchServer(item) { return item.serverName === command.serverName; });
|
|
346
|
+
if (!server)
|
|
347
|
+
throw new AgentRuntimeError("INVALID_INPUT", "MCP_SERVER_NOT_FOUND");
|
|
348
|
+
if (command.type === "mcp_approve") {
|
|
349
|
+
if (server.executionMode === "host") {
|
|
350
|
+
streams.stderr.write("WARNING: MCP server will run directly on the host without OS sandbox isolation.\n");
|
|
351
|
+
}
|
|
352
|
+
const confirmed = command.yes || await confirmMcpApproval(streams, server);
|
|
353
|
+
if (!confirmed) {
|
|
354
|
+
streams.stderr.write("MCP_APPROVAL_CONFIRMATION_REQUIRED\n");
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
writeManagementResult(streams, { serverName: (await root.approveMcp(trust.workspaceRoot, server.serverName)).serverName, approved: true, executionMode: server.executionMode, isolation: server.isolation });
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
writeManagementResult(streams, { serverName: server.serverName, approved: false, revoked: await root.revokeMcp(trust.workspaceRoot, server.serverName) });
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
const runtime = await root.createRuntime(trust.workspaceRoot);
|
|
364
|
+
try {
|
|
365
|
+
if (command.type === "job_list" || command.type === "job_show" || command.type === "job_output" || command.type === "job_kill") {
|
|
366
|
+
if (command.type === "job_list") {
|
|
367
|
+
const jobs = await runtime.jobs.list(trust.workspaceRoot);
|
|
368
|
+
const summaries = await Promise.all(jobs.map(async function summarizeIndependentJob(job) {
|
|
369
|
+
const scoped = runtime.jobs.forSession({ workspaceId: trust.workspaceRoot, sessionId: job.sessionId });
|
|
370
|
+
const read = await scoped.read({ jobId: job.id, waitMs: 0 });
|
|
371
|
+
return summarizeJob(job, read.job);
|
|
372
|
+
}));
|
|
373
|
+
writeManagementResult(streams, summaries);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
const found = await readIndependentJob(runtime, trust.workspaceRoot, command.jobId);
|
|
377
|
+
if (command.type === "job_kill") {
|
|
378
|
+
const stopped = await found.scoped.stop({ jobId: command.jobId, source: "host" });
|
|
379
|
+
writeManagementResult(streams, { job: summarizeJob(found.job, stopped) });
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
writeManagementResult(streams, command.type === "job_output"
|
|
383
|
+
? { job: summarizeJob(found.job, found.read.job), output: found.read.output, waitOutcome: found.read.waitOutcome }
|
|
384
|
+
: { job: summarizeJob(found.job, found.read.job) });
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
if (command.type === "undo") {
|
|
388
|
+
if (!command.yes) {
|
|
389
|
+
streams.stderr.write("UNDO_CONFIRMATION_REQUIRED: use --yes after reviewing an interactive /undo preview\n");
|
|
390
|
+
return 2;
|
|
391
|
+
}
|
|
392
|
+
const session = await runtime.sessions.get(command.sessionId);
|
|
393
|
+
if (!session)
|
|
394
|
+
throw new AgentRuntimeError("SESSION_NOT_FOUND", `Session ${command.sessionId} 不存在`);
|
|
395
|
+
const preview = await runtime.undo.preview({ sessionId: session.id });
|
|
396
|
+
if (preview.status !== "available") {
|
|
397
|
+
streams.stderr.write(`UNDO_NOT_AVAILABLE: ${preview.reason ?? "UNKNOWN"}\n`);
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
const completed = await runtime.undo.execute({ sessionId: session.id, confirmed: true });
|
|
401
|
+
writeManagementResult(streams, completed);
|
|
402
|
+
return completed.status === "completed" ? 0 : 1;
|
|
403
|
+
}
|
|
404
|
+
if (command.type === "project_open") {
|
|
405
|
+
const project = await ensureProject(runtime, trust.workspaceRoot);
|
|
406
|
+
streams.stdout.write(`project ready: ${project.id}\n`);
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
if (command.type === "session_list" || command.type === "session_rename" || command.type === "session_delete") {
|
|
410
|
+
const project = await ensureProject(runtime, trust.workspaceRoot);
|
|
411
|
+
const sessions = (await runtime.sessions.list())
|
|
412
|
+
.filter(function inCurrentProject(item) { return !item.projectId || item.projectId === project.id; })
|
|
413
|
+
.sort(function sortRecent(left, right) { return right.updatedAt - left.updatedAt; });
|
|
414
|
+
if (command.type === "session_list") {
|
|
415
|
+
writeManagementResult(streams, sessions.map(function summarizeSession(session) {
|
|
416
|
+
return { id: session.id, title: session.title ?? "", status: session.status, updatedAt: session.updatedAt };
|
|
417
|
+
}));
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
const session = sessions.find(function findSession(item) { return item.id === command.sessionId; });
|
|
421
|
+
if (!session)
|
|
422
|
+
throw new AgentRuntimeError("SESSION_NOT_FOUND", `Session ${command.sessionId} 不存在或不属于当前 Project`);
|
|
423
|
+
if (command.type === "session_rename") {
|
|
424
|
+
const renamed = await runtime.sessions.rename(session.id, command.title);
|
|
425
|
+
writeManagementResult(streams, { id: renamed.id, title: renamed.title, updatedAt: renamed.updatedAt });
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
const confirmed = command.yes || await confirmSessionDelete(streams, session);
|
|
429
|
+
if (!confirmed) {
|
|
430
|
+
streams.stderr.write("SESSION_DELETE_CONFIRMATION_REQUIRED: pass --yes in non-interactive mode\n");
|
|
431
|
+
return 2;
|
|
432
|
+
}
|
|
433
|
+
const deleted = await runtime.sessions.delete(session.id);
|
|
434
|
+
writeManagementResult(streams, { id: session.id, deleted });
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
if (command.type !== "chat" && command.type !== "resume") {
|
|
438
|
+
throw new AgentRuntimeError("INVALID_INPUT", "不支持的 CLI 管理命令");
|
|
439
|
+
}
|
|
440
|
+
const configurationError = root.getStartupConfigurationError?.();
|
|
441
|
+
if (configurationError) {
|
|
442
|
+
streams.stderr.write(`${configurationError}\n`);
|
|
443
|
+
return 1;
|
|
444
|
+
}
|
|
445
|
+
const project = await ensureProject(runtime, trust.workspaceRoot);
|
|
446
|
+
const session = command.type === "resume"
|
|
447
|
+
? await selectResumeSession(runtime, project, command.sessionId)
|
|
448
|
+
: await ensureChatSession(runtime, project, command.sessionId, command.prompt);
|
|
449
|
+
const renderer = command.output === "interactive" && hasTty
|
|
450
|
+
? { format: "text", render: function renderTuiEvent() { return ""; } }
|
|
451
|
+
: createPlainRenderer(command.output === "jsonl" ? "jsonl" : "text");
|
|
452
|
+
const controller = createCliController({
|
|
453
|
+
runtime,
|
|
454
|
+
renderer,
|
|
455
|
+
sessionId: session.id,
|
|
456
|
+
workspaceRoot: trust.workspaceRoot,
|
|
457
|
+
getRuntimeStatusDefaults: root.getRuntimeStatusDefaults,
|
|
458
|
+
listSkills: root.listSkills,
|
|
459
|
+
resolveSkill: root.resolveSkill,
|
|
460
|
+
});
|
|
461
|
+
const recoverable = command.type === "chat" && !command.prompt
|
|
462
|
+
? await runtime.turns.getRecoverableState({ sessionId: session.id })
|
|
463
|
+
: undefined;
|
|
464
|
+
if (recoverable?.recoverable && recoverable.status === "awaiting_user_input") {
|
|
465
|
+
const blocked = await controller.resume();
|
|
466
|
+
writeControllerResult(blocked, streams);
|
|
467
|
+
return blocked.exitCode;
|
|
468
|
+
}
|
|
469
|
+
if (command.output === "interactive" && hasTty) {
|
|
470
|
+
const initial = command.type === "resume"
|
|
471
|
+
? { type: "resume" }
|
|
472
|
+
: command.prompt
|
|
473
|
+
? { type: "start", content: command.prompt }
|
|
474
|
+
: undefined;
|
|
475
|
+
const tuiResult = await runCliTui({
|
|
476
|
+
controller,
|
|
477
|
+
sessionId: session.id,
|
|
478
|
+
workspaceRoot: trust.workspaceRoot,
|
|
479
|
+
streams: {
|
|
480
|
+
stdout: streams.stdout,
|
|
481
|
+
stderr: streams.stderr,
|
|
482
|
+
stdin: input,
|
|
483
|
+
},
|
|
484
|
+
signalSource,
|
|
485
|
+
initial,
|
|
486
|
+
closeWhenIdle: initial !== undefined,
|
|
487
|
+
env: process.env,
|
|
488
|
+
forceExit: dependencies.forceExit ?? function forceProcessExit(code) { process.exit(code); },
|
|
489
|
+
});
|
|
490
|
+
return tuiResult.exitCode;
|
|
491
|
+
}
|
|
492
|
+
streams.stderr.write(`${WEB_SEARCH_DATA_FLOW_NOTICE}\n`);
|
|
493
|
+
const effectiveOutput = command.output === "interactive" ? "text" : command.output;
|
|
494
|
+
const showPlainPrompt = command.output !== "interactive" && effectiveOutput !== "jsonl";
|
|
495
|
+
if (command.type === "chat" && command.prompt) {
|
|
496
|
+
const executed = await runWithSignals(controller, function startPrompt() {
|
|
497
|
+
return controller.dispatch({ type: "submit_prompt", content: command.prompt });
|
|
498
|
+
}, signalSource);
|
|
499
|
+
writeControllerResult(executed.result, streams);
|
|
500
|
+
if (executed.result.projection.status === "waiting_approval" && effectiveOutput !== "jsonl") {
|
|
501
|
+
return runInputLoop(controller, effectiveOutput, streams, signalSource, showPlainPrompt);
|
|
502
|
+
}
|
|
503
|
+
return executed.exitCode;
|
|
504
|
+
}
|
|
505
|
+
if (command.type === "chat")
|
|
506
|
+
return runInputLoop(controller, effectiveOutput, streams, signalSource, showPlainPrompt);
|
|
507
|
+
const resumed = await runWithSignals(controller, function resumeTurn() {
|
|
508
|
+
return controller.resume();
|
|
509
|
+
}, signalSource);
|
|
510
|
+
writeControllerResult(resumed.result, streams);
|
|
511
|
+
if (resumed.result.projection.status === "waiting_approval" && effectiveOutput !== "jsonl") {
|
|
512
|
+
return runInputLoop(controller, effectiveOutput, streams, signalSource, showPlainPrompt);
|
|
513
|
+
}
|
|
514
|
+
return resumed.exitCode;
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
await runtime.shutdown?.();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** 执行 Step 5.3 CLI 入口并返回固定退出码。 */
|
|
521
|
+
export async function main(argv = process.argv.slice(2), streams = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }, dependencies = {}) {
|
|
522
|
+
const parsed = parseArgv(argv);
|
|
523
|
+
if (parsed.kind === "help") {
|
|
524
|
+
streams.stdout.write(`${parsed.text}\n`);
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
if (parsed.kind === "error") {
|
|
528
|
+
streams.stderr.write(`${parsed.message}\n\n${renderHelp()}\n`);
|
|
529
|
+
return 2;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
return await executeInvocation(parsed.invocation, streams, dependencies);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
streams.stderr.write(`${formatCliError(error)}\n`);
|
|
536
|
+
return 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
540
|
+
main().then(function finish(exitCode) {
|
|
541
|
+
process.exitCode = exitCode;
|
|
542
|
+
});
|
|
543
|
+
}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argv 与 slash command 的最小确定性 parser。
|
|
3
|
+
*
|
|
4
|
+
* parser 只做语法、选项归属和局部值校验,不创建 Runtime、不读取工作区,
|
|
5
|
+
* 也不会把未知命令静默转换为普通模型 prompt。
|
|
6
|
+
*/
|
|
7
|
+
import type { CliActionParseResult, CliParseResult, CliSlashCommand } from "./types.js";
|
|
8
|
+
/** 交互 TTY 可用的 slash 命令目录;只列出 parser 已真正支持的命令。 */
|
|
9
|
+
export declare const CLI_SLASH_COMMANDS: readonly CliSlashCommand[];
|
|
10
|
+
/** 返回当前 prompt 可用的 slash 命令建议;只有输入 `/` 或命令前缀时打开。 */
|
|
11
|
+
export declare function getSlashCommandSuggestions(input: string): CliSlashCommand[];
|
|
12
|
+
/** 返回稳定的 CLI 帮助文本。 */
|
|
13
|
+
export declare function renderHelp(): string;
|
|
14
|
+
/** 解析 argv;不触碰 Runtime 或文件系统。 */
|
|
15
|
+
export declare function parseArgv(argv: readonly string[]): CliParseResult;
|
|
16
|
+
/** 解析单行 slash command;普通文本不在此函数内改写。 */
|
|
17
|
+
export declare function parseSlashAction(input: string): CliActionParseResult;
|