jinzd-ai-cli 0.4.206 → 0.4.207

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.
@@ -15731,12 +15731,9 @@ async function startWebServer(options = {}) {
15731
15731
  "Content-Security-Policy",
15732
15732
  [
15733
15733
  "default-src 'self'",
15734
- // v0.4.162: the client UI is driven by inline onclick= handlers and
15735
- // inline <script> blocks (SW registration, theme init). 'self' alone
15736
- // silently blocked EVERY button (click → nothing, no JS error, just a
15737
- // CSP violation in the console). localhost dev/admin tool — the
15738
- // unsafe-inline XSS surface is acceptable under that threat model.
15739
- "script-src 'self' 'unsafe-inline'",
15734
+ // Inline event handlers and startup scripts live in app.js, so script
15735
+ // execution stays locked to same-origin files only.
15736
+ "script-src 'self'",
15740
15737
  "style-src 'self' 'unsafe-inline'",
15741
15738
  "img-src 'self' data: blob:",
15742
15739
  "font-src 'self' data:",
package/dist/index.js CHANGED
@@ -144,8 +144,8 @@ import { program } from "commander";
144
144
 
145
145
  // src/repl/repl.ts
146
146
  import * as readline from "readline";
147
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3, mkdirSync as mkdirSync4 } from "fs";
148
- import { join as join4, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
147
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync4, mkdirSync as mkdirSync4 } from "fs";
148
+ import { join as join5, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
149
149
  import chalk4 from "chalk";
150
150
 
151
151
  // src/session/title-generator.ts
@@ -637,10 +637,10 @@ Error${typeName}: ${lines.join("\n")}
637
637
  };
638
638
 
639
639
  // src/repl/commands/index.ts
640
- import { writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync2, readFileSync, readdirSync, statSync as statSync2, appendFileSync } from "fs";
640
+ import { writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3, appendFileSync } from "fs";
641
641
  import { execSync as execSync2 } from "child_process";
642
642
  import { platform } from "os";
643
- import { resolve, dirname as dirname2, join as join2, basename } from "path";
643
+ import { resolve, dirname as dirname2, join as join3, basename } from "path";
644
644
  import chalk2 from "chalk";
645
645
 
646
646
  // src/repl/clipboard.ts
@@ -730,12 +730,9 @@ function getClipboardHint() {
730
730
  return "";
731
731
  }
732
732
 
733
- // src/repl/commands/index.ts
734
- function fmtCtx(tokens) {
735
- if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
736
- if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
737
- return `${tokens}`;
738
- }
733
+ // src/repl/commands/project-init.ts
734
+ import { existsSync as existsSync2, readFileSync, readdirSync, statSync as statSync2 } from "fs";
735
+ import { join as join2 } from "path";
739
736
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
740
737
  "node_modules",
741
738
  ".git",
@@ -793,7 +790,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
793
790
  const name = sorted[i];
794
791
  const fullPath = join2(d, name);
795
792
  const isLast = i === sorted.length - 1;
796
- const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
793
+ const connector = isLast ? "+-- " : "|-- ";
797
794
  let isDir;
798
795
  try {
799
796
  isDir = statSync2(fullPath).isDirectory();
@@ -803,7 +800,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
803
800
  lines.push(prefix + connector + name + (isDir ? "/" : ""));
804
801
  count++;
805
802
  if (isDir) {
806
- walk(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
803
+ walk(fullPath, prefix + (isLast ? " " : "| "), depth + 1);
807
804
  }
808
805
  }
809
806
  };
@@ -847,9 +844,9 @@ function scanProject(cwd) {
847
844
  try {
848
845
  const pkg = JSON.parse(readFileSync(join2(cwd, "package.json"), "utf-8"));
849
846
  const scripts = pkg.scripts ?? {};
850
- info.buildCommand = scripts.build ? `npm run build` : void 0;
851
- info.testCommand = scripts.test ? `npm test` : void 0;
852
- info.devCommand = scripts.dev ? `npm run dev` : void 0;
847
+ info.buildCommand = scripts.build ? "npm run build" : void 0;
848
+ info.testCommand = scripts.test ? "npm test" : void 0;
849
+ info.devCommand = scripts.dev ? "npm run dev" : void 0;
853
850
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
854
851
  if (allDeps["react"]) info.framework = "React";
855
852
  else if (allDeps["vue"]) info.framework = "Vue";
@@ -891,10 +888,8 @@ function scanProject(cwd) {
891
888
  }
892
889
  function buildInitPrompt(info, cwd) {
893
890
  const parts = [
894
- `Please generate an AICLI.md context file (Markdown format) for the following project. This file will be injected into the AI conversation's system prompt to help the AI understand the project structure and coding conventions.`,
895
- `
896
- ## Project Info
897
- `,
891
+ "Please generate an AICLI.md context file (Markdown format) for the following project. This file will be injected into the AI conversation system prompt to help the AI understand the project structure and coding conventions.",
892
+ "\n## Project Info\n",
898
893
  `- Working directory: ${cwd}`,
899
894
  `- Type: ${info.type}`,
900
895
  `- Language: ${info.language}`
@@ -923,6 +918,13 @@ Please generate a structured Markdown file containing:
923
918
  Output the Markdown content directly, do not wrap the entire file in a code block. Keep it concise, within 200 lines.`);
924
919
  return parts.join("\n");
925
920
  }
921
+
922
+ // src/repl/commands/index.ts
923
+ function fmtCtx(tokens) {
924
+ if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
925
+ if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
926
+ return `${tokens}`;
927
+ }
926
928
  function copyToClipboard(text) {
927
929
  const plat = platform();
928
930
  if (plat === "win32") {
@@ -1827,16 +1829,16 @@ No tools match "${filter}".
1827
1829
  usage: "/mcp [reconnect [serverId] | trust-project]",
1828
1830
  async execute(args, ctx) {
1829
1831
  if (args[0] === "trust-project") {
1830
- const { join: join5 } = await import("path");
1831
- const { existsSync: existsSync5 } = await import("fs");
1832
+ const { join: join6 } = await import("path");
1833
+ const { existsSync: existsSync6 } = await import("fs");
1832
1834
  const { getGitRoot: getGitRoot2 } = await import("./git-context-EXOEHQSF.js");
1833
1835
  const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-NHGTSHKT.js");
1834
1836
  const { approveProject, hashMcpFile } = await import("./project-trust-NKYHL3VZ.js");
1835
1837
  const cwd = process.cwd();
1836
1838
  const projectRoot = getGitRoot2(cwd) ?? cwd;
1837
- const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME2);
1839
+ const mcpPath = join6(projectRoot, MCP_PROJECT_CONFIG_NAME2);
1838
1840
  console.log();
1839
- if (!existsSync5(mcpPath)) {
1841
+ if (!existsSync6(mcpPath)) {
1840
1842
  console.log(theme.dim(` No .mcp.json in ${projectRoot}.`));
1841
1843
  console.log();
1842
1844
  return;
@@ -2169,9 +2171,9 @@ No tools match "${filter}".
2169
2171
  usage: "/init [--force]",
2170
2172
  async execute(args, ctx) {
2171
2173
  const cwd = process.cwd();
2172
- const targetPath = join2(cwd, "AICLI.md");
2174
+ const targetPath = join3(cwd, "AICLI.md");
2173
2175
  const force = args.includes("--force");
2174
- if (existsSync2(targetPath) && !force) {
2176
+ if (existsSync3(targetPath) && !force) {
2175
2177
  ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
2176
2178
  ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
2177
2179
  return;
@@ -3049,7 +3051,7 @@ ${hint}` : "")
3049
3051
  description: "Persistent memory (memory.md) + chat memory recall index (v0.4.89+)",
3050
3052
  usage: "/memory [show|add <text>|clear|path|rebuild|refresh|status|recall <query>|index-clear]",
3051
3053
  async execute(args, ctx) {
3052
- const memoryFile = join2(ctx.config.getConfigDir(), MEMORY_FILE_NAME);
3054
+ const memoryFile = join3(ctx.config.getConfigDir(), MEMORY_FILE_NAME);
3053
3055
  const sub = args[0] ?? "show";
3054
3056
  if (sub === "rebuild" || sub === "refresh") {
3055
3057
  const full = sub === "rebuild";
@@ -3133,11 +3135,11 @@ ${hint}` : "")
3133
3135
  return;
3134
3136
  }
3135
3137
  if (sub === "show" || sub === "view") {
3136
- if (!existsSync2(memoryFile)) {
3138
+ if (!existsSync3(memoryFile)) {
3137
3139
  ctx.renderer.printInfo("Memory is empty (memory.md not found)");
3138
3140
  return;
3139
3141
  }
3140
- const content = readFileSync(memoryFile, "utf-8").trim();
3142
+ const content = readFileSync2(memoryFile, "utf-8").trim();
3141
3143
  if (!content) {
3142
3144
  ctx.renderer.printInfo("Memory is empty");
3143
3145
  return;
@@ -3168,7 +3170,7 @@ ${text}
3168
3170
  ctx.renderer.renderError(`Failed to write memory: ${err instanceof Error ? err.message : String(err)}`);
3169
3171
  }
3170
3172
  } else if (sub === "clear") {
3171
- if (!existsSync2(memoryFile)) {
3173
+ if (!existsSync3(memoryFile)) {
3172
3174
  ctx.renderer.printInfo("Memory is already empty");
3173
3175
  return;
3174
3176
  }
@@ -3204,20 +3206,20 @@ ${text}
3204
3206
  console.log(theme.heading("Config Files:"));
3205
3207
  console.log(` Dir: ${theme.accent(configDir)}`);
3206
3208
  const checkFile = (label, filePath) => {
3207
- const exists = existsSync2(filePath);
3209
+ const exists = existsSync3(filePath);
3208
3210
  const icon = exists ? theme.success("\u2713") : theme.dim("\u2013");
3209
3211
  let extra = "";
3210
3212
  if (exists) {
3211
3213
  try {
3212
- extra = theme.dim(` (${statSync2(filePath).size} bytes)`);
3214
+ extra = theme.dim(` (${statSync3(filePath).size} bytes)`);
3213
3215
  } catch {
3214
3216
  }
3215
3217
  }
3216
3218
  console.log(` ${icon} ${label.padEnd(14)} ${exists ? filePath : theme.dim("(not found)")}${extra}`);
3217
3219
  };
3218
- checkFile("config.json", join2(configDir, "config.json"));
3219
- checkFile("memory.md", join2(configDir, MEMORY_FILE_NAME));
3220
- checkFile("dev-state.md", join2(configDir, "dev-state.md"));
3220
+ checkFile("config.json", join3(configDir, "config.json"));
3221
+ checkFile("memory.md", join3(configDir, MEMORY_FILE_NAME));
3222
+ checkFile("dev-state.md", join3(configDir, "dev-state.md"));
3221
3223
  console.log();
3222
3224
  const mcpManager = ctx.getMcpManager();
3223
3225
  if (mcpManager) {
@@ -3381,9 +3383,9 @@ ${text}
3381
3383
  let newFiles = 0;
3382
3384
  let modifiedFiles = 0;
3383
3385
  for (const [filePath, { earliest }] of fileMap) {
3384
- const currentContent = existsSync2(filePath) ? (() => {
3386
+ const currentContent = existsSync3(filePath) ? (() => {
3385
3387
  try {
3386
- return readFileSync(filePath, "utf-8");
3388
+ return readFileSync2(filePath, "utf-8");
3387
3389
  } catch {
3388
3390
  return null;
3389
3391
  }
@@ -3509,12 +3511,12 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
3509
3511
  if (scanAll) {
3510
3512
  const metas = ctx.sessions.listSessions();
3511
3513
  console.log(theme.info(` Scanning ${metas.length} session(s)\u2026`));
3512
- const { readFileSync: readFileSync4 } = await import("fs");
3513
- const { join: join5 } = await import("path");
3514
+ const { readFileSync: readFileSync5 } = await import("fs");
3515
+ const { join: join6 } = await import("path");
3514
3516
  const historyDir = ctx.config.getHistoryDir();
3515
3517
  for (const m of metas) {
3516
3518
  try {
3517
- const content = readFileSync4(join5(historyDir, `${m.id}.json`), "utf-8");
3519
+ const content = readFileSync5(join6(historyDir, `${m.id}.json`), "utf-8");
3518
3520
  const hits2 = scanString(content, opts);
3519
3521
  if (hits2.length) {
3520
3522
  filesWithHits++;
@@ -4199,13 +4201,13 @@ Managing ${displayName} API Key`);
4199
4201
  };
4200
4202
 
4201
4203
  // src/repl/custom-commands.ts
4202
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "fs";
4203
- import { join as join3, extname } from "path";
4204
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "fs";
4205
+ import { join as join4, extname } from "path";
4204
4206
  import { execSync as execSync3 } from "child_process";
4205
4207
  function parseCommandFile(filePath) {
4206
4208
  let content;
4207
4209
  try {
4208
- content = readFileSync2(filePath, "utf-8");
4210
+ content = readFileSync3(filePath, "utf-8");
4209
4211
  } catch {
4210
4212
  return null;
4211
4213
  }
@@ -4229,7 +4231,7 @@ function expandTemplate(template, args) {
4229
4231
  result = result.replace(/\{\{input\}\}/g, args.join(" "));
4230
4232
  result = result.replace(/\{\{file:([^}]+)\}\}/g, (_m, p) => {
4231
4233
  try {
4232
- return readFileSync2(p.trim(), "utf-8");
4234
+ return readFileSync3(p.trim(), "utf-8");
4233
4235
  } catch {
4234
4236
  return `[Error: cannot read ${p.trim()}]`;
4235
4237
  }
@@ -4254,14 +4256,14 @@ var CustomCommandManager = class {
4254
4256
  commands = /* @__PURE__ */ new Map();
4255
4257
  loadCommands() {
4256
4258
  this.commands.clear();
4257
- if (!existsSync3(this.commandsDir)) {
4259
+ if (!existsSync4(this.commandsDir)) {
4258
4260
  mkdirSync3(this.commandsDir, { recursive: true });
4259
4261
  return 0;
4260
4262
  }
4261
4263
  let count = 0;
4262
4264
  for (const file of readdirSync2(this.commandsDir)) {
4263
4265
  if (extname(file) !== ".md") continue;
4264
- const cmd = parseCommandFile(join3(this.commandsDir, file));
4266
+ const cmd = parseCommandFile(join4(this.commandsDir, file));
4265
4267
  if (cmd) {
4266
4268
  this.commands.set(cmd.meta.name, cmd);
4267
4269
  count++;
@@ -4566,17 +4568,17 @@ function parseAtReferences(input2, cwd) {
4566
4568
  const absPath = resolve2(cwd, rawPath);
4567
4569
  const ext = extname2(rawPath).toLowerCase();
4568
4570
  const mime = IMAGE_MIME[ext];
4569
- if (!existsSync4(absPath)) {
4571
+ if (!existsSync5(absPath)) {
4570
4572
  refs.push({ path: rawPath, type: "notfound" });
4571
4573
  continue;
4572
4574
  }
4573
4575
  if (mime) {
4574
- const fileSize = statSync3(absPath).size;
4576
+ const fileSize = statSync4(absPath).size;
4575
4577
  if (fileSize > MAX_IMAGE_BYTES) {
4576
4578
  refs.push({ path: rawPath, type: "toolarge" });
4577
4579
  continue;
4578
4580
  }
4579
- const data = readFileSync3(absPath).toString("base64");
4581
+ const data = readFileSync4(absPath).toString("base64");
4580
4582
  imageParts.push({
4581
4583
  type: "image_url",
4582
4584
  image_url: { url: `data:${mime};base64,${data}` }
@@ -4584,7 +4586,7 @@ function parseAtReferences(input2, cwd) {
4584
4586
  refs.push({ path: rawPath, type: "image" });
4585
4587
  textBody = textBody.replace(match[0], "").trim();
4586
4588
  } else {
4587
- const content = readFileSync3(absPath, "utf-8");
4589
+ const content = readFileSync4(absPath, "utf-8");
4588
4590
  const inlined = `
4589
4591
 
4590
4592
  [File: ${rawPath}]
@@ -4809,12 +4811,12 @@ var Repl = class {
4809
4811
  const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
4810
4812
  for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
4811
4813
  const name = filtered[i];
4812
- const fullPath = join4(dir, name);
4814
+ const fullPath = join5(dir, name);
4813
4815
  const isLast = i === filtered.length - 1;
4814
4816
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4815
4817
  let isDir;
4816
4818
  try {
4817
- isDir = statSync3(fullPath).isDirectory();
4819
+ isDir = statSync4(fullPath).isDirectory();
4818
4820
  } catch {
4819
4821
  continue;
4820
4822
  }
@@ -4843,10 +4845,10 @@ ${treeLines.join("\n")}`
4843
4845
  for (const name of entries) {
4844
4846
  if (totalChars >= MAX_TOTAL_CHARS) break;
4845
4847
  if (SKIP_DIRS_SET.has(name)) continue;
4846
- const fullPath = join4(dir, name);
4848
+ const fullPath = join5(dir, name);
4847
4849
  let st;
4848
4850
  try {
4849
- st = statSync3(fullPath);
4851
+ st = statSync4(fullPath);
4850
4852
  } catch {
4851
4853
  continue;
4852
4854
  }
@@ -4858,7 +4860,7 @@ ${treeLines.join("\n")}`
4858
4860
  if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
4859
4861
  if (st.size > MAX_FILE_CHARS * 3) continue;
4860
4862
  try {
4861
- let content = readFileSync3(fullPath, "utf-8");
4863
+ let content = readFileSync4(fullPath, "utf-8");
4862
4864
  if (content.length > MAX_FILE_CHARS) {
4863
4865
  content = content.slice(0, MAX_FILE_CHARS) + `
4864
4866
  ... (truncated, ${content.length} chars total)`;
@@ -4888,12 +4890,12 @@ ${content}
4888
4890
  */
4889
4891
  addExtraContextDir(dirPath) {
4890
4892
  const absPath = resolve2(dirPath);
4891
- if (!existsSync4(absPath)) {
4893
+ if (!existsSync5(absPath)) {
4892
4894
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
4893
4895
  }
4894
4896
  let isDir;
4895
4897
  try {
4896
- isDir = statSync3(absPath).isDirectory();
4898
+ isDir = statSync4(absPath).isDirectory();
4897
4899
  } catch {
4898
4900
  return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
4899
4901
  }
@@ -4922,9 +4924,9 @@ ${content}
4922
4924
  */
4923
4925
  findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
4924
4926
  for (const candidate of candidates) {
4925
- const fullPath = join4(dir, candidate);
4926
- if (existsSync4(fullPath)) {
4927
- const content = readFileSync3(fullPath, "utf-8").trim();
4927
+ const fullPath = join5(dir, candidate);
4928
+ if (existsSync5(fullPath)) {
4929
+ const content = readFileSync4(fullPath, "utf-8").trim();
4928
4930
  if (content) return { filePath: fullPath, content };
4929
4931
  }
4930
4932
  }
@@ -4952,10 +4954,10 @@ ${content}
4952
4954
  const cwd = process.cwd();
4953
4955
  const gitRoot = getGitRoot(cwd);
4954
4956
  const projectRoot = gitRoot ?? cwd;
4955
- const mcpPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
4956
- if (!existsSync4(mcpPath)) return null;
4957
+ const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
4958
+ if (!existsSync5(mcpPath)) return null;
4957
4959
  try {
4958
- const raw = JSON.parse(readFileSync3(mcpPath, "utf-8"));
4960
+ const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
4959
4961
  const servers = raw?.mcpServers;
4960
4962
  if (!servers || typeof servers !== "object") {
4961
4963
  process.stderr.write(
@@ -5001,8 +5003,8 @@ ${content}
5001
5003
  );
5002
5004
  return { layers: [], mergedContent: "" };
5003
5005
  }
5004
- if (existsSync4(fullPath)) {
5005
- const content = readFileSync3(fullPath, "utf-8").trim();
5006
+ if (existsSync5(fullPath)) {
5007
+ const content = readFileSync4(fullPath, "utf-8").trim();
5006
5008
  if (content) {
5007
5009
  const layer = {
5008
5010
  level: "project",
@@ -5059,9 +5061,9 @@ ${content}
5059
5061
  * 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
5060
5062
  */
5061
5063
  loadMemoryContent() {
5062
- const memoryPath = join4(this.config.getConfigDir(), MEMORY_FILE_NAME);
5063
- if (!existsSync4(memoryPath)) return null;
5064
- let content = readFileSync3(memoryPath, "utf-8").trim();
5064
+ const memoryPath = join5(this.config.getConfigDir(), MEMORY_FILE_NAME);
5065
+ if (!existsSync5(memoryPath)) return null;
5066
+ let content = readFileSync4(memoryPath, "utf-8").trim();
5065
5067
  if (!content) return null;
5066
5068
  if (content.length > MEMORY_MAX_CHARS) {
5067
5069
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -5449,14 +5451,14 @@ Session '${this.resumeSessionId}' not found.
5449
5451
  process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
5450
5452
  `));
5451
5453
  }
5452
- const skillsDir = join4(this.config.getConfigDir(), SKILLS_DIR_NAME);
5454
+ const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
5453
5455
  this.skillManager = new SkillManager(skillsDir, this.config.get("ui").skillSizeWarn);
5454
5456
  const skillCount = this.skillManager.loadSkills();
5455
5457
  if (skillCount > 0) {
5456
5458
  process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
5457
5459
  `));
5458
5460
  }
5459
- const commandsDir = join4(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
5461
+ const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
5460
5462
  this.customCommandManager = new CustomCommandManager(commandsDir);
5461
5463
  const customCmdCount = this.customCommandManager.loadCommands();
5462
5464
  if (customCmdCount > 0) {
@@ -6092,15 +6094,15 @@ Session '${this.resumeSessionId}' not found.
6092
6094
  const dir = normalized.includes("/") ? dirname3(normalized) : ".";
6093
6095
  const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
6094
6096
  const absDir = resolve2(process.cwd(), dir);
6095
- if (!existsSync4(absDir)) return [];
6097
+ if (!existsSync5(absDir)) return [];
6096
6098
  const entries = readdirSync3(absDir);
6097
6099
  const results = [];
6098
6100
  for (const entry of entries) {
6099
6101
  if (entry.startsWith(".")) continue;
6100
6102
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
6101
6103
  try {
6102
- const fullPath = join4(absDir, entry);
6103
- const stat = statSync3(fullPath);
6104
+ const fullPath = join5(absDir, entry);
6105
+ const stat = statSync4(fullPath);
6104
6106
  const rel = dir === "." ? entry : `${dir}/${entry}`;
6105
6107
  results.push(stat.isDirectory() ? `${rel}/` : rel);
6106
6108
  } catch {
@@ -7269,7 +7271,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7269
7271
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7270
7272
  process.exit(1);
7271
7273
  }
7272
- const { startWebServer } = await import("./server-LDPQ3DLK.js");
7274
+ const { startWebServer } = await import("./server-L2MXWYF7.js");
7273
7275
  await startWebServer({ port, host: options.host });
7274
7276
  });
7275
7277
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -3528,12 +3528,9 @@ async function startWebServer(options = {}) {
3528
3528
  "Content-Security-Policy",
3529
3529
  [
3530
3530
  "default-src 'self'",
3531
- // v0.4.162: the client UI is driven by inline onclick= handlers and
3532
- // inline <script> blocks (SW registration, theme init). 'self' alone
3533
- // silently blocked EVERY button (click → nothing, no JS error, just a
3534
- // CSP violation in the console). localhost dev/admin tool — the
3535
- // unsafe-inline XSS surface is acceptable under that threat model.
3536
- "script-src 'self' 'unsafe-inline'",
3531
+ // Inline event handlers and startup scripts live in app.js, so script
3532
+ // execution stays locked to same-origin files only.
3533
+ "script-src 'self'",
3537
3534
  "style-src 'self' 'unsafe-inline'",
3538
3535
  "img-src 'self' data: blob:",
3539
3536
  "font-src 'self' data:",
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Declarative Web UI actions and startup hooks.
3
+ * Kept separate from app.js so static and dynamic UI markup can avoid inline handlers.
4
+ */
5
+
6
+ function handleDeclarativeAction(action, el) {
7
+ switch (action) {
8
+ case 'toggle-auth-mode': toggleAuthMode(); break;
9
+ case 'show-enable-auth': showEnableAuthDialog(); break;
10
+ case 'logout': handleLogout(); break;
11
+ case 'toggle-batch-select': toggleBatchSelect(); break;
12
+ case 'select-all-sessions': selectAllSessions(); break;
13
+ case 'batch-delete-selected': batchDeleteSelected(); break;
14
+ case 'start-hub': window.startHub(); break;
15
+ case 'abort-hub': window.abortHub(); break;
16
+ case 'close-sidebar': closeSidebar(); break;
17
+ case 'import-templates': importTemplates(); break;
18
+ case 'export-templates': exportTemplates(); break;
19
+ case 'show-add-template': showAddTemplate(); break;
20
+ case 'cancel-template-form': cancelTemplateForm(); break;
21
+ case 'save-template-form': saveTemplateForm(); break;
22
+ case 'confirm-response': window.respondConfirm(el.dataset.requestId, el.dataset.approved === 'true'); break;
23
+ case 'batch-confirm-response': window.respondBatchConfirm(el.dataset.requestId, el); break;
24
+ case 'batch-deny-response': window.respondBatchDeny(el.dataset.requestId, el); break;
25
+ case 'ask-user-response': window.respondAskUser(el.dataset.requestId); break;
26
+ case 'auto-pause-response': window.respondAutoPause(el.dataset.requestId, el.dataset.pauseAction); break;
27
+ case 'use-template': useTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
28
+ case 'edit-template': editTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
29
+ case 'delete-template': deleteTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
30
+ case 'insert-file-ref': insertFileRef(el.dataset.filePath || ''); break;
31
+ case 'show-full-preview':
32
+ if (el.previousElementSibling) el.previousElementSibling.style.maxHeight = 'none';
33
+ el.remove();
34
+ break;
35
+ case 'load-session': send({ type: 'command', name: 'session', args: ['load', el.dataset.sessionId] }); break;
36
+ }
37
+ }
38
+
39
+ document.addEventListener('click', (event) => {
40
+ const themeEl = event.target.closest('[data-theme-choice]');
41
+ if (themeEl) {
42
+ event.preventDefault();
43
+ window.setTheme(themeEl.dataset.themeChoice);
44
+ return;
45
+ }
46
+
47
+ const actionEl = event.target.closest('[data-action]');
48
+ if (!actionEl) return;
49
+ event.preventDefault();
50
+ handleDeclarativeAction(actionEl.dataset.action, actionEl);
51
+ });
52
+
53
+ document.addEventListener('keydown', (event) => {
54
+ if (event.key !== 'Enter') return;
55
+ const actionEl = event.target.closest('[data-enter-action]');
56
+ if (!actionEl) return;
57
+ event.preventDefault();
58
+ handleDeclarativeAction(actionEl.dataset.enterAction, actionEl);
59
+ });
60
+
61
+ document.getElementById('auth-form')?.addEventListener('submit', handleAuth);
62
+
63
+ function registerServiceWorker() {
64
+ // Self-updating service worker (v0.4.159). A new SW postMessages
65
+ // 'sw-updated' (and triggers controllerchange) when it takes over; reload
66
+ // once so users do not stay on stale index.html/app.js after an upgrade.
67
+ if (!('serviceWorker' in navigator)) return;
68
+ let swReloaded = false;
69
+ const reloadOnce = () => {
70
+ if (!swReloaded) {
71
+ swReloaded = true;
72
+ location.reload();
73
+ }
74
+ };
75
+ navigator.serviceWorker.addEventListener('message', (e) => {
76
+ if (e.data && e.data.type === 'sw-updated') reloadOnce();
77
+ });
78
+ navigator.serviceWorker.addEventListener('controllerchange', reloadOnce);
79
+ navigator.serviceWorker.register('sw.js').then((reg) => {
80
+ reg.update();
81
+ }).catch(() => {});
82
+ }
83
+
84
+ registerServiceWorker();
@@ -336,8 +336,8 @@ function handleConfirmRequest(msg) {
336
336
  </div>
337
337
  ${msg.diff ? `<div class="confirm-diff w-full">${renderDiffHtml(msg.diff)}</div>` : ''}
338
338
  <div class="flex gap-2 mt-2">
339
- <button class="btn btn-success btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', true)">✓ Approve</button>
340
- <button class="btn btn-error btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', false)">✗ Deny</button>
339
+ <button class="btn btn-success btn-sm btn-outline" data-action="confirm-response" data-request-id="${msg.requestId}" data-approved="true">✓ Approve</button>
340
+ <button class="btn btn-error btn-sm btn-outline" data-action="confirm-response" data-request-id="${msg.requestId}" data-approved="false">✗ Deny</button>
341
341
  </div>
342
342
  `;
343
343
  messagesEl.appendChild(el);
@@ -363,8 +363,8 @@ function handleBatchConfirmRequest(msg) {
363
363
  </div>
364
364
  <div class="w-full my-1">${fileListHtml}</div>
365
365
  <div class="flex gap-2 mt-2">
366
- <button class="btn btn-success btn-sm btn-outline" onclick="respondBatchConfirm('${msg.requestId}', this)">✓ Approve Selected</button>
367
- <button class="btn btn-error btn-sm btn-outline" onclick="respondBatchDeny('${msg.requestId}', this)">✗ Reject All</button>
366
+ <button class="btn btn-success btn-sm btn-outline" data-action="batch-confirm-response" data-request-id="${msg.requestId}">✓ Approve Selected</button>
367
+ <button class="btn btn-error btn-sm btn-outline" data-action="batch-deny-response" data-request-id="${msg.requestId}">✗ Reject All</button>
368
368
  </div>
369
369
  `;
370
370
  messagesEl.appendChild(el);
@@ -383,8 +383,8 @@ function handleAskUserRequest(msg) {
383
383
  <input type="text" id="ask-input-${msg.requestId}"
384
384
  class="input input-sm input-bordered flex-1"
385
385
  placeholder="Your answer..."
386
- onkeydown="if(event.key==='Enter')respondAskUser('${msg.requestId}')">
387
- <button class="btn btn-primary btn-sm" onclick="respondAskUser('${msg.requestId}')">Send</button>
386
+ data-enter-action="ask-user-response" data-request-id="${msg.requestId}">
387
+ <button class="btn btn-primary btn-sm" data-action="ask-user-response" data-request-id="${msg.requestId}">Send</button>
388
388
  </div>
389
389
  `;
390
390
  messagesEl.appendChild(el);
@@ -407,12 +407,12 @@ function handleAutoPauseRequest(msg) {
407
407
  <input type="text" id="pause-input-${msg.requestId}"
408
408
  class="input input-sm input-bordered flex-1"
409
409
  placeholder="Optional: redirect AI with a new instruction..."
410
- onkeydown="if(event.key==='Enter')respondAutoPause('${msg.requestId}','redirect')">
410
+ data-enter-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="redirect">
411
411
  </div>
412
412
  <div class="flex gap-2">
413
- <button class="btn btn-success btn-sm flex-1" onclick="respondAutoPause('${msg.requestId}','continue')">▶ Continue</button>
414
- <button class="btn btn-primary btn-sm flex-1" onclick="respondAutoPause('${msg.requestId}','redirect')">↪ Redirect</button>
415
- <button class="btn btn-error btn-sm flex-1" onclick="respondAutoPause('${msg.requestId}','stop')">⏹ Stop</button>
413
+ <button class="btn btn-success btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="continue">▶ Continue</button>
414
+ <button class="btn btn-primary btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="redirect">↪ Redirect</button>
415
+ <button class="btn btn-error btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="stop">⏹ Stop</button>
416
416
  </div>
417
417
  `;
418
418
  messagesEl.appendChild(el);
@@ -666,6 +666,7 @@ window.setTheme = function(theme) {
666
666
 
667
667
  // ── UI helpers ─────────────────────────────────────────────────────
668
668
 
669
+
669
670
  function createAssistantMessage() {
670
671
  const el = document.createElement('div');
671
672
  el.className = 'msg-assistant';
@@ -714,11 +715,11 @@ function renderMarkdown(el, text) {
714
715
  const btn = document.createElement('button');
715
716
  btn.className = 'copy-code-btn btn btn-ghost btn-xs';
716
717
  btn.textContent = '📋 Copy';
717
- btn.onclick = () => {
718
- navigator.clipboard.writeText(block.textContent || '');
719
- btn.textContent = 'Copied';
720
- setTimeout(() => btn.textContent = '📋 Copy', 1500);
721
- };
718
+ btn.addEventListener('click', () => {
719
+ navigator.clipboard.writeText(block.textContent || '');
720
+ btn.textContent = 'Copied';
721
+ setTimeout(() => btn.textContent = 'Copy', 1500);
722
+ });
722
723
  pre.style.position = 'relative';
723
724
  pre.appendChild(btn);
724
725
  }
@@ -1920,12 +1921,12 @@ function addImageToPreview(file) {
1920
1921
  <img src="${reader.result}" class="rounded max-h-16 max-w-[100px] object-contain border border-base-content/20" alt="${escapeHtml(file.name)}">
1921
1922
  <button class="btn btn-xs btn-circle btn-error absolute -top-1 -right-1 opacity-80" title="Remove">✕</button>
1922
1923
  `;
1923
- thumb.querySelector('button').onclick = () => {
1924
+ thumb.querySelector('button').addEventListener('click', () => {
1924
1925
  const idx = pendingImages.findIndex(img => img.name === file.name && img.data === base64);
1925
1926
  if (idx >= 0) pendingImages.splice(idx, 1);
1926
1927
  thumb.remove();
1927
1928
  if (pendingImages.length === 0) imagePreviewArea.classList.add('hidden');
1928
- };
1929
+ });
1929
1930
  imagePreviewArea.appendChild(thumb);
1930
1931
  imagePreviewArea.classList.remove('hidden');
1931
1932
  };
@@ -1991,189 +1992,6 @@ function handleMemoryContent(msg) {
1991
1992
 
1992
1993
  // ── Prompt Templates ───────────────────────────────────────────────
1993
1994
 
1994
- const TEMPLATES_KEY = 'aicli-templates';
1995
-
1996
- function loadTemplates() {
1997
- try {
1998
- return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
1999
- } catch { return []; }
2000
- }
2001
-
2002
- function saveTemplatesToStorage(templates) {
2003
- localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
2004
- }
2005
-
2006
- function openTemplatesModal() {
2007
- const modal = document.getElementById('templates-modal');
2008
- if (!modal) return;
2009
- cancelTemplateForm();
2010
- renderTemplateList();
2011
- document.getElementById('template-search').value = '';
2012
- modal.showModal();
2013
- }
2014
-
2015
- function renderTemplateList(filter) {
2016
- const listEl = document.getElementById('template-list');
2017
- if (!listEl) return;
2018
- const templates = loadTemplates();
2019
- const q = (filter || '').toLowerCase();
2020
- const filtered = q
2021
- ? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
2022
- : templates;
2023
-
2024
- if (filtered.length === 0) {
2025
- listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
2026
- return;
2027
- }
2028
-
2029
- listEl.innerHTML = filtered.map(t => {
2030
- const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
2031
- const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
2032
- return `
2033
- <div class="template-item" data-tpl-id="${t.id}" onclick="useTemplate('${t.id}')">
2034
- <div class="template-item-body">
2035
- <div class="template-item-name">${escapeHtml(t.name)}</div>
2036
- <div class="template-item-preview">${escapeHtml(preview)}</div>
2037
- ${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
2038
- </div>
2039
- <div class="template-item-actions">
2040
- <button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); editTemplate('${t.id}')" title="Edit">✏️</button>
2041
- <button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); deleteTemplate('${t.id}')" title="Delete">🗑️</button>
2042
- </div>
2043
- </div>`;
2044
- }).join('');
2045
- }
2046
-
2047
- function useTemplate(id) {
2048
- const templates = loadTemplates();
2049
- const tpl = templates.find(t => t.id === id);
2050
- if (!tpl) return;
2051
- userInput.value = tpl.content;
2052
- userInput.focus();
2053
- userInput.style.height = 'auto';
2054
- userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
2055
- document.getElementById('templates-modal')?.close();
2056
- }
2057
-
2058
- function showAddTemplate() {
2059
- const form = document.getElementById('template-form');
2060
- form.classList.remove('hidden');
2061
- document.getElementById('tpl-edit-id').value = '';
2062
- document.getElementById('tpl-name').value = '';
2063
- document.getElementById('tpl-content').value = '';
2064
- document.getElementById('tpl-tags').value = '';
2065
- document.getElementById('tpl-name').focus();
2066
- }
2067
-
2068
- function editTemplate(id) {
2069
- const templates = loadTemplates();
2070
- const tpl = templates.find(t => t.id === id);
2071
- if (!tpl) return;
2072
- const form = document.getElementById('template-form');
2073
- form.classList.remove('hidden');
2074
- document.getElementById('tpl-edit-id').value = tpl.id;
2075
- document.getElementById('tpl-name').value = tpl.name;
2076
- document.getElementById('tpl-content').value = tpl.content;
2077
- document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
2078
- document.getElementById('tpl-name').focus();
2079
- }
2080
-
2081
- function cancelTemplateForm() {
2082
- document.getElementById('template-form')?.classList.add('hidden');
2083
- }
2084
-
2085
- function saveTemplateForm() {
2086
- const name = document.getElementById('tpl-name').value.trim();
2087
- const content = document.getElementById('tpl-content').value.trim();
2088
- const tagsRaw = document.getElementById('tpl-tags').value.trim();
2089
- const editId = document.getElementById('tpl-edit-id').value;
2090
-
2091
- if (!name || !content) return;
2092
-
2093
- const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
2094
- const templates = loadTemplates();
2095
-
2096
- if (editId) {
2097
- const idx = templates.findIndex(t => t.id === editId);
2098
- if (idx >= 0) {
2099
- templates[idx].name = name;
2100
- templates[idx].content = content;
2101
- templates[idx].tags = tags;
2102
- }
2103
- } else {
2104
- templates.unshift({
2105
- id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
2106
- name,
2107
- content,
2108
- tags,
2109
- createdAt: new Date().toISOString(),
2110
- });
2111
- }
2112
-
2113
- saveTemplatesToStorage(templates);
2114
- cancelTemplateForm();
2115
- renderTemplateList();
2116
- }
2117
-
2118
- function deleteTemplate(id) {
2119
- const templates = loadTemplates().filter(t => t.id !== id);
2120
- saveTemplatesToStorage(templates);
2121
- renderTemplateList(document.getElementById('template-search')?.value);
2122
- }
2123
-
2124
- function exportTemplates() {
2125
- const templates = loadTemplates();
2126
- if (templates.length === 0) return;
2127
- const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
2128
- const url = URL.createObjectURL(blob);
2129
- const a = document.createElement('a');
2130
- a.href = url;
2131
- a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
2132
- a.click();
2133
- URL.revokeObjectURL(url);
2134
- }
2135
-
2136
- function importTemplates() {
2137
- const input = document.createElement('input');
2138
- input.type = 'file';
2139
- input.accept = '.json';
2140
- input.onchange = () => {
2141
- const file = input.files[0];
2142
- if (!file) return;
2143
- const reader = new FileReader();
2144
- reader.onload = () => {
2145
- try {
2146
- const imported = JSON.parse(reader.result);
2147
- if (!Array.isArray(imported)) throw new Error('Not an array');
2148
- const existing = loadTemplates();
2149
- const existingIds = new Set(existing.map(t => t.id));
2150
- let added = 0;
2151
- for (const t of imported) {
2152
- if (t.id && t.name && t.content && !existingIds.has(t.id)) {
2153
- existing.push(t);
2154
- added++;
2155
- }
2156
- }
2157
- saveTemplatesToStorage(existing);
2158
- renderTemplateList();
2159
- addInfoMessage(`📥 Imported ${added} template(s).`);
2160
- } catch {
2161
- addErrorMessage('Failed to import templates: invalid JSON format.');
2162
- }
2163
- };
2164
- reader.readAsText(file);
2165
- };
2166
- input.click();
2167
- }
2168
-
2169
- // Template button + search binding
2170
- document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
2171
- document.getElementById('template-search')?.addEventListener('input', (e) => {
2172
- renderTemplateList(e.target.value);
2173
- });
2174
-
2175
- // ── File Tree ──────────────────────────────────────────────────────
2176
-
2177
1995
  const fileTreeEl = document.getElementById('file-tree');
2178
1996
  const fileTreeCwdEl = document.getElementById('file-tree-cwd');
2179
1997
  const btnFileTreeRefresh = document.getElementById('btn-file-tree-refresh');
@@ -2324,10 +2142,10 @@ async function previewFileInChat(filePath) {
2324
2142
  <div class="flex items-center gap-2 mb-1">
2325
2143
  <span class="text-xs font-semibold opacity-70">📄 ${escapeHtml(filePath)}</span>
2326
2144
  <span class="badge badge-ghost badge-xs">${sizeKb} KB</span>
2327
- <button class="btn btn-xs btn-ghost ml-auto opacity-50" onclick="insertFileRef('${escapeHtml(filePath).replace(/'/g, "\\'")}')">Insert @ref</button>
2145
+ <button class="btn btn-xs btn-ghost ml-auto opacity-50" data-action="insert-file-ref" data-file-path="${escapeHtml(filePath)}">Insert @ref</button>
2328
2146
  </div>
2329
2147
  <pre style="max-height:300px"><code class="language-${escapeHtml(ext)}">${escapeHtml(data.content)}</code></pre>
2330
- <button class="btn btn-xs btn-ghost mt-1 opacity-50 text-xs" onclick="this.previousElementSibling.style.maxHeight='none';this.remove()">▼ Show all</button>
2148
+ <button class="btn btn-xs btn-ghost mt-1 opacity-50 text-xs" data-action="show-full-preview">▼ Show all</button>
2331
2149
  `;
2332
2150
  el.querySelectorAll('pre code').forEach(block => {
2333
2151
  try { hljs.highlightElement(block); } catch {}
@@ -3308,7 +3126,7 @@ function hubDone(msg) {
3308
3126
  document.getElementById('hub-abort-btn').classList.add('hidden');
3309
3127
  const status = document.getElementById('hub-status');
3310
3128
  if (msg.saved && msg.sessionId) {
3311
- status.innerHTML = `Saved. <a class="link link-primary" onclick="send({type:'command',name:'session',args:['load','${msg.sessionId}']})">Open session</a>`;
3129
+ status.innerHTML = `Saved. <a class="link link-primary" data-action="load-session" data-session-id="${msg.sessionId}">Open session</a>`;
3312
3130
  } else {
3313
3131
  status.textContent = msg.saved ? 'Saved to history.' : 'Done (not saved).';
3314
3132
  }
@@ -36,7 +36,7 @@
36
36
  <p id="auth-subtitle" class="text-sm text-base-content/60 mt-1">Sign in to continue</p>
37
37
  </div>
38
38
  <div id="auth-error" class="alert alert-error text-sm mb-4 hidden"></div>
39
- <form id="auth-form" onsubmit="handleAuth(event)">
39
+ <form id="auth-form" >
40
40
  <div class="form-control mb-3">
41
41
  <label class="label"><span class="label-text">Username</span></label>
42
42
  <input id="auth-username" type="text" class="input input-bordered w-full" placeholder="username" autocomplete="username" required minlength="2" maxlength="32">
@@ -48,7 +48,7 @@
48
48
  <button id="auth-submit" type="submit" class="btn btn-primary w-full">Sign In</button>
49
49
  </form>
50
50
  <div class="text-center mt-4">
51
- <button id="auth-toggle" class="btn btn-ghost btn-sm" onclick="toggleAuthMode()">
51
+ <button id="auth-toggle" class="btn btn-ghost btn-sm" data-action="toggle-auth-mode" type="button">
52
52
  Don't have an account? <span class="text-primary">Register</span>
53
53
  </button>
54
54
  </div>
@@ -76,18 +76,18 @@
76
76
  <div class="dropdown dropdown-end">
77
77
  <div tabindex="0" role="button" class="btn btn-sm btn-ghost">🎨</div>
78
78
  <ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-40 p-2 shadow-lg border border-base-content/10">
79
- <li><a onclick="setTheme('dark')">🌙 Dark</a></li>
80
- <li><a onclick="setTheme('night')">🌃 Night</a></li>
81
- <li><a onclick="setTheme('dim')">🌑 Dim</a></li>
82
- <li><a onclick="setTheme('synthwave')">🎵 Synthwave</a></li>
83
- <li><a onclick="setTheme('cyberpunk')">🤖 Cyberpunk</a></li>
84
- <li><a onclick="setTheme('light')">☀️ Light</a></li>
85
- <li><a onclick="setTheme('cupcake')">🧁 Cupcake</a></li>
86
- <li><a onclick="setTheme('nord')">❄️ Nord</a></li>
79
+ <li><a data-theme-choice="dark">🌙 Dark</a></li>
80
+ <li><a data-theme-choice="night">🌃 Night</a></li>
81
+ <li><a data-theme-choice="dim">🌑 Dim</a></li>
82
+ <li><a data-theme-choice="synthwave">🎵 Synthwave</a></li>
83
+ <li><a data-theme-choice="cyberpunk">🤖 Cyberpunk</a></li>
84
+ <li><a data-theme-choice="light">☀️ Light</a></li>
85
+ <li><a data-theme-choice="cupcake">🧁 Cupcake</a></li>
86
+ <li><a data-theme-choice="nord">❄️ Nord</a></li>
87
87
  </ul>
88
88
  </div>
89
89
  <!-- Enable Auth button (shown when no users registered) -->
90
- <button id="btn-enable-auth" class="btn btn-sm btn-ghost hidden" title="Enable multi-user auth" onclick="showEnableAuthDialog()">👤 Enable Auth</button>
90
+ <button id="btn-enable-auth" class="btn btn-sm btn-ghost hidden" title="Enable multi-user auth" data-action="show-enable-auth">👤 Enable Auth</button>
91
91
  <!-- User menu (shown when authenticated) -->
92
92
  <div id="user-menu" class="dropdown dropdown-end hidden">
93
93
  <div tabindex="0" role="button" class="btn btn-sm btn-ghost gap-1">
@@ -95,7 +95,7 @@
95
95
  </div>
96
96
  <ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-40 p-2 shadow-lg border border-base-content/10">
97
97
  <li><a id="user-label" class="text-sm font-semibold pointer-events-none"></a></li>
98
- <li><a onclick="handleLogout()">🚪 Logout</a></li>
98
+ <li><a data-action="logout">🚪 Logout</a></li>
99
99
  </ul>
100
100
  </div>
101
101
  </div>
@@ -119,16 +119,16 @@
119
119
  <div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
120
120
  <div class="p-2 border-b border-base-content/10 flex items-center justify-between gap-1">
121
121
  <input id="session-search" type="text" class="input input-xs input-bordered flex-1 min-w-0" placeholder="Search...">
122
- <button id="btn-batch-select" class="btn btn-xs btn-ghost flex-shrink-0" title="Select multiple" onclick="toggleBatchSelect()">☑</button>
122
+ <button id="btn-batch-select" class="btn btn-xs btn-ghost flex-shrink-0" title="Select multiple" data-action="toggle-batch-select">☑</button>
123
123
  <button id="btn-new-session" class="btn btn-xs btn-primary btn-outline flex-shrink-0 whitespace-nowrap" title="New session">+ New</button>
124
124
  </div>
125
125
  <!-- Batch action bar (hidden by default) -->
126
126
  <div id="batch-bar" class="hidden px-2 py-1 border-b border-base-content/10 flex items-center gap-1 bg-base-300 text-xs">
127
127
  <span class="batch-count opacity-60">0 selected</span>
128
128
  <span class="flex-1"></span>
129
- <button class="btn btn-xs btn-ghost" onclick="selectAllSessions()">All</button>
130
- <button class="btn btn-xs btn-error btn-outline" onclick="batchDeleteSelected()">🗑 Delete</button>
131
- <button class="btn btn-xs btn-ghost" onclick="toggleBatchSelect()">Cancel</button>
129
+ <button class="btn btn-xs btn-ghost" data-action="select-all-sessions">All</button>
130
+ <button class="btn btn-xs btn-error btn-outline" data-action="batch-delete-selected">🗑 Delete</button>
131
+ <button class="btn btn-xs btn-ghost" data-action="toggle-batch-select">Cancel</button>
132
132
  </div>
133
133
  <div id="session-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
134
134
  <div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
@@ -216,8 +216,8 @@
216
216
  <label class="flex items-center gap-2 text-xs cursor-pointer">
217
217
  <input id="hub-lanes" type="checkbox" class="checkbox checkbox-xs"> Lanes view (one column per agent)
218
218
  </label>
219
- <button id="hub-start-btn" class="btn btn-primary btn-sm w-full" onclick="startHub()">🏛 Start discussion</button>
220
- <button id="hub-abort-btn" class="btn btn-error btn-outline btn-xs w-full hidden" onclick="abortHub()">⏹ Stop</button>
219
+ <button id="hub-start-btn" class="btn btn-primary btn-sm w-full" data-action="start-hub">🏛 Start discussion</button>
220
+ <button id="hub-abort-btn" class="btn btn-error btn-outline btn-xs w-full hidden" data-action="abort-hub">⏹ Stop</button>
221
221
  <div id="hub-status" class="text-xs opacity-60"></div>
222
222
  </div>
223
223
  </div>
@@ -225,7 +225,7 @@
225
225
  <!-- Sidebar resize handle -->
226
226
  <div id="sidebar-resize" class="sidebar-resize-handle" title="Drag to resize sidebar"></div>
227
227
  <!-- Sidebar backdrop (mobile overlay) -->
228
- <div id="sidebar-backdrop" class="sidebar-backdrop hidden" onclick="closeSidebar()"></div>
228
+ <div id="sidebar-backdrop" class="sidebar-backdrop hidden" data-action="close-sidebar"></div>
229
229
 
230
230
  <!-- Chat Area -->
231
231
  <div class="flex-1 flex flex-col overflow-hidden">
@@ -293,9 +293,9 @@
293
293
  <div class="flex items-center justify-between mb-3">
294
294
  <h3 class="font-bold text-lg">📝 Prompt Templates</h3>
295
295
  <div class="flex gap-1">
296
- <button class="btn btn-xs btn-ghost" onclick="importTemplates()" title="Import">📥 Import</button>
297
- <button class="btn btn-xs btn-ghost" onclick="exportTemplates()" title="Export">📤 Export</button>
298
- <button class="btn btn-xs btn-ghost" onclick="showAddTemplate()" title="New template">+ New</button>
296
+ <button class="btn btn-xs btn-ghost" data-action="import-templates" title="Import">📥 Import</button>
297
+ <button class="btn btn-xs btn-ghost" data-action="export-templates" title="Export">📤 Export</button>
298
+ <button class="btn btn-xs btn-ghost" data-action="show-add-template" title="New template">+ New</button>
299
299
  </div>
300
300
  </div>
301
301
 
@@ -305,8 +305,8 @@
305
305
  <textarea id="tpl-content" class="textarea textarea-bordered w-full text-sm mb-2" rows="4" placeholder="Prompt content..."></textarea>
306
306
  <input id="tpl-tags" type="text" class="input input-sm input-bordered w-full mb-2" placeholder="Tags (comma-separated, optional)">
307
307
  <div class="flex gap-2 justify-end">
308
- <button class="btn btn-sm btn-ghost" onclick="cancelTemplateForm()">Cancel</button>
309
- <button class="btn btn-sm btn-primary" onclick="saveTemplateForm()">Save</button>
308
+ <button class="btn btn-sm btn-ghost" data-action="cancel-template-form">Cancel</button>
309
+ <button class="btn btn-sm btn-primary" data-action="save-template-form">Save</button>
310
310
  </div>
311
311
  <input type="hidden" id="tpl-edit-id">
312
312
  </div>
@@ -338,23 +338,8 @@
338
338
  <form method="dialog" class="modal-backdrop"><button>close</button></form>
339
339
  </dialog>
340
340
 
341
- <script src="app.js"></script>
342
- <script>
343
- // Self-updating service worker (v0.4.159). A new SW postMessages
344
- // 'sw-updated' (and triggers controllerchange) when it takes over; we
345
- // reload once so the user never gets stuck on a stale index.html/app.js
346
- // after upgrading aicli. index.html/app.js are also network-only in sw.js.
347
- if ('serviceWorker' in navigator) {
348
- let swReloaded = false;
349
- const reloadOnce = () => { if (!swReloaded) { swReloaded = true; location.reload(); } };
350
- navigator.serviceWorker.addEventListener('message', (e) => {
351
- if (e.data && e.data.type === 'sw-updated') reloadOnce();
352
- });
353
- navigator.serviceWorker.addEventListener('controllerchange', reloadOnce);
354
- navigator.serviceWorker.register('sw.js').then((reg) => {
355
- reg.update();
356
- }).catch(() => {});
357
- }
358
- </script>
341
+ <script src="app.js"></script>
342
+ <script src="templates.js"></script>
343
+ <script src="actions.js"></script>
359
344
  </body>
360
345
  </html>
@@ -16,7 +16,7 @@ const CACHE_NAME = 'aicli-v3';
16
16
  // HTML/JS that must always be fresh — never served from the SW cache, so an
17
17
  // upgraded server is reflected on the very next load. Other assets (vendored
18
18
  // CSS/JS, icons) are large + content-stable and fine to cache.
19
- const NEVER_CACHE = new Set(['/', '/index.html', '/app.js']);
19
+ const NEVER_CACHE = new Set(['/', '/index.html', '/app.js', '/templates.js', '/actions.js']);
20
20
 
21
21
  // App shell to precache (offline-capable startup). Deliberately EXCLUDES the
22
22
  // always-fresh HTML/JS in NEVER_CACHE — only large content-stable assets.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Prompt template modal and import/export helpers.
3
+ * Loaded after app.js so it can use shared UI helpers.
4
+ */
5
+
6
+ const TEMPLATES_KEY = 'aicli-templates';
7
+
8
+ function loadTemplates() {
9
+ try {
10
+ return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ function saveTemplatesToStorage(templates) {
17
+ localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
18
+ }
19
+
20
+ function openTemplatesModal() {
21
+ const modal = document.getElementById('templates-modal');
22
+ if (!modal) return;
23
+ cancelTemplateForm();
24
+ renderTemplateList();
25
+ document.getElementById('template-search').value = '';
26
+ modal.showModal();
27
+ }
28
+
29
+ function renderTemplateList(filter) {
30
+ const listEl = document.getElementById('template-list');
31
+ if (!listEl) return;
32
+ const templates = loadTemplates();
33
+ const q = (filter || '').toLowerCase();
34
+ const filtered = q
35
+ ? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
36
+ : templates;
37
+
38
+ if (filtered.length === 0) {
39
+ listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
40
+ return;
41
+ }
42
+
43
+ listEl.innerHTML = filtered.map(t => {
44
+ const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
45
+ const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
46
+ return `
47
+ <div class="template-item" data-tpl-id="${t.id}" data-action="use-template">
48
+ <div class="template-item-body">
49
+ <div class="template-item-name">${escapeHtml(t.name)}</div>
50
+ <div class="template-item-preview">${escapeHtml(preview)}</div>
51
+ ${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
52
+ </div>
53
+ <div class="template-item-actions">
54
+ <button class="btn btn-xs btn-ghost" data-action="edit-template" title="Edit">Edit</button>
55
+ <button class="btn btn-xs btn-ghost" data-action="delete-template" title="Delete">Delete</button>
56
+ </div>
57
+ </div>`;
58
+ }).join('');
59
+ }
60
+
61
+ function useTemplate(id) {
62
+ const templates = loadTemplates();
63
+ const tpl = templates.find(t => t.id === id);
64
+ if (!tpl) return;
65
+ const input = document.getElementById('user-input');
66
+ if (!input) return;
67
+ input.value = tpl.content;
68
+ input.focus();
69
+ input.style.height = 'auto';
70
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
71
+ document.getElementById('templates-modal')?.close();
72
+ }
73
+
74
+ function showAddTemplate() {
75
+ const form = document.getElementById('template-form');
76
+ form.classList.remove('hidden');
77
+ document.getElementById('tpl-edit-id').value = '';
78
+ document.getElementById('tpl-name').value = '';
79
+ document.getElementById('tpl-content').value = '';
80
+ document.getElementById('tpl-tags').value = '';
81
+ document.getElementById('tpl-name').focus();
82
+ }
83
+
84
+ function editTemplate(id) {
85
+ const templates = loadTemplates();
86
+ const tpl = templates.find(t => t.id === id);
87
+ if (!tpl) return;
88
+ const form = document.getElementById('template-form');
89
+ form.classList.remove('hidden');
90
+ document.getElementById('tpl-edit-id').value = tpl.id;
91
+ document.getElementById('tpl-name').value = tpl.name;
92
+ document.getElementById('tpl-content').value = tpl.content;
93
+ document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
94
+ document.getElementById('tpl-name').focus();
95
+ }
96
+
97
+ function cancelTemplateForm() {
98
+ document.getElementById('template-form')?.classList.add('hidden');
99
+ }
100
+
101
+ function saveTemplateForm() {
102
+ const name = document.getElementById('tpl-name').value.trim();
103
+ const content = document.getElementById('tpl-content').value.trim();
104
+ const tagsRaw = document.getElementById('tpl-tags').value.trim();
105
+ const editId = document.getElementById('tpl-edit-id').value;
106
+
107
+ if (!name || !content) return;
108
+
109
+ const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
110
+ const templates = loadTemplates();
111
+
112
+ if (editId) {
113
+ const idx = templates.findIndex(t => t.id === editId);
114
+ if (idx >= 0) {
115
+ templates[idx].name = name;
116
+ templates[idx].content = content;
117
+ templates[idx].tags = tags;
118
+ }
119
+ } else {
120
+ templates.unshift({
121
+ id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
122
+ name,
123
+ content,
124
+ tags,
125
+ createdAt: new Date().toISOString(),
126
+ });
127
+ }
128
+
129
+ saveTemplatesToStorage(templates);
130
+ cancelTemplateForm();
131
+ renderTemplateList();
132
+ }
133
+
134
+ function deleteTemplate(id) {
135
+ const templates = loadTemplates().filter(t => t.id !== id);
136
+ saveTemplatesToStorage(templates);
137
+ renderTemplateList(document.getElementById('template-search')?.value);
138
+ }
139
+
140
+ function exportTemplates() {
141
+ const templates = loadTemplates();
142
+ if (templates.length === 0) return;
143
+ const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
144
+ const url = URL.createObjectURL(blob);
145
+ const a = document.createElement('a');
146
+ a.href = url;
147
+ a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
148
+ a.click();
149
+ URL.revokeObjectURL(url);
150
+ }
151
+
152
+ function importTemplates() {
153
+ const input = document.createElement('input');
154
+ input.type = 'file';
155
+ input.accept = '.json';
156
+ input.addEventListener('change', () => {
157
+ const file = input.files[0];
158
+ if (!file) return;
159
+ const reader = new FileReader();
160
+ reader.addEventListener('load', () => {
161
+ try {
162
+ const imported = JSON.parse(reader.result);
163
+ if (!Array.isArray(imported)) throw new Error('Not an array');
164
+ const existing = loadTemplates();
165
+ const existingIds = new Set(existing.map(t => t.id));
166
+ let added = 0;
167
+ for (const t of imported) {
168
+ if (t.id && t.name && t.content && !existingIds.has(t.id)) {
169
+ existing.push(t);
170
+ added++;
171
+ }
172
+ }
173
+ saveTemplatesToStorage(existing);
174
+ renderTemplateList();
175
+ addInfoMessage(`Imported ${added} template(s).`);
176
+ } catch {
177
+ addErrorMessage('Failed to import templates: invalid JSON format.');
178
+ }
179
+ });
180
+ reader.readAsText(file);
181
+ });
182
+ input.click();
183
+ }
184
+
185
+ document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
186
+ document.getElementById('template-search')?.addEventListener('input', (e) => {
187
+ renderTemplateList(e.target.value);
188
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.206",
3
+ "version": "0.4.207",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",