@co0ontty/wand 1.6.1 → 1.6.2

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/config.js CHANGED
@@ -79,6 +79,28 @@ export async function saveConfig(configPath, config) {
79
79
  await mkdir(path.dirname(configPath), { recursive: true });
80
80
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
81
81
  }
82
+ function normalizeStructuredChatPersona(input) {
83
+ if (!input || typeof input !== "object")
84
+ return undefined;
85
+ const normalizeRole = (roleInput) => {
86
+ if (!roleInput || typeof roleInput !== "object")
87
+ return undefined;
88
+ const role = roleInput;
89
+ const normalized = {
90
+ name: typeof role.name === "string" ? role.name.trim() : undefined,
91
+ avatar: typeof role.avatar === "string" ? role.avatar.trim() : undefined,
92
+ };
93
+ if (!normalized.name && !normalized.avatar)
94
+ return undefined;
95
+ return normalized;
96
+ };
97
+ const personaInput = input;
98
+ const user = normalizeRole(personaInput.user);
99
+ const assistant = normalizeRole(personaInput.assistant);
100
+ if (!user && !assistant)
101
+ return undefined;
102
+ return { user, assistant };
103
+ }
82
104
  function mergeWithDefaults(input) {
83
105
  const defaults = defaultConfig();
84
106
  return {
@@ -108,6 +130,7 @@ function mergeWithDefaults(input) {
108
130
  mode: isExecutionMode(preset.mode) ? preset.mode : undefined
109
131
  }))
110
132
  : defaults.commandPresets,
133
+ structuredChatPersona: normalizeStructuredChatPersona(input.structuredChatPersona),
111
134
  language: typeof input.language === "string" ? input.language.trim() : defaults.language,
112
135
  };
113
136
  }
package/dist/server.js CHANGED
@@ -8,6 +8,19 @@ import { promisify } from "node:util";
8
8
  import path from "node:path";
9
9
  import process from "node:process";
10
10
  import { WebSocketServer } from "ws";
11
+ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
12
+ import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
13
+ import { ensureCertificates } from "./cert.js";
14
+ import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
15
+ import { ProcessManager } from "./process-manager.js";
16
+ import { StructuredSessionManager } from "./structured-session-manager.js";
17
+ import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
18
+ import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
19
+ import { resolveDatabasePath, WandStorage } from "./storage.js";
20
+ import { renderApp } from "./web-ui/index.js";
21
+ import { WsBroadcastManager } from "./ws-broadcast.js";
22
+ import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
23
+ import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
11
24
  const execAsync = promisify(exec);
12
25
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
13
26
  const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
@@ -52,19 +65,83 @@ function compareSemver(a, b) {
52
65
  }
53
66
  return 0;
54
67
  }
55
- import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
56
- import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
57
- import { ensureCertificates } from "./cert.js";
58
- import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
59
- import { ProcessManager } from "./process-manager.js";
60
- import { StructuredSessionManager } from "./structured-session-manager.js";
61
- import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
62
- import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
63
- import { resolveDatabasePath, WandStorage } from "./storage.js";
64
- import { renderApp } from "./web-ui/index.js";
65
- import { WsBroadcastManager } from "./ws-broadcast.js";
66
- import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
67
- import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
68
+ function isExternalAvatarSource(value) {
69
+ return /^(https?:|data:)/i.test(value);
70
+ }
71
+ function normalizePersonaName(value) {
72
+ if (typeof value !== "string")
73
+ return undefined;
74
+ const trimmed = value.trim();
75
+ return trimmed || undefined;
76
+ }
77
+ function normalizePersonaAvatar(value) {
78
+ if (typeof value !== "string")
79
+ return undefined;
80
+ const trimmed = value.trim();
81
+ return trimmed || undefined;
82
+ }
83
+ function resolveStructuredChatPersona(config) {
84
+ const persona = config.structuredChatPersona;
85
+ if (!persona)
86
+ return undefined;
87
+ const userName = normalizePersonaName(persona.user?.name);
88
+ const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
89
+ const assistantName = normalizePersonaName(persona.assistant?.name);
90
+ const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
91
+ if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
92
+ return undefined;
93
+ }
94
+ return {
95
+ user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
96
+ assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
97
+ };
98
+ }
99
+ function resolveStructuredChatAvatarPath(configPath, config, role) {
100
+ const avatar = role === "user"
101
+ ? config.structuredChatPersona?.user?.avatar
102
+ : config.structuredChatPersona?.assistant?.avatar;
103
+ if (!avatar || isExternalAvatarSource(avatar)) {
104
+ return null;
105
+ }
106
+ const configDir = resolveConfigDir(configPath);
107
+ return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
108
+ }
109
+ async function buildStructuredChatPersonaPayload(configPath, config) {
110
+ const persona = resolveStructuredChatPersona(config);
111
+ if (!persona)
112
+ return undefined;
113
+ const buildRole = async (role) => {
114
+ const roleConfig = role === "user" ? persona.user : persona.assistant;
115
+ if (!roleConfig)
116
+ return undefined;
117
+ let avatar = roleConfig.avatar;
118
+ if (avatar && !isExternalAvatarSource(avatar)) {
119
+ const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
120
+ if (!resolvedPath) {
121
+ avatar = undefined;
122
+ }
123
+ else {
124
+ try {
125
+ const fileStat = await stat(resolvedPath);
126
+ avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
127
+ }
128
+ catch {
129
+ avatar = undefined;
130
+ }
131
+ }
132
+ }
133
+ if (!roleConfig.name && !avatar)
134
+ return undefined;
135
+ return {
136
+ name: roleConfig.name,
137
+ avatar,
138
+ };
139
+ };
140
+ const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
141
+ if (!user && !assistant)
142
+ return undefined;
143
+ return { user, assistant };
144
+ }
68
145
  // ── Git helpers ──
