@turmind/halo 1.0.1 → 1.0.3

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.
Files changed (37) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/212-c5f046048be9c367.js +1 -0
  3. package/admin-out/_next/static/chunks/4-e7ba0b1ced30190b.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-5a6cf2b73a12b849.js +1 -0
  5. package/admin-out/_next/static/css/62b8bf49e6404ffc.css +3 -0
  6. package/admin-out/index.html +1 -1
  7. package/admin-out/index.txt +5 -5
  8. package/bundled-docs/design/agent.md +2 -1
  9. package/bundled-docs/design/cron.md +9 -3
  10. package/bundled-docs/design/session.md +3 -1
  11. package/bundled-docs/design/storage.md +2 -1
  12. package/bundled-docs/design/ws.md +30 -8
  13. package/bundled-docs/dev/add-model-provider.md +1 -1
  14. package/bundled-docs/dev/api.md +37 -3
  15. package/bundled-docs/dev/env.md +2 -0
  16. package/bundled-docs/dev/tools.md +13 -4
  17. package/bundled-docs/guide/chat.md +1 -1
  18. package/bundled-docs/guide/workspace.md +2 -2
  19. package/bundled-docs/requirements/chat.md +1 -1
  20. package/bundled-docs/requirements/settings.md +8 -0
  21. package/dist/index.js +440 -263
  22. package/package.json +1 -1
  23. package/templates/agents/__apply_agent__/agent.yaml +1 -1
  24. package/templates/agents/__evo_agent__/agent.yaml +1 -1
  25. package/templates/agents/__score__/agent.yaml +1 -1
  26. package/templates/agents/deep-executor/agent.yaml +1 -1
  27. package/templates/agents/default/agent.yaml +1 -1
  28. package/templates/agents/goal/agent.yaml +1 -1
  29. package/templates/models/aws-bedrock-claude-invoke.yaml +27 -0
  30. package/templates/models/aws-bedrock-mantle.yaml +13 -2
  31. package/templates/skills/agent/SKILL.md +2 -2
  32. package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +0 -1
  33. package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +0 -1
  34. package/admin-out/_next/static/chunks/app/page-19d5f2a0a3412b4a.js +0 -1
  35. package/admin-out/_next/static/css/9e7dadb24830255a.css +0 -3
  36. /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_buildManifest.js +0 -0
  37. /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -479,6 +479,17 @@ function modelsRegistry() {
479
479
  _modelsRegistryCache = loadModelsRegistry();
480
480
  return _modelsRegistryCache;
481
481
  }
482
+ function getSystemConfig() {
483
+ let mtimeMs = 0;
484
+ try {
485
+ mtimeMs = fs3.statSync(SYSTEM_CONFIG_PATH).mtimeMs;
486
+ } catch {
487
+ }
488
+ if (!_systemConfigCache || _systemConfigCache.mtimeMs !== mtimeMs) {
489
+ _systemConfigCache = { mtimeMs, data: loadYamlFile(SYSTEM_CONFIG_PATH) };
490
+ }
491
+ return _systemConfigCache.data;
492
+ }
482
493
  function getSettings() {
483
494
  let mtimeMs = 0;
484
495
  try {
@@ -514,7 +525,7 @@ function settingsValue(dotPath) {
514
525
  return v == null ? void 0 : typeof v === "string" ? v : String(v);
515
526
  }
516
527
  function configValue(dotPath) {
517
- return readYamlValue(_systemConfig, dotPath);
528
+ return readYamlValue(getSystemConfig(), dotPath);
518
529
  }
519
530
  function envInt(key, fallback) {
520
531
  const v = process.env[key];
@@ -711,7 +722,7 @@ function modelSupportsAudio(modelId, override) {
711
722
  function getModelsRegistry() {
712
723
  return modelsRegistry();
713
724
  }
714
- var HALO_HOME2, HALO_GLOBAL_DIR, HALO_SECRETS_DIR, _modelsRegistryCache, _systemConfig, SETTINGS_PATH, _settingsCache, _schemaDefaultsCache, ENV_PATTERN, _wsSettingsCache, config, HIDDEN_DIRS_DEFAULT, HIDDEN_FILES_DEFAULT;
725
+ var HALO_HOME2, HALO_GLOBAL_DIR, HALO_SECRETS_DIR, _modelsRegistryCache, SYSTEM_CONFIG_PATH, _systemConfigCache, SETTINGS_PATH, _settingsCache, _schemaDefaultsCache, ENV_PATTERN, _wsSettingsCache, config, HIDDEN_DIRS_DEFAULT, HIDDEN_FILES_DEFAULT;
715
726
  var init_config = __esm({
716
727
  "packages/server/dist/config.js"() {
717
728
  "use strict";
@@ -720,7 +731,8 @@ var init_config = __esm({
720
731
  HALO_GLOBAL_DIR = path3.join(HALO_HOME2, "global");
721
732
  HALO_SECRETS_DIR = path3.join(HALO_HOME2, "secrets");
722
733
  _modelsRegistryCache = null;
723
- _systemConfig = loadYamlFile(path3.join(HALO_SECRETS_DIR, "config.yaml"));
734
+ SYSTEM_CONFIG_PATH = path3.join(HALO_SECRETS_DIR, "config.yaml");
735
+ _systemConfigCache = null;
724
736
  SETTINGS_PATH = path3.join(HALO_SECRETS_DIR, "settings.yaml");
725
737
  _settingsCache = null;
726
738
  _schemaDefaultsCache = null;
@@ -2206,7 +2218,7 @@ var init_init = __esm({
2206
2218
  path6.resolve(__dirname, "..", "bundled-docs"),
2207
2219
  path6.resolve(__dirname, "..", "..", "..", ".halo", "docs")
2208
2220
  ];
2209
- TEMPLATE_VERSION = 47;
2221
+ TEMPLATE_VERSION = 49;
2210
2222
  VERSION_FILE = ".template-version";
2211
2223
  SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
2212
2224
  BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
@@ -4126,7 +4138,7 @@ function applyEvent(state, event) {
4126
4138
  break;
4127
4139
  case "tool_call": {
4128
4140
  const inputStr = formatToolInput(event.toolInput);
4129
- const entry = { name: event.toolName ?? "", input: inputStr };
4141
+ const entry = { name: event.toolName ?? "", input: inputStr, toolUseId: event.toolUseId };
4130
4142
  const msg = {
4131
4143
  id: genId(),
4132
4144
  type: "tool_call",
@@ -6276,7 +6288,8 @@ var init_mantle_agent = __esm({
6276
6288
  };
6277
6289
  if (this.config.thinking?.enabled && this.config.thinking.effort) {
6278
6290
  const e = this.config.thinking.effort;
6279
- body.reasoning = { effort: e === "low" || e === "medium" || e === "high" ? e : "high" };
6291
+ const valid = ["low", "medium", "high", "xhigh", "max"];
6292
+ body.reasoning = { effort: valid.includes(e) ? e : "high" };
6280
6293
  }
6281
6294
  const payload = JSON.stringify(body);
6282
6295
  const response = this.config.apiKey ? await fetch(url, {
@@ -6934,6 +6947,16 @@ function buildBwrapArgs(opts) {
6934
6947
  args.push("--bind", dir, dir);
6935
6948
  }
6936
6949
  }
6950
+ for (const rel of WORKSPACE_HIDDEN_DIRS) {
6951
+ const dir = path15.join(opts.workspaceRoot, rel);
6952
+ if (existsSync(dir))
6953
+ args.push("--tmpfs", dir);
6954
+ }
6955
+ for (const rel of WORKSPACE_HIDDEN_FILES) {
6956
+ const file = path15.join(opts.workspaceRoot, rel);
6957
+ if (existsSync(file))
6958
+ args.push("--ro-bind", "/dev/null", file);
6959
+ }
6937
6960
  args.push("--proc", "/proc");
6938
6961
  args.push("--dev", "/dev");
6939
6962
  args.push("--clearenv");
@@ -6968,6 +6991,9 @@ function assertPathAllowed(filePath, opts, write = false) {
6968
6991
  const resolved = realpathBounded(filePath);
6969
6992
  const wsRoot = realpathBounded(opts.workspaceRoot);
6970
6993
  if (resolved === wsRoot || resolved.startsWith(wsRoot + "/")) {
6994
+ if (isHiddenWorkspacePath(resolved, wsRoot)) {
6995
+ throw new Error(`Access denied: "${filePath}" is outside the allowed sandbox paths`);
6996
+ }
6971
6997
  if (write && opts.accessLevel === "readonly") {
6972
6998
  throw new Error(`Access denied: readonly session cannot write to "${filePath}"`);
6973
6999
  }
@@ -6990,6 +7016,18 @@ function isHiddenGlobalPath(resolved) {
6990
7016
  }
6991
7017
  return false;
6992
7018
  }
7019
+ function isHiddenWorkspacePath(resolved, wsRoot) {
7020
+ for (const rel of WORKSPACE_HIDDEN_FILES) {
7021
+ if (resolved === path15.join(wsRoot, rel))
7022
+ return true;
7023
+ }
7024
+ for (const rel of WORKSPACE_HIDDEN_DIRS) {
7025
+ const dir = path15.join(wsRoot, rel);
7026
+ if (resolved === dir || resolved.startsWith(dir + "/"))
7027
+ return true;
7028
+ }
7029
+ return false;
7030
+ }
6993
7031
  function buildExecScriptArgs(workspaceRoot, command) {
6994
7032
  return ["bash", "-c", `cd "$1" && shift && ${command}`, "bash", workspaceRoot];
6995
7033
  }
@@ -7193,7 +7231,7 @@ async function sandboxReaddir(dirPath, opts) {
7193
7231
  return { name: isDir ? name.slice(0, -1) : name, isDirectory: isDir };
7194
7232
  });
7195
7233
  }
7196
- var execAsync, execFileAsync, HOME, HALO_HOME3, GLOBAL_DIR, KILL_GRACE_MS, _bwrapAvailable, DEFAULT_HIDDEN_DIRS, DEFAULT_HIDDEN_FILES, _hiddenDirs, _hiddenFiles, _writableDirs;
7234
+ var execAsync, execFileAsync, HOME, HALO_HOME3, GLOBAL_DIR, KILL_GRACE_MS, _bwrapAvailable, DEFAULT_HIDDEN_DIRS, DEFAULT_HIDDEN_FILES, WORKSPACE_HIDDEN_DIRS, WORKSPACE_HIDDEN_FILES, _hiddenDirs, _hiddenFiles, _writableDirs;
7197
7235
  var init_sandbox = __esm({
7198
7236
  "packages/server/dist/tools/sandbox.js"() {
7199
7237
  "use strict";
@@ -7236,6 +7274,8 @@ var init_sandbox = __esm({
7236
7274
  "~/.halo/global/cron.db-wal",
7237
7275
  "~/.halo/global/cron.db-shm"
7238
7276
  ];
7277
+ WORKSPACE_HIDDEN_DIRS = [".halo/sessions", ".halo/logs", ".halo/evo"];
7278
+ WORKSPACE_HIDDEN_FILES = [".halo/halo.db", ".halo/halo.db-wal", ".halo/halo.db-shm"];
7239
7279
  _hiddenDirs = DEFAULT_HIDDEN_DIRS;
7240
7280
  _hiddenFiles = DEFAULT_HIDDEN_FILES;
7241
7281
  _writableDirs = [];
@@ -8703,8 +8743,15 @@ var init_i18n = __esm({
8703
8743
  "cmd.unknown": { zh: "\u672A\u77E5\u547D\u4EE4: {cmd}\n\u53D1 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4", en: "Unknown command: {cmd}\nSend /help to see available commands" },
8704
8744
  // ── Channel handler messages ──
8705
8745
  "handler.workspace_missing": { zh: "\u26A0\uFE0F \u8FD9\u4E2A bot \u7ED1\u5B9A\u7684 workspace \u5DF2\u4E0D\u5B58\u5728\uFF1A\n{path}\n\n\u8BF7\u5230 web \u7AEF\u7684 Channels \u8BBE\u7F6E\u91CC\u66F4\u65B0\u7ED1\u5B9A\u76EE\u5F55\u3002", en: "\u26A0\uFE0F The workspace bound to this bot no longer exists:\n{path}\n\nPlease update the binding in web Channels settings." },
8706
- "handler.compacting": { zh: "\u23F3 \u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u6D88\u606F\uFF08\u901A\u5E38 30 \u79D2\u5185\u5B8C\u6210\uFF09", en: "\u23F3 Compacting context, please wait (usually under 30s)" },
8746
+ // Both hints must read as "received, will be answered" — the message IS
8747
+ // queued (sendUserMessage queues compacting/busy sessions). Telling the user
8748
+ // to re-send would make them duplicate a message that was never lost.
8749
+ "handler.compacting": { zh: "\u23F3 \u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587\uFF0C\u6D88\u606F\u5DF2\u6536\u5230\uFF0C\u6574\u7406\u5B8C\u6210\u540E\u7ACB\u5373\u5904\u7406\uFF08\u901A\u5E38 30 \u79D2\u5185\uFF09", en: "\u23F3 Compacting context \u2014 got your message, will process as soon as it finishes (usually under 30s)" },
8707
8750
  "handler.queued": { zh: "\u{1F504} \u521A\u624D\u90A3\u6761\u8FD8\u5728\u5904\u7406\u4E2D\uFF0C\u6D88\u606F\u5DF2\u6392\u961F\uFF0C\u8BF7\u7A0D\u5019", en: "\u{1F504} Previous message still processing, queued" },
8751
+ // Outbound media upload failed — tells the user why no attachment showed up.
8752
+ // Plain text (no backticks): slack/feishu render markdown differently, and the
8753
+ // filename is already delimited by the surrounding punctuation.
8754
+ "handler.upload_failed": { zh: "\u26A0\uFE0F \u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF1A{name} \u2014 {error}", en: "\u26A0\uFE0F Failed to upload {name}: {error}" },
8708
8755
  "handler.workspace_gone": { zh: "\u26A0\uFE0F workspace \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5230 web \u7AEF\u66F4\u65B0\u7ED1\u5B9A", en: "\u26A0\uFE0F Workspace does not exist, please update binding in web" },
8709
8756
  "handler.not_allowed": { zh: "\u26A0\uFE0F \u4F60\u4E0D\u5728\u8FD9\u4E2A bot \u7684\u5141\u8BB8\u5217\u8868\u4E2D", en: "\u26A0\uFE0F You are not in this bot's allowed list" },
8710
8757
  "handler.start_greeting": { zh: "\u{1F44B} Halo bot \u5DF2\u5C31\u7EEA\u3002\u76F4\u63A5\u53D1\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\u3002\u53D1 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4\u3002", en: "\u{1F44B} Halo bot ready. Send a message to start. /help for commands." },
@@ -9765,12 +9812,12 @@ var init_session_manager = __esm({
9765
9812
  if (loopStatus === "warn") {
9766
9813
  this.emitEvent(session.id, { type: "system", text: `\u26A0\uFE0F Tool "${event.toolName}" called repeatedly with identical input. Consider a different approach.` });
9767
9814
  }
9768
- this.emitEvent(session.id, { type: "tool_call", toolName: event.toolName, toolInput: event.toolInput, agentName, agentId, taskId });
9815
+ this.emitEvent(session.id, { type: "tool_call", toolName: event.toolName, toolUseId: event.toolUseId, toolInput: event.toolInput, agentName, agentId, taskId });
9769
9816
  break;
9770
9817
  }
9771
9818
  case "tool_result": {
9772
9819
  const resultStr = event.toolResultFull ?? event.toolResult ?? "";
9773
- this.emitEvent(session.id, { type: "tool_result", toolName: event.toolName, toolResult: resultStr, durationMs: event.durationMs, agentName, agentId, taskId });
9820
+ this.emitEvent(session.id, { type: "tool_result", toolName: event.toolName, toolUseId: event.toolUseId, toolResult: resultStr, durationMs: event.durationMs, agentName, agentId, taskId });
9774
9821
  break;
9775
9822
  }
9776
9823
  case "usage": {
@@ -10136,6 +10183,7 @@ ${q.text}` : q.text).join("\n\n");
10136
10183
  this.emitEvent(sessionId, {
10137
10184
  type: "tool_result",
10138
10185
  toolName: tc.name,
10186
+ toolUseId: tc.toolUseId,
10139
10187
  toolResult: "[interrupted by user]",
10140
10188
  agentName: session?.agentName,
10141
10189
  agentId: session?.agentId,
@@ -10469,9 +10517,11 @@ ${cleaned}` : cleaned;
10469
10517
  const overThreshold = session.lastContextTokens > session.contextConfig.maxTokens * session.contextConfig.compressAt;
10470
10518
  if (!overThreshold)
10471
10519
  return;
10520
+ if (this.compactCut(session.agent.messages) === 0)
10521
+ return;
10522
+ const taskId = session.parentId ? session.id : void 0;
10472
10523
  try {
10473
10524
  session.isCompacting = true;
10474
- const taskId = session.parentId ? session.id : void 0;
10475
10525
  const preflightText = `Compacting context (${Math.round(session.lastContextTokens / 1e3)}K tokens)\u2026`;
10476
10526
  this.emitEvent(session.id, { type: "system", text: preflightText, taskId });
10477
10527
  const result = await this.selfCompactSession(session.id);
@@ -10482,9 +10532,12 @@ ${cleaned}` : cleaned;
10482
10532
  ${result.summary}`, agentName: "user", report: true, taskId });
10483
10533
  this.emitEvent(session.id, { type: "compacted", totalTokens: result.estimatedTokens, taskId });
10484
10534
  console.debug(`[SessionManager] Auto-compact ${session.id}: ${result.olderCount} messages compacted`);
10535
+ } else {
10536
+ this.emitEvent(session.id, { type: "system", text: "Compaction skipped \u2014 no summary produced", taskId });
10485
10537
  }
10486
10538
  } catch (err) {
10487
10539
  console.debug(`[SessionManager] Auto-compact ${session.id} failed: ${err instanceof Error ? err.message : String(err)}`);
10540
+ this.emitEvent(session.id, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
10488
10541
  } finally {
10489
10542
  session.isCompacting = false;
10490
10543
  }
@@ -10576,19 +10629,25 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10576
10629
  }
10577
10630
  }
10578
10631
  /**
10579
- * Self-compact: let the current agent summarize its own conversation.
10580
- * The agent already has full context cached, so this is fast and lossless.
10581
- * Returns the summary text, or null if compaction was not needed/failed.
10632
+ * Compute the compaction cut point: messages[0..cut) get summarized,
10633
+ * messages[cut..] are kept. Returns 0 when there is nothing to compact
10634
+ * (too few messages) — the "compaction is feasible" predicate.
10635
+ *
10636
+ * Single source of truth shared by selfCompactSession AND the
10637
+ * pre-preflight feasibility gates in maybeAutoCompact / compactSession:
10638
+ * gate and actual compact must run the same computation on the same
10639
+ * `session.agent.messages` reference or they drift ("gate said yes,
10640
+ * compact said no" re-creates the orphan-preflight bug).
10641
+ *
10642
+ * Note the tail loop only ever moves `cut` UP (a tail of user tool_result
10643
+ * messages extends the summarized region — they're protocol continuations
10644
+ * of an assistant turn, not standalone turns), so cut === 0 is exactly
10645
+ * the `messages.length <= keepCount` case.
10582
10646
  */
10583
- async selfCompactSession(sessionId, signal) {
10584
- const session = this.sessions.get(sessionId);
10585
- if (!session)
10586
- return null;
10587
- const messages2 = session.agent.messages;
10647
+ compactCut(messages2) {
10588
10648
  const keepCount = config.compact.keep_messages;
10589
10649
  if (messages2.length <= keepCount)
10590
- return null;
10591
- session.compactedThisTurn = true;
10650
+ return 0;
10592
10651
  let cut = Math.max(0, messages2.length - keepCount);
10593
10652
  while (cut < messages2.length) {
10594
10653
  const m = messages2[cut];
@@ -10599,8 +10658,22 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10599
10658
  }
10600
10659
  break;
10601
10660
  }
10661
+ return cut;
10662
+ }
10663
+ /**
10664
+ * Self-compact: let the current agent summarize its own conversation.
10665
+ * The agent already has full context cached, so this is fast and lossless.
10666
+ * Returns the summary text, or null if compaction was not needed/failed.
10667
+ */
10668
+ async selfCompactSession(sessionId, signal) {
10669
+ const session = this.sessions.get(sessionId);
10670
+ if (!session)
10671
+ return null;
10672
+ const messages2 = session.agent.messages;
10673
+ const cut = this.compactCut(messages2);
10602
10674
  if (cut === 0)
10603
10675
  return null;
10676
+ session.compactedThisTurn = true;
10604
10677
  const olderCount = cut;
10605
10678
  const compactInstruction = [
10606
10679
  "Summarize the conversation so far into a concise summary that preserves:",
@@ -10684,9 +10757,8 @@ ${summaryText}` }]
10684
10757
  return "running";
10685
10758
  if (session.isCompacting)
10686
10759
  return "already";
10687
- const keepCount = config.compact.keep_messages;
10688
10760
  const rawCount = session.agent.messages.length;
10689
- if (rawCount <= keepCount)
10761
+ if (this.compactCut(session.agent.messages) === 0)
10690
10762
  return "nothing";
10691
10763
  const ac = await this.beginCompact(sessionId);
10692
10764
  if (!ac)
@@ -10698,8 +10770,10 @@ ${summaryText}` }]
10698
10770
  this.emitEvent(sessionId, { type: "system", text: preflightText, taskId });
10699
10771
  try {
10700
10772
  const result = await this.selfCompactSession(sessionId, ac.signal);
10701
- if (!result)
10773
+ if (!result) {
10774
+ this.emitEvent(sessionId, { type: "system", text: "Compaction skipped \u2014 no summary produced", taskId });
10702
10775
  return "nothing";
10776
+ }
10703
10777
  session.lastContextTokens = result.estimatedTokens;
10704
10778
  const noticeText = `Context compacted: ${result.olderCount} older messages summarized`;
10705
10779
  this.emitEvent(sessionId, { type: "system", text: noticeText, taskId });
@@ -10715,6 +10789,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10715
10789
  this.emitEvent(sessionId, { type: "system", text: "Compact cancelled" });
10716
10790
  return "nothing";
10717
10791
  }
10792
+ this.emitEvent(sessionId, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
10718
10793
  throw err;
10719
10794
  } finally {
10720
10795
  this.endCompact(sessionId);
@@ -13572,8 +13647,8 @@ function applyPathPick(value, ref, pick) {
13572
13647
  const before = value.slice(0, markerStart);
13573
13648
  const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
13574
13649
  const needsQuote = pick.insertPath.includes(" ");
13575
- const path64 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
13576
- return `${before}@${kind} ${path64}`;
13650
+ const path63 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
13651
+ return `${before}@${kind} ${path63}`;
13577
13652
  }
13578
13653
  var IMAGE_EXTS2;
13579
13654
  var init_path_suggest_engine = __esm({
@@ -15093,12 +15168,12 @@ None yet.
15093
15168
  }
15094
15169
  console.log(`[Workspace] Initialized workspace: ${this.projectRoot}`);
15095
15170
  }
15096
- async readFile(path64) {
15097
- const fullPath = this.validatePath(path64);
15171
+ async readFile(path63) {
15172
+ const fullPath = this.validatePath(path63);
15098
15173
  return readFile(fullPath, "utf-8");
15099
15174
  }
15100
- async writeFile(path64, content) {
15101
- const fullPath = this.validatePath(path64);
15175
+ async writeFile(path63, content) {
15176
+ const fullPath = this.validatePath(path63);
15102
15177
  const dir = resolve(fullPath, "..");
15103
15178
  await mkdir(dir, { recursive: true });
15104
15179
  await writeFile(fullPath, content, "utf-8");
@@ -15118,26 +15193,26 @@ None yet.
15118
15193
  }
15119
15194
  return results;
15120
15195
  }
15121
- async fileExists(path64) {
15196
+ async fileExists(path63) {
15122
15197
  try {
15123
- const fullPath = this.validatePath(path64);
15198
+ const fullPath = this.validatePath(path63);
15124
15199
  await access(fullPath);
15125
15200
  return true;
15126
15201
  } catch {
15127
15202
  return false;
15128
15203
  }
15129
15204
  }
15130
- validatePath(path64) {
15131
- const fullPath = isAbsolute(path64) ? resolve(path64) : resolve(this.projectRoot, path64);
15205
+ validatePath(path63) {
15206
+ const fullPath = isAbsolute(path63) ? resolve(path63) : resolve(this.projectRoot, path63);
15132
15207
  const root = resolve(this.projectRoot);
15133
15208
  if (fullPath !== root && !fullPath.startsWith(root + sep)) {
15134
- throw new WorkspaceError(`Path "${path64}" is outside the workspace directory "${this.projectRoot}"`);
15209
+ throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
15135
15210
  }
15136
15211
  try {
15137
15212
  const real = realpathSync2(fullPath);
15138
15213
  const realRoot = realpathSync2(root);
15139
15214
  if (real !== realRoot && !real.startsWith(realRoot + sep)) {
15140
- throw new WorkspaceError(`Path "${path64}" is outside the workspace directory "${this.projectRoot}"`);
15215
+ throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
15141
15216
  }
15142
15217
  } catch (err) {
15143
15218
  if (err instanceof WorkspaceError)
@@ -15250,10 +15325,10 @@ var init_git_manager = __esm({
15250
15325
  throw new GitManagerError(`Failed to commit: ${message2}`);
15251
15326
  }
15252
15327
  }
15253
- async getDiff(path64) {
15328
+ async getDiff(path63) {
15254
15329
  try {
15255
- if (path64) {
15256
- return await this.git.diff(["HEAD", "--", path64]);
15330
+ if (path63) {
15331
+ return await this.git.diff(["HEAD", "--", path63]);
15257
15332
  }
15258
15333
  return await this.git.diff(["HEAD"]);
15259
15334
  } catch {
@@ -15342,12 +15417,12 @@ var init_git_manager = __esm({
15342
15417
  const code = parts[0];
15343
15418
  const status = code[0];
15344
15419
  const isRenameOrCopy = status === "R" || status === "C";
15345
- const path64 = isRenameOrCopy ? parts[2] : parts[1];
15346
- if (!path64 || seen.has(path64))
15420
+ const path63 = isRenameOrCopy ? parts[2] : parts[1];
15421
+ if (!path63 || seen.has(path63))
15347
15422
  continue;
15348
- seen.add(path64);
15423
+ seen.add(path63);
15349
15424
  files.push({
15350
- path: path64,
15425
+ path: path63,
15351
15426
  status,
15352
15427
  from: isRenameOrCopy ? parts[1] : void 0
15353
15428
  });
@@ -15495,9 +15570,9 @@ var init_git_manager = __esm({
15495
15570
  * empty for an added file or the root commit) and `modified` is the version
15496
15571
  * at `commit` (empty for a deletion). The working-tree/staged path above is
15497
15572
  * left entirely untouched (commit === undefined preserves existing behavior). */
15498
- async getFileDiff(path64, staged, from, commit) {
15573
+ async getFileDiff(path63, staged, from, commit) {
15499
15574
  if (commit) {
15500
- const parentPath = from || path64;
15575
+ const parentPath = from || path63;
15501
15576
  let original2 = "";
15502
15577
  try {
15503
15578
  original2 = await this.git.show([`${commit}^:${parentPath}`]);
@@ -15506,13 +15581,13 @@ var init_git_manager = __esm({
15506
15581
  }
15507
15582
  let modified2 = "";
15508
15583
  try {
15509
- modified2 = await this.git.show([`${commit}:${path64}`]);
15584
+ modified2 = await this.git.show([`${commit}:${path63}`]);
15510
15585
  } catch {
15511
15586
  modified2 = "";
15512
15587
  }
15513
15588
  return { original: original2, modified: modified2 };
15514
15589
  }
15515
- const headPath = from || path64;
15590
+ const headPath = from || path63;
15516
15591
  let original = "";
15517
15592
  try {
15518
15593
  original = await this.git.show([`HEAD:${headPath}`]);
@@ -15522,13 +15597,13 @@ var init_git_manager = __esm({
15522
15597
  let modified = "";
15523
15598
  if (staged) {
15524
15599
  try {
15525
- modified = await this.git.show([`:${path64}`]);
15600
+ modified = await this.git.show([`:${path63}`]);
15526
15601
  } catch {
15527
15602
  modified = "";
15528
15603
  }
15529
15604
  } else {
15530
15605
  try {
15531
- modified = await this.workspace.readFile(path64);
15606
+ modified = await this.workspace.readFile(path63);
15532
15607
  } catch {
15533
15608
  modified = "";
15534
15609
  }
@@ -15559,6 +15634,24 @@ function isInTempDir(filePath) {
15559
15634
  const tmp = tempDir();
15560
15635
  return resolved === tmp || resolved.startsWith(tmp + path32.sep);
15561
15636
  }
15637
+ function extractMediaPaths(text5) {
15638
+ const mediaPaths = [];
15639
+ const stripped = text5.replace(MEDIA_MARKER_RE, (_m, p) => {
15640
+ if (p)
15641
+ mediaPaths.push(p);
15642
+ return "";
15643
+ });
15644
+ return { text: stripped, mediaPaths };
15645
+ }
15646
+ function extractMediaMessage(text5) {
15647
+ const { text: stripped, mediaPaths } = extractMediaPaths(text5);
15648
+ return { text: stripped.replace(/\n{3,}/g, "\n\n").trim(), mediaPaths };
15649
+ }
15650
+ function isMediaPathAllowed(filePath, workspacePath) {
15651
+ const resolved = path32.resolve(filePath);
15652
+ const ws = path32.resolve(workspacePath);
15653
+ return resolved === ws || resolved.startsWith(ws + path32.sep) || isInTempDir(resolved);
15654
+ }
15562
15655
  function classifyMedia(filePath) {
15563
15656
  const ext = path32.extname(filePath).toLowerCase();
15564
15657
  if (IMAGE_EXTS3.has(ext))
@@ -15569,10 +15662,11 @@ function classifyMedia(filePath) {
15569
15662
  return "voice";
15570
15663
  return "other";
15571
15664
  }
15572
- var IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
15665
+ var MEDIA_MARKER_RE, IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
15573
15666
  var init_media = __esm({
15574
15667
  "packages/server/dist/channels/shared/media.js"() {
15575
15668
  "use strict";
15669
+ MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
15576
15670
  IMAGE_EXTS3 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
15577
15671
  VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".avi"]);
15578
15672
  VOICE_EXTS = /* @__PURE__ */ new Set([".ogg", ".oga", ".opus"]);
@@ -16484,12 +16578,20 @@ function createGitRoutes() {
16484
16578
  const git = new GitManager(new Workspace(projectPath));
16485
16579
  return { git, projectPath };
16486
16580
  }
16581
+ async function getGitForRead(projectId) {
16582
+ const res = await getGit(projectId);
16583
+ if ("error" in res)
16584
+ return res;
16585
+ if (!await res.git.isRepoRoot())
16586
+ return { notRepo: true };
16587
+ return res;
16588
+ }
16487
16589
  app2.get("/git/status", async (c) => {
16488
16590
  try {
16489
- const res = await getGit(c.req.query("projectId"));
16591
+ const res = await getGitForRead(c.req.query("projectId"));
16490
16592
  if ("error" in res)
16491
16593
  return c.json({ error: res.error }, res.status);
16492
- if (!await res.git.isRepoRoot())
16594
+ if ("notRepo" in res)
16493
16595
  return c.json({ isRepo: false });
16494
16596
  const status = await res.git.getStatus();
16495
16597
  return c.json({ isRepo: true, ...status });
@@ -16501,9 +16603,11 @@ function createGitRoutes() {
16501
16603
  });
16502
16604
  app2.get("/git/ignored", async (c) => {
16503
16605
  try {
16504
- const res = await getGit(c.req.query("projectId"));
16606
+ const res = await getGitForRead(c.req.query("projectId"));
16505
16607
  if ("error" in res)
16506
16608
  return c.json({ error: res.error }, res.status);
16609
+ if ("notRepo" in res)
16610
+ return c.json({ isRepo: false, ignored: [] });
16507
16611
  const ignored = await res.git.getIgnoredPaths();
16508
16612
  return c.json({ ignored });
16509
16613
  } catch (err) {
@@ -16518,11 +16622,13 @@ function createGitRoutes() {
16518
16622
  const staged = c.req.query("staged") === "1";
16519
16623
  const from = c.req.query("from") || void 0;
16520
16624
  const commit = c.req.query("commit") || void 0;
16521
- const res = await getGit(c.req.query("projectId"));
16625
+ const res = await getGitForRead(c.req.query("projectId"));
16522
16626
  if ("error" in res)
16523
16627
  return c.json({ error: res.error }, res.status);
16524
16628
  if (!filePath)
16525
16629
  return c.json({ error: "path is required" }, 400);
16630
+ if ("notRepo" in res)
16631
+ return c.json({ path: filePath, original: "", modified: "" });
16526
16632
  if (!validatePath(filePath, res.projectPath)) {
16527
16633
  return c.json({ error: "Path traversal not allowed" }, 403);
16528
16634
  }
@@ -16536,9 +16642,11 @@ function createGitRoutes() {
16536
16642
  });
16537
16643
  app2.get("/git/log", async (c) => {
16538
16644
  try {
16539
- const res = await getGit(c.req.query("projectId"));
16645
+ const res = await getGitForRead(c.req.query("projectId"));
16540
16646
  if ("error" in res)
16541
16647
  return c.json({ error: res.error }, res.status);
16648
+ if ("notRepo" in res)
16649
+ return c.json({ commits: [] });
16542
16650
  const limitRaw = Number(c.req.query("limit"));
16543
16651
  const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 2e3) : 50;
16544
16652
  const commits = await res.git.getLog(limit);
@@ -16552,11 +16660,13 @@ function createGitRoutes() {
16552
16660
  app2.get("/git/commit-files", async (c) => {
16553
16661
  try {
16554
16662
  const hash = c.req.query("hash");
16555
- const res = await getGit(c.req.query("projectId"));
16663
+ const res = await getGitForRead(c.req.query("projectId"));
16556
16664
  if ("error" in res)
16557
16665
  return c.json({ error: res.error }, res.status);
16558
16666
  if (!hash)
16559
16667
  return c.json({ error: "hash is required" }, 400);
16668
+ if ("notRepo" in res)
16669
+ return c.json({ files: [] });
16560
16670
  const files = await res.git.getCommitFiles(hash);
16561
16671
  return c.json({ files });
16562
16672
  } catch (err) {
@@ -16665,9 +16775,11 @@ function createGitRoutes() {
16665
16775
  });
16666
16776
  app2.get("/git/remotes", async (c) => {
16667
16777
  try {
16668
- const res = await getGit(c.req.query("projectId"));
16778
+ const res = await getGitForRead(c.req.query("projectId"));
16669
16779
  if ("error" in res)
16670
16780
  return c.json({ error: res.error }, res.status);
16781
+ if ("notRepo" in res)
16782
+ return c.json({ remotes: [] });
16671
16783
  const remotes = await res.git.getRemotes();
16672
16784
  return c.json({ remotes });
16673
16785
  } catch (err) {
@@ -16790,9 +16902,11 @@ function createGitRoutes() {
16790
16902
  });
16791
16903
  app2.get("/git/remote/protocol", async (c) => {
16792
16904
  try {
16793
- const res = await getGit(c.req.query("projectId"));
16905
+ const res = await getGitForRead(c.req.query("projectId"));
16794
16906
  if ("error" in res)
16795
16907
  return c.json({ error: res.error }, res.status);
16908
+ if ("notRepo" in res)
16909
+ return c.json({ url: "", protocol: "other" });
16796
16910
  return c.json(getRemoteProtocol(res.projectPath));
16797
16911
  } catch (err) {
16798
16912
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -17655,8 +17769,8 @@ function detectOrphans(tree, declared) {
17655
17769
  if (!block || typeof block !== "object")
17656
17770
  continue;
17657
17771
  for (const k of Object.keys(block)) {
17658
- const path64 = `${namespaceId}.${kindWord}.${k}`;
17659
- if (!declaredMap.has(path64)) {
17772
+ const path63 = `${namespaceId}.${kindWord}.${k}`;
17773
+ if (!declaredMap.has(path63)) {
17660
17774
  out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
17661
17775
  }
17662
17776
  }
@@ -20074,10 +20188,10 @@ function sendWsNotification(event, state, turnId, ctx) {
20074
20188
  sendJson(ctx.ws, { type: "agent:done", agentName, taskId });
20075
20189
  break;
20076
20190
  case "tool_call":
20077
- sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, input: event.toolInput, agentName, taskId, turnId });
20191
+ sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, toolUseId: event.toolUseId, input: event.toolInput, agentName, taskId, turnId });
20078
20192
  break;
20079
20193
  case "tool_result":
20080
- sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, agentName, taskId, durationMs: event.durationMs });
20194
+ sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, toolUseId: event.toolUseId, agentName, taskId, durationMs: event.durationMs });
20081
20195
  break;
20082
20196
  case "followup_start":
20083
20197
  case "queued_message":
@@ -20457,6 +20571,17 @@ function setupWebSocketHandler(deps) {
20457
20571
  });
20458
20572
  };
20459
20573
  }
20574
+ function reclaimIfAbandoned(client) {
20575
+ if (!client.unsubscribeEvents)
20576
+ return;
20577
+ const closed = client.ws.readyState === client.ws.CLOSED;
20578
+ if (!closed && Date.now() - client.lastClientPingAt <= CLIENT_SILENCE_LIMIT_MS)
20579
+ return;
20580
+ client.unsubscribeEvents();
20581
+ client.unsubscribeEvents = null;
20582
+ console.debug(`[WS] Released listener for ${closed ? "closed" : "abandoned"} connection session=${client.sessionId}`);
20583
+ sendJson(client.ws, { type: "listener:released", sessionId: client.sessionId });
20584
+ }
20460
20585
  wss2.on("connection", (ws) => {
20461
20586
  const fileWatcher = new WorkspaceWatcher();
20462
20587
  const gitDirWatcher = new GitDirWatcher();
@@ -20471,7 +20596,8 @@ function setupWebSocketHandler(deps) {
20471
20596
  gitDirWatcher,
20472
20597
  terminalManager,
20473
20598
  backgroundSaves: /* @__PURE__ */ new Map(),
20474
- unsubscribeEvents: null
20599
+ unsubscribeEvents: null,
20600
+ lastClientPingAt: Date.now()
20475
20601
  };
20476
20602
  fileWatcher.setCallback((evt) => {
20477
20603
  sendJson(ws, { type: "file:changed", path: evt.path, action: evt.action });
@@ -20486,6 +20612,7 @@ function setupWebSocketHandler(deps) {
20486
20612
  missedPongs = 0;
20487
20613
  });
20488
20614
  const keepaliveTimer = setInterval(() => {
20615
+ reclaimIfAbandoned(client);
20489
20616
  if (missedPongs >= 2) {
20490
20617
  ws.terminate();
20491
20618
  return;
@@ -20499,6 +20626,7 @@ function setupWebSocketHandler(deps) {
20499
20626
  sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], maxContextTokens: config.model.maxContextTokens } });
20500
20627
  let messageQueue = Promise.resolve();
20501
20628
  ws.on("message", (raw) => {
20629
+ client.lastClientPingAt = Date.now();
20502
20630
  let msg;
20503
20631
  try {
20504
20632
  const text5 = typeof raw === "string" ? raw : raw.toString("utf-8");
@@ -20676,7 +20804,7 @@ function setupWebSocketHandler(deps) {
20676
20804
  client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
20677
20805
  const ctxConfig = await sm.getContextConfig(client2.sessionId);
20678
20806
  sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], sessionId: client2.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId } });
20679
- } else if (client2.sessionId !== msg.sessionId) {
20807
+ } else if (client2.sessionId !== msg.sessionId || !client2.unsubscribeEvents) {
20680
20808
  client2.unsubscribeEvents?.();
20681
20809
  client2.sessionId = msg.sessionId;
20682
20810
  client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
@@ -20800,7 +20928,7 @@ ${msg.message}` : markers.join("\n");
20800
20928
  if (client2.sessionId && client2.sessionId !== msg.sessionId) {
20801
20929
  saveSession(client2);
20802
20930
  }
20803
- if (client2.sessionId && client2.sessionManager) {
20931
+ if (client2.sessionId) {
20804
20932
  client2.unsubscribeEvents?.();
20805
20933
  client2.unsubscribeEvents = null;
20806
20934
  client2.sessionId = null;
@@ -20819,7 +20947,8 @@ ${msg.message}` : markers.join("\n");
20819
20947
  client2.projectId = detached.projectId;
20820
20948
  const state2 = getState(client2);
20821
20949
  const ctxConfig = await client2.sessionManager.getContextConfig(client2.sessionId);
20822
- const messages3 = state2 ? [...createSaveSnapshot(state2)] : [];
20950
+ const running = client2.sessionManager.isSessionRunning(client2.sessionId);
20951
+ const messages3 = state2 ? running ? [...state2.messageLog] : [...createSaveSnapshot(state2)] : [];
20823
20952
  const detachedSession = client2.sessionManager.getSessionById(client2.sessionId);
20824
20953
  sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: messages3, sessionId: msg.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId } });
20825
20954
  if (state2 && state2.contextTokens > 0) {
@@ -20832,16 +20961,21 @@ ${msg.message}` : markers.join("\n");
20832
20961
  }
20833
20962
  detached.unsubscribe();
20834
20963
  client2.unsubscribeEvents = client2.sessionManager.registerEventListener(client2.sessionId, createEventListener(client2));
20835
- if (state2 && client2.sessionManager.isSessionRunning(client2.sessionId)) {
20964
+ if (state2 && running) {
20836
20965
  console.debug(`[WS] Session still running \u2014 synthesizing in-progress state`);
20837
- sendJson(ws, { type: "chat:followup", agentName: state2.streamingAgent || "default" });
20838
- if (state2.streamBuffer) {
20839
- sendJson(ws, { type: "chat:stream", text: state2.streamBuffer, agentName: state2.streamingAgent || "default" });
20840
- }
20841
- for (const tc of state2.turnToolCalls) {
20842
- sendJson(ws, { type: "agent:tool_call", tool: tc.name, input: tc.input, agentName: "default" });
20843
- if (tc.output) {
20844
- sendJson(ws, { type: "agent:tool_result", result: tc.output, agentName: "default", durationMs: tc.durationMs });
20966
+ const agentName = detachedSession?.agentName ?? (state2.streamingAgent || "default");
20967
+ sendJson(ws, { type: "chat:followup", agentName, replay: true });
20968
+ for (const block of state2.turnContentBlocks) {
20969
+ if (block.type === "thinking") {
20970
+ sendJson(ws, { type: "chat:thinking", text: block.text, agentName, turnId: block.turnId, replay: true });
20971
+ } else if (block.type === "text") {
20972
+ sendJson(ws, { type: "chat:stream", text: block.text, agentName, turnId: block.turnId, replay: true });
20973
+ } else {
20974
+ const tc = block.toolCall;
20975
+ sendJson(ws, { type: "agent:tool_call", tool: tc.name, toolUseId: tc.toolUseId, input: tc.input, agentName, turnId: block.turnId, replay: true });
20976
+ if (tc.output) {
20977
+ sendJson(ws, { type: "agent:tool_result", result: tc.output, toolUseId: tc.toolUseId, agentName, durationMs: tc.durationMs, replay: true });
20978
+ }
20845
20979
  }
20846
20980
  }
20847
20981
  }
@@ -20903,7 +21037,7 @@ ${msg.message}` : markers.join("\n");
20903
21037
  });
20904
21038
  console.log("[WS] WebSocket handler initialized");
20905
21039
  }
20906
- var detachedSessions;
21040
+ var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
20907
21041
  var init_handler = __esm({
20908
21042
  "packages/server/dist/ws/handler.js"() {
20909
21043
  "use strict";
@@ -20916,6 +21050,7 @@ var init_handler = __esm({
20916
21050
  init_terminal_manager();
20917
21051
  init_commands2();
20918
21052
  init_goal_mode();
21053
+ CLIENT_SILENCE_LIMIT_MS = 3 * 6e4;
20919
21054
  detachedSessions = /* @__PURE__ */ new Map();
20920
21055
  }
20921
21056
  });
@@ -21070,7 +21205,9 @@ function listAllCronTargets() {
21070
21205
  }
21071
21206
  return out;
21072
21207
  }
21073
- async function dispatchToTargets(text5, targets) {
21208
+ async function dispatchToTargets(rawText, targets, workspacePath) {
21209
+ const { text: strippedText, mediaPaths } = extractMediaMessage(rawText);
21210
+ const media = mediaPaths.length > 0 ? { paths: mediaPaths, workspacePath } : void 0;
21074
21211
  const out = [];
21075
21212
  for (const t2 of targets) {
21076
21213
  const handler = _registry.get(t2.channelType);
@@ -21085,7 +21222,7 @@ async function dispatchToTargets(text5, targets) {
21085
21222
  continue;
21086
21223
  }
21087
21224
  try {
21088
- const results = await handler.dispatch(t2.accountId, text5, t2.chatId);
21225
+ const results = handler.supportsMedia ? await handler.dispatch(t2.accountId, strippedText, t2.chatId, media) : await handler.dispatch(t2.accountId, rawText, t2.chatId);
21089
21226
  for (const r of results)
21090
21227
  out.push(r);
21091
21228
  } catch (err) {
@@ -21104,6 +21241,7 @@ var _registry;
21104
21241
  var init_dispatcher = __esm({
21105
21242
  "packages/server/dist/cron/dispatcher.js"() {
21106
21243
  "use strict";
21244
+ init_media();
21107
21245
  _registry = /* @__PURE__ */ new Map();
21108
21246
  }
21109
21247
  });
@@ -21534,7 +21672,7 @@ async function runJob(jobId, triggerKind) {
21534
21672
  }
21535
21673
  let dispatchResults = [];
21536
21674
  if (status === "succeeded" && targets.length > 0) {
21537
- dispatchResults = await dispatchToTargets(trimmedOut, targets);
21675
+ dispatchResults = await dispatchToTargets(trimmedOut, targets, job.workspacePath);
21538
21676
  if (dispatchResults.length > 0 && dispatchResults.every((r) => !r.ok)) {
21539
21677
  status = "failed";
21540
21678
  failureReason = `all ${dispatchResults.length} target(s) failed: ${dispatchResults.map((r) => `${r.channelType}:${r.error ?? "?"}`).join("; ")}`;
@@ -22269,12 +22407,12 @@ var init_accounts3 = __esm({
22269
22407
  });
22270
22408
 
22271
22409
  // packages/server/dist/channels/telegram/event-adapter.js
22272
- var HARD_CHARS, MEDIA_MARKER_RE, TelegramResponder;
22410
+ var HARD_CHARS, TelegramResponder;
22273
22411
  var init_event_adapter = __esm({
22274
22412
  "packages/server/dist/channels/telegram/event-adapter.js"() {
22275
22413
  "use strict";
22414
+ init_media();
22276
22415
  HARD_CHARS = 4e3;
22277
- MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
22278
22416
  TelegramResponder = class {
22279
22417
  buffer = "";
22280
22418
  deps;
@@ -22347,12 +22485,7 @@ var init_event_adapter = __esm({
22347
22485
  return limit;
22348
22486
  }
22349
22487
  async dispatchChunk(chunk) {
22350
- const mediaPaths = [];
22351
- const text5 = chunk.replace(MEDIA_MARKER_RE, (_m, p) => {
22352
- if (p)
22353
- mediaPaths.push(p.trim());
22354
- return "";
22355
- }).replace(/\n{3,}/g, "\n\n").trim();
22488
+ const { text: text5, mediaPaths } = extractMediaMessage(chunk);
22356
22489
  if (text5) {
22357
22490
  try {
22358
22491
  await this.deps.sendText(text5);
@@ -22372,6 +22505,21 @@ var init_event_adapter = __esm({
22372
22505
  }
22373
22506
  });
22374
22507
 
22508
+ // packages/server/dist/channels/shared/busy-hint.js
22509
+ function busyHint(sm, sessionId, lang) {
22510
+ if (sm.isSessionCompacting(sessionId))
22511
+ return t("handler.compacting", lang);
22512
+ if (sm.isSessionRunning(sessionId))
22513
+ return t("handler.queued", lang);
22514
+ return null;
22515
+ }
22516
+ var init_busy_hint = __esm({
22517
+ "packages/server/dist/channels/shared/busy-hint.js"() {
22518
+ "use strict";
22519
+ init_i18n();
22520
+ }
22521
+ });
22522
+
22375
22523
  // packages/server/dist/channels/shared/session-prefix.js
22376
22524
  function sessionPrefix(channel, userId) {
22377
22525
  return `${channel}_${userId}_`;
@@ -22384,7 +22532,6 @@ var init_session_prefix = __esm({
22384
22532
 
22385
22533
  // packages/server/dist/channels/telegram/handler.js
22386
22534
  import { Bot, InputFile } from "grammy";
22387
- import path52 from "node:path";
22388
22535
  function inferMediaKind(filePath) {
22389
22536
  const cls = classifyMedia(filePath);
22390
22537
  if (cls === "image")
@@ -22660,23 +22807,16 @@ async function handleUserMessage(args) {
22660
22807
  const sm = registry2.getOrCreate(workspace);
22661
22808
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
22662
22809
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession(sm, userId, activeOverrides2, accessLevel, workspace));
22663
- if (sm.isSessionCompacting(sessionId)) {
22664
- await ctx.reply(t("handler.compacting", lang));
22665
- return;
22666
- }
22667
- if (sm.isSessionRunning(sessionId)) {
22668
- await ctx.reply(t("handler.queued", lang));
22669
- }
22810
+ const hint = busyHint(sm, sessionId, lang);
22811
+ if (hint)
22812
+ await ctx.reply(hint);
22670
22813
  if (!unsubscribers.has(sessionId)) {
22671
22814
  const responder = new TelegramResponder({
22672
22815
  sendText: async (chunk) => {
22673
22816
  await bot.api.sendMessage(chatId, chunk, { parse_mode: void 0 });
22674
22817
  },
22675
22818
  sendMedia: async (filePath) => {
22676
- const resolved = path52.resolve(filePath);
22677
- const ws = path52.resolve(workspace);
22678
- const inWorkspace = resolved === ws || resolved.startsWith(ws + path52.sep);
22679
- if (!inWorkspace && !isInTempDir(resolved)) {
22819
+ if (!isMediaPathAllowed(filePath, workspace)) {
22680
22820
  console.log(`[telegram] sendMedia blocked: ${filePath} not under workspace`);
22681
22821
  return;
22682
22822
  }
@@ -22734,6 +22874,7 @@ var init_handler2 = __esm({
22734
22874
  init_commands2();
22735
22875
  init_goal_mode();
22736
22876
  init_i18n();
22877
+ init_busy_hint();
22737
22878
  init_commands();
22738
22879
  init_media();
22739
22880
  init_session_prefix();
@@ -22742,7 +22883,7 @@ var init_handler2 = __esm({
22742
22883
 
22743
22884
  // packages/server/dist/routes/telegram.js
22744
22885
  import { Hono as Hono13 } from "hono";
22745
- import path53 from "node:path";
22886
+ import path52 from "node:path";
22746
22887
  import { Bot as Bot2 } from "grammy";
22747
22888
  import fs47 from "node:fs";
22748
22889
  function createTelegramRoutes(deps) {
@@ -22772,7 +22913,7 @@ function createTelegramRoutes(deps) {
22772
22913
  return c.json({ error: "botToken required" }, 400);
22773
22914
  if (!body.workspacePath)
22774
22915
  return c.json({ error: "workspacePath required" }, 400);
22775
- if (!path53.isAbsolute(body.workspacePath))
22916
+ if (!path52.isAbsolute(body.workspacePath))
22776
22917
  return c.json({ error: "workspacePath must be absolute" }, 400);
22777
22918
  if (!fs47.existsSync(body.workspacePath))
22778
22919
  return c.json({ error: "workspace path not found" }, 400);
@@ -23261,12 +23402,12 @@ var init_accounts4 = __esm({
23261
23402
  });
23262
23403
 
23263
23404
  // packages/server/dist/channels/wechat/event-adapter.js
23264
- var HARD_CHARS2, MEDIA_MARKER_RE2, WeixinResponder;
23405
+ var HARD_CHARS2, WeixinResponder;
23265
23406
  var init_event_adapter2 = __esm({
23266
23407
  "packages/server/dist/channels/wechat/event-adapter.js"() {
23267
23408
  "use strict";
23409
+ init_media();
23268
23410
  HARD_CHARS2 = 3500;
23269
- MEDIA_MARKER_RE2 = /^MEDIA:\s*(\S.*?)\s*$/gm;
23270
23411
  WeixinResponder = class {
23271
23412
  buffer = "";
23272
23413
  deps;
@@ -23287,13 +23428,13 @@ var init_event_adapter2 = __esm({
23287
23428
  case "error":
23288
23429
  if (event.error) {
23289
23430
  this.flushAll();
23290
- void this.dispatchChunk(`[\u9519\u8BEF] ${event.error}`);
23431
+ void this.dispatchChunk(`\u274C ${event.error}`);
23291
23432
  }
23292
23433
  break;
23293
23434
  case "system":
23294
23435
  if (event.text) {
23295
23436
  this.flushAll();
23296
- void this.dispatchChunk(`[\u7CFB\u7EDF] ${event.text}`);
23437
+ void this.dispatchChunk(`\u2139\uFE0F ${event.text}`);
23297
23438
  }
23298
23439
  break;
23299
23440
  case "complete":
@@ -23347,12 +23488,7 @@ var init_event_adapter2 = __esm({
23347
23488
  * goes out as a WeChat message (if non-empty after trim).
23348
23489
  */
23349
23490
  async dispatchChunk(chunk) {
23350
- const mediaPaths = [];
23351
- const text5 = chunk.replace(MEDIA_MARKER_RE2, (_m, p) => {
23352
- if (p)
23353
- mediaPaths.push(p.trim());
23354
- return "";
23355
- }).replace(/\n{3,}/g, "\n\n").trim();
23491
+ const { text: text5, mediaPaths } = extractMediaMessage(chunk);
23356
23492
  if (text5) {
23357
23493
  try {
23358
23494
  await this.deps.sendText(text5);
@@ -23480,7 +23616,7 @@ var init_cdn = __esm({
23480
23616
 
23481
23617
  // packages/server/dist/channels/wechat/send-media.js
23482
23618
  import fs48 from "node:fs/promises";
23483
- import path54 from "node:path";
23619
+ import path53 from "node:path";
23484
23620
  function inferKind(filePath) {
23485
23621
  const cls = classifyMedia(filePath);
23486
23622
  if (cls === "image")
@@ -23530,7 +23666,7 @@ async function sendMediaFile(params) {
23530
23666
  aesKeyBase64,
23531
23667
  fileSizeCiphertext: prep.filesize,
23532
23668
  fileSize: prep.rawsize,
23533
- fileName: path54.basename(filePath)
23669
+ fileName: path53.basename(filePath)
23534
23670
  });
23535
23671
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
23536
23672
  const body = {
@@ -23694,12 +23830,8 @@ var init_login = __esm({
23694
23830
 
23695
23831
  // packages/server/dist/channels/wechat/handler.js
23696
23832
  import fs49 from "node:fs";
23697
- import path55 from "node:path";
23833
+ import path54 from "node:path";
23698
23834
  import QRCode from "qrcode";
23699
- function isPathAllowed(filePath, workspacePath) {
23700
- const resolved = path55.resolve(filePath);
23701
- return resolved.startsWith(path55.resolve(workspacePath)) || isInTempDir(resolved);
23702
- }
23703
23835
  function startWeixinChannel(deps) {
23704
23836
  const { registry: registry2, db } = deps;
23705
23837
  const runners = /* @__PURE__ */ new Map();
@@ -23983,21 +24115,13 @@ async function handleInbound(args) {
23983
24115
  const sm = registry2.getOrCreate(account.workspacePath);
23984
24116
  const sessionAccessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
23985
24117
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession2(sm, fromUserId, activeOverrides2, sessionAccessLevel, account.workspacePath));
23986
- if (sm.isSessionCompacting(sessionId)) {
24118
+ const hint = busyHint(sm, sessionId, lang);
24119
+ if (hint) {
23987
24120
  await sendToUser({
23988
24121
  account,
23989
24122
  toUserId: fromUserId,
23990
24123
  contextToken: msg.context_token,
23991
- text: t("handler.compacting", lang)
23992
- });
23993
- return;
23994
- }
23995
- if (sm.isSessionRunning(sessionId)) {
23996
- await sendToUser({
23997
- account,
23998
- toUserId: fromUserId,
23999
- contextToken: msg.context_token,
24000
- text: t("handler.queued", lang)
24124
+ text: hint
24001
24125
  });
24002
24126
  }
24003
24127
  if (msg.context_token)
@@ -24011,7 +24135,7 @@ async function handleInbound(args) {
24011
24135
  contextToken: sessionContextTokens.get(sessionId)
24012
24136
  }),
24013
24137
  sendMedia: async (filePath) => {
24014
- if (!isPathAllowed(filePath, account.workspacePath)) {
24138
+ if (!isMediaPathAllowed(filePath, account.workspacePath)) {
24015
24139
  console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
24016
24140
  return;
24017
24141
  }
@@ -24053,7 +24177,7 @@ async function handleSlashCommand(args) {
24053
24177
  contextToken: sessionContextTokens.get(sessionId)
24054
24178
  }),
24055
24179
  sendMedia: async (filePath) => {
24056
- if (!isPathAllowed(filePath, account.workspacePath)) {
24180
+ if (!isMediaPathAllowed(filePath, account.workspacePath)) {
24057
24181
  console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
24058
24182
  return;
24059
24183
  }
@@ -24135,7 +24259,7 @@ async function handleSlashCommand(args) {
24135
24259
  await reply(login.message);
24136
24260
  return true;
24137
24261
  }
24138
- const tmpPath = path55.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
24262
+ const tmpPath = path54.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
24139
24263
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
24140
24264
  await sendMediaFile({
24141
24265
  baseUrl: account.baseUrl,
@@ -24215,6 +24339,7 @@ var init_handler3 = __esm({
24215
24339
  init_commands2();
24216
24340
  init_goal_mode();
24217
24341
  init_i18n();
24342
+ init_busy_hint();
24218
24343
  init_session_prefix();
24219
24344
  MAX_CONSECUTIVE_FAILURES = 3;
24220
24345
  BACKOFF_DELAY_MS = 3e4;
@@ -24224,7 +24349,7 @@ var init_handler3 = __esm({
24224
24349
 
24225
24350
  // packages/server/dist/routes/weixin.js
24226
24351
  import { Hono as Hono14 } from "hono";
24227
- import path56 from "node:path";
24352
+ import path55 from "node:path";
24228
24353
  import fs50 from "node:fs";
24229
24354
  function createWeixinRoutes(deps) {
24230
24355
  const { db, channel } = deps;
@@ -24250,7 +24375,7 @@ function createWeixinRoutes(deps) {
24250
24375
  return c.json({ error: "sessionKey required" }, 400);
24251
24376
  if (!body.workspacePath)
24252
24377
  return c.json({ error: "workspacePath required" }, 400);
24253
- if (!path56.isAbsolute(body.workspacePath))
24378
+ if (!path55.isAbsolute(body.workspacePath))
24254
24379
  return c.json({ error: "workspacePath must be absolute" }, 400);
24255
24380
  const accessLevel = body.accessLevel === "full" ? "full" : body.accessLevel === "workspace" ? "workspace" : "readonly";
24256
24381
  const result = await waitLogin({ sessionKey: body.sessionKey, timeoutMs: body.timeoutMs });
@@ -24295,7 +24420,7 @@ function createWeixinRoutes(deps) {
24295
24420
  if (!existing)
24296
24421
  return c.json({ error: "not found" }, 404);
24297
24422
  const body = await c.req.json().catch(() => ({}));
24298
- if (body.workspacePath !== void 0 && !path56.isAbsolute(body.workspacePath)) {
24423
+ if (body.workspacePath !== void 0 && !path55.isAbsolute(body.workspacePath)) {
24299
24424
  return c.json({ error: "workspacePath must be absolute" }, 400);
24300
24425
  }
24301
24426
  if (body.accessLevel !== void 0 && body.accessLevel !== "full" && body.accessLevel !== "workspace" && body.accessLevel !== "readonly") {
@@ -24353,7 +24478,7 @@ function readLastActiveChatId(accountId) {
24353
24478
  const v = acct.config?.lastActiveChatId;
24354
24479
  return typeof v === "string" && v.length > 0 ? v : null;
24355
24480
  }
24356
- async function dispatch2(accountId, text5, explicitChatId) {
24481
+ async function dispatch2(accountId, text5, explicitChatId, media) {
24357
24482
  const acct = getAccount4(getChannelDb(), accountId);
24358
24483
  if (!acct)
24359
24484
  throw new Error(`wechat account ${accountId} not found`);
@@ -24363,8 +24488,32 @@ async function dispatch2(accountId, text5, explicitChatId) {
24363
24488
  if (!chatId) {
24364
24489
  throw new Error("no wechat target \u2014 bind the account first (the QR-login owner becomes the default cron recipient)");
24365
24490
  }
24366
- await sendToUser({ account: acct, toUserId: chatId, text: text5 });
24367
- return [{ channelType: "wechat", accountId, chatId, ok: true }];
24491
+ const out = [];
24492
+ if (text5) {
24493
+ await sendToUser({ account: acct, toUserId: chatId, text: text5 });
24494
+ out.push({ channelType: "wechat", accountId, chatId, ok: true });
24495
+ }
24496
+ for (const filePath of media?.paths ?? []) {
24497
+ if (!isMediaPathAllowed(filePath, media.workspacePath)) {
24498
+ console.log(`[weixin] cron sendMediaFile blocked: ${filePath} not under ${media.workspacePath}`);
24499
+ out.push({ channelType: "wechat", accountId, chatId, ok: false, error: `media path not under job workspace: ${filePath}` });
24500
+ continue;
24501
+ }
24502
+ try {
24503
+ await sendMediaFile({
24504
+ baseUrl: acct.baseUrl,
24505
+ token: acct.botToken,
24506
+ toUserId: chatId,
24507
+ filePath
24508
+ });
24509
+ out.push({ channelType: "wechat", accountId, chatId, ok: true });
24510
+ } catch (err) {
24511
+ const msg = err instanceof Error ? err.message : String(err);
24512
+ console.log(`[weixin] cron sendMediaFile ${filePath} failed: ${msg}`);
24513
+ out.push({ channelType: "wechat", accountId, chatId, ok: false, error: `media ${filePath}: ${msg}` });
24514
+ }
24515
+ }
24516
+ return out;
24368
24517
  }
24369
24518
  function listTargets2() {
24370
24519
  const cdb = getChannelDb();
@@ -24385,7 +24534,7 @@ function listTargets2() {
24385
24534
  return out;
24386
24535
  }
24387
24536
  function registerWechatCronDispatcher() {
24388
- registerCronDispatcher({ channelType: "wechat", dispatch: dispatch2, listTargets: listTargets2 });
24537
+ registerCronDispatcher({ channelType: "wechat", supportsMedia: true, dispatch: dispatch2, listTargets: listTargets2 });
24389
24538
  }
24390
24539
  var init_cron_dispatcher2 = __esm({
24391
24540
  "packages/server/dist/channels/wechat/cron-dispatcher.js"() {
@@ -24394,6 +24543,8 @@ var init_cron_dispatcher2 = __esm({
24394
24543
  init_accounts();
24395
24544
  init_accounts4();
24396
24545
  init_handler3();
24546
+ init_send_media();
24547
+ init_media();
24397
24548
  init_dispatcher();
24398
24549
  }
24399
24550
  });
@@ -24428,14 +24579,14 @@ function createWebChannel(deps) {
24428
24579
  return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
24429
24580
  }
24430
24581
  function resolveWorkspace(account, override) {
24431
- let path64 = account.workspacePath;
24582
+ let path63 = account.workspacePath;
24432
24583
  if (override && override !== account.workspacePath) {
24433
24584
  if (account.accessLevel !== "full") {
24434
24585
  return { ok: false, error: "workspace override requires a full-access token" };
24435
24586
  }
24436
- path64 = override;
24587
+ path63 = override;
24437
24588
  }
24438
- const resolved = resolveAccountWorkspace({ ...account, workspacePath: path64 });
24589
+ const resolved = resolveAccountWorkspace({ ...account, workspacePath: path63 });
24439
24590
  if (!resolved)
24440
24591
  return { ok: false, error: "workspace not found" };
24441
24592
  return { ok: true, path: resolved };
@@ -24736,15 +24887,11 @@ function sseData(obj) {
24736
24887
  `;
24737
24888
  }
24738
24889
  function flushText(text5) {
24739
- const files = [];
24740
- const cleaned = text5.replace(MEDIA_MARKER_RE3, (_match, p) => {
24741
- files.push(p);
24742
- return "";
24743
- });
24890
+ const { text: cleaned, mediaPaths } = extractMediaPaths(text5);
24744
24891
  let out = "";
24745
24892
  if (cleaned)
24746
24893
  out += sseData({ type: "stream", text: cleaned });
24747
- for (const filePath of files)
24894
+ for (const filePath of mediaPaths)
24748
24895
  out += sseData({ type: "file", path: filePath });
24749
24896
  return out;
24750
24897
  }
@@ -24802,7 +24949,6 @@ function createMediaBuffer() {
24802
24949
  }
24803
24950
  };
24804
24951
  }
24805
- var MEDIA_MARKER_RE3;
24806
24952
  var init_handler4 = __esm({
24807
24953
  "packages/server/dist/channels/web/handler.js"() {
24808
24954
  "use strict";
@@ -24810,12 +24956,12 @@ var init_handler4 = __esm({
24810
24956
  init_accounts2();
24811
24957
  init_accounts2();
24812
24958
  init_media_store();
24959
+ init_media();
24813
24960
  init_accounts();
24814
24961
  init_commands2();
24815
24962
  init_goal_mode();
24816
24963
  init_i18n();
24817
24964
  init_session_prefix();
24818
- MEDIA_MARKER_RE3 = /^MEDIA:\s*(\S.*?)\s*$/gm;
24819
24965
  }
24820
24966
  });
24821
24967
 
@@ -24823,7 +24969,7 @@ var init_handler4 = __esm({
24823
24969
  import { Hono as Hono15 } from "hono";
24824
24970
  import { streamSSE } from "hono/streaming";
24825
24971
  import crypto4 from "node:crypto";
24826
- import path57 from "node:path";
24972
+ import path56 from "node:path";
24827
24973
  import fs51 from "node:fs";
24828
24974
  function createWebRoutes(deps) {
24829
24975
  const { db, channel } = deps;
@@ -24849,7 +24995,7 @@ function createWebRoutes(deps) {
24849
24995
  const body = await c.req.json().catch(() => ({}));
24850
24996
  if (!body.workspacePath)
24851
24997
  return c.json({ error: "workspacePath required" }, 400);
24852
- if (!path57.isAbsolute(body.workspacePath))
24998
+ if (!path56.isAbsolute(body.workspacePath))
24853
24999
  return c.json({ error: "workspacePath must be absolute" }, 400);
24854
25000
  if (!fs51.existsSync(body.workspacePath))
24855
25001
  return c.json({ error: "workspace path not found" }, 400);
@@ -24987,10 +25133,10 @@ function createWebRoutes(deps) {
24987
25133
  const filePath = c.req.query("path");
24988
25134
  if (!filePath)
24989
25135
  return c.json({ error: "path required" }, 400);
24990
- const path64 = await import("node:path");
24991
- const resolved = path64.default.resolve(account.workspacePath, filePath);
24992
- const root = path64.default.resolve(account.workspacePath);
24993
- if (resolved !== root && !resolved.startsWith(root + path64.default.sep)) {
25136
+ const path63 = await import("node:path");
25137
+ const resolved = path63.default.resolve(account.workspacePath, filePath);
25138
+ const root = path63.default.resolve(account.workspacePath);
25139
+ if (resolved !== root && !resolved.startsWith(root + path63.default.sep)) {
24994
25140
  return c.json({ error: "path traversal not allowed" }, 403);
24995
25141
  }
24996
25142
  let real;
@@ -25001,13 +25147,13 @@ function createWebRoutes(deps) {
25001
25147
  } catch {
25002
25148
  return c.json({ error: "file not found" }, 404);
25003
25149
  }
25004
- if (real !== realRoot && !real.startsWith(realRoot + path64.default.sep)) {
25150
+ if (real !== realRoot && !real.startsWith(realRoot + path63.default.sep)) {
25005
25151
  return c.json({ error: "path traversal not allowed" }, 403);
25006
25152
  }
25007
25153
  const stat = fs51.statSync(real);
25008
25154
  if (stat.isDirectory())
25009
25155
  return c.json({ error: "cannot serve directory" }, 400);
25010
- const ext = path64.default.extname(real).toLowerCase();
25156
+ const ext = path63.default.extname(real).toLowerCase();
25011
25157
  const mimeMap = {
25012
25158
  ".png": "image/png",
25013
25159
  ".jpg": "image/jpeg",
@@ -25021,7 +25167,7 @@ function createWebRoutes(deps) {
25021
25167
  ".txt": "text/plain"
25022
25168
  };
25023
25169
  const contentType = mimeMap[ext] || "application/octet-stream";
25024
- const fileName = path64.default.basename(resolved);
25170
+ const fileName = path63.default.basename(resolved);
25025
25171
  const data = fs51.readFileSync(real);
25026
25172
  c.header("content-type", contentType);
25027
25173
  c.header("content-disposition", `inline; filename="${fileName}"`);
@@ -25227,13 +25373,13 @@ var init_markdown = __esm({
25227
25373
  });
25228
25374
 
25229
25375
  // packages/server/dist/channels/slack/event-adapter.js
25230
- var HARD_CHARS3, MEDIA_MARKER_RE4, SlackResponder;
25376
+ var HARD_CHARS3, SlackResponder;
25231
25377
  var init_event_adapter3 = __esm({
25232
25378
  "packages/server/dist/channels/slack/event-adapter.js"() {
25233
25379
  "use strict";
25234
25380
  init_markdown();
25381
+ init_media();
25235
25382
  HARD_CHARS3 = 35e3;
25236
- MEDIA_MARKER_RE4 = /^MEDIA:\s*(\S.*?)\s*$/gm;
25237
25383
  SlackResponder = class {
25238
25384
  buffer = "";
25239
25385
  deps;
@@ -25297,12 +25443,7 @@ var init_event_adapter3 = __esm({
25297
25443
  return limit;
25298
25444
  }
25299
25445
  async dispatchChunk(chunk) {
25300
- const mediaPaths = [];
25301
- const stripped = chunk.replace(MEDIA_MARKER_RE4, (_m, p) => {
25302
- if (p)
25303
- mediaPaths.push(p.trim());
25304
- return "";
25305
- }).replace(/\n{3,}/g, "\n\n").trim();
25446
+ const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
25306
25447
  const text5 = formatForSlack(stripped);
25307
25448
  if (text5) {
25308
25449
  try {
@@ -25353,8 +25494,8 @@ async function authTest(botToken) {
25353
25494
  }
25354
25495
  async function uploadFile(args) {
25355
25496
  const fs56 = await import("node:fs");
25356
- const path64 = await import("node:path");
25357
- const filename = args.filename ?? path64.basename(args.filePath);
25497
+ const path63 = await import("node:path");
25498
+ const filename = args.filename ?? path63.basename(args.filePath);
25358
25499
  const stat = fs56.statSync(args.filePath);
25359
25500
  if (!stat.isFile())
25360
25501
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
@@ -25518,7 +25659,7 @@ var init_api2 = __esm({
25518
25659
  });
25519
25660
 
25520
25661
  // packages/server/dist/channels/slack/handler.js
25521
- import path58 from "node:path";
25662
+ import path57 from "node:path";
25522
25663
  import { WebSocket } from "ws";
25523
25664
  function slashToBang(text5, commandNames) {
25524
25665
  if (commandNames.length === 0)
@@ -25527,11 +25668,6 @@ function slashToBang(text5, commandNames) {
25527
25668
  const re = new RegExp(`(^|[\\s(\\[,\uFF0C\u3001])\\/(${alternation})\\b`, "g");
25528
25669
  return text5.replace(re, "$1!$2");
25529
25670
  }
25530
- function isPathAllowed2(filePath, workspacePath) {
25531
- const resolved = path58.resolve(filePath);
25532
- const wsResolved = path58.resolve(workspacePath);
25533
- return resolved.startsWith(wsResolved + path58.sep) || resolved === wsResolved || isInTempDir(resolved);
25534
- }
25535
25671
  function rememberEventId(id) {
25536
25672
  if (SEEN_EVENT_IDS.has(id))
25537
25673
  return true;
@@ -25789,12 +25925,9 @@ async function handleInbound2(args) {
25789
25925
  const sm = registry2.getOrCreate(workspace);
25790
25926
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
25791
25927
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread({ sm, channelId, rootTs, userId, accessLevel, state, workspacePath: workspace }));
25792
- if (sm.isSessionCompacting(sessionId)) {
25793
- await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: "\u23F3 \u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u6D88\u606F\uFF08\u901A\u5E38 30 \u79D2\u5185\u5B8C\u6210\uFF09" });
25794
- return;
25795
- }
25796
- if (sm.isSessionRunning(sessionId)) {
25797
- await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: "\u5DF2\u6536\u5230\uFF0C\u4F1A\u5728\u5F53\u524D\u8F6E\u7ED3\u675F\u540E\u5904\u7406\u3002" });
25928
+ const hint = busyHint(sm, sessionId, getLang(account));
25929
+ if (hint) {
25930
+ await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: hint });
25798
25931
  }
25799
25932
  if (!state.unsubscribers.has(sessionId)) {
25800
25933
  const responder = new SlackResponder({
@@ -25802,8 +25935,8 @@ async function handleInbound2(args) {
25802
25935
  await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: chunk });
25803
25936
  },
25804
25937
  sendMedia: async (filePath) => {
25805
- const resolved = path58.resolve(filePath);
25806
- if (!isPathAllowed2(resolved, workspace)) {
25938
+ const resolved = path57.resolve(filePath);
25939
+ if (!isMediaPathAllowed(resolved, workspace)) {
25807
25940
  console.log(`[slack] sendMedia blocked: ${filePath} not under workspace`);
25808
25941
  return;
25809
25942
  }
@@ -25820,7 +25953,10 @@ async function handleInbound2(args) {
25820
25953
  botToken: account.botToken,
25821
25954
  channel: channelId,
25822
25955
  threadTs: replyTs,
25823
- text: `\u26A0\uFE0F Failed to upload \`${path58.basename(filePath)}\`: ${err instanceof Error ? err.message : String(err)}`
25956
+ text: t("handler.upload_failed", getLang(account), {
25957
+ name: path57.basename(filePath),
25958
+ error: err instanceof Error ? err.message : String(err)
25959
+ })
25824
25960
  }).catch(() => {
25825
25961
  });
25826
25962
  }
@@ -25890,6 +26026,7 @@ var init_handler5 = __esm({
25890
26026
  init_session_prefix();
25891
26027
  init_commands();
25892
26028
  init_i18n();
26029
+ init_busy_hint();
25893
26030
  SLACK_EXTRA_COMMANDS = ["qr"];
25894
26031
  SEEN_EVENT_IDS = /* @__PURE__ */ new Set();
25895
26032
  SEEN_EVENT_IDS_MAX = 2e3;
@@ -25899,7 +26036,7 @@ var init_handler5 = __esm({
25899
26036
  // packages/server/dist/routes/slack.js
25900
26037
  import { Hono as Hono16 } from "hono";
25901
26038
  import fs52 from "node:fs";
25902
- import path59 from "node:path";
26039
+ import path58 from "node:path";
25903
26040
  function createSlackRoutes(deps) {
25904
26041
  const { db, channel } = deps;
25905
26042
  const app2 = new Hono16();
@@ -25942,7 +26079,7 @@ function createSlackRoutes(deps) {
25942
26079
  return c.json({ error: "appToken must start with xapp-" }, 400);
25943
26080
  if (!body.workspacePath)
25944
26081
  return c.json({ error: "workspacePath required" }, 400);
25945
- if (!path59.isAbsolute(body.workspacePath))
26082
+ if (!path58.isAbsolute(body.workspacePath))
25946
26083
  return c.json({ error: "workspacePath must be absolute" }, 400);
25947
26084
  if (!fs52.existsSync(body.workspacePath))
25948
26085
  return c.json({ error: "workspace path not found" }, 400);
@@ -26042,7 +26179,7 @@ function parseChatId(raw) {
26042
26179
  return { channel: raw };
26043
26180
  return { channel: raw.slice(0, idx), threadTs: raw.slice(idx + 1) || void 0 };
26044
26181
  }
26045
- async function dispatch3(accountId, text5, explicitChatId) {
26182
+ async function dispatch3(accountId, text5, explicitChatId, media) {
26046
26183
  const acct = getAccount5(getChannelDb(), accountId);
26047
26184
  if (!acct)
26048
26185
  throw new Error(`slack account ${accountId} not found`);
@@ -26052,18 +26189,37 @@ async function dispatch3(accountId, text5, explicitChatId) {
26052
26189
  throw new Error('slack cron target requires an explicit chatId (e.g. "D01234567" for a DM, or "C0123:1700.0" for a thread). Create the cron from inside a Slack chat to auto-pin, or pass --targets slack:<accountId>:<chatId>.');
26053
26190
  }
26054
26191
  const { channel, threadTs } = parseChatId(explicitChatId);
26055
- try {
26056
- await postMessage({ botToken: acct.botToken, channel, threadTs, text: text5 });
26057
- return [{ channelType: "slack", accountId, chatId: explicitChatId, ok: true }];
26058
- } catch (err) {
26059
- return [{
26060
- channelType: "slack",
26061
- accountId,
26062
- chatId: explicitChatId,
26063
- ok: false,
26064
- error: err instanceof Error ? err.message : String(err)
26065
- }];
26192
+ const out = [];
26193
+ if (text5) {
26194
+ try {
26195
+ await postMessage({ botToken: acct.botToken, channel, threadTs, text: text5 });
26196
+ out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
26197
+ } catch (err) {
26198
+ out.push({
26199
+ channelType: "slack",
26200
+ accountId,
26201
+ chatId: explicitChatId,
26202
+ ok: false,
26203
+ error: err instanceof Error ? err.message : String(err)
26204
+ });
26205
+ }
26066
26206
  }
26207
+ for (const filePath of media?.paths ?? []) {
26208
+ if (!isMediaPathAllowed(filePath, media.workspacePath)) {
26209
+ console.log(`[slack] cron uploadFile blocked: ${filePath} not under ${media.workspacePath}`);
26210
+ out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: `media path not under job workspace: ${filePath}` });
26211
+ continue;
26212
+ }
26213
+ try {
26214
+ await uploadFile({ botToken: acct.botToken, channel, threadTs, filePath });
26215
+ out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
26216
+ } catch (err) {
26217
+ const msg = err instanceof Error ? err.message : String(err);
26218
+ console.log(`[slack] cron uploadFile ${filePath} failed: ${msg}`);
26219
+ out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: `media ${filePath}: ${msg}` });
26220
+ }
26221
+ }
26222
+ return out;
26067
26223
  }
26068
26224
  function listTargets3() {
26069
26225
  const cdb = getChannelDb();
@@ -26077,7 +26233,7 @@ function listTargets3() {
26077
26233
  }));
26078
26234
  }
26079
26235
  function registerSlackCronDispatcher() {
26080
- registerCronDispatcher({ channelType: "slack", dispatch: dispatch3, listTargets: listTargets3 });
26236
+ registerCronDispatcher({ channelType: "slack", supportsMedia: true, dispatch: dispatch3, listTargets: listTargets3 });
26081
26237
  }
26082
26238
  var init_cron_dispatcher3 = __esm({
26083
26239
  "packages/server/dist/channels/slack/cron-dispatcher.js"() {
@@ -26085,6 +26241,7 @@ var init_cron_dispatcher3 = __esm({
26085
26241
  init_channel_db();
26086
26242
  init_accounts5();
26087
26243
  init_api2();
26244
+ init_media();
26088
26245
  init_dispatcher();
26089
26246
  }
26090
26247
  });
@@ -26193,13 +26350,13 @@ var init_accounts6 = __esm({
26193
26350
  });
26194
26351
 
26195
26352
  // packages/server/dist/channels/feishu/event-adapter.js
26196
- var HARD_CHARS4, MEDIA_MARKER_RE5, FeishuResponder;
26353
+ var HARD_CHARS4, FeishuResponder;
26197
26354
  var init_event_adapter4 = __esm({
26198
26355
  "packages/server/dist/channels/feishu/event-adapter.js"() {
26199
26356
  "use strict";
26200
26357
  init_markdown();
26358
+ init_media();
26201
26359
  HARD_CHARS4 = 4500;
26202
- MEDIA_MARKER_RE5 = /^MEDIA:\s*(\S.*?)\s*$/gm;
26203
26360
  FeishuResponder = class {
26204
26361
  buffer = "";
26205
26362
  deps;
@@ -26263,12 +26420,7 @@ var init_event_adapter4 = __esm({
26263
26420
  return limit;
26264
26421
  }
26265
26422
  async dispatchChunk(chunk) {
26266
- const mediaPaths = [];
26267
- const stripped = chunk.replace(MEDIA_MARKER_RE5, (_m, p) => {
26268
- if (p)
26269
- mediaPaths.push(p.trim());
26270
- return "";
26271
- }).replace(/\n{3,}/g, "\n\n").trim();
26423
+ const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
26272
26424
  const text5 = formatForFeishu(stripped);
26273
26425
  if (text5) {
26274
26426
  try {
@@ -26419,9 +26571,9 @@ async function replyMessage(args) {
26419
26571
  }
26420
26572
  async function uploadImage(args) {
26421
26573
  const fs56 = await import("node:fs");
26422
- const path64 = await import("node:path");
26574
+ const path63 = await import("node:path");
26423
26575
  const buf = fs56.readFileSync(args.filePath);
26424
- const filename = path64.basename(args.filePath);
26576
+ const filename = path63.basename(args.filePath);
26425
26577
  const send = async (token2) => {
26426
26578
  const form = new FormData();
26427
26579
  form.append("image_type", "message");
@@ -26453,9 +26605,9 @@ async function uploadImage(args) {
26453
26605
  }
26454
26606
  async function uploadFile2(args) {
26455
26607
  const fs56 = await import("node:fs");
26456
- const path64 = await import("node:path");
26608
+ const path63 = await import("node:path");
26457
26609
  const buf = fs56.readFileSync(args.filePath);
26458
- const filename = args.filename ?? path64.basename(args.filePath);
26610
+ const filename = args.filename ?? path63.basename(args.filePath);
26459
26611
  const send = async (token2) => {
26460
26612
  const form = new FormData();
26461
26613
  form.append("file_type", args.fileType);
@@ -26561,13 +26713,8 @@ var init_api3 = __esm({
26561
26713
  });
26562
26714
 
26563
26715
  // packages/server/dist/channels/feishu/handler.js
26564
- import path60 from "node:path";
26716
+ import path59 from "node:path";
26565
26717
  import * as Lark from "@larksuiteoapi/node-sdk";
26566
- function isPathAllowed3(filePath, workspacePath) {
26567
- const resolved = path60.resolve(filePath);
26568
- const wsResolved = path60.resolve(workspacePath);
26569
- return resolved.startsWith(wsResolved + path60.sep) || resolved === wsResolved || isInTempDir(resolved);
26570
- }
26571
26718
  function newAccountState2() {
26572
26719
  return {
26573
26720
  wsClient: null,
@@ -26795,12 +26942,9 @@ async function handleInbound3(args) {
26795
26942
  const sm = registry2.getOrCreate(workspace);
26796
26943
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
26797
26944
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread2({ sm, chatId, rootId, userId, accessLevel, state, workspacePath: workspace }));
26798
- if (sm.isSessionCompacting(sessionId)) {
26799
- await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: "\u23F3 \u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u6D88\u606F\uFF08\u901A\u5E38 30 \u79D2\u5185\u5B8C\u6210\uFF09" });
26800
- return;
26801
- }
26802
- if (sm.isSessionRunning(sessionId)) {
26803
- await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: "\u5DF2\u6536\u5230\uFF0C\u4F1A\u5728\u5F53\u524D\u8F6E\u7ED3\u675F\u540E\u5904\u7406\u3002" });
26945
+ const hint = busyHint(sm, sessionId, getLang(account));
26946
+ if (hint) {
26947
+ await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: hint });
26804
26948
  }
26805
26949
  if (!state.unsubscribers.has(sessionId)) {
26806
26950
  const responder = new FeishuResponder({
@@ -26808,8 +26952,8 @@ async function handleInbound3(args) {
26808
26952
  await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: chunk });
26809
26953
  },
26810
26954
  sendMedia: async (filePath) => {
26811
- const resolved = path60.resolve(filePath);
26812
- if (!isPathAllowed3(resolved, workspace)) {
26955
+ const resolved = path59.resolve(filePath);
26956
+ if (!isMediaPathAllowed(resolved, workspace)) {
26813
26957
  console.log(`[feishu] sendMedia blocked: ${filePath} not under workspace`);
26814
26958
  return;
26815
26959
  }
@@ -26828,7 +26972,10 @@ async function handleInbound3(args) {
26828
26972
  inboundMessageId,
26829
26973
  isP2P,
26830
26974
  chatId,
26831
- text: `\u26A0\uFE0F \u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF1A${path60.basename(filePath)} \u2014 ${err instanceof Error ? err.message : String(err)}`
26975
+ text: t("handler.upload_failed", getLang(account), {
26976
+ name: path59.basename(filePath),
26977
+ error: err instanceof Error ? err.message : String(err)
26978
+ })
26832
26979
  }).catch(() => {
26833
26980
  });
26834
26981
  }
@@ -26858,7 +27005,7 @@ async function replyToInbound(args) {
26858
27005
  async function sendFeishuMedia(args) {
26859
27006
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
26860
27007
  const cls = classifyMedia(filePath);
26861
- const ext = path60.extname(filePath).toLowerCase();
27008
+ const ext = path59.extname(filePath).toLowerCase();
26862
27009
  if (cls === "image") {
26863
27010
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
26864
27011
  await replyToInboundRaw({
@@ -26993,13 +27140,14 @@ var init_handler6 = __esm({
26993
27140
  init_goal_mode();
26994
27141
  init_session_prefix();
26995
27142
  init_i18n();
27143
+ init_busy_hint();
26996
27144
  }
26997
27145
  });
26998
27146
 
26999
27147
  // packages/server/dist/routes/feishu.js
27000
27148
  import { Hono as Hono17 } from "hono";
27001
27149
  import fs53 from "node:fs";
27002
- import path61 from "node:path";
27150
+ import path60 from "node:path";
27003
27151
  function createFeishuRoutes(deps) {
27004
27152
  const { db, channel } = deps;
27005
27153
  const app2 = new Hono17();
@@ -27041,7 +27189,7 @@ function createFeishuRoutes(deps) {
27041
27189
  return c.json({ error: "appSecret required" }, 400);
27042
27190
  if (!body.workspacePath)
27043
27191
  return c.json({ error: "workspacePath required" }, 400);
27044
- if (!path61.isAbsolute(body.workspacePath))
27192
+ if (!path60.isAbsolute(body.workspacePath))
27045
27193
  return c.json({ error: "workspacePath must be absolute" }, 400);
27046
27194
  if (!fs53.existsSync(body.workspacePath))
27047
27195
  return c.json({ error: "workspace path not found" }, 400);
@@ -27375,18 +27523,46 @@ function createAuthRoutes() {
27375
27523
  setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
27376
27524
  return c.json({ ok: true });
27377
27525
  });
27526
+ app2.post("/auth/change-password", async (c) => {
27527
+ const body = await c.req.json().catch(() => ({}));
27528
+ const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
27529
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
27530
+ if (passwordEnvPlaintext() != null) {
27531
+ return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
27532
+ }
27533
+ if (!await verifyPassword(oldPassword, passwordHash())) {
27534
+ return c.json({ error: "Incorrect current password" }, 401);
27535
+ }
27536
+ const weak = passwordStrengthError(newPassword);
27537
+ if (weak)
27538
+ return c.json({ error: weak }, 400);
27539
+ if (newPassword === oldPassword) {
27540
+ return c.json({ error: "New password must differ from the current password" }, 400);
27541
+ }
27542
+ updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
27543
+ return c.json({ ok: true });
27544
+ });
27378
27545
  return app2;
27379
27546
  }
27547
+ function passwordStrengthError(pw) {
27548
+ if (pw.length < 8)
27549
+ return "Password must be at least 8 characters";
27550
+ if (!/[A-Za-z]/.test(pw))
27551
+ return "Password must contain at least one letter";
27552
+ if (!/[0-9]/.test(pw))
27553
+ return "Password must contain at least one digit";
27554
+ return null;
27555
+ }
27380
27556
  function authMiddleware() {
27381
27557
  return async (c, next) => {
27382
- const path64 = c.req.path;
27383
- if (PUBLIC_PATHS.some((p) => path64 === p || path64.startsWith(p + "?"))) {
27558
+ const path63 = c.req.path;
27559
+ if (PUBLIC_PATHS.some((p) => path63 === p || path63.startsWith(p + "?"))) {
27384
27560
  return next();
27385
27561
  }
27386
27562
  const token = getCookie(c, COOKIE_NAME);
27387
27563
  const payload = validateToken(token);
27388
27564
  if (!payload) {
27389
- if (!path64.startsWith("/api/")) {
27565
+ if (!path63.startsWith("/api/")) {
27390
27566
  return next();
27391
27567
  }
27392
27568
  return c.json({ error: "Unauthorized" }, 401);
@@ -27404,6 +27580,7 @@ var init_auth = __esm({
27404
27580
  "use strict";
27405
27581
  init_config();
27406
27582
  init_password_hash();
27583
+ init_setup_config();
27407
27584
  init_brute_force();
27408
27585
  TOKEN_MAX_AGE = config.auth.tokenMaxAge;
27409
27586
  REFRESH_AFTER = config.auth.refreshAfter;
@@ -27423,7 +27600,7 @@ import { cors } from "hono/cors";
27423
27600
  import { serve } from "@hono/node-server";
27424
27601
  import { serveStatic } from "@hono/node-server/serve-static";
27425
27602
  import { WebSocketServer } from "ws";
27426
- import path62 from "node:path";
27603
+ import path61 from "node:path";
27427
27604
  import fs54 from "node:fs";
27428
27605
  import { spawnSync as spawnSync4 } from "node:child_process";
27429
27606
  import { homedir as homedir24 } from "node:os";
@@ -27431,9 +27608,9 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
27431
27608
  function findProjectRoot() {
27432
27609
  let dir = process.cwd();
27433
27610
  for (let i = 0; i < 32; i++) {
27434
- if (fs54.existsSync(path62.join(dir, "pnpm-workspace.yaml")))
27611
+ if (fs54.existsSync(path61.join(dir, "pnpm-workspace.yaml")))
27435
27612
  return dir;
27436
- const parent = path62.dirname(dir);
27613
+ const parent = path61.dirname(dir);
27437
27614
  if (parent === dir)
27438
27615
  break;
27439
27616
  dir = parent;
@@ -27442,7 +27619,7 @@ function findProjectRoot() {
27442
27619
  return process.cwd();
27443
27620
  }
27444
27621
  function acquireSingleInstanceLock(lockFile) {
27445
- fs54.mkdirSync(path62.dirname(lockFile), { recursive: true });
27622
+ fs54.mkdirSync(path61.dirname(lockFile), { recursive: true });
27446
27623
  const fd = fs54.openSync(lockFile, fs54.constants.O_RDWR | fs54.constants.O_CREAT, 420);
27447
27624
  const result = spawnSync4("flock", ["-n", "3"], {
27448
27625
  stdio: ["ignore", "ignore", "ignore", fd]
@@ -27499,7 +27676,7 @@ function isProcessAlive(pid) {
27499
27676
  }
27500
27677
  }
27501
27678
  function readGitSha() {
27502
- if (!fs54.existsSync(path62.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs54.existsSync(path62.join(PROJECT_ROOT, ".git")))
27679
+ if (!fs54.existsSync(path61.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs54.existsSync(path61.join(PROJECT_ROOT, ".git")))
27503
27680
  return null;
27504
27681
  try {
27505
27682
  const rev = spawnSync4("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
@@ -27516,14 +27693,14 @@ function readGitSha() {
27516
27693
  }
27517
27694
  function resolveFrontendDir() {
27518
27695
  if (process.env.HALO_FRONTEND_DIR)
27519
- return path62.resolve(process.env.HALO_FRONTEND_DIR);
27520
- const bundled = path62.resolve(__dirname3, "..", "admin-out");
27521
- if (fs54.existsSync(path62.join(bundled, "index.html")))
27696
+ return path61.resolve(process.env.HALO_FRONTEND_DIR);
27697
+ const bundled = path61.resolve(__dirname3, "..", "admin-out");
27698
+ if (fs54.existsSync(path61.join(bundled, "index.html")))
27522
27699
  return bundled;
27523
- const sibling = path62.resolve(__dirname3, "..", "..", "admin", "out");
27524
- if (fs54.existsSync(path62.join(sibling, "index.html")))
27700
+ const sibling = path61.resolve(__dirname3, "..", "..", "admin", "out");
27701
+ if (fs54.existsSync(path61.join(sibling, "index.html")))
27525
27702
  return sibling;
27526
- return path62.resolve(PROJECT_ROOT, "packages", "admin", "out");
27703
+ return path61.resolve(PROJECT_ROOT, "packages", "admin", "out");
27527
27704
  }
27528
27705
  async function gracefulShutdown(signal) {
27529
27706
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
@@ -27576,13 +27753,13 @@ var init_dist2 = __esm({
27576
27753
  init_sandbox();
27577
27754
  init_init();
27578
27755
  init_git_ssh();
27579
- __dirname3 = path62.dirname(fileURLToPath3(import.meta.url));
27756
+ __dirname3 = path61.dirname(fileURLToPath3(import.meta.url));
27580
27757
  PORT = config.server.port;
27581
- HALO_VERSION = "1.0.1";
27758
+ HALO_VERSION = "1.0.3";
27582
27759
  PROJECT_ROOT = findProjectRoot();
27583
- HALO_HOME4 = path62.join(homedir24(), ".halo");
27760
+ HALO_HOME4 = path61.join(homedir24(), ".halo");
27584
27761
  GIT_SHA = readGitSha();
27585
- if (!fs54.existsSync(path62.join(HALO_HOME4, "global", ".template-version"))) {
27762
+ if (!fs54.existsSync(path61.join(HALO_HOME4, "global", ".template-version"))) {
27586
27763
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
27587
27764
  process.exit(1);
27588
27765
  }
@@ -27603,7 +27780,7 @@ var init_dist2 = __esm({
27603
27780
  process.exit(1);
27604
27781
  }
27605
27782
  if (!AGENTCORE)
27606
- acquireSingleInstanceLock(path62.join(HALO_HOME4, "global", "server.lock"));
27783
+ acquireSingleInstanceLock(path61.join(HALO_HOME4, "global", "server.lock"));
27607
27784
  {
27608
27785
  const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
27609
27786
  const dispatched = new Set(DISPATCH_COMMANDS);
@@ -27669,10 +27846,10 @@ var init_dist2 = __esm({
27669
27846
  app.route("/api", evolutionRoutes);
27670
27847
  cronRoutes = createCronRoutes();
27671
27848
  app.route("/api", cronRoutes);
27672
- channelDb = createChannelDb(path62.join(HALO_HOME4, "secrets"));
27849
+ channelDb = createChannelDb(path61.join(HALO_HOME4, "secrets"));
27673
27850
  setChannelDb(channelDb);
27674
- setEvoDb(createEvoDb(path62.join(HALO_HOME4, "global")));
27675
- setCronDb(createCronDb(path62.join(HALO_HOME4, "global")));
27851
+ setEvoDb(createEvoDb(path61.join(HALO_HOME4, "global")));
27852
+ setCronDb(createCronDb(path61.join(HALO_HOME4, "global")));
27676
27853
  setEvoSpawner(realEvoSpawner);
27677
27854
  if (!AGENTCORE) {
27678
27855
  startEvoTicker();
@@ -27696,9 +27873,9 @@ var init_dist2 = __esm({
27696
27873
  console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
27697
27874
  }
27698
27875
  FRONTEND_DIR = resolveFrontendDir();
27699
- app.use("/*", serveStatic({ root: path62.relative(process.cwd(), FRONTEND_DIR) }));
27876
+ app.use("/*", serveStatic({ root: path61.relative(process.cwd(), FRONTEND_DIR) }));
27700
27877
  app.get("/*", (c) => {
27701
- const indexPath = path62.join(FRONTEND_DIR, "index.html");
27878
+ const indexPath = path61.join(FRONTEND_DIR, "index.html");
27702
27879
  try {
27703
27880
  const html = fs54.readFileSync(indexPath, "utf-8");
27704
27881
  return c.html(html);
@@ -28469,11 +28646,11 @@ process.emitWarning = (warning, ...args) => {
28469
28646
  };
28470
28647
 
28471
28648
  // packages/cli/src/index.ts
28472
- import path63 from "node:path";
28649
+ import path62 from "node:path";
28473
28650
  import fs55 from "node:fs";
28474
28651
  import { homedir as homedir25 } from "node:os";
28475
28652
  import { parseArgs as parseArgs2 } from "node:util";
28476
- var VERSION = "1.0.1";
28653
+ var VERSION = "1.0.3";
28477
28654
  var HELP_TOP = `Usage: halo <command> [options]
28478
28655
 
28479
28656
  Commands:
@@ -28726,8 +28903,8 @@ async function cmdSetup(argv = []) {
28726
28903
  const { buildBindProviderPlan: buildBindProviderPlan2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
28727
28904
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
28728
28905
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
28729
- const haloHome2 = path63.join(homedir25(), ".halo");
28730
- const globalDir2 = path63.join(haloHome2, "global");
28906
+ const haloHome2 = path62.join(homedir25(), ".halo");
28907
+ const globalDir2 = path62.join(haloHome2, "global");
28731
28908
  ensureHaloHome2(haloHome2);
28732
28909
  process.stderr.write(`[setup] ${haloHome2}/ ready
28733
28910
  `);
@@ -29071,7 +29248,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
29071
29248
  async function cmdAgents(flags) {
29072
29249
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
29073
29250
  await initRuntime2();
29074
- const workspace = path63.resolve(flags.workspace ?? process.cwd());
29251
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
29075
29252
  const agents = await listAgents2(workspace);
29076
29253
  if (agents.length === 0) {
29077
29254
  process.stderr.write("No agents found.\n");
@@ -29087,7 +29264,7 @@ async function cmdAgents(flags) {
29087
29264
  async function cmdSessions(flags) {
29088
29265
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
29089
29266
  await initRuntime2();
29090
- const workspace = path63.resolve(flags.workspace ?? process.cwd());
29267
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
29091
29268
  const sessions2 = listSessions2(workspace);
29092
29269
  if (sessions2.length === 0) {
29093
29270
  process.stderr.write("No sessions found.\n");
@@ -29189,8 +29366,8 @@ ${stdinContent}` : stdinContent;
29189
29366
  harness.destroy();
29190
29367
  }
29191
29368
  }
29192
- var SERVER_LOCK_PATH = path63.join(homedir25(), ".halo", "global", "server.lock");
29193
- var SERVER_LOG_PATH = path63.join(homedir25(), ".halo", "logs", "server.log");
29369
+ var SERVER_LOCK_PATH = path62.join(homedir25(), ".halo", "global", "server.lock");
29370
+ var SERVER_LOG_PATH = path62.join(homedir25(), ".halo", "logs", "server.log");
29194
29371
  function readServerPid() {
29195
29372
  try {
29196
29373
  const txt = fs55.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
@@ -29275,7 +29452,7 @@ async function cmdServerStart(argv, _unused) {
29275
29452
  return;
29276
29453
  }
29277
29454
  const { spawn: spawn4 } = await import("node:child_process");
29278
- fs55.mkdirSync(path63.dirname(SERVER_LOG_PATH), { recursive: true });
29455
+ fs55.mkdirSync(path62.dirname(SERVER_LOG_PATH), { recursive: true });
29279
29456
  const out = fs55.openSync(SERVER_LOG_PATH, "a");
29280
29457
  const child = spawn4(process.argv[0], [process.argv[1], "server", "start"], {
29281
29458
  detached: true,
@@ -29394,7 +29571,7 @@ function exitMissingSetup() {
29394
29571
  process.exit(1);
29395
29572
  }
29396
29573
  function isHaloHomeReady() {
29397
- return fs55.existsSync(path63.join(homedir25(), ".halo", "global", ".template-version"));
29574
+ return fs55.existsSync(path62.join(homedir25(), ".halo", "global", ".template-version"));
29398
29575
  }
29399
29576
  async function main2() {
29400
29577
  const argv = process.argv.slice(2);