@co0ontty/wand 4.43.0 → 4.44.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/dist/build-info.json +3 -3
- package/dist/cli.js +3 -0
- package/dist/missions.d.ts +3 -1
- package/dist/missions.js +18 -0
- package/dist/password-manager.d.ts +4 -0
- package/dist/password-manager.js +37 -0
- package/dist/server-file-routes.js +12 -6
- package/dist/server-mission-routes.js +4 -2
- package/dist/server-session-routes.js +63 -13
- package/dist/server-update-routes.js +1 -1
- package/dist/server.js +37 -9
- package/dist/session-transport.d.ts +2 -0
- package/dist/session-transport.js +5 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +17 -5
- package/dist/structured-session-manager.js +3 -1
- package/dist/types.d.ts +1 -1
- package/dist/ws-broadcast.d.ts +3 -1
- package/dist/ws-broadcast.js +5 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "1fe712a2510fcedc297a8256f101b7c1638cf552",
|
|
3
|
+
"builtAt": "2026-08-22T02:47:13.556Z",
|
|
4
|
+
"version": "4.44.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -177,6 +177,7 @@ async function main() {
|
|
|
177
177
|
case "session:read":
|
|
178
178
|
case "session:send":
|
|
179
179
|
case "session:wait":
|
|
180
|
+
case "inbox:list":
|
|
180
181
|
case "mission:list":
|
|
181
182
|
case "mission:create":
|
|
182
183
|
case "mission:diff":
|
|
@@ -224,6 +225,7 @@ Commands:
|
|
|
224
225
|
|
|
225
226
|
Agent runtime:
|
|
226
227
|
wand session:list List sessions as JSON
|
|
228
|
+
wand inbox:list List mission inbox items as JSON
|
|
227
229
|
wand session:read <id> Read a complete session snapshot
|
|
228
230
|
wand session:send <id> <text>
|
|
229
231
|
wand session:wait <id> [--timeout 900]
|
|
@@ -260,6 +262,7 @@ async function runAgentCliCommand(command, args, configPath) {
|
|
|
260
262
|
return value;
|
|
261
263
|
};
|
|
262
264
|
switch (command) {
|
|
265
|
+
case "inbox:list": return output(await api.get("/api/inbox"));
|
|
263
266
|
case "session:list": return output(await api.get("/api/sessions"));
|
|
264
267
|
case "session:read": {
|
|
265
268
|
const id = required(args[1], "wand session:read <id>");
|
package/dist/missions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
1
|
+
import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
2
2
|
import type { SessionRegistry } from "./session-registry.js";
|
|
3
3
|
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import type { WandStorage } from "./storage.js";
|
|
@@ -16,6 +16,8 @@ export declare class Missions {
|
|
|
16
16
|
get(id: string): MissionDetails | null;
|
|
17
17
|
create(input: CreateMissionInput): MissionDetails;
|
|
18
18
|
ingest(event: ProcessEvent): void;
|
|
19
|
+
inbox(): AgentActivityItem[];
|
|
20
|
+
markInboxRead(sessionId?: string): void;
|
|
19
21
|
diff(missionId: string, attemptId: string): MissionDiff;
|
|
20
22
|
addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
|
|
21
23
|
sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
|
package/dist/missions.js
CHANGED
|
@@ -165,8 +165,26 @@ export class Missions {
|
|
|
165
165
|
error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
|
|
166
166
|
updatedAt,
|
|
167
167
|
});
|
|
168
|
+
this.storage.upsertAgentActivity({
|
|
169
|
+
sessionId: event.sessionId,
|
|
170
|
+
missionId: attempt.missionId,
|
|
171
|
+
attemptId: attempt.id,
|
|
172
|
+
state,
|
|
173
|
+
title: snapshot.title?.trim() || snapshot.summary?.trim() || firstPromptLine(this.storage.getMission(attempt.missionId)?.prompt ?? ""),
|
|
174
|
+
summary: sessionSummary(snapshot),
|
|
175
|
+
provider: snapshot.provider ?? attempt.provider,
|
|
176
|
+
cwd: snapshot.cwd,
|
|
177
|
+
updatedAt,
|
|
178
|
+
readAt: null,
|
|
179
|
+
});
|
|
168
180
|
this.refreshMissionStatus(attempt.missionId);
|
|
169
181
|
}
|
|
182
|
+
inbox() {
|
|
183
|
+
return this.storage.listAgentActivity();
|
|
184
|
+
}
|
|
185
|
+
markInboxRead(sessionId) {
|
|
186
|
+
this.storage.markAgentActivityRead(sessionId);
|
|
187
|
+
}
|
|
170
188
|
diff(missionId, attemptId) {
|
|
171
189
|
const attempt = this.requireAttempt(missionId, attemptId);
|
|
172
190
|
if (!attempt.worktreePath || !attempt.baseRef)
|
|
@@ -77,6 +77,10 @@ export interface PasswordGeneratorOptions {
|
|
|
77
77
|
digits?: boolean;
|
|
78
78
|
symbols?: boolean;
|
|
79
79
|
}
|
|
80
|
+
/** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
|
|
81
|
+
export declare function encryptVaultSecret(plaintext: string, secret: string): string;
|
|
82
|
+
export declare function decryptVaultSecret(value: string | undefined, secret: string | null): string | undefined;
|
|
83
|
+
export declare function isEncryptedVaultSecret(value: string | undefined): boolean;
|
|
80
84
|
export declare function generatePassword(options?: PasswordGeneratorOptions): string;
|
|
81
85
|
export declare function generateTotpCode(secret: string, timeMs?: number, digits?: number, period?: number): string;
|
|
82
86
|
export declare function decodeTotpSecret(secret: string): Buffer;
|
package/dist/password-manager.js
CHANGED
|
@@ -240,6 +240,43 @@ export function buildPasswordSecurityReport(items, now = Date.now()) {
|
|
|
240
240
|
issues: issues.sort((a, b) => issueRank(b.severity) - issueRank(a.severity)),
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
+
const VAULT_CIPHER_PREFIX = "enc:v1:";
|
|
244
|
+
function deriveVaultKey(secret) {
|
|
245
|
+
return crypto.createHash("sha256").update(secret, "utf8").digest();
|
|
246
|
+
}
|
|
247
|
+
/** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
|
|
248
|
+
export function encryptVaultSecret(plaintext, secret) {
|
|
249
|
+
if (!plaintext || !secret)
|
|
250
|
+
return plaintext;
|
|
251
|
+
if (plaintext.startsWith(VAULT_CIPHER_PREFIX))
|
|
252
|
+
return plaintext;
|
|
253
|
+
const iv = crypto.randomBytes(12);
|
|
254
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", deriveVaultKey(secret), iv);
|
|
255
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
256
|
+
return `${VAULT_CIPHER_PREFIX}${iv.toString("base64url")}.${cipher.getAuthTag().toString("base64url")}.${encrypted.toString("base64url")}`;
|
|
257
|
+
}
|
|
258
|
+
export function decryptVaultSecret(value, secret) {
|
|
259
|
+
if (!value)
|
|
260
|
+
return undefined;
|
|
261
|
+
if (!value.startsWith(VAULT_CIPHER_PREFIX))
|
|
262
|
+
return value;
|
|
263
|
+
if (!secret)
|
|
264
|
+
return undefined;
|
|
265
|
+
try {
|
|
266
|
+
const [ivB64, tagB64, dataB64] = value.slice(VAULT_CIPHER_PREFIX.length).split(".");
|
|
267
|
+
if (!ivB64 || !tagB64 || !dataB64)
|
|
268
|
+
return undefined;
|
|
269
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", deriveVaultKey(secret), Buffer.from(ivB64, "base64url"));
|
|
270
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
|
|
271
|
+
return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64url")), decipher.final()]).toString("utf8");
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function isEncryptedVaultSecret(value) {
|
|
278
|
+
return typeof value === "string" && value.startsWith(VAULT_CIPHER_PREFIX);
|
|
279
|
+
}
|
|
243
280
|
export function generatePassword(options = {}) {
|
|
244
281
|
const length = clampInteger(options.length ?? 20, 8, 80);
|
|
245
282
|
const lower = "abcdefghijkmnopqrstuvwxyz";
|
|
@@ -7,7 +7,7 @@ import process from "node:process";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getErrorMessage } from "./error-utils.js";
|
|
9
9
|
import { asyncRoute } from "./express-async.js";
|
|
10
|
-
import { isBlockedFolderPath,
|
|
10
|
+
import { isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
11
11
|
import { parseBoundedInteger } from "./request-limits.js";
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
const DIRECTORY_MAX_ITEMS = 200;
|
|
@@ -493,15 +493,21 @@ export function registerFileRoutes(app, deps) {
|
|
|
493
493
|
}));
|
|
494
494
|
app.get("/api/file-search", asyncRoute(async (req, res) => {
|
|
495
495
|
const query = typeof req.query.q === "string" ? req.query.q.trim().slice(0, 256) : "";
|
|
496
|
-
const cwd = typeof req.query.cwd === "string" ? req.query.cwd :
|
|
496
|
+
const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
|
|
497
497
|
const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
|
|
498
498
|
const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
|
|
499
499
|
const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads"]);
|
|
500
500
|
const maxVisitedEntries = 20_000;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
501
|
+
let resolvedCwd;
|
|
502
|
+
try {
|
|
503
|
+
resolvedCwd = normalizeFolderPath(cwd);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
res.status(400).json({ error: "无效的搜索目录。" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (isBlockedFolderPath(resolvedCwd)) {
|
|
510
|
+
res.status(403).json({ error: "访问被拒绝:不能搜索系统目录。" });
|
|
505
511
|
return;
|
|
506
512
|
}
|
|
507
513
|
if (!query) {
|
|
@@ -6,9 +6,11 @@ function sendMissionError(res, error) {
|
|
|
6
6
|
}
|
|
7
7
|
export function registerMissionRoutes(app, missions) {
|
|
8
8
|
app.get("/api/inbox", (_req, res) => {
|
|
9
|
-
res.json({ items:
|
|
9
|
+
res.json({ items: missions.inbox() });
|
|
10
10
|
});
|
|
11
|
-
app.post("/api/inbox/read", (
|
|
11
|
+
app.post("/api/inbox/read", (req, res) => {
|
|
12
|
+
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId.trim() : "";
|
|
13
|
+
missions.markInboxRead(sessionId || undefined);
|
|
12
14
|
res.json({ ok: true });
|
|
13
15
|
});
|
|
14
16
|
app.get("/api/missions", (_req, res) => {
|
|
@@ -1067,7 +1067,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1067
1067
|
return;
|
|
1068
1068
|
}
|
|
1069
1069
|
const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
|
|
1070
|
-
res.status(201).json(newSnapshot);
|
|
1070
|
+
res.status(201).json(sessionResponseDTO(newSnapshot));
|
|
1071
1071
|
}
|
|
1072
1072
|
catch (error) {
|
|
1073
1073
|
res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
|
|
@@ -1316,6 +1316,50 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1316
1316
|
res.status(400).json({ error: getErrorMessage(error, "无法按 Qoder 会话 ID 恢复会话。") });
|
|
1317
1317
|
}
|
|
1318
1318
|
});
|
|
1319
|
+
const resumeNamedStructuredSession = (req, res, spec) => {
|
|
1320
|
+
const sessionId = String(req.params.sessionId || "").trim();
|
|
1321
|
+
const body = req.body;
|
|
1322
|
+
try {
|
|
1323
|
+
if (!isSafeProviderSessionId(sessionId)) {
|
|
1324
|
+
res.status(400).json({ error: `${spec.label} 会话 ID 格式无效。` });
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const cwd = body.cwd?.trim();
|
|
1328
|
+
if (!cwd) {
|
|
1329
|
+
res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const snapshot = structured.createSession({
|
|
1333
|
+
cwd,
|
|
1334
|
+
mode: parseExecutionMode(body.mode, defaultMode),
|
|
1335
|
+
provider: spec.provider,
|
|
1336
|
+
runner: spec.runner,
|
|
1337
|
+
worktreeEnabled: body.worktreeEnabled === true,
|
|
1338
|
+
claudeSessionId: sessionId,
|
|
1339
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1340
|
+
...parseSessionCreationOrigin(body),
|
|
1341
|
+
});
|
|
1342
|
+
onSessionCreated?.(cwd);
|
|
1343
|
+
res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
app.post("/api/grok-sessions/:sessionId/resume", (req, res) => {
|
|
1350
|
+
resumeNamedStructuredSession(req, res, {
|
|
1351
|
+
provider: "grok",
|
|
1352
|
+
runner: "grok-cli-headless",
|
|
1353
|
+
label: "Grok",
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
app.post("/api/pi-sessions/:sessionId/resume", (req, res) => {
|
|
1357
|
+
resumeNamedStructuredSession(req, res, {
|
|
1358
|
+
provider: "pi",
|
|
1359
|
+
runner: "pi-cli-json",
|
|
1360
|
+
label: "Pi",
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1319
1363
|
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
1320
1364
|
const body = req.body;
|
|
1321
1365
|
try {
|
|
@@ -1333,7 +1377,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1333
1377
|
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
1334
1378
|
try {
|
|
1335
1379
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1336
|
-
res.status(
|
|
1380
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1337
1381
|
return;
|
|
1338
1382
|
}
|
|
1339
1383
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1341,7 +1385,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1341
1385
|
res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
|
|
1342
1386
|
return;
|
|
1343
1387
|
}
|
|
1344
|
-
res.json(processes.approvePermission(req.params.id));
|
|
1388
|
+
res.json(sessionResponseDTO(processes.approvePermission(req.params.id)));
|
|
1345
1389
|
}
|
|
1346
1390
|
catch (error) {
|
|
1347
1391
|
res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
|
|
@@ -1350,7 +1394,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1350
1394
|
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
1351
1395
|
try {
|
|
1352
1396
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1353
|
-
res.status(
|
|
1397
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1354
1398
|
return;
|
|
1355
1399
|
}
|
|
1356
1400
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1358,7 +1402,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1358
1402
|
res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
|
|
1359
1403
|
return;
|
|
1360
1404
|
}
|
|
1361
|
-
res.json(processes.denyPermission(req.params.id));
|
|
1405
|
+
res.json(sessionResponseDTO(processes.denyPermission(req.params.id)));
|
|
1362
1406
|
}
|
|
1363
1407
|
catch (error) {
|
|
1364
1408
|
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
@@ -1367,7 +1411,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1367
1411
|
app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
|
|
1368
1412
|
try {
|
|
1369
1413
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1370
|
-
res.status(
|
|
1414
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1371
1415
|
return;
|
|
1372
1416
|
}
|
|
1373
1417
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1375,7 +1419,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1375
1419
|
res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
|
|
1376
1420
|
return;
|
|
1377
1421
|
}
|
|
1378
|
-
res.json(processes.toggleAutoApprove(req.params.id));
|
|
1422
|
+
res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id)));
|
|
1379
1423
|
}
|
|
1380
1424
|
catch (error) {
|
|
1381
1425
|
res.status(400).json({ error: getErrorMessage(error, "无法切换自动批准状态。") });
|
|
@@ -1391,10 +1435,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1391
1435
|
return;
|
|
1392
1436
|
}
|
|
1393
1437
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1394
|
-
res.
|
|
1438
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1395
1439
|
return;
|
|
1396
1440
|
}
|
|
1397
|
-
res.json(processes.resolveEscalation(req.params.id, requestId, resolution));
|
|
1441
|
+
res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution)));
|
|
1398
1442
|
}
|
|
1399
1443
|
catch (error) {
|
|
1400
1444
|
res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
|
|
@@ -1403,10 +1447,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1403
1447
|
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
1404
1448
|
try {
|
|
1405
1449
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1406
|
-
res.json(structured.stop(req.params.id));
|
|
1450
|
+
res.json(sessionResponseDTO(structured.stop(req.params.id)));
|
|
1407
1451
|
return;
|
|
1408
1452
|
}
|
|
1409
|
-
res.json(processes.stop(req.params.id));
|
|
1453
|
+
res.json(sessionResponseDTO(processes.stop(req.params.id)));
|
|
1410
1454
|
}
|
|
1411
1455
|
catch (error) {
|
|
1412
1456
|
res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
|
|
@@ -1423,8 +1467,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1423
1467
|
});
|
|
1424
1468
|
}
|
|
1425
1469
|
export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
|
|
1426
|
-
//
|
|
1427
|
-
// longer imports provider-native history into its session
|
|
1470
|
+
// Intentional compatibility stub: older native clients still GET these
|
|
1471
|
+
// endpoints. Wand no longer imports provider-native history into its session
|
|
1472
|
+
// list, so the supported response remains an empty array.
|
|
1428
1473
|
app.get("/api/claude-history", (_req, res) => {
|
|
1429
1474
|
res.json([]);
|
|
1430
1475
|
});
|
|
@@ -1595,6 +1640,11 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
|
|
|
1595
1640
|
remove: (ids) => processes.deleteQoderHistoryFiles(ids),
|
|
1596
1641
|
},
|
|
1597
1642
|
];
|
|
1643
|
+
for (const provider of ["grok", "pi"]) {
|
|
1644
|
+
app.get(`/api/${provider}-history`, (_req, res) => {
|
|
1645
|
+
res.json([]);
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1598
1648
|
for (const config of externalHistoryProviders) {
|
|
1599
1649
|
app.get(`/api/${config.provider}-history`, (_req, res) => {
|
|
1600
1650
|
res.json([]);
|
|
@@ -36,7 +36,7 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
36
36
|
});
|
|
37
37
|
}));
|
|
38
38
|
app.get("/android/download", asyncRoute(async (req, res) => {
|
|
39
|
-
const channel = req.query.channel === "
|
|
39
|
+
const channel = req.query.channel === "beta" ? "beta" : "stable";
|
|
40
40
|
const asset = await deps.resolveAndroidDownload(channel);
|
|
41
41
|
if (!asset) {
|
|
42
42
|
res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
|
package/dist/server.js
CHANGED
|
@@ -37,10 +37,11 @@ import { computeRelaunch } from "./relaunch.js";
|
|
|
37
37
|
import { RuntimeConfigState } from "./runtime-config.js";
|
|
38
38
|
import { isServiceInstalled } from "./tui/commands.js";
|
|
39
39
|
import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
|
|
40
|
+
import { toSessionDetailDTO } from "./session-transport.js";
|
|
40
41
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
41
42
|
import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
|
|
42
43
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
43
|
-
import {
|
|
44
|
+
import { buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
|
|
44
45
|
import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
|
|
45
46
|
import { DistributionManager } from "./distribution-manager.js";
|
|
46
47
|
import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
@@ -254,10 +255,19 @@ function authenticateBearerAppToken(req, storage, config) {
|
|
|
254
255
|
return null;
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
function
|
|
258
|
+
function resolveRequestServerUrl(req, config, useHttps) {
|
|
259
|
+
const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
|
|
260
|
+
const requestHost = getPublicRequestHost(req, config);
|
|
261
|
+
const originHeader = firstHeaderValue(req.headers.origin);
|
|
262
|
+
const browserOrigin = isBrowserExtensionOrigin(originHeader)
|
|
263
|
+
? undefined
|
|
264
|
+
: normalizePublicOrigin(originHeader);
|
|
265
|
+
return resolveAppConnectOrigin(browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
|
|
266
|
+
}
|
|
267
|
+
function appTokenLoginPayload(req, storage, config, useHttps) {
|
|
258
268
|
return {
|
|
259
269
|
appToken: generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? ""),
|
|
260
|
-
serverUrl:
|
|
270
|
+
serverUrl: resolveRequestServerUrl(req, config, useHttps),
|
|
261
271
|
};
|
|
262
272
|
}
|
|
263
273
|
// ── App connection token helpers ──
|
|
@@ -563,7 +573,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
563
573
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
564
574
|
res.type("html").send(renderApp(configPath));
|
|
565
575
|
});
|
|
566
|
-
app.get("/api/structured-chat-avatar/:role", asyncRoute(async (req, res) => {
|
|
576
|
+
app.get("/api/structured-chat-avatar/:role", requireAuth, asyncRoute(async (req, res) => {
|
|
567
577
|
const role = req.params.role === "user" || req.params.role === "assistant"
|
|
568
578
|
? req.params.role
|
|
569
579
|
: null;
|
|
@@ -662,7 +672,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
662
672
|
res.json({
|
|
663
673
|
ok: true,
|
|
664
674
|
principal,
|
|
665
|
-
...(client === "browser-extension" ? appTokenLoginPayload(storage, config) : {}),
|
|
675
|
+
...(client === "browser-extension" ? appTokenLoginPayload(req, storage, config, useHttps) : {}),
|
|
666
676
|
});
|
|
667
677
|
});
|
|
668
678
|
app.post("/api/logout", (req, res) => {
|
|
@@ -700,17 +710,28 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
700
710
|
"/api/config",
|
|
701
711
|
"/api/models",
|
|
702
712
|
"/api/sessions",
|
|
713
|
+
"/api/session-list",
|
|
703
714
|
"/api/session-directories",
|
|
704
715
|
"/api/structured-sessions",
|
|
705
716
|
"/api/commands",
|
|
706
717
|
"/api/claude-skills",
|
|
707
718
|
"/api/claude-history",
|
|
708
719
|
"/api/codex-history",
|
|
720
|
+
"/api/opencode-history",
|
|
721
|
+
"/api/qoder-history",
|
|
722
|
+
"/api/grok-history",
|
|
723
|
+
"/api/pi-history",
|
|
709
724
|
"/api/claude-sessions",
|
|
710
725
|
"/api/codex-sessions",
|
|
726
|
+
"/api/opencode-sessions",
|
|
727
|
+
"/api/qoder-sessions",
|
|
728
|
+
"/api/grok-sessions",
|
|
729
|
+
"/api/pi-sessions",
|
|
711
730
|
"/api/optimize-prompt",
|
|
712
731
|
"/api/inbox",
|
|
713
732
|
"/api/missions",
|
|
733
|
+
"/api/workspaces",
|
|
734
|
+
"/api/workspace-tasks",
|
|
714
735
|
], requireSessions);
|
|
715
736
|
app.use([
|
|
716
737
|
"/api/directory",
|
|
@@ -720,6 +741,13 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
720
741
|
"/api/file-preview",
|
|
721
742
|
"/api/file-raw",
|
|
722
743
|
"/api/file-write",
|
|
744
|
+
"/api/file-create",
|
|
745
|
+
"/api/dir-create",
|
|
746
|
+
"/api/file-rename",
|
|
747
|
+
"/api/file-delete",
|
|
748
|
+
"/api/quick-paths",
|
|
749
|
+
"/api/validate-path",
|
|
750
|
+
"/api/file-search",
|
|
723
751
|
], requireFiles);
|
|
724
752
|
app.use("/api/browser-extension", requirePasswordVault);
|
|
725
753
|
// ── Config & Session info ──
|
|
@@ -774,10 +802,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
774
802
|
res.status(400).json({ error: getErrorMessage(error, "无法读取 skills。") });
|
|
775
803
|
}
|
|
776
804
|
});
|
|
777
|
-
app.get("/api/browser-extension/status", (
|
|
805
|
+
app.get("/api/browser-extension/status", (req, res) => {
|
|
778
806
|
res.json({
|
|
779
807
|
ok: true,
|
|
780
|
-
serverUrl:
|
|
808
|
+
serverUrl: resolveRequestServerUrl(req, config, useHttps),
|
|
781
809
|
features: {
|
|
782
810
|
loginAutofill: true,
|
|
783
811
|
saveLogins: true,
|
|
@@ -1044,7 +1072,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1044
1072
|
...origin,
|
|
1045
1073
|
}));
|
|
1046
1074
|
recordRecentPath(storage, snapshot.cwd);
|
|
1047
|
-
res.status(201).json(snapshot);
|
|
1075
|
+
res.status(201).json(toSessionDetailDTO(snapshot));
|
|
1048
1076
|
}
|
|
1049
1077
|
catch (error) {
|
|
1050
1078
|
res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
|
|
@@ -1098,7 +1126,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1098
1126
|
concurrencyLimit: 10,
|
|
1099
1127
|
},
|
|
1100
1128
|
});
|
|
1101
|
-
const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps, authService);
|
|
1129
|
+
const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps, authService, (req) => authenticateBearerAppToken(req, storage, config) !== null);
|
|
1102
1130
|
wsManager.setup({
|
|
1103
1131
|
getSession: (id) => sessionRegistry.get(id),
|
|
1104
1132
|
getTerminalState: (id) => processes.getTerminalState(id),
|
|
@@ -3,6 +3,8 @@ export declare const SESSION_TRANSPORT_OUTPUT_LIMIT = 200000;
|
|
|
3
3
|
export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages" | "title" | "ptyOutputSeq" | "ptyLaunchMarkerToken"> & {
|
|
4
4
|
/** Canonical server-resolved title. Clients must not invent their own fallback. */
|
|
5
5
|
title: string;
|
|
6
|
+
/** Alias of claudeSessionId; that field stores every provider's native resume id. */
|
|
7
|
+
providerSessionId?: string | null;
|
|
6
8
|
};
|
|
7
9
|
export interface SessionListItemDTO extends SessionBaseDTO {
|
|
8
10
|
/** Kept for compatibility with clients that initialize terminal state from the list. */
|
|
@@ -45,7 +45,9 @@ function sessionBase(snapshot) {
|
|
|
45
45
|
pendingEscalation: snapshot.pendingEscalation,
|
|
46
46
|
lastEscalationResult: snapshot.lastEscalationResult,
|
|
47
47
|
claudeSessionId: snapshot.claudeSessionId,
|
|
48
|
+
providerSessionId: snapshot.claudeSessionId,
|
|
48
49
|
queuedMessages: snapshot.queuedMessages,
|
|
50
|
+
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
49
51
|
structuredState: snapshot.structuredState,
|
|
50
52
|
resumedFromSessionId: snapshot.resumedFromSessionId,
|
|
51
53
|
autoRecovered: snapshot.autoRecovered,
|
|
@@ -54,7 +56,10 @@ function sessionBase(snapshot) {
|
|
|
54
56
|
summary: snapshot.summary,
|
|
55
57
|
title: resolveSessionDisplayTitle(snapshot),
|
|
56
58
|
description: snapshot.description,
|
|
59
|
+
titleGenerating: snapshot.titleGenerating,
|
|
57
60
|
currentTaskTitle: snapshot.currentTaskTitle,
|
|
61
|
+
workspaceId: snapshot.workspaceId,
|
|
62
|
+
workspaceTaskId: snapshot.workspaceTaskId,
|
|
58
63
|
selectedModel: snapshot.selectedModel,
|
|
59
64
|
thinkingEffort: snapshot.thinkingEffort,
|
|
60
65
|
ptyCols: snapshot.ptyCols,
|
package/dist/storage.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export declare class WandStorage {
|
|
|
98
98
|
getAppSecret(): string | null;
|
|
99
99
|
/** Persist appSecret in database (DB is the authoritative source after first migration) */
|
|
100
100
|
setAppSecret(value: string): void;
|
|
101
|
+
private encryptStoredPassword;
|
|
102
|
+
private decryptPasswordItem;
|
|
101
103
|
ensureDefaultPasswordVault(): void;
|
|
102
104
|
listPasswordVaults(): PasswordVault[];
|
|
103
105
|
createPasswordVault(nameInput: unknown): PasswordVault;
|
package/dist/storage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { normalizeSessionDirectory } from "./session-directory-tree.js";
|
|
6
|
-
import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
|
|
6
|
+
import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
|
|
7
7
|
function safeJsonParse(raw) {
|
|
8
8
|
if (!raw)
|
|
9
9
|
return undefined;
|
|
@@ -987,6 +987,15 @@ export class WandStorage {
|
|
|
987
987
|
setAppSecret(value) {
|
|
988
988
|
this.setConfigValue("appSecret", value);
|
|
989
989
|
}
|
|
990
|
+
encryptStoredPassword(password) {
|
|
991
|
+
if (!password)
|
|
992
|
+
return null;
|
|
993
|
+
const secret = this.getAppSecret();
|
|
994
|
+
return secret ? encryptVaultSecret(password, secret) : password;
|
|
995
|
+
}
|
|
996
|
+
decryptPasswordItem(item) {
|
|
997
|
+
return { ...item, password: decryptVaultSecret(item.password, this.getAppSecret()) };
|
|
998
|
+
}
|
|
990
999
|
// ============ Browser Extension Password Vault Methods ============
|
|
991
1000
|
ensureDefaultPasswordVault() {
|
|
992
1001
|
const now = nowIso();
|
|
@@ -1030,7 +1039,8 @@ export class WandStorage {
|
|
|
1030
1039
|
const limit = typeof filter.limit === "number" && Number.isFinite(filter.limit)
|
|
1031
1040
|
? Math.max(1, Math.min(200, Math.floor(filter.limit)))
|
|
1032
1041
|
: 100;
|
|
1033
|
-
return rows.map(
|
|
1042
|
+
return rows.map((row) => this.decryptPasswordItem(mapPasswordItemRow(row)))
|
|
1043
|
+
.filter((item) => itemMatchesFilter(item, filter)).slice(0, limit);
|
|
1034
1044
|
}
|
|
1035
1045
|
getPasswordItem(id) {
|
|
1036
1046
|
const row = this.db
|
|
@@ -1039,7 +1049,7 @@ export class WandStorage {
|
|
|
1039
1049
|
FROM password_items
|
|
1040
1050
|
WHERE id = ? AND archived = 0`)
|
|
1041
1051
|
.get(id);
|
|
1042
|
-
return row ? mapPasswordItemRow(row) : null;
|
|
1052
|
+
return row ? this.decryptPasswordItem(mapPasswordItemRow(row)) : null;
|
|
1043
1053
|
}
|
|
1044
1054
|
createPasswordItem(input) {
|
|
1045
1055
|
this.ensureDefaultPasswordVault();
|
|
@@ -1055,7 +1065,7 @@ export class WandStorage {
|
|
|
1055
1065
|
id, vault_id, type, title, username, password, urls, notes, fields, tags, favorite, archived,
|
|
1056
1066
|
created_at, updated_at, last_used_at, password_updated_at
|
|
1057
1067
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?)`)
|
|
1058
|
-
.run(id, vaultId, normalized.type, normalized.title, normalized.username ?? null, normalized.password
|
|
1068
|
+
.run(id, vaultId, normalized.type, normalized.title, normalized.username ?? null, this.encryptStoredPassword(normalized.password), JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, now, passwordUpdatedAt ?? null);
|
|
1059
1069
|
return this.getPasswordItem(id);
|
|
1060
1070
|
}
|
|
1061
1071
|
updatePasswordItem(id, input) {
|
|
@@ -1086,7 +1096,7 @@ export class WandStorage {
|
|
|
1086
1096
|
SET vault_id = ?, type = ?, title = ?, username = ?, password = ?, urls = ?, notes = ?,
|
|
1087
1097
|
fields = ?, tags = ?, favorite = ?, updated_at = ?, password_updated_at = ?
|
|
1088
1098
|
WHERE id = ? AND archived = 0`)
|
|
1089
|
-
.run(vaultId, normalized.type, normalized.title, normalized.username ?? null, normalized.password
|
|
1099
|
+
.run(vaultId, normalized.type, normalized.title, normalized.username ?? null, this.encryptStoredPassword(normalized.password), JSON.stringify(normalized.urls), normalized.notes ?? null, JSON.stringify(normalized.fields), JSON.stringify(normalized.tags), normalized.favorite ? 1 : 0, now, passwordUpdatedAt, id);
|
|
1090
1100
|
return this.getPasswordItem(id);
|
|
1091
1101
|
}
|
|
1092
1102
|
touchPasswordItem(id) {
|
|
@@ -1416,6 +1426,8 @@ const SCHEMA_MIGRATIONS = [
|
|
|
1416
1426
|
["queued_message_skills", "ALTER TABLE command_sessions ADD COLUMN queued_message_skills TEXT"],
|
|
1417
1427
|
["structured_state", "ALTER TABLE command_sessions ADD COLUMN structured_state TEXT"],
|
|
1418
1428
|
["resumed_from_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT"],
|
|
1429
|
+
// Legacy column: never written by current persist helpers. Kept because
|
|
1430
|
+
// schema migrations are additive-only and must not DROP columns.
|
|
1419
1431
|
["resumed_to_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT"],
|
|
1420
1432
|
["auto_recovered", "ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0"],
|
|
1421
1433
|
["worktree_enabled", "ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -255,7 +255,9 @@ export class StructuredSessionManager {
|
|
|
255
255
|
provider,
|
|
256
256
|
runner,
|
|
257
257
|
model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
|
|
258
|
-
lastError: snapshot.
|
|
258
|
+
lastError: snapshot.status === "running"
|
|
259
|
+
? "服务重启,上一轮已中断。"
|
|
260
|
+
: snapshot.structuredState?.lastError ?? null,
|
|
259
261
|
inFlight: false,
|
|
260
262
|
activeRequestId: null,
|
|
261
263
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -539,7 +539,7 @@ export interface SessionSnapshot {
|
|
|
539
539
|
resolution: EscalationResolution;
|
|
540
540
|
reason: string;
|
|
541
541
|
} | null;
|
|
542
|
-
/** Claude
|
|
542
|
+
/** Native resume id for every provider (Claude UUID, Codex thread, OpenCode ses_*, …). Wire alias: providerSessionId. */
|
|
543
543
|
claudeSessionId: string | null;
|
|
544
544
|
/** Structured conversation messages derived from PTY output. */
|
|
545
545
|
messages?: ConversationTurn[];
|
package/dist/ws-broadcast.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* WebSocket broadcast manager for process events.
|
|
3
3
|
* Handles debounced output events, backpressure control, and client subscriptions.
|
|
4
4
|
*/
|
|
5
|
+
import type { IncomingMessage } from "node:http";
|
|
5
6
|
import { WebSocketServer } from "ws";
|
|
6
7
|
import type { CardExpandDefaults, SessionSnapshot, ProcessEvent } from "./types.js";
|
|
7
8
|
import { type AuthService } from "./auth.js";
|
|
@@ -26,7 +27,8 @@ export declare class WsBroadcastManager {
|
|
|
26
27
|
private getCardDefaults;
|
|
27
28
|
private useHttps;
|
|
28
29
|
private authService?;
|
|
29
|
-
|
|
30
|
+
private authenticateRequest?;
|
|
31
|
+
constructor(wss: WebSocketServer, getCardDefaults?: () => CardExpandDefaults, useHttps?: boolean, authService?: Pick<AuthService, "validateSession">, authenticateRequest?: (req: IncomingMessage) => boolean);
|
|
30
32
|
/** Immediately disconnect all authenticated clients after global revocation. */
|
|
31
33
|
disconnectAll(): void;
|
|
32
34
|
/** Stop timers, discard deferred output, and terminate every client. */
|
package/dist/ws-broadcast.js
CHANGED
|
@@ -39,11 +39,13 @@ export class WsBroadcastManager {
|
|
|
39
39
|
getCardDefaults;
|
|
40
40
|
useHttps;
|
|
41
41
|
authService;
|
|
42
|
-
|
|
42
|
+
authenticateRequest;
|
|
43
|
+
constructor(wss, getCardDefaults, useHttps = false, authService, authenticateRequest) {
|
|
43
44
|
this.wss = wss;
|
|
44
45
|
this.getCardDefaults = getCardDefaults ?? (() => ({}));
|
|
45
46
|
this.useHttps = useHttps;
|
|
46
47
|
this.authService = authService;
|
|
48
|
+
this.authenticateRequest = authenticateRequest;
|
|
47
49
|
}
|
|
48
50
|
/** Immediately disconnect all authenticated clients after global revocation. */
|
|
49
51
|
disconnectAll() {
|
|
@@ -79,7 +81,8 @@ export class WsBroadcastManager {
|
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
const sessionToken = readSessionCookie(req, this.useHttps);
|
|
82
|
-
|
|
84
|
+
const cookieOk = !!sessionToken && !!this.authService?.validateSession(sessionToken);
|
|
85
|
+
if (!cookieOk && !this.authenticateRequest?.(req)) {
|
|
83
86
|
ws.close(1008, "Unauthorized");
|
|
84
87
|
return;
|
|
85
88
|
}
|