69
146
  async function isGitRepo(dirPath) {
70
147
  try {
@@ -266,6 +343,50 @@ export async function startServer(config, configPath) {
266
343
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
267
344
  res.type("html").send(renderApp(configPath));
268
345
  });
346
+ app.get("/api/structured-chat-avatar/:role", async (req, res) => {
347
+ const role = req.params.role === "user" || req.params.role === "assistant"
348
+ ? req.params.role
349
+ : null;
350
+ if (!role) {
351
+ res.status(404).end();
352
+ return;
353
+ }
354
+ const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
355
+ if (!resolvedPath) {
356
+ res.status(404).end();
357
+ return;
358
+ }
359
+ try {
360
+ const fileStat = await stat(resolvedPath);
361
+ if (!fileStat.isFile()) {
362
+ res.status(404).end();
363
+ return;
364
+ }
365
+ const ext = path.extname(resolvedPath).toLowerCase();
366
+ const contentType = ext === ".svg"
367
+ ? "image/svg+xml"
368
+ : ext === ".png"
369
+ ? "image/png"
370
+ : ext === ".jpg" || ext === ".jpeg"
371
+ ? "image/jpeg"
372
+ : ext === ".webp"
373
+ ? "image/webp"
374
+ : ext === ".gif"
375
+ ? "image/gif"
376
+ : ext === ".avif"
377
+ ? "image/avif"
378
+ : null;
379
+ if (!contentType) {
380
+ res.status(415).json({ error: "不支持的头像格式。" });
381
+ return;
382
+ }
383
+ res.setHeader("X-Content-Type-Options", "nosniff");
384
+ res.type(contentType).sendFile(resolvedPath);
385
+ }
386
+ catch {
387
+ res.status(404).end();
388
+ }
389
+ });
269
390
  app.get("/manifest.json", (_req, res) => {
270
391
  res.setHeader("Content-Type", "application/manifest+json");
271
392
  res.send(generatePwaManifest());
@@ -328,7 +449,8 @@ export async function startServer(config, configPath) {
328
449
  });
329
450
  app.use("/api", requireAuth);
330
451
  // ── Config & Session info ──
331
- app.get("/api/config", (_req, res) => {
452
+ app.get("/api/config", async (_req, res) => {
453
+ const structuredChatPersona = await buildStructuredChatPersonaPayload(configPath, config);
332
454
  res.json({
333
455
  host: config.host,
334
456
  port: config.port,
@@ -336,6 +458,7 @@ export async function startServer(config, configPath) {
336
458
  defaultCwd: config.defaultCwd,
337
459
  commandPresets: config.commandPresets,
338
460
  structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
461
+ structuredChatPersona,
339
462
  updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
340
463
  latestVersion: cachedUpdateInfo?.latest ?? null,
341
464
  currentVersion: PKG_VERSION,
@@ -345,19 +345,21 @@ export class StructuredSessionManager {
345
345
  // ---------------------------------------------------------------------------
346
346
  // CLI argument construction
347
347
  // ---------------------------------------------------------------------------
348
- buildPermissionArgs(mode) {
348
+ buildPermissionArgs(mode, autoApprove) {
349
+ const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
350
+ const shouldAcceptEdits = mode === "auto-edit";
349
351
  if (!isRunningAsRoot()) {
350
- if (mode === "full-access" || mode === "managed") {
352
+ if (shouldBypass) {
351
353
  return ["--permission-mode", "bypassPermissions"];
352
354
  }
353
- if (mode === "auto-edit") {
355
+ if (shouldAcceptEdits) {
354
356
  return ["--permission-mode", "acceptEdits"];
355
357
  }
356
358
  return [];
357
359
  }
358
360
  // Root: Claude CLI refuses bypassPermissions.
359
361
  // acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
360
- if (shouldAutoApproveForMode(mode)) {
362
+ if (shouldBypass || shouldAcceptEdits) {
361
363
  return [
362
364
  "--permission-mode", "acceptEdits",
363
365
  "--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
@@ -384,8 +386,8 @@ export class StructuredSessionManager {
384
386
  return new Promise((resolve, reject) => {
385
387
  const args = ["-p", "--verbose", "--output-format", "stream-json"];
386
388
  console.log("[WAND] runClaudeStreaming sessionId:", sessionId, "mode:", session.mode, "claudeSessionId:", session.claudeSessionId);
387
- // Add permission args based on mode
388
- const permArgs = this.buildPermissionArgs(session.mode);
389
+ // Add permission args based on mode + autoApprovePermissions toggle
390
+ const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
389
391
  args.push(...permArgs);
390
392
  // In managed mode, append autonomous system prompt
391
393
  if (session.mode === "managed") {
package/dist/types.d.ts CHANGED
@@ -38,6 +38,14 @@ export interface CommandPreset {
38
38
  command: string;
39
39
  mode?: ExecutionMode;
40
40
  }
41
+ export interface StructuredChatPersonaRoleConfig {
42
+ name?: string;
43
+ avatar?: string;
44
+ }
45
+ export interface StructuredChatPersonaConfig {
46
+ user?: StructuredChatPersonaRoleConfig;
47
+ assistant?: StructuredChatPersonaRoleConfig;
48
+ }
41
49
  export interface WandConfig {
42
50
  host: string;
43
51
  port: number;
@@ -50,6 +58,7 @@ export interface WandConfig {
50
58
  startupCommands: string[];
51
59
  allowedCommandPrefixes: string[];
52
60
  commandPresets: CommandPreset[];
61
+ structuredChatPersona?: StructuredChatPersonaConfig;
53
62
  /** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
54
63
  shortcutLogMaxBytes?: number;
55
64
  /** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
@@ -9318,13 +9318,51 @@
9318
9318
  };
9319
9319
  })();
9320
9320
 
9321
+ var DEFAULT_CHAT_PERSONA = {
9322
+ user: {
9323
+ name: "赛博虎妞",
9324
+ avatarSvg: PIXEL_AVATAR.user
9325
+ },
9326
+ assistant: {
9327
+ name: "勤劳初二",
9328
+ avatarSvg: PIXEL_AVATAR.assistant
9329
+ }
9330
+ };
9331
+
9332
+ function getStructuredChatPersona(role) {
9333
+ var configPersona = state.config && state.config.structuredChatPersona;
9334
+ var roleConfig = configPersona && configPersona[role] ? configPersona[role] : null;
9335
+ var defaults = DEFAULT_CHAT_PERSONA[role] || DEFAULT_CHAT_PERSONA.assistant;
9336
+ return {
9337
+ name: roleConfig && typeof roleConfig.name === "string" && roleConfig.name.trim()
9338
+ ? roleConfig.name.trim()
9339
+ : defaults.name,
9340
+ avatar: roleConfig && typeof roleConfig.avatar === "string" && roleConfig.avatar.trim()
9341
+ ? roleConfig.avatar.trim()
9342
+ : null,
9343
+ avatarSvg: defaults.avatarSvg
9344
+ };
9345
+ }
9346
+
9347
+ function renderAvatarFallback(svg) {
9348
+ return '<div class="pixel-avatar">' + svg + '</div>';
9349
+ }
9350
+
9351
+ function handleChatAvatarImageError(img, role) {
9352
+ if (!img || !img.parentNode) return;
9353
+ var persona = getStructuredChatPersona(role === "user" ? "user" : "assistant");
9354
+ img.outerHTML = renderAvatarFallback(persona.avatarSvg);
9355
+ }
9356
+
9321
9357
  function chatAvatar(role) {
9322
- var isUser = role === "user";
9323
- var svg = isUser ? PIXEL_AVATAR.user : PIXEL_AVATAR.assistant;
9324
- var name = isUser ? "赛博虎妞" : "勤劳初二";
9358
+ var personaRole = role === "user" ? "user" : "assistant";
9359
+ var persona = getStructuredChatPersona(personaRole);
9360
+ var avatarInner = persona.avatar
9361
+ ? '<img class="pixel-avatar-image" src="' + escapeHtml(persona.avatar) + '" alt="' + escapeHtml(persona.name) + '" onerror="handleChatAvatarImageError(this, ' + JSON.stringify(personaRole) + ')" />'
9362
+ : renderAvatarFallback(persona.avatarSvg);
9325
9363
  return '<div class="chat-message-avatar ' + role + '">' +
9326
- '<div class="pixel-avatar">' + svg + '</div>' +
9327
- '<span class="avatar-name">' + name + '</span>' +
9364
+ avatarInner +
9365
+ '<span class="avatar-name">' + escapeHtml(persona.name) + '</span>' +
9328
9366
  '</div>';
9329
9367
  }
9330
9368
 
@@ -2328,6 +2328,13 @@
2328
2328
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
2329
2329
  }
2330
2330
 
2331
+ .pixel-avatar-image {
2332
+ display: block;
2333
+ width: 100%;
2334
+ height: 100%;
2335
+ object-fit: cover;
2336
+ }
2337
+
2331
2338
  .pixel-avatar-svg {
2332
2339
  display: block;
2333
2340
  width: 100%;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {