jinzd-ai-cli 0.4.106 → 0.4.108

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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigManager
4
- } from "./chunk-OSGGBJSW.js";
4
+ } from "./chunk-A26EOV2J.js";
5
5
  import "./chunk-2ZD3YTVM.js";
6
- import "./chunk-ITHFJSJQ.js";
6
+ import "./chunk-DNKI3JIL.js";
7
7
  import "./chunk-PDX44BCA.js";
8
8
 
9
9
  // src/cli/batch.ts
@@ -6,7 +6,7 @@ import {
6
6
  getChatIndexStatus,
7
7
  loadChatIndex,
8
8
  searchChatMemory
9
- } from "./chunk-ANYYM4CF.js";
9
+ } from "./chunk-7ZJN4KLV.js";
10
10
  import "./chunk-KHYD3WXE.js";
11
11
  import "./chunk-PDX44BCA.js";
12
12
  export {
@@ -5,7 +5,7 @@ import {
5
5
  getChatIndexStatus,
6
6
  loadChatIndex,
7
7
  searchChatMemory
8
- } from "./chunk-5S3PIG5O.js";
8
+ } from "./chunk-WJ32NAW3.js";
9
9
  import "./chunk-JV5N65KN.js";
10
10
  import "./chunk-3RG5ZIWI.js";
