@co0ontty/wand 1.21.4 → 1.21.7
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/claude-pty-bridge.d.ts +4 -9
- package/dist/claude-pty-bridge.js +6 -16
- package/dist/cli.js +44 -18
- package/dist/config.d.ts +34 -0
- package/dist/config.js +165 -1
- package/dist/process-manager.js +2 -2
- package/dist/pty-text-utils.d.ts +6 -0
- package/dist/pty-text-utils.js +6 -0
- package/dist/server-session-routes.js +9 -3
- package/dist/server.js +48 -47
- package/dist/session-logger.d.ts +3 -1
- package/dist/session-logger.js +29 -16
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +29 -0
- package/dist/structured-session-manager.d.ts +33 -0
- package/dist/structured-session-manager.js +616 -31
- package/dist/types.d.ts +3 -1
- package/dist/web-ui/content/scripts.js +301 -181
- package/dist/web-ui/content/styles.css +1471 -254
- package/dist/ws-broadcast.d.ts +6 -0
- package/dist/ws-broadcast.js +25 -38
- package/package.json +2 -3
|
@@ -177,16 +177,11 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
177
177
|
private finalizeResponse;
|
|
178
178
|
/**
|
|
179
179
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
180
|
-
*
|
|
181
|
-
* Returns the index after the last character of the echo.
|
|
180
|
+
* Returns 0 if the echo cannot be fully matched.
|
|
182
181
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* common symbols like `/`, `(`, `:`, space — which made commands such as
|
|
187
|
-
* `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
|
|
188
|
-
* - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
|
|
189
|
-
* so wrapped echoes (line continuation, padded columns) still align.
|
|
182
|
+
* Why: ANSI escapes and whitespace can interleave the echoed characters
|
|
183
|
+
* (line wrapping, padding, color codes), so matching skips them while
|
|
184
|
+
* comparing every printable codepoint of `userInput` in order.
|
|
190
185
|
*/
|
|
191
186
|
private findEchoEndIndex;
|
|
192
187
|
private cleanForChat;
|
|
@@ -7,14 +7,9 @@
|
|
|
7
7
|
* 2. Structured messages for chat view (parsed)
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
-
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu, stripForEchoMatch, skipAnsiSequence } from "./pty-text-utils.js";
|
|
10
|
+
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu, stripForEchoMatch, skipAnsiSequence, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
11
11
|
// ── Constants ──
|
|
12
|
-
|
|
13
|
-
* Hard cap on the in-memory PTY replay buffer. Aligned with the non-bridge
|
|
14
|
-
* branch of `ProcessManager.start()` so a session keeps the same amount of
|
|
15
|
-
* history regardless of which capture path is active.
|
|
16
|
-
*/
|
|
17
|
-
const OUTPUT_MAX_SIZE = 200000;
|
|
12
|
+
const OUTPUT_MAX_SIZE = PTY_OUTPUT_MAX_SIZE;
|
|
18
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
19
14
|
const PERMISSION_WINDOW_SIZE = 2000;
|
|
20
15
|
const AUTO_APPROVE_DELAY_MS = 350;
|
|
@@ -835,16 +830,11 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
835
830
|
// ── Text Processing Utilities ──
|
|
836
831
|
/**
|
|
837
832
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
838
|
-
*
|
|
839
|
-
* Returns the index after the last character of the echo.
|
|
833
|
+
* Returns 0 if the echo cannot be fully matched.
|
|
840
834
|
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* common symbols like `/`, `(`, `:`, space — which made commands such as
|
|
845
|
-
* `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
|
|
846
|
-
* - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
|
|
847
|
-
* so wrapped echoes (line continuation, padded columns) still align.
|
|
835
|
+
* Why: ANSI escapes and whitespace can interleave the echoed characters
|
|
836
|
+
* (line wrapping, padding, color codes), so matching skips them while
|
|
837
|
+
* comparing every printable codepoint of `userInput` in order.
|
|
848
838
|
*/
|
|
849
839
|
findEchoEndIndex(buffer, userInput) {
|
|
850
840
|
const inputChars = stripForEchoMatch(userInput);
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import {
|
|
3
|
+
import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
4
4
|
async function main() {
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const command = args[0] || "help";
|
|
@@ -50,8 +50,18 @@ async function main() {
|
|
|
50
50
|
break;
|
|
51
51
|
}
|
|
52
52
|
case "config:show": {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。
|
|
54
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
55
|
+
const dbPath = resolveDatabasePath(configPath);
|
|
56
|
+
ensureDatabaseFile(dbPath);
|
|
57
|
+
const storage = new WandStorage(dbPath);
|
|
58
|
+
try {
|
|
59
|
+
const config = await loadConfigWithStorage(configPath, storage);
|
|
60
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
storage.close();
|
|
64
|
+
}
|
|
55
65
|
break;
|
|
56
66
|
}
|
|
57
67
|
case "config:set": {
|
|
@@ -60,10 +70,26 @@ async function main() {
|
|
|
60
70
|
if (!key || typeof value === "undefined") {
|
|
61
71
|
throw new Error("Usage: wand config:set <key> <value>");
|
|
62
72
|
}
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
74
|
+
const dbPath = resolveDatabasePath(configPath);
|
|
75
|
+
ensureDatabaseFile(dbPath);
|
|
76
|
+
const storage = new WandStorage(dbPath);
|
|
77
|
+
try {
|
|
78
|
+
const config = await loadConfigWithStorage(configPath, storage);
|
|
79
|
+
if (isPreferenceKey(key)) {
|
|
80
|
+
// 偏好字段写 DB,无需重启
|
|
81
|
+
writePreferenceToStorage(config, storage, key, value);
|
|
82
|
+
process.stdout.write(`[wand] Updated preference ${key} in ${dbPath}\n`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const nextConfig = setConfigValue(config, key, value);
|
|
86
|
+
await saveConfig(configPath, nextConfig);
|
|
87
|
+
process.stdout.write(`[wand] Updated ${key} in ${configPath}\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
storage.close();
|
|
92
|
+
}
|
|
67
93
|
break;
|
|
68
94
|
}
|
|
69
95
|
case "help":
|
|
@@ -95,11 +121,19 @@ Options:
|
|
|
95
121
|
`);
|
|
96
122
|
}
|
|
97
123
|
async function ensureRequiredFiles(configPath, opts = {}) {
|
|
98
|
-
const { ensureDatabaseFile, resolveDatabasePath } = await import("./storage.js");
|
|
124
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
99
125
|
const dbPath = resolveDatabasePath(configPath);
|
|
100
126
|
const hadConfig = hasConfigFile(configPath);
|
|
101
|
-
|
|
127
|
+
// 先建 DB 文件,再加载 config(loadConfigWithStorage 需要 storage 来迁移老 JSON 偏好字段并应用 DB 覆盖)。
|
|
102
128
|
const createdDb = ensureDatabaseFile(dbPath);
|
|
129
|
+
const storage = new WandStorage(dbPath);
|
|
130
|
+
let config;
|
|
131
|
+
try {
|
|
132
|
+
config = await loadConfigWithStorage(configPath, storage);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
storage.close();
|
|
136
|
+
}
|
|
103
137
|
// 已存在的 ready 信息在 TUI 模式下由启动 banner 统一展示,此处静默;
|
|
104
138
|
// 但 created 是首次创建事件,无论 TUI 与否都值得提示。
|
|
105
139
|
if (!hadConfig) {
|
|
@@ -153,11 +187,11 @@ function printStartupBanner(handle) {
|
|
|
153
187
|
process.stdout.write(lines.join("\n") + "\n");
|
|
154
188
|
}
|
|
155
189
|
function setConfigValue(config, key, value) {
|
|
190
|
+
// 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,这里只处理 JSON 字段。
|
|
156
191
|
switch (key) {
|
|
157
192
|
case "host":
|
|
158
193
|
case "password":
|
|
159
194
|
case "shell":
|
|
160
|
-
case "defaultCwd":
|
|
161
195
|
return {
|
|
162
196
|
...config,
|
|
163
197
|
[key]: value
|
|
@@ -170,14 +204,6 @@ function setConfigValue(config, key, value) {
|
|
|
170
204
|
...config,
|
|
171
205
|
port: Number(value)
|
|
172
206
|
};
|
|
173
|
-
case "defaultMode":
|
|
174
|
-
if (!isExecutionMode(value)) {
|
|
175
|
-
throw new Error(`defaultMode must be one of: assist, agent, agent-max, auto-edit, default, full-access, managed, native`);
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
...config,
|
|
179
|
-
defaultMode: value
|
|
180
|
-
};
|
|
181
207
|
case "https":
|
|
182
208
|
if (value !== "true" && value !== "false") {
|
|
183
209
|
throw new Error("https must be 'true' or 'false'");
|
package/dist/config.d.ts
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
import { CardExpandDefaults, ExecutionMode, WandConfig } from "./types.js";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
/**
|
|
4
|
+
* 通过 UI 设置面板可改的"用户偏好"字段。这些字段从 SQLite app_config 表读取,
|
|
5
|
+
* 不再写入 ~/.wand/config.json。JSON 只保留服务部署/启动期参数(host/port/shell 等)。
|
|
6
|
+
*
|
|
7
|
+
* 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
|
|
8
|
+
* 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
|
|
9
|
+
*/
|
|
10
|
+
export declare const PREFERENCE_KEYS: readonly ["defaultMode", "defaultCwd", "defaultModel", "structuredRunner", "language", "cardDefaults"];
|
|
11
|
+
export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
|
|
12
|
+
export declare function isPreferenceKey(key: string): key is PreferenceKey;
|
|
2
13
|
export declare const defaultConfig: () => WandConfig;
|
|
3
14
|
export declare function resolveConfigPath(inputPath?: string): string;
|
|
4
15
|
export declare function resolveConfigDir(configPath: string): string;
|
|
5
16
|
export declare function hasConfigFile(configPath: string): boolean;
|
|
6
17
|
export declare function ensureConfig(configPath: string): Promise<WandConfig>;
|
|
18
|
+
/** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
|
|
7
19
|
export declare function saveConfig(configPath: string, config: WandConfig): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* 启动期合并 JSON + DB 偏好。语义:
|
|
22
|
+
* 1. 读 raw JSON
|
|
23
|
+
* 2. 把 JSON 中残留的偏好字段搬到 DB(只在 DB 没值时迁移)
|
|
24
|
+
* 3. mergeWithDefaults(raw) 得到 baseline
|
|
25
|
+
* 4. applyStoragePreferences 用 DB 覆盖 baseline 的偏好字段
|
|
26
|
+
* 5. 如果 JSON 里仍含偏好字段(来自 1),用 saveConfig 重写一次 JSON(剥离后)
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadConfigWithStorage(configPath: string, storage: WandStorage): Promise<WandConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* 老版本 JSON 里如果还存有偏好字段,且 DB 里对应 key 没有写过,
|
|
31
|
+
* 把 JSON 的值搬到 DB。注意:必须在 mergeWithDefaults 之前操作 raw input,
|
|
32
|
+
* 这样能区分"用户显式写过 X" 和 "X 是 mergeWithDefaults 注入的默认值"。
|
|
33
|
+
*/
|
|
34
|
+
export declare function migrateLegacyPreferencesToDb(rawJsonInput: Partial<WandConfig> | null | undefined, storage: WandStorage): void;
|
|
35
|
+
/**
|
|
36
|
+
* 用 DB 里的偏好覆盖 config 对象(in-place),返回同一个引用,
|
|
37
|
+
* 方便各 manager 持有引用后继续工作。DB 里没有的字段不动,保留 JSON 默认值。
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyStoragePreferences(config: WandConfig, storage: WandStorage): WandConfig;
|
|
40
|
+
/** Write a single preference value to DB and (in-place) update the live config object. */
|
|
41
|
+
export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown): void;
|
|
8
42
|
export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
|
|
9
43
|
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,28 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
const DEFAULT_CONFIG_DIR = ".wand";
|
|
7
7
|
const DEFAULT_CONFIG_FILE = "config.json";
|
|
8
|
+
/**
|
|
9
|
+
* 通过 UI 设置面板可改的"用户偏好"字段。这些字段从 SQLite app_config 表读取,
|
|
10
|
+
* 不再写入 ~/.wand/config.json。JSON 只保留服务部署/启动期参数(host/port/shell 等)。
|
|
11
|
+
*
|
|
12
|
+
* 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
|
|
13
|
+
* 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
|
|
14
|
+
*/
|
|
15
|
+
export const PREFERENCE_KEYS = [
|
|
16
|
+
"defaultMode",
|
|
17
|
+
"defaultCwd",
|
|
18
|
+
"defaultModel",
|
|
19
|
+
"structuredRunner",
|
|
20
|
+
"language",
|
|
21
|
+
"cardDefaults",
|
|
22
|
+
];
|
|
23
|
+
const PREFERENCE_KEY_SET = new Set(PREFERENCE_KEYS);
|
|
24
|
+
function preferenceStorageKey(key) {
|
|
25
|
+
return `pref:${key}`;
|
|
26
|
+
}
|
|
27
|
+
export function isPreferenceKey(key) {
|
|
28
|
+
return PREFERENCE_KEY_SET.has(key);
|
|
29
|
+
}
|
|
8
30
|
export const defaultConfig = () => ({
|
|
9
31
|
host: "127.0.0.1",
|
|
10
32
|
port: 8443,
|
|
@@ -20,6 +42,7 @@ export const defaultConfig = () => ({
|
|
|
20
42
|
android: defaultAndroidApkConfig(),
|
|
21
43
|
cardDefaults: defaultCardExpandDefaults(),
|
|
22
44
|
defaultModel: "",
|
|
45
|
+
structuredRunner: "cli",
|
|
23
46
|
commandPresets: [
|
|
24
47
|
{
|
|
25
48
|
label: "Claude",
|
|
@@ -79,9 +102,149 @@ export async function ensureConfig(configPath) {
|
|
|
79
102
|
return config;
|
|
80
103
|
}
|
|
81
104
|
}
|
|
105
|
+
/** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
|
|
82
106
|
export async function saveConfig(configPath, config) {
|
|
83
107
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
84
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
108
|
+
await writeFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`, "utf8");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 启动期合并 JSON + DB 偏好。语义:
|
|
112
|
+
* 1. 读 raw JSON
|
|
113
|
+
* 2. 把 JSON 中残留的偏好字段搬到 DB(只在 DB 没值时迁移)
|
|
114
|
+
* 3. mergeWithDefaults(raw) 得到 baseline
|
|
115
|
+
* 4. applyStoragePreferences 用 DB 覆盖 baseline 的偏好字段
|
|
116
|
+
* 5. 如果 JSON 里仍含偏好字段(来自 1),用 saveConfig 重写一次 JSON(剥离后)
|
|
117
|
+
*/
|
|
118
|
+
export async function loadConfigWithStorage(configPath, storage) {
|
|
119
|
+
const dir = path.dirname(configPath);
|
|
120
|
+
await mkdir(dir, { recursive: true });
|
|
121
|
+
let rawInput = {};
|
|
122
|
+
let hadFile = false;
|
|
123
|
+
try {
|
|
124
|
+
const raw = await readFile(configPath, "utf8");
|
|
125
|
+
rawInput = JSON.parse(raw);
|
|
126
|
+
hadFile = true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
rawInput = {};
|
|
130
|
+
}
|
|
131
|
+
migrateLegacyPreferencesToDb(rawInput, storage);
|
|
132
|
+
const config = mergeWithDefaults(rawInput);
|
|
133
|
+
applyStoragePreferences(config, storage);
|
|
134
|
+
// 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
|
|
135
|
+
const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
|
|
136
|
+
if (!hadFile || hasLegacyPrefs) {
|
|
137
|
+
await saveConfig(configPath, config);
|
|
138
|
+
}
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
/** Build a JSON-safe view of WandConfig that excludes preference fields (which live in DB). */
|
|
142
|
+
function stripPreferenceFields(config) {
|
|
143
|
+
const out = { ...config };
|
|
144
|
+
for (const key of PREFERENCE_KEYS) {
|
|
145
|
+
delete out[key];
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 老版本 JSON 里如果还存有偏好字段,且 DB 里对应 key 没有写过,
|
|
151
|
+
* 把 JSON 的值搬到 DB。注意:必须在 mergeWithDefaults 之前操作 raw input,
|
|
152
|
+
* 这样能区分"用户显式写过 X" 和 "X 是 mergeWithDefaults 注入的默认值"。
|
|
153
|
+
*/
|
|
154
|
+
export function migrateLegacyPreferencesToDb(rawJsonInput, storage) {
|
|
155
|
+
if (!rawJsonInput || typeof rawJsonInput !== "object")
|
|
156
|
+
return;
|
|
157
|
+
for (const key of PREFERENCE_KEYS) {
|
|
158
|
+
if (!(key in rawJsonInput))
|
|
159
|
+
continue;
|
|
160
|
+
const value = rawJsonInput[key];
|
|
161
|
+
if (value === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
const dbKey = preferenceStorageKey(key);
|
|
164
|
+
if (storage.hasPreference(dbKey))
|
|
165
|
+
continue;
|
|
166
|
+
storage.setPreference(dbKey, value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 用 DB 里的偏好覆盖 config 对象(in-place),返回同一个引用,
|
|
171
|
+
* 方便各 manager 持有引用后继续工作。DB 里没有的字段不动,保留 JSON 默认值。
|
|
172
|
+
*/
|
|
173
|
+
export function applyStoragePreferences(config, storage) {
|
|
174
|
+
const defaults = defaultConfig();
|
|
175
|
+
if (storage.hasPreference(preferenceStorageKey("defaultMode"))) {
|
|
176
|
+
const v = storage.getPreference(preferenceStorageKey("defaultMode"), defaults.defaultMode);
|
|
177
|
+
if (isExecutionMode(v))
|
|
178
|
+
config.defaultMode = v;
|
|
179
|
+
}
|
|
180
|
+
if (storage.hasPreference(preferenceStorageKey("defaultCwd"))) {
|
|
181
|
+
const v = storage.getPreference(preferenceStorageKey("defaultCwd"), defaults.defaultCwd);
|
|
182
|
+
if (typeof v === "string" && v.trim())
|
|
183
|
+
config.defaultCwd = v;
|
|
184
|
+
}
|
|
185
|
+
if (storage.hasPreference(preferenceStorageKey("defaultModel"))) {
|
|
186
|
+
const v = storage.getPreference(preferenceStorageKey("defaultModel"), defaults.defaultModel ?? "");
|
|
187
|
+
if (typeof v === "string")
|
|
188
|
+
config.defaultModel = v.trim();
|
|
189
|
+
}
|
|
190
|
+
if (storage.hasPreference(preferenceStorageKey("structuredRunner"))) {
|
|
191
|
+
const v = storage.getPreference(preferenceStorageKey("structuredRunner"), defaults.structuredRunner ?? "sdk");
|
|
192
|
+
if (v === "cli" || v === "sdk")
|
|
193
|
+
config.structuredRunner = v;
|
|
194
|
+
}
|
|
195
|
+
if (storage.hasPreference(preferenceStorageKey("language"))) {
|
|
196
|
+
const v = storage.getPreference(preferenceStorageKey("language"), defaults.language ?? "");
|
|
197
|
+
if (typeof v === "string")
|
|
198
|
+
config.language = v.trim();
|
|
199
|
+
}
|
|
200
|
+
if (storage.hasPreference(preferenceStorageKey("cardDefaults"))) {
|
|
201
|
+
const v = storage.getPreference(preferenceStorageKey("cardDefaults"), defaults.cardDefaults);
|
|
202
|
+
config.cardDefaults = normalizeCardDefaults(v);
|
|
203
|
+
}
|
|
204
|
+
return config;
|
|
205
|
+
}
|
|
206
|
+
/** Write a single preference value to DB and (in-place) update the live config object. */
|
|
207
|
+
export function writePreferenceToStorage(config, storage, key, value) {
|
|
208
|
+
const dbKey = preferenceStorageKey(key);
|
|
209
|
+
switch (key) {
|
|
210
|
+
case "defaultMode": {
|
|
211
|
+
if (!isExecutionMode(value))
|
|
212
|
+
throw new Error(`无效执行模式: ${value}`);
|
|
213
|
+
storage.setPreference(dbKey, value);
|
|
214
|
+
config.defaultMode = value;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "defaultCwd": {
|
|
218
|
+
const v = typeof value === "string" ? value : "";
|
|
219
|
+
storage.setPreference(dbKey, v);
|
|
220
|
+
config.defaultCwd = v || defaultConfig().defaultCwd;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "defaultModel": {
|
|
224
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
225
|
+
storage.setPreference(dbKey, v);
|
|
226
|
+
config.defaultModel = v;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "structuredRunner": {
|
|
230
|
+
const v = value === "cli" ? "cli" : "sdk";
|
|
231
|
+
storage.setPreference(dbKey, v);
|
|
232
|
+
config.structuredRunner = v;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "language": {
|
|
236
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
237
|
+
storage.setPreference(dbKey, v);
|
|
238
|
+
config.language = v;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "cardDefaults": {
|
|
242
|
+
const normalized = normalizeCardDefaults(value);
|
|
243
|
+
storage.setPreference(dbKey, normalized);
|
|
244
|
+
config.cardDefaults = normalized;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
85
248
|
}
|
|
86
249
|
function defaultCardExpandDefaults() {
|
|
87
250
|
return {
|
|
@@ -185,6 +348,7 @@ function mergeWithDefaults(input) {
|
|
|
185
348
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
186
349
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
187
350
|
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
351
|
+
structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
|
|
188
352
|
};
|
|
189
353
|
}
|
|
190
354
|
export function isExecutionMode(value) {
|
package/dist/process-manager.js
CHANGED
|
@@ -8,7 +8,7 @@ import pty from "node-pty";
|
|
|
8
8
|
import { SessionLogger } from "./session-logger.js";
|
|
9
9
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
10
10
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
11
|
-
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
|
|
11
|
+
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
12
12
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
13
13
|
import { getResumeCommandSessionId } from "./resume-policy.js";
|
|
14
14
|
function resolveProviderFromCommand(command) {
|
|
@@ -744,7 +744,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
744
744
|
rec.output = rec.ptyBridge.getRawOutput();
|
|
745
745
|
}
|
|
746
746
|
else {
|
|
747
|
-
rec.output = appendWindow(rec.output, chunk,
|
|
747
|
+
rec.output = appendWindow(rec.output, chunk, PTY_OUTPUT_MAX_SIZE);
|
|
748
748
|
}
|
|
749
749
|
this.logger.appendPtyOutput(id, chunk);
|
|
750
750
|
if (!rec.ptyBridge) {
|
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Hard cap on the in-memory PTY replay buffer. Shared between ProcessManager
|
|
6
|
+
* and ClaudePtyBridge so a session keeps the same amount of history regardless
|
|
7
|
+
* of which capture path is active.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PTY_OUTPUT_MAX_SIZE = 200000;
|
|
4
10
|
/** Strip ANSI escape sequences and control characters from raw PTY output. */
|
|
5
11
|
export declare function stripAnsi(text: string): string;
|
|
6
12
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Hard cap on the in-memory PTY replay buffer. Shared between ProcessManager
|
|
6
|
+
* and ClaudePtyBridge so a session keeps the same amount of history regardless
|
|
7
|
+
* of which capture path is active.
|
|
8
|
+
*/
|
|
9
|
+
export const PTY_OUTPUT_MAX_SIZE = 200_000;
|
|
4
10
|
/** Strip ANSI escape sequences and control characters from raw PTY output. */
|
|
5
11
|
export function stripAnsi(text) {
|
|
6
12
|
return text
|
|
@@ -207,13 +207,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
207
207
|
app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
|
|
208
208
|
const input = String(req.body?.input ?? "");
|
|
209
209
|
const interrupt = !!req.body?.interrupt;
|
|
210
|
-
|
|
210
|
+
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
211
|
+
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "idempotencyKey:", idempotencyKey);
|
|
211
212
|
try {
|
|
212
|
-
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt });
|
|
213
|
+
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, idempotencyKey });
|
|
213
214
|
res.json(snapshot);
|
|
214
215
|
}
|
|
215
216
|
catch (error) {
|
|
216
|
-
|
|
217
|
+
const errorCode = error?.code;
|
|
218
|
+
const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
|
|
219
|
+
res.status(status).json({
|
|
220
|
+
error: getErrorMessage(error, "无法发送结构化消息。"),
|
|
221
|
+
errorCode,
|
|
222
|
+
});
|
|
217
223
|
}
|
|
218
224
|
});
|
|
219
225
|
// ── Tool content lazy-load endpoint ──
|
package/dist/server.js
CHANGED
|
@@ -13,7 +13,7 @@ import { WebSocketServer } from "ws";
|
|
|
13
13
|
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
14
14
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
15
15
|
import { ensureCertificates } from "./cert.js";
|
|
16
|
-
import { isExecutionMode,
|
|
16
|
+
import { isExecutionMode, PREFERENCE_KEYS, resolveConfigDir, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
17
17
|
import { getCachedModels, refreshModels } from "./models.js";
|
|
18
18
|
import { ProcessManager } from "./process-manager.js";
|
|
19
19
|
import { SessionLogger } from "./session-logger.js";
|
|
@@ -724,6 +724,7 @@ export async function startServer(config, configPath) {
|
|
|
724
724
|
defaultMode: config.defaultMode,
|
|
725
725
|
defaultCwd: config.defaultCwd,
|
|
726
726
|
commandPresets: config.commandPresets,
|
|
727
|
+
structuredRunner: config.structuredRunner ?? "sdk",
|
|
727
728
|
structuredRunners: [
|
|
728
729
|
{ label: "Claude Structured", runner: "claude-cli-print" },
|
|
729
730
|
{ label: "Codex Structured", runner: "codex-cli-exec" },
|
|
@@ -815,61 +816,61 @@ export async function startServer(config, configPath) {
|
|
|
815
816
|
});
|
|
816
817
|
app.post("/api/settings/config", async (req, res) => {
|
|
817
818
|
const body = req.body;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
else if (field === "https") {
|
|
831
|
-
config.https = body.https === true;
|
|
832
|
-
}
|
|
833
|
-
else if (field === "defaultMode") {
|
|
834
|
-
if (!isExecutionMode(body.defaultMode)) {
|
|
835
|
-
res.status(400).json({ error: `无效执行模式: ${body.defaultMode}` });
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
config.defaultMode = body.defaultMode;
|
|
839
|
-
}
|
|
840
|
-
else if (field === "host") {
|
|
841
|
-
config.host = String(body.host);
|
|
842
|
-
}
|
|
843
|
-
else if (field === "defaultCwd") {
|
|
844
|
-
config.defaultCwd = String(body.defaultCwd);
|
|
845
|
-
}
|
|
846
|
-
else if (field === "shell") {
|
|
847
|
-
config.shell = String(body.shell);
|
|
848
|
-
}
|
|
849
|
-
else if (field === "language") {
|
|
850
|
-
config.language = typeof body.language === "string" ? body.language.trim() : "";
|
|
851
|
-
}
|
|
852
|
-
else if (field === "defaultModel") {
|
|
853
|
-
config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
|
|
819
|
+
// 部署字段:写 JSON,需要重启服务才生效(host/port/https 影响监听,shell 影响新 PTY)
|
|
820
|
+
const deployFields = ["host", "port", "https", "shell"];
|
|
821
|
+
let touchedDeployField = false;
|
|
822
|
+
let touchedPreferenceField = false;
|
|
823
|
+
for (const field of deployFields) {
|
|
824
|
+
if (!(field in body) || body[field] === undefined)
|
|
825
|
+
continue;
|
|
826
|
+
if (field === "port") {
|
|
827
|
+
const p = Number(body.port);
|
|
828
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) {
|
|
829
|
+
res.status(400).json({ error: `无效端口号: ${body.port}` });
|
|
830
|
+
return;
|
|
854
831
|
}
|
|
855
|
-
|
|
832
|
+
config.port = p;
|
|
856
833
|
}
|
|
834
|
+
else if (field === "https") {
|
|
835
|
+
config.https = body.https === true;
|
|
836
|
+
}
|
|
837
|
+
else if (field === "host") {
|
|
838
|
+
config.host = String(body.host);
|
|
839
|
+
}
|
|
840
|
+
else if (field === "shell") {
|
|
841
|
+
config.shell = String(body.shell);
|
|
842
|
+
}
|
|
843
|
+
touchedDeployField = true;
|
|
857
844
|
}
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
845
|
+
// 偏好字段:写 SQLite app_config,立即热生效(manager 持有 config 同一引用)。
|
|
846
|
+
// defaultMode 单独做严格校验以保留 400 错误响应,其余字段走 writePreferenceToStorage 的统一类型化处理。
|
|
847
|
+
if (body.defaultMode !== undefined && !isExecutionMode(body.defaultMode)) {
|
|
848
|
+
res.status(400).json({ error: `无效执行模式: ${body.defaultMode}` });
|
|
849
|
+
return;
|
|
862
850
|
}
|
|
863
|
-
|
|
851
|
+
for (const field of PREFERENCE_KEYS) {
|
|
852
|
+
if (!(field in body) || body[field] === undefined)
|
|
853
|
+
continue;
|
|
854
|
+
try {
|
|
855
|
+
writePreferenceToStorage(config, storage, field, body[field]);
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
res.status(400).json({ error: getErrorMessage(err, `字段 ${field} 校验失败`) });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
touchedPreferenceField = true;
|
|
862
|
+
}
|
|
863
|
+
if (!touchedDeployField && !touchedPreferenceField) {
|
|
864
864
|
res.status(400).json({ error: "没有可更新的配置字段。" });
|
|
865
865
|
return;
|
|
866
866
|
}
|
|
867
|
-
// cardDefaults-only changes don't need restart
|
|
868
|
-
const restartRequired = allowedFields.some((f) => f in body && body[f] !== undefined);
|
|
869
867
|
try {
|
|
870
|
-
|
|
868
|
+
if (touchedDeployField) {
|
|
869
|
+
await saveConfig(configPath, config);
|
|
870
|
+
}
|
|
871
871
|
const { password: _pw, ...safeConfig } = config;
|
|
872
|
-
|
|
872
|
+
// 只有部署字段才需要重启;偏好字段已经热生效。
|
|
873
|
+
res.json({ ok: true, config: safeConfig, restartRequired: touchedDeployField });
|
|
873
874
|
}
|
|
874
875
|
catch (error) {
|
|
875
876
|
res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface ShortcutLogContext {
|
|
|
35
35
|
export declare class SessionLogger {
|
|
36
36
|
private readonly baseDir;
|
|
37
37
|
private readonly dirs;
|
|
38
|
+
/** Cached on-disk size of hot-path log files so we can rotate without stat'ing on every chunk. */
|
|
39
|
+
private readonly logSizes;
|
|
38
40
|
private readonly shortcutLogMaxBytes;
|
|
39
41
|
constructor(configDir: string, shortcutLogMaxBytes?: number);
|
|
40
42
|
private ensureDir;
|
|
@@ -67,6 +69,6 @@ export declare class SessionLogger {
|
|
|
67
69
|
deleteSession(sessionId: string): void;
|
|
68
70
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
69
71
|
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string, ctx?: ShortcutLogContext): void;
|
|
70
|
-
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
72
|
+
/** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
|
|
71
73
|
private truncateShortcutLog;
|
|
72
74
|
}
|