@co0ontty/wand 1.0.1 → 1.1.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 +7 -1
- package/dist/avatar.d.ts +2 -1
- package/dist/avatar.js +29 -37
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +16 -5
- package/dist/pwa.js +6 -2
- package/dist/server.js +155 -3
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +15 -0
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +614 -32
- package/dist/web-ui/content/styles.css +223 -0
- package/dist/web-ui/index.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/avatar.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export declare function ensureAvatarSeed(configDir: string): Promise<string>;
|
|
10
10
|
/**
|
|
11
11
|
* Generate an SVG identicon from a seed string.
|
|
12
|
-
* Uses a 5x5 symmetric grid
|
|
12
|
+
* Uses a 5x5 symmetric grid with white cells on a colored background.
|
|
13
|
+
* The pattern is mirrored horizontally for visual balance.
|
|
13
14
|
*/
|
|
14
15
|
export declare function getAvatarSvg(seed: string, size: 192 | 512): string;
|
package/dist/avatar.js
CHANGED
|
@@ -37,68 +37,60 @@ function generateRandomSeed() {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Generate an SVG identicon from a seed string.
|
|
40
|
-
* Uses a 5x5 symmetric grid
|
|
40
|
+
* Uses a 5x5 symmetric grid with white cells on a colored background.
|
|
41
|
+
* The pattern is mirrored horizontally for visual balance.
|
|
41
42
|
*/
|
|
42
43
|
export function getAvatarSvg(seed, size) {
|
|
43
|
-
const cellSize = Math.round(size * 0.08);
|
|
44
|
-
const gap = Math.max(1, Math.round(size * 0.01));
|
|
45
|
-
const gridSize = 5;
|
|
46
|
-
const totalWidth = gridSize * cellSize + (gridSize - 1) * gap;
|
|
47
44
|
const svgSize = size;
|
|
48
|
-
|
|
45
|
+
const gridSize = 5;
|
|
46
|
+
const padding = Math.round(svgSize * 0.12);
|
|
47
|
+
const innerSize = svgSize - padding * 2;
|
|
48
|
+
const cellSize = Math.floor(innerSize / gridSize);
|
|
49
|
+
const gridWidth = cellSize * gridSize;
|
|
50
|
+
const gridOffset = (svgSize - gridWidth) / 2;
|
|
51
|
+
// Derive color from seed — warm tones that match the Wand theme
|
|
49
52
|
const h = Math.abs(hashString(seed + "h")) % 360;
|
|
50
|
-
const s =
|
|
51
|
-
const l =
|
|
52
|
-
const
|
|
53
|
-
// Derive fill states from seed
|
|
53
|
+
const s = 55 + (Math.abs(hashString(seed + "s")) % 30);
|
|
54
|
+
const l = 42 + (Math.abs(hashString(seed + "l")) % 16);
|
|
55
|
+
const bgColor = `hsl(${h},${s}%,${l}%)`;
|
|
56
|
+
// Derive fill states from seed
|
|
54
57
|
const seedChars = seed.slice(0, 25);
|
|
55
58
|
const cells = [];
|
|
56
59
|
for (let i = 0; i < 25; i++) {
|
|
57
60
|
const char = seedChars[i] || "0";
|
|
58
61
|
cells.push(parseInt(char, 16) % 2 === 0);
|
|
59
62
|
}
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const parts = [];
|
|
64
|
+
const cornerR = Math.round(svgSize * 0.14);
|
|
65
|
+
// Full SVG background with rounded corners
|
|
66
|
+
parts.push(`<rect width="${svgSize}" height="${svgSize}" rx="${cornerR}" fill="${bgColor}"/>`);
|
|
67
|
+
// White cells on colored background — high contrast
|
|
68
|
+
const cellR = Math.round(cellSize * 0.12);
|
|
69
|
+
const cellGap = Math.max(1, Math.round(cellSize * 0.08));
|
|
70
|
+
const actualCell = cellSize - cellGap;
|
|
67
71
|
for (let row = 0; row < gridSize; row++) {
|
|
68
72
|
for (let col = 0; col < gridSize; col++) {
|
|
69
73
|
const mirrorCol = gridSize - 1 - col;
|
|
70
74
|
const idx = row * gridSize + col;
|
|
71
|
-
// Only draw for left half + middle column (right half mirrors)
|
|
72
75
|
if (col > mirrorCol)
|
|
73
76
|
continue;
|
|
74
77
|
if (!cells[idx])
|
|
75
78
|
continue;
|
|
76
|
-
|
|
77
|
-
const x1 = col * (cellSize + gap);
|
|
78
|
-
const x2 = mirrorCol * (cellSize + gap);
|
|
79
|
-
const y = row * (cellSize + gap);
|
|
80
|
-
const offsetX = (svgSize - totalWidth) / 2;
|
|
81
|
-
const offsetY = (svgSize - totalWidth) / 2;
|
|
82
|
-
// Darken color for filled cells
|
|
83
|
-
const cellColor = `hsl(${h},${s}%,${l - 15}%)`;
|
|
84
|
-
const rx = Math.round(cellSize * 0.15);
|
|
79
|
+
const y = gridOffset + row * cellSize + cellGap / 2;
|
|
85
80
|
if (col === mirrorCol) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const my = offsetY + y;
|
|
89
|
-
rects.push(`<rect x="${mx}" y="${my}" width="${cellSize}" height="${cellSize}" rx="${rx}" fill="${cellColor}"/>`);
|
|
81
|
+
const x = gridOffset + col * cellSize + cellGap / 2;
|
|
82
|
+
parts.push(`<rect x="${x}" y="${y}" width="${actualCell}" height="${actualCell}" rx="${cellR}" fill="rgba(255,255,255,0.9)"/>`);
|
|
90
83
|
}
|
|
91
84
|
else {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
rects.push(`<rect x="${mx2}" y="${my}" width="${cellSize}" height="${cellSize}" rx="${rx}" fill="${cellColor}"/>`);
|
|
85
|
+
const x1 = gridOffset + col * cellSize + cellGap / 2;
|
|
86
|
+
const x2 = gridOffset + mirrorCol * cellSize + cellGap / 2;
|
|
87
|
+
parts.push(`<rect x="${x1}" y="${y}" width="${actualCell}" height="${actualCell}" rx="${cellR}" fill="rgba(255,255,255,0.9)"/>`);
|
|
88
|
+
parts.push(`<rect x="${x2}" y="${y}" width="${actualCell}" height="${actualCell}" rx="${cellR}" fill="rgba(255,255,255,0.9)"/>`);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgSize} ${svgSize}" width="${svgSize}" height="${svgSize}">
|
|
101
|
-
${
|
|
93
|
+
${parts.join("\n ")}
|
|
102
94
|
</svg>`;
|
|
103
95
|
}
|
|
104
96
|
function hashString(s) {
|
|
@@ -53,7 +53,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
53
53
|
private static readonly HISTORY_CACHE_TTL_MS;
|
|
54
54
|
listClaudeHistorySessions(): ClaudeHistorySession[];
|
|
55
55
|
get(id: string): SessionSnapshot | null;
|
|
56
|
-
sendInput(id: string, input: string, view?: "chat" | "terminal"): SessionSnapshot;
|
|
56
|
+
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
57
57
|
/** Emit a task event for a session, debounced to avoid flooding */
|
|
58
58
|
private emitTask;
|
|
59
59
|
resize(id: string, cols: number, rows: number): SessionSnapshot;
|
package/dist/process-manager.js
CHANGED
|
@@ -1147,7 +1147,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
return this.snapshot(record);
|
|
1149
1149
|
}
|
|
1150
|
-
sendInput(id, input, view) {
|
|
1150
|
+
sendInput(id, input, view, shortcutKey) {
|
|
1151
1151
|
const record = this.mustGet(id);
|
|
1152
1152
|
if (record.status !== "running") {
|
|
1153
1153
|
console.error("[ProcessManager] Rejecting input for non-running session", {
|
|
@@ -1179,6 +1179,12 @@ export class ProcessManager extends EventEmitter {
|
|
|
1179
1179
|
inputLength: input.length,
|
|
1180
1180
|
view: view ?? "chat"
|
|
1181
1181
|
});
|
|
1182
|
+
// Log shortcut key interactions in managed/full-access modes for auto-confirm analysis
|
|
1183
|
+
if (shortcutKey && record.autoApprovePermissions) {
|
|
1184
|
+
const outputLines = record.output.split("\n");
|
|
1185
|
+
const tailLines = outputLines.slice(-15).join("\n");
|
|
1186
|
+
this.logger.appendShortcutLog(id, shortcutKey, tailLines);
|
|
1187
|
+
}
|
|
1182
1188
|
// Track user input via bridge for Chat mode
|
|
1183
1189
|
if (record.ptyBridge) {
|
|
1184
1190
|
record.ptyBridge.onUserInput(input);
|
|
@@ -1740,10 +1746,15 @@ export class ProcessManager extends EventEmitter {
|
|
|
1740
1746
|
}
|
|
1741
1747
|
return false;
|
|
1742
1748
|
}
|
|
1743
|
-
processCommandForMode(command,
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
|
|
1749
|
+
processCommandForMode(command, mode) {
|
|
1750
|
+
// In managed mode, append a system prompt instructing Claude to act autonomously
|
|
1751
|
+
// without asking the user for confirmation, since the user may not be monitoring.
|
|
1752
|
+
if (mode === "managed" && /^claude(?:\s|$)/.test(command)) {
|
|
1753
|
+
const autonomousPrompt = "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
1754
|
+
// Escape single quotes for shell safety
|
|
1755
|
+
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1756
|
+
return `${command} --append-system-prompt '${escaped}'`;
|
|
1757
|
+
}
|
|
1747
1758
|
return command;
|
|
1748
1759
|
}
|
|
1749
1760
|
}
|
package/dist/pwa.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PWA manifest and Service Worker generation.
|
|
3
3
|
*/
|
|
4
|
+
/** Cache version is fixed per server process to avoid mid-session cache busting */
|
|
5
|
+
const CACHE_VERSION = Date.now().toString(36);
|
|
4
6
|
export function generatePwaManifest() {
|
|
5
7
|
return JSON.stringify({
|
|
6
8
|
id: "/wand",
|
|
@@ -20,6 +22,8 @@ export function generatePwaManifest() {
|
|
|
20
22
|
icons: [
|
|
21
23
|
{ src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
|
|
22
24
|
{ src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "maskable" },
|
|
25
|
+
{ src: "/icon-192.png", sizes: "192x192", type: "image/svg+xml", purpose: "any" },
|
|
26
|
+
{ src: "/icon-512.png", sizes: "512x512", type: "image/svg+xml", purpose: "any" },
|
|
23
27
|
],
|
|
24
28
|
categories: ["developer tools", "productivity"],
|
|
25
29
|
shortcuts: [
|
|
@@ -34,8 +38,8 @@ export function generatePwaManifest() {
|
|
|
34
38
|
}
|
|
35
39
|
export function generateServiceWorker() {
|
|
36
40
|
return `
|
|
37
|
-
const STATIC_CACHE = 'wand-static
|
|
38
|
-
const RUNTIME_CACHE = 'wand-runtime
|
|
41
|
+
const STATIC_CACHE = 'wand-static-${CACHE_VERSION}';
|
|
42
|
+
const RUNTIME_CACHE = 'wand-runtime-${CACHE_VERSION}';
|
|
39
43
|
const APP_SHELL = '/';
|
|
40
44
|
const STATIC_ASSETS = [
|
|
41
45
|
'/manifest.json',
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createServer as createHttpServer } from "node:http";
|
|
5
5
|
import { createServer as createHttpsServer } from "node:https";
|
|
6
6
|
import { exec } from "node:child_process";
|
|
@@ -11,10 +11,45 @@ import { WebSocketServer } from "ws";
|
|
|
11
11
|
const execAsync = promisify(exec);
|
|
12
12
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
13
13
|
const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
|
|
14
|
+
// ── Package info ──
|
|
15
|
+
const PKG_JSON = JSON.parse(readFileSync(path.join(RUNTIME_ROOT_DIR, "package.json"), "utf8"));
|
|
16
|
+
const PKG_NAME = PKG_JSON.name;
|
|
17
|
+
const PKG_VERSION = PKG_JSON.version;
|
|
18
|
+
const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=22.5.0";
|
|
19
|
+
const PKG_REPO_URL = "https://github.com/co0ontty/wand";
|
|
20
|
+
// ── Update check cache ──
|
|
21
|
+
let cachedLatestVersion = null;
|
|
22
|
+
async function checkNpmLatestVersion() {
|
|
23
|
+
if (!cachedLatestVersion) {
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await execAsync(`npm view ${PKG_NAME} version`, { timeout: 15000 });
|
|
26
|
+
cachedLatestVersion = stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
cachedLatestVersion = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const latest = cachedLatestVersion || PKG_VERSION;
|
|
33
|
+
return {
|
|
34
|
+
current: PKG_VERSION,
|
|
35
|
+
latest,
|
|
36
|
+
updateAvailable: latest !== PKG_VERSION && compareSemver(latest, PKG_VERSION) > 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function compareSemver(a, b) {
|
|
40
|
+
const pa = a.split(".").map(Number);
|
|
41
|
+
const pb = b.split(".").map(Number);
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
44
|
+
if (diff !== 0)
|
|
45
|
+
return diff;
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
14
49
|
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
15
50
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
16
51
|
import { ensureCertificates } from "./cert.js";
|
|
17
|
-
import { isExecutionMode, resolveConfigDir } from "./config.js";
|
|
52
|
+
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
18
53
|
import { ProcessManager, SessionInputError } from "./process-manager.js";
|
|
19
54
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
20
55
|
import { renderApp } from "./web-ui/index.js";
|
|
@@ -337,6 +372,116 @@ export async function startServer(config, configPath) {
|
|
|
337
372
|
commandPresets: config.commandPresets,
|
|
338
373
|
});
|
|
339
374
|
});
|
|
375
|
+
// ── Settings endpoints ──
|
|
376
|
+
app.get("/api/settings", (_req, res) => {
|
|
377
|
+
const certPaths = {
|
|
378
|
+
keyPath: path.join(configDir, "server.key"),
|
|
379
|
+
certPath: path.join(configDir, "server.crt"),
|
|
380
|
+
};
|
|
381
|
+
const { password: _pw, ...safeConfig } = config;
|
|
382
|
+
res.json({
|
|
383
|
+
version: PKG_VERSION,
|
|
384
|
+
packageName: PKG_NAME,
|
|
385
|
+
nodeVersion: PKG_NODE_REQ,
|
|
386
|
+
repoUrl: PKG_REPO_URL,
|
|
387
|
+
config: safeConfig,
|
|
388
|
+
hasCert: existsSync(certPaths.keyPath) && existsSync(certPaths.certPath),
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
app.post("/api/settings/config", async (req, res) => {
|
|
392
|
+
const body = req.body;
|
|
393
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell"];
|
|
394
|
+
let changed = false;
|
|
395
|
+
for (const field of allowedFields) {
|
|
396
|
+
if (field in body && body[field] !== undefined) {
|
|
397
|
+
if (field === "port") {
|
|
398
|
+
const p = Number(body.port);
|
|
399
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) {
|
|
400
|
+
res.status(400).json({ error: `无效端口号: ${body.port}` });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
config.port = p;
|
|
404
|
+
}
|
|
405
|
+
else if (field === "https") {
|
|
406
|
+
config.https = body.https === true;
|
|
407
|
+
}
|
|
408
|
+
else if (field === "defaultMode") {
|
|
409
|
+
if (!isExecutionMode(body.defaultMode)) {
|
|
410
|
+
res.status(400).json({ error: `无效执行模式: ${body.defaultMode}` });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
config.defaultMode = body.defaultMode;
|
|
414
|
+
}
|
|
415
|
+
else if (field === "host") {
|
|
416
|
+
config.host = String(body.host);
|
|
417
|
+
}
|
|
418
|
+
else if (field === "defaultCwd") {
|
|
419
|
+
config.defaultCwd = String(body.defaultCwd);
|
|
420
|
+
}
|
|
421
|
+
else if (field === "shell") {
|
|
422
|
+
config.shell = String(body.shell);
|
|
423
|
+
}
|
|
424
|
+
changed = true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!changed) {
|
|
428
|
+
res.status(400).json({ error: "没有可更新的配置字段。" });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await saveConfig(configPath, config);
|
|
433
|
+
const { password: _pw, ...safeConfig } = config;
|
|
434
|
+
res.json({ ok: true, config: safeConfig, restartRequired: true });
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
app.post("/api/settings/upload-cert", async (req, res) => {
|
|
441
|
+
const { key, cert } = req.body;
|
|
442
|
+
if (!key || !cert) {
|
|
443
|
+
res.status(400).json({ error: "请提供 key 和 cert 内容。" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!key.includes("-----BEGIN") || !cert.includes("-----BEGIN")) {
|
|
447
|
+
res.status(400).json({ error: "证书内容格式无效,请上传 PEM 格式的文件。" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const keyPath = path.join(configDir, "server.key");
|
|
452
|
+
const certPath = path.join(configDir, "server.crt");
|
|
453
|
+
writeFileSync(keyPath, key, { mode: 0o600 });
|
|
454
|
+
writeFileSync(certPath, cert, { mode: 0o644 });
|
|
455
|
+
res.json({ ok: true, restartRequired: true });
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
res.status(500).json({ error: getErrorMessage(error, "保存证书失败。") });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
app.get("/api/check-update", async (_req, res) => {
|
|
462
|
+
try {
|
|
463
|
+
const result = await checkNpmLatestVersion();
|
|
464
|
+
res.json(result);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
res.status(500).json({ error: getErrorMessage(error, "检查更新失败。") });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
app.post("/api/update", async (_req, res) => {
|
|
471
|
+
try {
|
|
472
|
+
const { updateAvailable } = await checkNpmLatestVersion();
|
|
473
|
+
if (!updateAvailable) {
|
|
474
|
+
res.json({ ok: true, message: "已经是最新版本。" });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
res.json({ ok: true, message: "正在更新,请稍候..." });
|
|
478
|
+
// Run update in background — the server will restart
|
|
479
|
+
execAsync(`npm install -g ${PKG_NAME}@latest`, { timeout: 120000 }).catch(() => { });
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
340
485
|
app.get("/api/sessions", (_req, res) => {
|
|
341
486
|
res.json(processes.list());
|
|
342
487
|
});
|
|
@@ -870,9 +1015,10 @@ export async function startServer(config, configPath) {
|
|
|
870
1015
|
const sessionId = req.params.id;
|
|
871
1016
|
const input = body.input ?? "";
|
|
872
1017
|
const view = body.view;
|
|
1018
|
+
const shortcutKey = body.shortcutKey;
|
|
873
1019
|
console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
|
|
874
1020
|
try {
|
|
875
|
-
const snapshot = processes.sendInput(sessionId, input, view);
|
|
1021
|
+
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
876
1022
|
console.error("[wand] Input request succeeded", { sessionId, status: snapshot.status, inputLength: input.length, view: view ?? "chat" });
|
|
877
1023
|
res.json(snapshot);
|
|
878
1024
|
}
|
|
@@ -974,4 +1120,10 @@ export async function startServer(config, configPath) {
|
|
|
974
1120
|
}
|
|
975
1121
|
// Start configured background sessions after the server is already reachable.
|
|
976
1122
|
processes.runStartupCommands();
|
|
1123
|
+
// Background update check on startup
|
|
1124
|
+
checkNpmLatestVersion().then(({ current, latest, updateAvailable }) => {
|
|
1125
|
+
if (updateAvailable) {
|
|
1126
|
+
process.stdout.write(`[wand] 发现新版本 ${latest}(当前 ${current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
|
|
1127
|
+
}
|
|
1128
|
+
}).catch(() => { });
|
|
977
1129
|
}
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -30,4 +30,6 @@ export declare class SessionLogger {
|
|
|
30
30
|
saveMetadata(sessionId: string, meta: Record<string, unknown>): void;
|
|
31
31
|
/** Delete all log files for a session */
|
|
32
32
|
deleteSession(sessionId: string): void;
|
|
33
|
+
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
34
|
+
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string): void;
|
|
33
35
|
}
|
package/dist/session-logger.js
CHANGED
|
@@ -126,4 +126,19 @@ export class SessionLogger {
|
|
|
126
126
|
}
|
|
127
127
|
this.dirs.delete(sessionId);
|
|
128
128
|
}
|
|
129
|
+
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
130
|
+
appendShortcutLog(sessionId, shortcutKey, tailLines) {
|
|
131
|
+
try {
|
|
132
|
+
const dir = this.ensureDir(sessionId);
|
|
133
|
+
const entry = JSON.stringify({
|
|
134
|
+
ts: new Date().toISOString(),
|
|
135
|
+
key: shortcutKey,
|
|
136
|
+
tail: tailLines,
|
|
137
|
+
}) + "\n";
|
|
138
|
+
appendFileSync(path.join(dir, "shortcut-interactions.jsonl"), entry);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Non-critical
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface InputRequest {
|
|
|
55
55
|
approvalPolicy?: ApprovalPolicy;
|
|
56
56
|
allowedScopes?: EscalationScope[];
|
|
57
57
|
turn?: TurnRequest;
|
|
58
|
+
/** Shortcut key name that triggered this input (e.g. "enter", "yes", "ctrl_c"). Used for interaction logging in managed/full-access modes. */
|
|
59
|
+
shortcutKey?: string;
|
|
58
60
|
}
|
|
59
61
|
export interface ResizeRequest {
|
|
60
62
|
cols?: number;
|