11
11
  export {
@@ -23,6 +23,11 @@ var DEFAULT_PATTERNS = [
23
23
  { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
24
24
  // Anthropic API keys
25
25
  { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
26
+ // L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
27
+ // Two segments separated by a dot, each safely identifiable by length
28
+ // and char class. Conservative on the lower bound so we don't eat
29
+ // version strings like `1.0.0` or filenames.
30
+ { kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
26
31
  // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
27
32
  { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
28
33
  // GitHub personal access tokens
@@ -46,18 +51,24 @@ var DEFAULT_PATTERNS = [
46
51
  // Private key PEM blocks — catch the header+footer together
47
52
  { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
48
53
  ];
54
+ var MAX_CUSTOM = 32;
55
+ var MAX_PATTERN_LEN = 500;
56
+ var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
49
57
  function render(placeholder, kind) {
50
58
  return placeholder.replace("{kind}", kind);
51
59
  }
52
60
  function redactString(input, options) {
53
61
  if (!options.enabled || !input) return { redacted: input, hits: [] };
54
62
  const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
63
+ const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
55
64
  const patterns = [
56
65
  ...options.patterns ?? DEFAULT_PATTERNS,
57
- ...(options.customRegexes ?? []).flatMap((src, i) => {
66
+ ...customSrcs.flatMap((src, i) => {
67
+ if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
58
68
  try {
59
69
  const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
60
70
  const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
71
+ if (SUSPICIOUS_REDOS.test(body)) return [];
61
72
  const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
62
73
  return [{ kind: `custom-${i}`, regex }];
63
74
  } catch {
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-ITHFJSJQ.js";
11
+ } from "./chunk-DNKI3JIL.js";
12
12
 
13
13
  // src/config/config-manager.ts
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -268,9 +268,23 @@ ${err}`
268
268
  /**
269
269
  * 仅修改内存中的配置值,不持久化到磁盘。
270
270
  * 用于 CLI 命令行参数覆盖(-p, -m, --no-stream),使其只对当前进程生效。
271
+ *
272
+ * M6 fix (v0.4.107):原版直接赋值不走 Zod,调用点必须自律确保来源可信
273
+ * (如 CLI flag、global config)。如果未来谁把外部输入接到这里,类型系统
274
+ * 不会阻止恶意值偷渡进 config。现在用 partial Zod parse 校验单字段,
275
+ * 失败时抛 ConfigError。setTransient 永远不接受外部不可信输入——这是
276
+ * 「调用方信任」契约的硬实现。
271
277
  */
272
278
  setTransient(key, value) {
273
- this.config[key] = value;
279
+ const draft = { ...this.config, [key]: value };
280
+ const result = ConfigSchema.safeParse(draft);
281
+ if (!result.success) {
282
+ const firstErr = result.error.errors[0];
283
+ throw new ConfigError(
284
+ `Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
285
+ );
286
+ }
287
+ this.config = result.data;
274
288
  }
275
289
  isFirstRun() {
276
290
  return !existsSync(this.configPath);
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-3BICTI5M.js";
6
6
  import {
7
7
  runTestsTool
8
- } from "./chunk-W7ZG4KRQ.js";
8
+ } from "./chunk-NN2B3UBA.js";
9
9
  import {
10
10
  EnvLoader,
11
11
  NetworkError,
@@ -18,14 +18,14 @@ import {
18
18
  SUBAGENT_ALLOWED_TOOLS,
19
19
  SUBAGENT_DEFAULT_MAX_ROUNDS,
20
20
  SUBAGENT_MAX_ROUNDS_LIMIT
21
- } from "./chunk-ITHFJSJQ.js";
21
+ } from "./chunk-DNKI3JIL.js";
22
22
  import {
23
23
  fileCheckpoints
24
24
  } from "./chunk-4BKXL7SM.js";
25
25
  import {
26
26
  loadChatIndex,
27
27
  searchChatMemory
28
- } from "./chunk-ANYYM4CF.js";
28
+ } from "./chunk-7ZJN4KLV.js";
29
29
  import {
30
30
  indexProject
31
31
  } from "./chunk-NHNWUBXB.js";
@@ -667,6 +667,20 @@ function createCell(cellType, content) {
667
667
 
668
668
  // src/tools/builtin/read-file.ts
669
669
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
670
+ function getHardRefusal(normalizedPath) {
671
+ const home2 = homedir();
672
+ const p = normalizedPath.toLowerCase();
673
+ const base = basename(normalizedPath).toLowerCase();
674
+ if (normalizedPath.startsWith(home2) && p.includes(".aicli") && base === "config.json") {
675
+ return `[Refused] Reading ${normalizedPath} is blocked because it contains all of your provider API keys.
676
+ If you genuinely need to inspect or edit it, open it manually outside ai-cli (e.g. via your editor).
677
+ For programmatic access, use \`aicli config\` or \`aicli config get <key>\` which never expose raw keys.`;
678
+ }
679
+ if (normalizedPath.startsWith(home2) && p.includes(".aicli") && base === "users.json") {
680
+ return `[Refused] Reading ${normalizedPath} is blocked because it contains password hashes and the token-signing secret.`;
681
+ }
682
+ return null;
683
+ }
670
684
  function getSensitiveWarning(normalizedPath) {
671
685
  const home2 = homedir();
672
686
  const p = normalizedPath.toLowerCase();
@@ -819,6 +833,8 @@ var readFileTool = {
819
833
  const limitLines = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
820
834
  if (!filePath) throw new ToolError("read_file", "path is required");
821
835
  const normalizedPath = resolve2(filePath);
836
+ const refusal = getHardRefusal(normalizedPath);
837
+ if (refusal) return refusal;
822
838
  if (!existsSync3(normalizedPath)) {
823
839
  const suggestions = findSimilarFiles(filePath);
824
840
  if (suggestions.length > 0) {
@@ -1136,21 +1152,26 @@ function simpleDiff(oldLines, newLines) {
1136
1152
 
1137
1153
  // src/tools/hooks.ts
1138
1154
  import { execSync } from "child_process";
1139
- function shellEscape(value) {
1140
- return "'" + value.replace(/'/g, "'\\''") + "'";
1155
+ function rewriteTemplate(template, isWindows) {
1156
+ const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
1157
+ return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
1141
1158
  }
1142
1159
  function runHook(template, vars) {
1143
1160
  if (!template) return;
1144
- let cmd = template;
1145
- cmd = cmd.replace(/\{tool\}/g, shellEscape(vars.tool));
1146
- cmd = cmd.replace(/\{dangerLevel\}/g, shellEscape(vars.dangerLevel ?? ""));
1147
- cmd = cmd.replace(/\{args\}/g, shellEscape(vars.args ?? ""));
1148
- cmd = cmd.replace(/\{status\}/g, shellEscape(vars.status ?? ""));
1161
+ const isWindows = process.platform === "win32";
1162
+ const cmd = rewriteTemplate(template, isWindows);
1149
1163
  try {
1150
1164
  execSync(cmd, {
1151
1165
  timeout: 5e3,
1152
1166
  stdio: ["pipe", "pipe", "pipe"],
1153
- encoding: "utf-8"
1167
+ encoding: "utf-8",
1168
+ env: {
1169
+ ...process.env,
1170
+ AICLI_HOOK_TOOL: vars.tool,
1171
+ AICLI_HOOK_DANGER_LEVEL: vars.dangerLevel ?? "",
1172
+ AICLI_HOOK_ARGS: vars.args ?? "",
1173
+ AICLI_HOOK_STATUS: vars.status ?? ""
1174
+ }
1154
1175
  });
1155
1176
  } catch {
1156
1177
  process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.106";
4
+ var VERSION = "0.4.108";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  schemaToJsonSchema,
4
4
  truncateForPersist
5
- } from "./chunk-ML7ISZ7A.js";
5
+ } from "./chunk-A3HZY7DA.js";
6
6
  import {
7
7
  AuthError,
8
8
  ProviderError,
@@ -18,10 +18,10 @@ import {
18
18
  MCP_PROTOCOL_VERSION,
19
19
  MCP_TOOL_PREFIX,
20
20
  VERSION
21
- } from "./chunk-ITHFJSJQ.js";
21
+ } from "./chunk-DNKI3JIL.js";
22
22
  import {
23
23
  redactJson
24
- } from "./chunk-ANYYM4CF.js";
24
+ } from "./chunk-7ZJN4KLV.js";
25
25
 
26
26
  // src/providers/claude.ts
27
27
  import Anthropic from "@anthropic-ai/sdk";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-ITHFJSJQ.js";
4
+ } from "./chunk-DNKI3JIL.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync, spawnSync } from "child_process";
@@ -22,6 +22,11 @@ var DEFAULT_PATTERNS = [
22
22
  { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
23
23
  // Anthropic API keys
24
24
  { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
25
+ // L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
26
+ // Two segments separated by a dot, each safely identifiable by length
27
+ // and char class. Conservative on the lower bound so we don't eat
28
+ // version strings like `1.0.0` or filenames.
29
+ { kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
25
30
  // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
26
31
  { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
27
32
  // GitHub personal access tokens
@@ -45,18 +50,24 @@ var DEFAULT_PATTERNS = [
45
50
  // Private key PEM blocks — catch the header+footer together
46
51
  { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
47
52
  ];
53
+ var MAX_CUSTOM = 32;
54
+ var MAX_PATTERN_LEN = 500;
55
+ var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
48
56
  function render(placeholder, kind) {
49
57
  return placeholder.replace("{kind}", kind);
50
58
  }
51
59
  function redactString(input, options) {
52
60
  if (!options.enabled || !input) return { redacted: input, hits: [] };
53
61
  const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
62
+ const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
54
63
  const patterns = [
55
64
  ...options.patterns ?? DEFAULT_PATTERNS,
56
- ...(options.customRegexes ?? []).flatMap((src, i) => {
65
+ ...customSrcs.flatMap((src, i) => {
66
+ if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
57
67
  try {
58
68
  const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
59
69
  const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
70
+ if (SUSPICIOUS_REDOS.test(body)) return [];
60
71
  const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
61
72
  return [{ kind: `custom-${i}`, regex }];
62
73
  } catch {
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.106";
9
+ var VERSION = "0.4.108";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -36,7 +36,7 @@ import {
36
36
  TEST_TIMEOUT,
37
37
  VERSION,
38
38
  buildUserIdentityPrompt
39
- } from "./chunk-ITHFJSJQ.js";
39
+ } from "./chunk-DNKI3JIL.js";
40
40
  import "./chunk-PDX44BCA.js";
41
41
  export {
42
42
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -36,7 +36,7 @@ import {
36
36
  VERSION,
37
37
  buildUserIdentityPrompt,
38
38
  runTestsTool
39
- } from "./chunk-RD4RCKIB.js";
39
+ } from "./chunk-XWJTZWS7.js";
40
40
  import {
41
41
  hasSemanticIndex,
42
42
  semanticSearch
@@ -49,7 +49,7 @@ import {
49
49
  loadChatIndex,
50
50
  redactJson,
51
51
  searchChatMemory
52
- } from "./chunk-5S3PIG5O.js";
52
+ } from "./chunk-WJ32NAW3.js";
53
53
  import "./chunk-JV5N65KN.js";
54
54
  import "./chunk-3RG5ZIWI.js";
55
55
 
@@ -421,9 +421,23 @@ ${err}`
421
421
  /**
422
422
  * 仅修改内存中的配置值,不持久化到磁盘。
423
423
  * 用于 CLI 命令行参数覆盖(-p, -m, --no-stream),使其只对当前进程生效。
424
+ *
425
+ * M6 fix (v0.4.107):原版直接赋值不走 Zod,调用点必须自律确保来源可信
426
+ * (如 CLI flag、global config)。如果未来谁把外部输入接到这里,类型系统
427
+ * 不会阻止恶意值偷渡进 config。现在用 partial Zod parse 校验单字段,
428
+ * 失败时抛 ConfigError。setTransient 永远不接受外部不可信输入——这是
429
+ * 「调用方信任」契约的硬实现。
424
430
  */
425
431
  setTransient(key, value) {
426
- this.config[key] = value;
432
+ const draft = { ...this.config, [key]: value };
433
+ const result = ConfigSchema.safeParse(draft);
434
+ if (!result.success) {
435
+ const firstErr = result.error.errors[0];
436
+ throw new ConfigError(
437
+ `Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
438
+ );
439
+ }
440
+ this.config = result.data;
427
441
  }
428
442
  isFirstRun() {
429
443
  return !existsSync(this.configPath);
@@ -4059,6 +4073,20 @@ function createCell(cellType, content) {
4059
4073
 
4060
4074
  // src/tools/builtin/read-file.ts
4061
4075
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
4076
+ function getHardRefusal(normalizedPath) {
4077
+ const home2 = homedir2();
4078
+ const p = normalizedPath.toLowerCase();
4079
+ const base = basename(normalizedPath).toLowerCase();
4080
+ if (normalizedPath.startsWith(home2) && p.includes(".aicli") && base === "config.json") {
4081
+ return `[Refused] Reading ${normalizedPath} is blocked because it contains all of your provider API keys.
4082
+ If you genuinely need to inspect or edit it, open it manually outside ai-cli (e.g. via your editor).
4083
+ For programmatic access, use \`aicli config\` or \`aicli config get <key>\` which never expose raw keys.`;
4084
+ }
4085
+ if (normalizedPath.startsWith(home2) && p.includes(".aicli") && base === "users.json") {
4086
+ return `[Refused] Reading ${normalizedPath} is blocked because it contains password hashes and the token-signing secret.`;
4087
+ }
4088
+ return null;
4089
+ }
4062
4090
  function getSensitiveWarning(normalizedPath) {
4063
4091
  const home2 = homedir2();
4064
4092
  const p = normalizedPath.toLowerCase();
@@ -4211,6 +4239,8 @@ var readFileTool = {
4211
4239
  const limitLines = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
4212
4240
  if (!filePath) throw new ToolError("read_file", "path is required");
4213
4241
  const normalizedPath = resolve2(filePath);
4242
+ const refusal = getHardRefusal(normalizedPath);
4243
+ if (refusal) return refusal;
4214
4244
  if (!existsSync5(normalizedPath)) {
4215
4245
  const suggestions = findSimilarFiles(filePath);
4216
4246
  if (suggestions.length > 0) {
@@ -4528,21 +4558,26 @@ function simpleDiff(oldLines, newLines) {
4528
4558
 
4529
4559
  // src/tools/hooks.ts
4530
4560
  import { execSync } from "child_process";
4531
- function shellEscape(value) {
4532
- return "'" + value.replace(/'/g, "'\\''") + "'";
4561
+ function rewriteTemplate(template, isWindows) {
4562
+ const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
4563
+ return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
4533
4564
  }
4534
4565
  function runHook(template, vars) {
4535
4566
  if (!template) return;
4536
- let cmd = template;
4537
- cmd = cmd.replace(/\{tool\}/g, shellEscape(vars.tool));
4538
- cmd = cmd.replace(/\{dangerLevel\}/g, shellEscape(vars.dangerLevel ?? ""));
4539
- cmd = cmd.replace(/\{args\}/g, shellEscape(vars.args ?? ""));
4540
- cmd = cmd.replace(/\{status\}/g, shellEscape(vars.status ?? ""));
4567
+ const isWindows = process.platform === "win32";
4568
+ const cmd = rewriteTemplate(template, isWindows);
4541
4569
  try {
4542
4570
  execSync(cmd, {
4543
4571
  timeout: 5e3,
4544
4572
  stdio: ["pipe", "pipe", "pipe"],
4545
- encoding: "utf-8"
4573
+ encoding: "utf-8",
4574
+ env: {
4575
+ ...process.env,
4576
+ AICLI_HOOK_TOOL: vars.tool,
4577
+ AICLI_HOOK_DANGER_LEVEL: vars.dangerLevel ?? "",
4578
+ AICLI_HOOK_ARGS: vars.args ?? "",
4579
+ AICLI_HOOK_STATUS: vars.status ?? ""
4580
+ }
4546
4581
  });
4547
4582
  } catch {
4548
4583
  process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
@@ -11754,7 +11789,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
11754
11789
  case "test": {
11755
11790
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
11756
11791
  try {
11757
- const { executeTests } = await import("./run-tests-6FVJND6Q.js");
11792
+ const { executeTests } = await import("./run-tests-VBJ5H3XR.js");
11758
11793
  const argStr = args.join(" ").trim();
11759
11794
  let testArgs = {};
11760
11795
  if (argStr) {
@@ -12278,7 +12313,7 @@ Add .md files to create commands.` });
12278
12313
  return;
12279
12314
  }
12280
12315
  try {
12281
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-ADG2GPCC.js");
12316
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-MY3DJTV3.js");
12282
12317
  const loaded = loadChatIndex2();
12283
12318
  if (!loaded || loaded.idx.chunks.length === 0) {
12284
12319
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -12314,7 +12349,7 @@ Add .md files to create commands.` });
12314
12349
  }
12315
12350
  async handleMemoryStatus() {
12316
12351
  try {
12317
- const { getChatIndexStatus } = await import("./chat-index-ADG2GPCC.js");
12352
+ const { getChatIndexStatus } = await import("./chat-index-MY3DJTV3.js");
12318
12353
  const s = getChatIndexStatus();
12319
12354
  this.send({
12320
12355
  type: "memory_status",
@@ -12339,7 +12374,7 @@ Add .md files to create commands.` });
12339
12374
  type: "info",
12340
12375
  message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
12341
12376
  });
12342
- const { buildChatIndex } = await import("./chat-index-ADG2GPCC.js");
12377
+ const { buildChatIndex } = await import("./chat-index-MY3DJTV3.js");
12343
12378
  const stats = await buildChatIndex({
12344
12379
  full,
12345
12380
  onProgress: (p) => {
@@ -13117,7 +13152,25 @@ async function startWebServer(options = {}) {
13117
13152
  const token = authManager.login(username, password);
13118
13153
  console.log(` \u2713 User registered via API: ${username}${firstRun ? " (first-run)" : ""}`);
13119
13154
  res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: 7 * 24 * 3600 * 1e3 });
13120
- res.json({ success: true, token, username });
13155
+ res.json({ success: true, username });
13156
+ });
13157
+ app.post("/api/auth/login", (req, res) => {
13158
+ const { username, password } = req.body ?? {};
13159
+ if (!username || !password) {
13160
+ res.status(400).json({ error: "Username and password required" });
13161
+ return;
13162
+ }
13163
+ const token = authManager.login(username, password);
13164
+ if (!token) {
13165
+ res.status(401).json({ error: "Invalid username or password" });
13166
+ return;
13167
+ }
13168
+ res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: 7 * 24 * 3600 * 1e3 });
13169
+ res.json({ success: true, username });
13170
+ });
13171
+ app.post("/api/auth/logout", (_req, res) => {
13172
+ res.clearCookie("aicli_token", { httpOnly: true, sameSite: "strict" });
13173
+ res.json({ success: true });
13121
13174
  });
13122
13175
  app.get("/api/files", requireAuth, (req, res) => {
13123
13176
  const cwd = process.cwd();
@@ -13358,7 +13411,7 @@ async function startWebServer(options = {}) {
13358
13411
  handlers.set(tabId, handler);
13359
13412
  clearPreAuthTimer();
13360
13413
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
13361
- ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username, setCookie: true }));
13414
+ ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
13362
13415
  return;
13363
13416
  }
13364
13417
  if (action === "token") {
@@ -13389,7 +13442,7 @@ async function startWebServer(options = {}) {
13389
13442
  handlers.set(tabId, handler);
13390
13443
  clearPreAuthTimer();
13391
13444
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
13392
- ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username, setCookie: true }));
13445
+ ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
13393
13446
  return;
13394
13447
  }
13395
13448
  ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
@@ -13502,10 +13555,14 @@ function parseCookie(cookie) {
13502
13555
  }
13503
13556
  async function openBrowser(url) {
13504
13557
  try {
13505
- const { exec } = await import("child_process");
13506
- const cmd = process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`;
13507
- exec(cmd, () => {
13508
- });
13558
+ const { spawn: spawn5 } = await import("child_process");
13559
+ if (process.platform === "win32") {
13560
+ spawn5("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
13561
+ } else if (process.platform === "darwin") {
13562
+ spawn5("open", [url], { detached: true, stdio: "ignore" }).unref();
13563
+ } else {
13564
+ spawn5("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
13565
+ }
13509
13566
  } catch {
13510
13567
  }
13511
13568
  }
@@ -386,7 +386,7 @@ ${content}`);
386
386
  }
387
387
  }
388
388
  async function runTaskMode(config, providers, configManager, topic) {
389
- const { TaskOrchestrator } = await import("./task-orchestrator-32DSRTMN.js");
389
+ const { TaskOrchestrator } = await import("./task-orchestrator-6WIYZPDB.js");
390
390
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
391
391
  let interrupted = false;
392
392
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -25,10 +25,10 @@ import {
25
25
  saveDevState,
26
26
  sessionHasMeaningfulContent,
27
27
  setupProxy
28
- } from "./chunk-RSZ75E2W.js";
28
+ } from "./chunk-IKX4D6C7.js";
29
29
  import {
30
30
  ConfigManager
31
- } from "./chunk-OSGGBJSW.js";
31
+ } from "./chunk-A26EOV2J.js";
32
32
  import {
33
33
  ToolExecutor,
34
34
  ToolRegistry,
@@ -47,10 +47,10 @@ import {
47
47
  spawnAgentContext,
48
48
  theme,
49
49
  undoStack
50
- } from "./chunk-ML7ISZ7A.js";
50
+ } from "./chunk-A3HZY7DA.js";
51
51
  import "./chunk-3BICTI5M.js";
52
52
  import "./chunk-2DXY7UGF.js";
53
- import "./chunk-W7ZG4KRQ.js";
53
+ import "./chunk-NN2B3UBA.js";
54
54
  import "./chunk-2ZD3YTVM.js";
55
55
  import {
56
56
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -73,7 +73,7 @@ import {
73
73
  SKILLS_DIR_NAME,
74
74
  VERSION,
75
75
  buildUserIdentityPrompt
76
- } from "./chunk-ITHFJSJQ.js";
76
+ } from "./chunk-DNKI3JIL.js";
77
77
  import {
78
78
  formatGitContextForPrompt,
79
79
  getGitContext,
@@ -89,7 +89,7 @@ import {
89
89
  getChatIndexStatus,
90
90
  scanString,
91
91
  searchChatMemory
92
- } from "./chunk-ANYYM4CF.js";
92
+ } from "./chunk-7ZJN4KLV.js";
93
93
  import "./chunk-KHYD3WXE.js";
94
94
  import "./chunk-NHNWUBXB.js";
95
95
  import "./chunk-6VRJGH25.js";
@@ -1594,7 +1594,7 @@ ${text}
1594
1594
  const { join: join6 } = await import("path");
1595
1595
  const { existsSync: existsSync6 } = await import("fs");
1596
1596
  const { getGitRoot: getGitRoot2 } = await import("./git-context-7KIP4X2V.js");
1597
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-FVJPGHS7.js");
1597
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-QZWMYEMB.js");
1598
1598
  const { approveProject, hashMcpFile } = await import("./project-trust-IFM7FXEV.js");
1599
1599
  const cwd = process.cwd();
1600
1600
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2644,7 +2644,7 @@ ${hint}` : "")
2644
2644
  usage: "/test [command|filter]",
2645
2645
  async execute(args, ctx) {
2646
2646
  try {
2647
- const { executeTests } = await import("./run-tests-UZGGOXOS.js");
2647
+ const { executeTests } = await import("./run-tests-LWSTPCZY.js");
2648
2648
  const argStr = args.join(" ").trim();
2649
2649
  let testArgs = {};
2650
2650
  if (argStr) {
@@ -5068,7 +5068,7 @@ Session '${this.resumeSessionId}' not found.
5068
5068
  })();
5069
5069
  void (async () => {
5070
5070
  try {
5071
- const { getChatIndexStatus: getChatIndexStatus2, buildChatIndex: buildChatIndex2 } = await import("./chat-index-7OHUKJY5.js");
5071
+ const { getChatIndexStatus: getChatIndexStatus2, buildChatIndex: buildChatIndex2 } = await import("./chat-index-JBF4ZIQI.js");
5072
5072
  const initial = getChatIndexStatus2();
5073
5073
  this.chatMemoryStatus = {
5074
5074
  exists: initial.exists,
@@ -6800,7 +6800,7 @@ program.command("web").description("Start Web UI server with browser-based chat
6800
6800
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
6801
6801
  process.exit(1);
6802
6802
  }
6803
- const { startWebServer } = await import("./server-CWDCYN3N.js");
6803
+ const { startWebServer } = await import("./server-6F2JF2JK.js");
6804
6804
  await startWebServer({ port, host: options.host });
6805
6805
  });
6806
6806
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -6923,7 +6923,7 @@ program.command("sessions").description("List recent conversation sessions").act
6923
6923
  });
6924
6924
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
6925
6925
  try {
6926
- const batch = await import("./batch-PF2EB4BE.js");
6926
+ const batch = await import("./batch-A6F3UA3N.js");
6927
6927
  switch (action) {
6928
6928
  case "submit":
6929
6929
  if (!arg) {
@@ -6966,7 +6966,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
6966
6966
  }
6967
6967
  });
6968
6968
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
6969
- const { startMcpServer } = await import("./server-5MJQOIZU.js");
6969
+ const { startMcpServer } = await import("./server-DSG3EYRA.js");
6970
6970
  await startMcpServer({
6971
6971
  allowDestructive: !!options.allowDestructive,
6972
6972
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7093,7 +7093,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
7093
7093
  }),
7094
7094
  config.get("customProviders")
7095
7095
  );
7096
- const { startHub } = await import("./hub-3KDAZUH4.js");
7096
+ const { startHub } = await import("./hub-3HEMIJTM.js");
7097
7097
  await startHub(
7098
7098
  {
7099
7099
  topic: topic ?? "",
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-W7ZG4KRQ.js";
6
- import "./chunk-ITHFJSJQ.js";
5
+ } from "./chunk-NN2B3UBA.js";
6
+ import "./chunk-DNKI3JIL.js";
7
7
  import "./chunk-PDX44BCA.js";
8
8
  export {
9
9
  executeTests,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-RD4RCKIB.js";
4
+ } from "./chunk-XWJTZWS7.js";
5
5
  import "./chunk-3RG5ZIWI.js";
6
6
  export {
7
7
  executeTests,
@@ -18,10 +18,10 @@ import {
18
18
  loadDevState,
19
19
  persistToolRound,
20
20
  setupProxy
21
- } from "./chunk-RSZ75E2W.js";
21
+ } from "./chunk-IKX4D6C7.js";
22
22
  import {
23
23
  ConfigManager
24
- } from "./chunk-OSGGBJSW.js";
24
+ } from "./chunk-A26EOV2J.js";
25
25
  import {
26
26
  ToolExecutor,
27
27
  ToolRegistry,
@@ -39,10 +39,10 @@ import {
39
39
  spawnAgentContext,
40
40
  truncateOutput,
41
41
  undoStack
42
- } from "./chunk-ML7ISZ7A.js";
42
+ } from "./chunk-A3HZY7DA.js";
43
43
  import "./chunk-3BICTI5M.js";
44
44
  import "./chunk-2DXY7UGF.js";
45
- import "./chunk-W7ZG4KRQ.js";
45
+ import "./chunk-NN2B3UBA.js";
46
46
  import "./chunk-2ZD3YTVM.js";
47
47
  import {
48
48
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -62,14 +62,14 @@ import {
62
62
  SKILLS_DIR_NAME,
63
63
  VERSION,
64
64
  buildUserIdentityPrompt
65
- } from "./chunk-ITHFJSJQ.js";
65
+ } from "./chunk-DNKI3JIL.js";
66
66
  import {
67
67
  formatGitContextForPrompt,
68
68
  getGitContext,
69
69
  getGitRoot
70
70
  } from "./chunk-HOSJZMQS.js";
71
71
  import "./chunk-4BKXL7SM.js";
72
- import "./chunk-ANYYM4CF.js";
72
+ import "./chunk-7ZJN4KLV.js";
73
73
  import "./chunk-KHYD3WXE.js";
74
74
  import "./chunk-NHNWUBXB.js";
75
75
  import "./chunk-6VRJGH25.js";
@@ -2383,7 +2383,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
2383
2383
  case "test": {
2384
2384
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
2385
2385
  try {
2386
- const { executeTests } = await import("./run-tests-UZGGOXOS.js");
2386
+ const { executeTests } = await import("./run-tests-LWSTPCZY.js");
2387
2387
  const argStr = args.join(" ").trim();
2388
2388
  let testArgs = {};
2389
2389
  if (argStr) {
@@ -2907,7 +2907,7 @@ Add .md files to create commands.` });
2907
2907
  return;
2908
2908
  }
2909
2909
  try {
2910
- const { searchChatMemory, loadChatIndex } = await import("./chat-index-7OHUKJY5.js");
2910
+ const { searchChatMemory, loadChatIndex } = await import("./chat-index-JBF4ZIQI.js");
2911
2911
  const loaded = loadChatIndex();
2912
2912
  if (!loaded || loaded.idx.chunks.length === 0) {
2913
2913
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -2943,7 +2943,7 @@ Add .md files to create commands.` });
2943
2943
  }
2944
2944
  async handleMemoryStatus() {
2945
2945
  try {
2946
- const { getChatIndexStatus } = await import("./chat-index-7OHUKJY5.js");
2946
+ const { getChatIndexStatus } = await import("./chat-index-JBF4ZIQI.js");
2947
2947
  const s = getChatIndexStatus();
2948
2948
  this.send({
2949
2949
  type: "memory_status",
@@ -2968,7 +2968,7 @@ Add .md files to create commands.` });
2968
2968
  type: "info",
2969
2969
  message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
2970
2970
  });
2971
- const { buildChatIndex } = await import("./chat-index-7OHUKJY5.js");
2971
+ const { buildChatIndex } = await import("./chat-index-JBF4ZIQI.js");
2972
2972
  const stats = await buildChatIndex({
2973
2973
  full,
2974
2974
  onProgress: (p) => {
@@ -3524,7 +3524,25 @@ async function startWebServer(options = {}) {
3524
3524
  const token = authManager.login(username, password);
3525
3525
  console.log(` \u2713 User registered via API: ${username}${firstRun ? " (first-run)" : ""}`);
3526
3526
  res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: 7 * 24 * 3600 * 1e3 });
3527
- res.json({ success: true, token, username });
3527
+ res.json({ success: true, username });
3528
+ });
3529
+ app.post("/api/auth/login", (req, res) => {
3530
+ const { username, password } = req.body ?? {};
3531
+ if (!username || !password) {
3532
+ res.status(400).json({ error: "Username and password required" });
3533
+ return;
3534
+ }
3535
+ const token = authManager.login(username, password);
3536
+ if (!token) {
3537
+ res.status(401).json({ error: "Invalid username or password" });
3538
+ return;
3539
+ }
3540
+ res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: 7 * 24 * 3600 * 1e3 });
3541
+ res.json({ success: true, username });
3542
+ });
3543
+ app.post("/api/auth/logout", (_req, res) => {
3544
+ res.clearCookie("aicli_token", { httpOnly: true, sameSite: "strict" });
3545
+ res.json({ success: true });
3528
3546
  });
3529
3547
  app.get("/api/files", requireAuth, (req, res) => {
3530
3548
  const cwd = process.cwd();
@@ -3765,7 +3783,7 @@ async function startWebServer(options = {}) {
3765
3783
  handlers.set(tabId, handler);
3766
3784
  clearPreAuthTimer();
3767
3785
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
3768
- ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username, setCookie: true }));
3786
+ ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
3769
3787
  return;
3770
3788
  }
3771
3789
  if (action === "token") {
@@ -3796,7 +3814,7 @@ async function startWebServer(options = {}) {
3796
3814
  handlers.set(tabId, handler);
3797
3815
  clearPreAuthTimer();
3798
3816
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
3799
- ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username, setCookie: true }));
3817
+ ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
3800
3818
  return;
3801
3819
  }
3802
3820
  ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
@@ -3909,10 +3927,14 @@ function parseCookie(cookie) {
3909
3927
  }
3910
3928
  async function openBrowser(url) {
3911
3929
  try {
3912
- const { exec } = await import("child_process");
3913
- const cmd = process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`;
3914
- exec(cmd, () => {
3915
- });
3930
+ const { spawn } = await import("child_process");
3931
+ if (process.platform === "win32") {
3932
+ spawn("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
3933
+ } else if (process.platform === "darwin") {
3934
+ spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
3935
+ } else {
3936
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
3937
+ }
3916
3938
  } catch {
3917
3939
  }
3918
3940
  }
@@ -3,16 +3,16 @@ import {
3
3
  ToolRegistry,
4
4
  getDangerLevel,
5
5
  schemaToJsonSchema
6
- } from "./chunk-ML7ISZ7A.js";
6
+ } from "./chunk-A3HZY7DA.js";
7
7
  import "./chunk-3BICTI5M.js";
8
8
  import "./chunk-2DXY7UGF.js";
9
- import "./chunk-W7ZG4KRQ.js";
9
+ import "./chunk-NN2B3UBA.js";
10
10
  import "./chunk-2ZD3YTVM.js";
11
11
  import {
12
12
  VERSION
13
- } from "./chunk-ITHFJSJQ.js";
13
+ } from "./chunk-DNKI3JIL.js";
14
14
  import "./chunk-4BKXL7SM.js";
15
- import "./chunk-ANYYM4CF.js";
15
+ import "./chunk-7ZJN4KLV.js";
16
16
  import "./chunk-KHYD3WXE.js";
17
17
  import "./chunk-NHNWUBXB.js";
18
18
  import "./chunk-6VRJGH25.js";
@@ -4,16 +4,16 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-ML7ISZ7A.js";
7
+ } from "./chunk-A3HZY7DA.js";
8
8
  import "./chunk-3BICTI5M.js";
9
9
  import "./chunk-2DXY7UGF.js";
10
- import "./chunk-W7ZG4KRQ.js";
10
+ import "./chunk-NN2B3UBA.js";
11
11
  import "./chunk-2ZD3YTVM.js";
12
12
  import {
13
13
  SUBAGENT_ALLOWED_TOOLS
14
- } from "./chunk-ITHFJSJQ.js";
14
+ } from "./chunk-DNKI3JIL.js";
15
15
  import "./chunk-4BKXL7SM.js";
16
- import "./chunk-ANYYM4CF.js";
16
+ import "./chunk-7ZJN4KLV.js";
17
17
  import "./chunk-KHYD3WXE.js";
18
18
  import "./chunk-NHNWUBXB.js";
19
19
  import "./chunk-6VRJGH25.js";
@@ -8,7 +8,13 @@
8
8
  let ws = null;
9
9
  let connected = false;
10
10
  let processing = false;
11
- let authToken = localStorage.getItem('aicli-auth-token') || '';
11
+ // M4 fix (v0.4.107): the auth token now lives only in an httpOnly cookie.
12
+ // `authToken` remains as a legacy variable so existing call sites compile,
13
+ // but it should always be empty — the cookie is what authenticates the WS
14
+ // upgrade and HTTP requests via `credentials: 'same-origin'`.
15
+ let authToken = '';
16
+ // Clear any legacy localStorage token from an older client.
17
+ try { localStorage.removeItem('aicli-auth-token'); } catch {}
12
18
  let authUsername = localStorage.getItem('aicli-auth-user') || '';
13
19
  let authMode = 'login'; // 'login' or 'register'
14
20
  let currentAssistantEl = null;
@@ -2718,11 +2724,15 @@ function toggleAuthMode() {
2718
2724
  updateAuthForm();
2719
2725
  }
2720
2726
 
2721
- function handleAuth(event) {
2727
+ // M4 fix (v0.4.107): auth now goes through HTTP /api/auth/{login,register}
2728
+ // so the server can set an httpOnly cookie. The WS upgrade automatically
2729
+ // forwards the cookie. Token is no longer in JSON / localStorage / JS-cookie.
2730
+ async function handleAuth(event) {
2722
2731
  event.preventDefault();
2723
2732
  const username = document.getElementById('auth-username').value.trim();
2724
2733
  const password = document.getElementById('auth-password').value;
2725
2734
  const errorEl = document.getElementById('auth-error');
2735
+ const submitBtn = document.getElementById('auth-submit');
2726
2736
 
2727
2737
  if (!username || !password) {
2728
2738
  errorEl.textContent = 'Please enter username and password';
@@ -2731,32 +2741,57 @@ function handleAuth(event) {
2731
2741
  }
2732
2742
 
2733
2743
  errorEl.classList.add('hidden');
2734
- document.getElementById('auth-submit').disabled = true;
2744
+ submitBtn.disabled = true;
2735
2745
 
2736
- send({ type: 'auth', action: authMode, username, password });
2746
+ try {
2747
+ const path = authMode === 'register' ? '/api/auth/register' : '/api/auth/login';
2748
+ const resp = await fetch(path, {
2749
+ method: 'POST',
2750
+ headers: { 'content-type': 'application/json' },
2751
+ credentials: 'same-origin', // ensure Set-Cookie is honored
2752
+ body: JSON.stringify({ username, password }),
2753
+ });
2754
+ const data = await resp.json().catch(() => ({}));
2755
+ if (!resp.ok || !data.success) {
2756
+ errorEl.textContent = data.error || `Authentication failed (HTTP ${resp.status})`;
2757
+ errorEl.classList.remove('hidden');
2758
+ submitBtn.disabled = false;
2759
+ return;
2760
+ }
2761
+ authUsername = data.username;
2762
+ // Cookie is httpOnly — we cannot read it from JS, and that's the point.
2763
+ // Drop any legacy localStorage token (M4 cleanup).
2764
+ localStorage.removeItem('aicli-auth-token');
2765
+ localStorage.setItem('aicli-auth-user', data.username);
2766
+ submitBtn.disabled = false;
2767
+ hideAuthScreen();
2768
+ // Reconnect WS so the upgrade request sends the new cookie.
2769
+ if (ws) ws.close();
2770
+ } catch (err) {
2771
+ errorEl.textContent = (err && err.message) ? err.message : 'Network error';
2772
+ errorEl.classList.remove('hidden');
2773
+ submitBtn.disabled = false;
2774
+ }
2737
2775
  }
2738
2776
 
2777
+ // Legacy WS auth_result handler — kept for backwards-compat with clients/
2778
+ // flows that still hit the WS auth path. Token field, if present, is ignored.
2739
2779
  function handleAuthResult(msg) {
2740
2780
  const errorEl = document.getElementById('auth-error');
2741
2781
  const submitBtn = document.getElementById('auth-submit');
2742
- submitBtn.disabled = false;
2782
+ if (submitBtn) submitBtn.disabled = false;
2743
2783
 
2744
2784
  if (msg.success) {
2745
- authToken = msg.token;
2746
2785
  authUsername = msg.username;
2747
- localStorage.setItem('aicli-auth-token', msg.token);
2786
+ localStorage.removeItem('aicli-auth-token'); // M4: never store token
2748
2787
  localStorage.setItem('aicli-auth-user', msg.username);
2749
- // Set cookie for secure WebSocket auth (httpOnly cookie set by server on HTTP login,
2750
- // but for WS-only login, set a JS cookie as fallback)
2751
- if (msg.setCookie && msg.token) {
2752
- document.cookie = `aicli_token=${encodeURIComponent(msg.token)}; path=/; max-age=${7 * 24 * 3600}; SameSite=Strict`;
2753
- }
2754
2788
  hideAuthScreen();
2755
- // Request session list now that we're authenticated
2756
2789
  requestSessionList();
2757
2790
  } else {
2758
- errorEl.textContent = msg.error || 'Authentication failed';
2759
- errorEl.classList.remove('hidden');
2791
+ if (errorEl) {
2792
+ errorEl.textContent = msg.error || 'Authentication failed';
2793
+ errorEl.classList.remove('hidden');
2794
+ }
2760
2795
  }
2761
2796
  }
2762
2797
 
@@ -2774,14 +2809,16 @@ function updateUserMenu() {
2774
2809
  }
2775
2810
  }
2776
2811
 
2777
- function handleLogout() {
2812
+ async function handleLogout() {
2813
+ // M4 fix (v0.4.107): only the server can clear an httpOnly cookie.
2814
+ try {
2815
+ await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' });
2816
+ } catch { /* ignore — close WS anyway */ }
2778
2817
  authToken = '';
2779
2818
  authUsername = '';
2780
- localStorage.removeItem('aicli-auth-token');
2781
- document.cookie = 'aicli_token=; path=/; max-age=0'; // Clear auth cookie
2819
+ localStorage.removeItem('aicli-auth-token'); // legacy cleanup
2782
2820
  localStorage.removeItem('aicli-auth-user');
2783
2821
  sessionStorage.removeItem('aicli-active-session');
2784
- // Reconnect — server will send auth_required
2785
2822
  if (ws) ws.close();
2786
2823
  }
2787
2824
 
@@ -2849,9 +2886,12 @@ function showEnableAuthDialog() {
2849
2886
  const password = prompt('Password (min 4 chars):');
2850
2887
  if (!password) return;
2851
2888
 
2889
+ // M4 fix (v0.4.107): credentials:'same-origin' lets the httpOnly cookie
2890
+ // come back; we no longer touch the token.
2852
2891
  fetch('/api/auth/register', {
2853
2892
  method: 'POST',
2854
2893
  headers: { 'Content-Type': 'application/json' },
2894
+ credentials: 'same-origin',
2855
2895
  body: JSON.stringify({ username, password }),
2856
2896
  })
2857
2897
  .then(r => r.json())
@@ -2860,14 +2900,12 @@ function showEnableAuthDialog() {
2860
2900
  alert('Error: ' + data.error);
2861
2901
  return;
2862
2902
  }
2863
- // Save token and reconnect with auth
2864
- authToken = data.token;
2865
2903
  authUsername = data.username;
2866
- localStorage.setItem('aicli-auth-token', data.token);
2904
+ localStorage.removeItem('aicli-auth-token');
2867
2905
  localStorage.setItem('aicli-auth-user', data.username);
2868
2906
  document.getElementById('btn-enable-auth').classList.add('hidden');
2869
2907
  updateUserMenu();
2870
- // Reconnect with new token
2908
+ // Reconnect WS upgrade will pick up the new httpOnly cookie.
2871
2909
  if (ws) ws.close();
2872
2910
  alert('✓ Auth enabled! User "' + data.username + '" created. Other users can register from the login screen.');
2873
2911
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.106",
3
+ "version": "0.4.108",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",