@co0ontty/wand 2.12.0 → 2.13.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 +20 -4
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +4 -3
- package/dist/config.js +22 -7
- package/dist/git-quick-commit.js +56 -3
- package/dist/models.d.ts +4 -0
- package/dist/models.js +35 -1
- package/dist/path-repair.d.ts +1 -1
- package/dist/path-repair.js +11 -6
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +23 -5
- package/dist/provider-cli-updater.d.ts +38 -0
- package/dist/provider-cli-updater.js +225 -0
- package/dist/pty-text-utils.js +76 -131
- package/dist/server-session-routes.js +4 -4
- package/dist/server.js +117 -5
- package/dist/session-ai-context.d.ts +2 -2
- package/dist/session-ai-context.js +8 -4
- package/dist/storage.js +6 -1
- package/dist/structured-session-manager.d.ts +6 -0
- package/dist/structured-session-manager.js +296 -5
- package/dist/types.d.ts +4 -2
- package/dist/web-ui/content/scripts.js +37 -37
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { buildChildEnv } from "./env-utils.js";
|
|
6
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
7
|
+
import { whichSync } from "./path-repair.js";
|
|
8
|
+
import { compareSemver, extractSemver } from "./version-utils.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const VERSION_TIMEOUT_MS = 8_000;
|
|
11
|
+
const REGISTRY_TIMEOUT_MS = 15_000;
|
|
12
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const MAX_BUFFER = 4 * 1024 * 1024;
|
|
14
|
+
const PROVIDER_CLI_SPECS = [
|
|
15
|
+
{
|
|
16
|
+
id: "claude",
|
|
17
|
+
label: "Claude Code",
|
|
18
|
+
command: "claude",
|
|
19
|
+
npmPackage: "@anthropic-ai/claude-code",
|
|
20
|
+
versionArgs: ["--version"],
|
|
21
|
+
updateArgs: ["update"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "codex",
|
|
25
|
+
label: "Codex",
|
|
26
|
+
command: "codex",
|
|
27
|
+
npmPackage: "@openai/codex",
|
|
28
|
+
versionArgs: ["--version"],
|
|
29
|
+
updateArgs: ["update"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "opencode",
|
|
33
|
+
label: "OpenCode",
|
|
34
|
+
command: "opencode",
|
|
35
|
+
npmPackage: "opencode-ai",
|
|
36
|
+
versionArgs: ["--version"],
|
|
37
|
+
updateArgs: ["upgrade"],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
function childEnv(options) {
|
|
41
|
+
return options.env ?? buildChildEnv(options.inheritEnv !== false);
|
|
42
|
+
}
|
|
43
|
+
async function runCommand(command, args, timeout, options) {
|
|
44
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
45
|
+
timeout,
|
|
46
|
+
env: childEnv(options),
|
|
47
|
+
maxBuffer: MAX_BUFFER,
|
|
48
|
+
});
|
|
49
|
+
return { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") };
|
|
50
|
+
}
|
|
51
|
+
export function parseProviderCliVersion(output) {
|
|
52
|
+
return extractSemver(output);
|
|
53
|
+
}
|
|
54
|
+
function resolveInstallKind(executable, id, version) {
|
|
55
|
+
if (!executable)
|
|
56
|
+
return "unknown";
|
|
57
|
+
let resolved = executable;
|
|
58
|
+
try {
|
|
59
|
+
resolved = realpathSync(executable);
|
|
60
|
+
}
|
|
61
|
+
catch { /* keep original */ }
|
|
62
|
+
const normalized = resolved.replace(/\\/g, "/").toLowerCase();
|
|
63
|
+
if (id === "opencode" && version && /^0\.0\./.test(version))
|
|
64
|
+
return "legacy";
|
|
65
|
+
if (normalized.includes("/node_modules/") || normalized.includes("/npm/"))
|
|
66
|
+
return "npm";
|
|
67
|
+
if (normalized.includes("/cellar/") || normalized.includes("/caskroom/") || normalized.includes("/homebrew/"))
|
|
68
|
+
return "brew";
|
|
69
|
+
if (normalized.includes("/.claude/") || normalized.includes("/.codex/") || normalized.includes("/.opencode/"))
|
|
70
|
+
return "native";
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
73
|
+
function isUpdateSupported(id, version) {
|
|
74
|
+
return !(id === "opencode" && version !== null && /^0\.0\./.test(version));
|
|
75
|
+
}
|
|
76
|
+
export function providerCliUpdateAvailable(currentVersion, latestVersion) {
|
|
77
|
+
if (!currentVersion || !latestVersion)
|
|
78
|
+
return false;
|
|
79
|
+
return compareSemver(latestVersion, currentVersion) > 0;
|
|
80
|
+
}
|
|
81
|
+
async function readInstalledVersion(spec, options) {
|
|
82
|
+
const env = childEnv(options);
|
|
83
|
+
const executable = whichSync(spec.command, { env, timeoutMs: options.versionTimeoutMs ?? VERSION_TIMEOUT_MS });
|
|
84
|
+
if (!executable)
|
|
85
|
+
return { executable: null, version: null };
|
|
86
|
+
try {
|
|
87
|
+
const result = await runCommand(executable, spec.versionArgs, options.versionTimeoutMs ?? VERSION_TIMEOUT_MS, options);
|
|
88
|
+
const version = parseProviderCliVersion(`${result.stdout}\n${result.stderr}`);
|
|
89
|
+
return version
|
|
90
|
+
? { executable, version }
|
|
91
|
+
: { executable, version: null, error: "无法解析已安装版本。" };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { executable, version: null, error: getErrorMessage(error, "读取版本失败。") };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function readLatestVersion(spec, options) {
|
|
98
|
+
const npm = options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
|
|
99
|
+
try {
|
|
100
|
+
const result = await runCommand(npm, ["view", `${spec.npmPackage}@latest`, "version"], options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
|
|
101
|
+
const version = parseProviderCliVersion(result.stdout);
|
|
102
|
+
return version ? { version } : { version: null, error: "npm registry 未返回版本。" };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { version: null, error: getErrorMessage(error, "无法连接 npm registry。") };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function checkProviderCliUpdates(options = {}) {
|
|
109
|
+
return Promise.all(PROVIDER_CLI_SPECS.map(async (spec) => {
|
|
110
|
+
const [installed, latest] = await Promise.all([
|
|
111
|
+
readInstalledVersion(spec, options),
|
|
112
|
+
readLatestVersion(spec, options),
|
|
113
|
+
]);
|
|
114
|
+
const updateSupported = isUpdateSupported(spec.id, installed.version);
|
|
115
|
+
const errors = [installed.error, latest.error].filter(Boolean);
|
|
116
|
+
if (!updateSupported) {
|
|
117
|
+
errors.push("检测到已归档的 OpenCode 0.0.x;请先卸载旧包并安装 opencode-ai@latest。");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: spec.id,
|
|
121
|
+
label: spec.label,
|
|
122
|
+
command: spec.command,
|
|
123
|
+
executable: installed.executable,
|
|
124
|
+
installed: installed.executable !== null,
|
|
125
|
+
currentVersion: installed.version,
|
|
126
|
+
latestVersion: latest.version,
|
|
127
|
+
updateAvailable: providerCliUpdateAvailable(installed.version, latest.version),
|
|
128
|
+
updateSupported,
|
|
129
|
+
installKind: resolveInstallKind(installed.executable, spec.id, installed.version),
|
|
130
|
+
...(errors.length ? { error: errors.join(";") } : {}),
|
|
131
|
+
};
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
function trimOutput(value) {
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
return trimmed.length > 4_096 ? `...${trimmed.slice(-4_096)}` : trimmed;
|
|
137
|
+
}
|
|
138
|
+
export async function updateProviderClis(statuses, ids, options = {}) {
|
|
139
|
+
const selected = new Set(ids?.length ? ids : statuses.filter((item) => item.updateAvailable).map((item) => item.id));
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const spec of PROVIDER_CLI_SPECS) {
|
|
142
|
+
if (!selected.has(spec.id))
|
|
143
|
+
continue;
|
|
144
|
+
const status = statuses.find((item) => item.id === spec.id);
|
|
145
|
+
if (!status?.installed) {
|
|
146
|
+
results.push({
|
|
147
|
+
id: spec.id,
|
|
148
|
+
label: spec.label,
|
|
149
|
+
ok: false,
|
|
150
|
+
skipped: true,
|
|
151
|
+
fromVersion: status?.currentVersion ?? null,
|
|
152
|
+
toVersion: status?.latestVersion ?? null,
|
|
153
|
+
message: `${spec.label} 未安装。`,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!status.updateAvailable) {
|
|
158
|
+
results.push({
|
|
159
|
+
id: spec.id,
|
|
160
|
+
label: spec.label,
|
|
161
|
+
ok: true,
|
|
162
|
+
skipped: true,
|
|
163
|
+
fromVersion: status.currentVersion,
|
|
164
|
+
toVersion: status.latestVersion,
|
|
165
|
+
message: `${spec.label} 已是最新版。`,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!status.updateSupported) {
|
|
170
|
+
results.push({
|
|
171
|
+
id: spec.id,
|
|
172
|
+
label: spec.label,
|
|
173
|
+
ok: false,
|
|
174
|
+
skipped: true,
|
|
175
|
+
fromVersion: status.currentVersion,
|
|
176
|
+
toVersion: status.latestVersion,
|
|
177
|
+
message: status.error ?? `${spec.label} 当前安装方式不支持自动更新。`,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const executable = status.executable;
|
|
182
|
+
options.onLog?.(`[CLI Update] ${spec.label}: ${status.currentVersion} -> ${status.latestVersion}`);
|
|
183
|
+
try {
|
|
184
|
+
const output = await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
|
|
185
|
+
const combined = trimOutput([output.stdout, output.stderr].filter(Boolean).join("\n"));
|
|
186
|
+
results.push({
|
|
187
|
+
id: spec.id,
|
|
188
|
+
label: spec.label,
|
|
189
|
+
ok: true,
|
|
190
|
+
skipped: false,
|
|
191
|
+
fromVersion: status.currentVersion,
|
|
192
|
+
toVersion: status.latestVersion,
|
|
193
|
+
message: `${spec.label} 更新命令执行完成。`,
|
|
194
|
+
...(combined ? { output: combined } : {}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
results.push({
|
|
199
|
+
id: spec.id,
|
|
200
|
+
label: spec.label,
|
|
201
|
+
ok: false,
|
|
202
|
+
skipped: false,
|
|
203
|
+
fromVersion: status.currentVersion,
|
|
204
|
+
toVersion: status.latestVersion,
|
|
205
|
+
message: getErrorMessage(error, `${spec.label} 更新失败。`),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
/** Re-check the active PATH after updating so duplicate installs cannot masquerade as success. */
|
|
212
|
+
export function verifyProviderCliUpdateResults(results, statuses) {
|
|
213
|
+
return results.map((result) => {
|
|
214
|
+
if (!result.ok || result.skipped)
|
|
215
|
+
return result;
|
|
216
|
+
const active = statuses.find((item) => item.id === result.id);
|
|
217
|
+
if (!active || !active.updateAvailable)
|
|
218
|
+
return result;
|
|
219
|
+
return {
|
|
220
|
+
...result,
|
|
221
|
+
ok: false,
|
|
222
|
+
message: `${result.label} updater 已执行,但当前 PATH 仍指向 ${active.currentVersion ?? "旧版本"};请检查是否存在多份安装。`,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -26,100 +26,62 @@ export function stripAnsi(text) {
|
|
|
26
26
|
.replace(/[\t ]+\n/g, "\n")
|
|
27
27
|
.replace(/\n{3,}/g, "\n\n");
|
|
28
28
|
}
|
|
29
|
+
const NOISE_LINE_FRAGMENTS = [
|
|
30
|
+
"esc to interrupt",
|
|
31
|
+
"Claude Code v",
|
|
32
|
+
"Failed to install Anthropic",
|
|
33
|
+
"Claude Code has switched",
|
|
34
|
+
"? for shortcuts",
|
|
35
|
+
"Claude is waiting",
|
|
36
|
+
"[wand]",
|
|
37
|
+
"ctrl+g",
|
|
38
|
+
"/effort",
|
|
39
|
+
"auto mode is unavailable",
|
|
40
|
+
"Germinating",
|
|
41
|
+
"Doodling",
|
|
42
|
+
"Brewing",
|
|
43
|
+
"Captured Claude session ID",
|
|
44
|
+
];
|
|
45
|
+
const NOISE_LINE_PATTERNS = [
|
|
46
|
+
/^────/,
|
|
47
|
+
/^[❯›]$/,
|
|
48
|
+
/^[╭╰│┌└┐┘├┤┬┴┼─═]{2,}$/,
|
|
49
|
+
/^[▁▂▃▄▅▆▇█▔▕▏▐]+$/,
|
|
50
|
+
/^Sonnet\b/,
|
|
51
|
+
/^(?:0;|9;)/,
|
|
52
|
+
/^Using .* for .* session/,
|
|
53
|
+
/MCP server.*failed/i,
|
|
54
|
+
/^●.*·/,
|
|
55
|
+
/^\[[<>]/,
|
|
56
|
+
/^>_\s*OpenAI Codex\b/,
|
|
57
|
+
/^OpenAI Codex\b/i,
|
|
58
|
+
/^(model|directory):\s+/i,
|
|
59
|
+
/^(tip|context):\s+/i,
|
|
60
|
+
/^work(tree|space):\s+/i,
|
|
61
|
+
/^(approvals?|sandbox|provider|session id):\s+/i,
|
|
62
|
+
/^(thinking|working)(\.\.\.|…)?$/i,
|
|
63
|
+
/^[•◦·]\s+Working\b/i,
|
|
64
|
+
/^[•◦·]\s+(Running|Planning|Applying|Reading|Searching)\b/i,
|
|
65
|
+
/^[•◦·]\s+(Inspecting|Reviewing|Summarizing|Editing|Updating|Writing)\b/i,
|
|
66
|
+
/^[•◦·]\s+Completed\b/i,
|
|
67
|
+
/^(ctrl|enter|tab|shift|esc|alt)\+/i,
|
|
68
|
+
/\b(open|close|toggle) (chat|terminal)\b/i,
|
|
69
|
+
/\b(approve|deny)\b.*\b(permission|approval)\b/i,
|
|
70
|
+
/^(use|press) .* (to|for) .*/i,
|
|
71
|
+
/^(?:token|context window|remaining context|conversation):\s+/i,
|
|
72
|
+
/^(?:cwd|path):\s+\//i,
|
|
73
|
+
];
|
|
29
74
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
|
30
75
|
export function isNoiseLine(line) {
|
|
31
|
-
if (!line)
|
|
32
|
-
return false;
|
|
33
76
|
const trimmed = line.trim();
|
|
34
77
|
if (!trimmed)
|
|
35
78
|
return false;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (/^[▁▂▃▄▅▆▇█▔▕▏▐]+$/.test(trimmed))
|
|
43
|
-
return true;
|
|
44
|
-
if (trimmed.includes("esc to interrupt"))
|
|
45
|
-
return true;
|
|
46
|
-
if (trimmed.includes("Claude Code v"))
|
|
47
|
-
return true;
|
|
48
|
-
if (/^Sonnet\b/.test(trimmed))
|
|
49
|
-
return true;
|
|
50
|
-
if (trimmed.includes("Failed to install Anthropic"))
|
|
51
|
-
return true;
|
|
52
|
-
if (trimmed.includes("Claude Code has switched"))
|
|
53
|
-
return true;
|
|
54
|
-
if (trimmed.includes("? for shortcuts"))
|
|
55
|
-
return true;
|
|
56
|
-
if (trimmed.includes("Claude is waiting"))
|
|
57
|
-
return true;
|
|
58
|
-
if (trimmed.includes("[wand]"))
|
|
59
|
-
return true;
|
|
60
|
-
if (trimmed.startsWith("0;") || trimmed.startsWith("9;"))
|
|
61
|
-
return true;
|
|
62
|
-
if (trimmed.includes("ctrl+g"))
|
|
63
|
-
return true;
|
|
64
|
-
if (trimmed.includes("/effort"))
|
|
65
|
-
return true;
|
|
66
|
-
if (/^Using .* for .* session/.test(trimmed))
|
|
67
|
-
return true;
|
|
68
|
-
if (trimmed.startsWith("Press ") && trimmed.includes(" for"))
|
|
69
|
-
return true;
|
|
70
|
-
if (trimmed.startsWith("type ") && trimmed.includes(" to "))
|
|
71
|
-
return true;
|
|
72
|
-
if (trimmed.includes("auto mode is unavailable"))
|
|
73
|
-
return true;
|
|
74
|
-
if (/MCP server.*failed/i.test(trimmed))
|
|
75
|
-
return true;
|
|
76
|
-
if (trimmed.includes("Germinating") || trimmed.includes("Doodling") || trimmed.includes("Brewing"))
|
|
77
|
-
return true;
|
|
78
|
-
if (trimmed.includes("Permissions") && trimmed.includes("mode"))
|
|
79
|
-
return true;
|
|
80
|
-
if (trimmed.startsWith("●") && trimmed.includes("·"))
|
|
81
|
-
return true;
|
|
82
|
-
if (trimmed.startsWith("[>") || trimmed.startsWith("[<"))
|
|
83
|
-
return true;
|
|
84
|
-
if (trimmed.includes("Captured Claude session ID"))
|
|
85
|
-
return true;
|
|
86
|
-
if (/^>_\s*OpenAI Codex\b/.test(trimmed))
|
|
87
|
-
return true;
|
|
88
|
-
if (/^OpenAI Codex\b/i.test(trimmed))
|
|
89
|
-
return true;
|
|
90
|
-
if (/^(model|directory):\s+/i.test(trimmed))
|
|
91
|
-
return true;
|
|
92
|
-
if (/^(tip|context):\s+/i.test(trimmed))
|
|
93
|
-
return true;
|
|
94
|
-
if (/^work(tree|space):\s+/i.test(trimmed))
|
|
95
|
-
return true;
|
|
96
|
-
if (/^(approvals?|sandbox|provider|session id):\s+/i.test(trimmed))
|
|
97
|
-
return true;
|
|
98
|
-
if (/^(thinking|working)(\.\.\.|…)?$/i.test(trimmed))
|
|
99
|
-
return true;
|
|
100
|
-
if (/^[•◦·]\s+Working\b/i.test(trimmed))
|
|
101
|
-
return true;
|
|
102
|
-
if (/^[•◦·]\s+(Running|Planning|Applying|Reading|Searching)\b/i.test(trimmed))
|
|
103
|
-
return true;
|
|
104
|
-
if (/^[•◦·]\s+(Inspecting|Reviewing|Summarizing|Editing|Updating|Writing)\b/i.test(trimmed))
|
|
105
|
-
return true;
|
|
106
|
-
if (/^[•◦·]\s+Completed\b/i.test(trimmed))
|
|
107
|
-
return true;
|
|
108
|
-
if (/^(ctrl|enter|tab|shift|esc|alt)\+/i.test(trimmed))
|
|
109
|
-
return true;
|
|
110
|
-
if (/\b(open|close|toggle) (chat|terminal)\b/i.test(trimmed))
|
|
111
|
-
return true;
|
|
112
|
-
if (/\b(approve|deny)\b.*\b(permission|approval)\b/i.test(trimmed))
|
|
113
|
-
return true;
|
|
114
|
-
if (/^(use|press) .* (to|for) .*/i.test(trimmed))
|
|
115
|
-
return true;
|
|
116
|
-
if (/^(?:token|context window|remaining context|conversation):\s+/i.test(trimmed))
|
|
117
|
-
return true;
|
|
118
|
-
if (/^(?:cwd|path):\s+\//i.test(trimmed))
|
|
119
|
-
return true;
|
|
120
|
-
if (/^[<>│┆╎].*[<>│┆╎]$/.test(trimmed) && trimmed.length < 8)
|
|
121
|
-
return true;
|
|
122
|
-
return false;
|
|
79
|
+
return NOISE_LINE_FRAGMENTS.some((fragment) => trimmed.includes(fragment))
|
|
80
|
+
|| NOISE_LINE_PATTERNS.some((pattern) => pattern.test(trimmed))
|
|
81
|
+
|| (trimmed.startsWith("Press ") && trimmed.includes(" for"))
|
|
82
|
+
|| (trimmed.startsWith("type ") && trimmed.includes(" to "))
|
|
83
|
+
|| (trimmed.includes("Permissions") && trimmed.includes("mode"))
|
|
84
|
+
|| (/^[<>│┆╎].*[<>│┆╎]$/.test(trimmed) && trimmed.length < 8);
|
|
123
85
|
}
|
|
124
86
|
/**
|
|
125
87
|
* Append text to a windowed buffer, trimming from start if over max size.
|
|
@@ -153,10 +115,9 @@ function safeSliceTail(text, maxSize) {
|
|
|
153
115
|
// in well-formed terminal output) and keep lines aligned for replay.
|
|
154
116
|
const LOOKAHEAD = 4096;
|
|
155
117
|
const upper = Math.min(start + LOOKAHEAD, text.length);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
118
|
+
const newlineAt = text.indexOf("\n", start);
|
|
119
|
+
if (newlineAt !== -1 && newlineAt < upper)
|
|
120
|
+
return text.slice(newlineAt + 1);
|
|
160
121
|
// 3. No nearby newline. Detect whether `start` lands inside an open ANSI
|
|
161
122
|
// escape sequence by scanning backward for an ESC (0x1b). If we find one
|
|
162
123
|
// that is not yet terminated, advance past the sequence's final byte.
|
|
@@ -179,44 +140,31 @@ function safeSliceTail(text, maxSize) {
|
|
|
179
140
|
// ST (`ESC \\` = 0x1b 0x5c) 终止。其它范围内字节(包括裸 `\`)
|
|
180
141
|
// 都属于 payload,不能当终止符。CSI 等序列才用 0x40-0x7e final byte。
|
|
181
142
|
const isOsc = escAt + 1 < text.length && text.charCodeAt(escAt + 1) === 0x5d;
|
|
182
|
-
|
|
183
|
-
for (let i = escAt + 1; i < start; i++) {
|
|
184
|
-
const code = text.charCodeAt(i);
|
|
185
|
-
if (code === 0x07) {
|
|
186
|
-
terminated = true;
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
if (isOsc) {
|
|
190
|
-
if (code === 0x1b && i + 1 < start && text.charCodeAt(i + 1) === 0x5c) {
|
|
191
|
-
terminated = true;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (code >= 0x40 && code <= 0x7e) {
|
|
197
|
-
terminated = true;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (!terminated) {
|
|
143
|
+
if (findAnsiEnd(text, escAt + 1, start, isOsc) === -1) {
|
|
202
144
|
const ansiUpper = Math.min(start + 256, text.length);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return text.slice(i + 1);
|
|
207
|
-
if (isOsc) {
|
|
208
|
-
if (code === 0x1b && i + 1 < ansiUpper && text.charCodeAt(i + 1) === 0x5c) {
|
|
209
|
-
return text.slice(i + 2);
|
|
210
|
-
}
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (code >= 0x40 && code <= 0x7e)
|
|
214
|
-
return text.slice(i + 1);
|
|
215
|
-
}
|
|
145
|
+
const ansiEnd = findAnsiEnd(text, start, ansiUpper, isOsc);
|
|
146
|
+
if (ansiEnd !== -1)
|
|
147
|
+
return text.slice(ansiEnd);
|
|
216
148
|
}
|
|
217
149
|
}
|
|
218
150
|
return text.slice(start);
|
|
219
151
|
}
|
|
152
|
+
/** Return the index after an ANSI terminator, or -1 if none exists in the range. */
|
|
153
|
+
function findAnsiEnd(text, start, upper, isOsc) {
|
|
154
|
+
for (let i = start; i < upper; i++) {
|
|
155
|
+
const code = text.charCodeAt(i);
|
|
156
|
+
if (code === 0x07)
|
|
157
|
+
return i + 1;
|
|
158
|
+
if (isOsc) {
|
|
159
|
+
if (code === 0x1b && i + 1 < upper && text.charCodeAt(i + 1) === 0x5c)
|
|
160
|
+
return i + 2;
|
|
161
|
+
}
|
|
162
|
+
else if (code >= 0x40 && code <= 0x7e) {
|
|
163
|
+
return i + 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
220
168
|
function isLikelyAnsiBody(text, idx) {
|
|
221
169
|
// CSI parameter/intermediate range covers most common ANSI bodies.
|
|
222
170
|
const code = text.charCodeAt(idx);
|
|
@@ -236,9 +184,7 @@ export function stripForEchoMatch(input) {
|
|
|
236
184
|
i = skipAnsiSequence(input, i) - 1;
|
|
237
185
|
continue;
|
|
238
186
|
}
|
|
239
|
-
if (code
|
|
240
|
-
continue;
|
|
241
|
-
if (code === 0x20)
|
|
187
|
+
if (code <= 0x20 || code === 0x7f)
|
|
242
188
|
continue;
|
|
243
189
|
out += input[i];
|
|
244
190
|
}
|
|
@@ -335,8 +281,7 @@ export const FALLBACK_SCORE_THRESHOLD = 8;
|
|
|
335
281
|
*/
|
|
336
282
|
export function scorePermissionLikelihood(normalized) {
|
|
337
283
|
// Take the last ~5 lines
|
|
338
|
-
const
|
|
339
|
-
const tail = lines.slice(-8).join("\n");
|
|
284
|
+
const tail = normalized.split("\n").slice(-8).join("\n");
|
|
340
285
|
// Slash-command menus are never permission prompts — zero the score so
|
|
341
286
|
// fallback auto-approve and idle-probe both skip them.
|
|
342
287
|
if (isSlashCommandMenu(tail)) {
|
|
@@ -247,18 +247,18 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
247
247
|
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
248
248
|
const body = req.body;
|
|
249
249
|
try {
|
|
250
|
-
if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
|
|
251
|
-
res.status(400).json({ error: "结构化会话当前仅支持 Claude 或
|
|
250
|
+
if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode") {
|
|
251
|
+
res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex 或 OpenCode provider。" });
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
|
-
const provider = body.provider === "codex"
|
|
254
|
+
const provider = body.provider === "codex" || body.provider === "opencode" ? body.provider : "claude";
|
|
255
255
|
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
256
256
|
const origin = parseSessionCreationOrigin(body);
|
|
257
257
|
const snapshot = structured.createSession({
|
|
258
258
|
cwd: resolveSessionCwd(body.cwd, config.defaultCwd),
|
|
259
259
|
mode: normalizeMode(body.mode, defaultMode),
|
|
260
260
|
provider,
|
|
261
|
-
runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
|
|
261
|
+
runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : provider === "opencode" ? "opencode-cli-run" : "claude-cli-print"),
|
|
262
262
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
263
263
|
model: rawModel || getDefaultModelForProvider(config, provider) || undefined,
|
|
264
264
|
thinkingEffort: typeof body.thinkingEffort === "string"
|