@turmind/halo 1.0.2 → 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 (32) 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/ws.md +21 -4
  12. package/bundled-docs/dev/api.md +37 -3
  13. package/bundled-docs/dev/env.md +2 -0
  14. package/bundled-docs/dev/tools.md +13 -4
  15. package/bundled-docs/guide/chat.md +1 -1
  16. package/bundled-docs/guide/workspace.md +2 -2
  17. package/bundled-docs/requirements/settings.md +8 -0
  18. package/dist/index.js +416 -247
  19. package/package.json +1 -1
  20. package/templates/agents/__apply_agent__/agent.yaml +1 -1
  21. package/templates/agents/__evo_agent__/agent.yaml +1 -1
  22. package/templates/agents/__score__/agent.yaml +1 -1
  23. package/templates/agents/deep-executor/agent.yaml +1 -1
  24. package/templates/agents/default/agent.yaml +1 -1
  25. package/templates/agents/goal/agent.yaml +1 -1
  26. package/templates/skills/agent/SKILL.md +2 -2
  27. package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +0 -1
  28. package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +0 -1
  29. package/admin-out/_next/static/chunks/app/page-f8208e0c7f315faa.js +0 -1
  30. package/admin-out/_next/static/css/80eca65cb72e5c69.css +0 -3
  31. /package/admin-out/_next/static/{WvYu_osMZVpohYN4ntkpS → CWrUwTypkebkhwZ5i82_G}/_buildManifest.js +0 -0
  32. /package/admin-out/_next/static/{WvYu_osMZVpohYN4ntkpS → 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 = 48;
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([
@@ -6935,6 +6947,16 @@ function buildBwrapArgs(opts) {
6935
6947
  args.push("--bind", dir, dir);
6936
6948
  }
6937
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
+ }
6938
6960
  args.push("--proc", "/proc");
6939
6961
  args.push("--dev", "/dev");
6940
6962
  args.push("--clearenv");
@@ -6969,6 +6991,9 @@ function assertPathAllowed(filePath, opts, write = false) {
6969
6991
  const resolved = realpathBounded(filePath);
6970
6992
  const wsRoot = realpathBounded(opts.workspaceRoot);
6971
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
+ }
6972
6997
  if (write && opts.accessLevel === "readonly") {
6973
6998
  throw new Error(`Access denied: readonly session cannot write to "${filePath}"`);
6974
6999
  }
@@ -6991,6 +7016,18 @@ function isHiddenGlobalPath(resolved) {
6991
7016
  }
6992
7017
  return false;
6993
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
+ }
6994
7031
  function buildExecScriptArgs(workspaceRoot, command) {
6995
7032
  return ["bash", "-c", `cd "$1" && shift && ${command}`, "bash", workspaceRoot];
6996
7033
  }
@@ -7194,7 +7231,7 @@ async function sandboxReaddir(dirPath, opts) {
7194
7231
  return { name: isDir ? name.slice(0, -1) : name, isDirectory: isDir };
7195
7232
  });
7196
7233
  }
7197
- 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;
7198
7235
  var init_sandbox = __esm({
7199
7236
  "packages/server/dist/tools/sandbox.js"() {
7200
7237
  "use strict";
@@ -7237,6 +7274,8 @@ var init_sandbox = __esm({
7237
7274
  "~/.halo/global/cron.db-wal",
7238
7275
  "~/.halo/global/cron.db-shm"
7239
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"];
7240
7279
  _hiddenDirs = DEFAULT_HIDDEN_DIRS;
7241
7280
  _hiddenFiles = DEFAULT_HIDDEN_FILES;
7242
7281
  _writableDirs = [];
@@ -8704,8 +8743,15 @@ var init_i18n = __esm({
8704
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" },
8705
8744
  // ── Channel handler messages ──
8706
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." },
8707
- "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)" },
8708
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}" },
8709
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" },
8710
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" },
8711
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." },
@@ -10471,9 +10517,11 @@ ${cleaned}` : cleaned;
10471
10517
  const overThreshold = session.lastContextTokens > session.contextConfig.maxTokens * session.contextConfig.compressAt;
10472
10518
  if (!overThreshold)
10473
10519
  return;
10520
+ if (this.compactCut(session.agent.messages) === 0)
10521
+ return;
10522
+ const taskId = session.parentId ? session.id : void 0;
10474
10523
  try {
10475
10524
  session.isCompacting = true;
10476
- const taskId = session.parentId ? session.id : void 0;
10477
10525
  const preflightText = `Compacting context (${Math.round(session.lastContextTokens / 1e3)}K tokens)\u2026`;
10478
10526
  this.emitEvent(session.id, { type: "system", text: preflightText, taskId });
10479
10527
  const result = await this.selfCompactSession(session.id);
@@ -10484,9 +10532,12 @@ ${cleaned}` : cleaned;
10484
10532
  ${result.summary}`, agentName: "user", report: true, taskId });
10485
10533
  this.emitEvent(session.id, { type: "compacted", totalTokens: result.estimatedTokens, taskId });
10486
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 });
10487
10537
  }
10488
10538
  } catch (err) {
10489
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 });
10490
10541
  } finally {
10491
10542
  session.isCompacting = false;
10492
10543
  }
@@ -10578,19 +10629,25 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10578
10629
  }
10579
10630
  }
10580
10631
  /**
10581
- * Self-compact: let the current agent summarize its own conversation.
10582
- * The agent already has full context cached, so this is fast and lossless.
10583
- * 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.
10584
10646
  */
10585
- async selfCompactSession(sessionId, signal) {
10586
- const session = this.sessions.get(sessionId);
10587
- if (!session)
10588
- return null;
10589
- const messages2 = session.agent.messages;
10647
+ compactCut(messages2) {
10590
10648
  const keepCount = config.compact.keep_messages;
10591
10649
  if (messages2.length <= keepCount)
10592
- return null;
10593
- session.compactedThisTurn = true;
10650
+ return 0;
10594
10651
  let cut = Math.max(0, messages2.length - keepCount);
10595
10652
  while (cut < messages2.length) {
10596
10653
  const m = messages2[cut];
@@ -10601,8 +10658,22 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10601
10658
  }
10602
10659
  break;
10603
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);
10604
10674
  if (cut === 0)
10605
10675
  return null;
10676
+ session.compactedThisTurn = true;
10606
10677
  const olderCount = cut;
10607
10678
  const compactInstruction = [
10608
10679
  "Summarize the conversation so far into a concise summary that preserves:",
@@ -10686,9 +10757,8 @@ ${summaryText}` }]
10686
10757
  return "running";
10687
10758
  if (session.isCompacting)
10688
10759
  return "already";
10689
- const keepCount = config.compact.keep_messages;
10690
10760
  const rawCount = session.agent.messages.length;
10691
- if (rawCount <= keepCount)
10761
+ if (this.compactCut(session.agent.messages) === 0)
10692
10762
  return "nothing";
10693
10763
  const ac = await this.beginCompact(sessionId);
10694
10764
  if (!ac)
@@ -10700,8 +10770,10 @@ ${summaryText}` }]
10700
10770
  this.emitEvent(sessionId, { type: "system", text: preflightText, taskId });
10701
10771
  try {
10702
10772
  const result = await this.selfCompactSession(sessionId, ac.signal);
10703
- if (!result)
10773
+ if (!result) {
10774
+ this.emitEvent(sessionId, { type: "system", text: "Compaction skipped \u2014 no summary produced", taskId });
10704
10775
  return "nothing";
10776
+ }
10705
10777
  session.lastContextTokens = result.estimatedTokens;
10706
10778
  const noticeText = `Context compacted: ${result.olderCount} older messages summarized`;
10707
10779
  this.emitEvent(sessionId, { type: "system", text: noticeText, taskId });
@@ -10717,6 +10789,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
10717
10789
  this.emitEvent(sessionId, { type: "system", text: "Compact cancelled" });
10718
10790
  return "nothing";
10719
10791
  }
10792
+ this.emitEvent(sessionId, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
10720
10793
  throw err;
10721
10794
  } finally {
10722
10795
  this.endCompact(sessionId);
@@ -13574,8 +13647,8 @@ function applyPathPick(value, ref, pick) {
13574
13647
  const before = value.slice(0, markerStart);
13575
13648
  const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
13576
13649
  const needsQuote = pick.insertPath.includes(" ");
13577
- const path64 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
13578
- return `${before}@${kind} ${path64}`;
13650
+ const path63 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
13651
+ return `${before}@${kind} ${path63}`;
13579
13652
  }
13580
13653
  var IMAGE_EXTS2;
13581
13654
  var init_path_suggest_engine = __esm({
@@ -15095,12 +15168,12 @@ None yet.
15095
15168
  }
15096
15169
  console.log(`[Workspace] Initialized workspace: ${this.projectRoot}`);
15097
15170
  }
15098
- async readFile(path64) {
15099
- const fullPath = this.validatePath(path64);
15171
+ async readFile(path63) {
15172
+ const fullPath = this.validatePath(path63);
15100
15173
  return readFile(fullPath, "utf-8");
15101
15174
  }
15102
- async writeFile(path64, content) {
15103
- const fullPath = this.validatePath(path64);
15175
+ async writeFile(path63, content) {
15176
+ const fullPath = this.validatePath(path63);
15104
15177
  const dir = resolve(fullPath, "..");
15105
15178
  await mkdir(dir, { recursive: true });
15106
15179
  await writeFile(fullPath, content, "utf-8");
@@ -15120,26 +15193,26 @@ None yet.
15120
15193
  }
15121
15194
  return results;
15122
15195
  }
15123
- async fileExists(path64) {
15196
+ async fileExists(path63) {
15124
15197
  try {
15125
- const fullPath = this.validatePath(path64);
15198
+ const fullPath = this.validatePath(path63);
15126
15199
  await access(fullPath);
15127
15200
  return true;
15128
15201
  } catch {
15129
15202
  return false;
15130
15203
  }
15131
15204
  }
15132
- validatePath(path64) {
15133
- const fullPath = isAbsolute(path64) ? resolve(path64) : resolve(this.projectRoot, path64);
15205
+ validatePath(path63) {
15206
+ const fullPath = isAbsolute(path63) ? resolve(path63) : resolve(this.projectRoot, path63);
15134
15207
  const root = resolve(this.projectRoot);
15135
15208
  if (fullPath !== root && !fullPath.startsWith(root + sep)) {
15136
- 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}"`);
15137
15210
  }
15138
15211
  try {
15139
15212
  const real = realpathSync2(fullPath);
15140
15213
  const realRoot = realpathSync2(root);
15141
15214
  if (real !== realRoot && !real.startsWith(realRoot + sep)) {
15142
- 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}"`);
15143
15216
  }
15144
15217
  } catch (err) {
15145
15218
  if (err instanceof WorkspaceError)
@@ -15252,10 +15325,10 @@ var init_git_manager = __esm({
15252
15325
  throw new GitManagerError(`Failed to commit: ${message2}`);
15253
15326
  }
15254
15327
  }
15255
- async getDiff(path64) {
15328
+ async getDiff(path63) {
15256
15329
  try {
15257
- if (path64) {
15258
- return await this.git.diff(["HEAD", "--", path64]);
15330
+ if (path63) {
15331
+ return await this.git.diff(["HEAD", "--", path63]);
15259
15332
  }
15260
15333
  return await this.git.diff(["HEAD"]);
15261
15334
  } catch {
@@ -15344,12 +15417,12 @@ var init_git_manager = __esm({
15344
15417
  const code = parts[0];
15345
15418
  const status = code[0];
15346
15419
  const isRenameOrCopy = status === "R" || status === "C";
15347
- const path64 = isRenameOrCopy ? parts[2] : parts[1];
15348
- if (!path64 || seen.has(path64))
15420
+ const path63 = isRenameOrCopy ? parts[2] : parts[1];
15421
+ if (!path63 || seen.has(path63))
15349
15422
  continue;
15350
- seen.add(path64);
15423
+ seen.add(path63);
15351
15424
  files.push({
15352
- path: path64,
15425
+ path: path63,
15353
15426
  status,
15354
15427
  from: isRenameOrCopy ? parts[1] : void 0
15355
15428
  });
@@ -15497,9 +15570,9 @@ var init_git_manager = __esm({
15497
15570
  * empty for an added file or the root commit) and `modified` is the version
15498
15571
  * at `commit` (empty for a deletion). The working-tree/staged path above is
15499
15572
  * left entirely untouched (commit === undefined preserves existing behavior). */
15500
- async getFileDiff(path64, staged, from, commit) {
15573
+ async getFileDiff(path63, staged, from, commit) {
15501
15574
  if (commit) {
15502
- const parentPath = from || path64;
15575
+ const parentPath = from || path63;
15503
15576
  let original2 = "";
15504
15577
  try {
15505
15578
  original2 = await this.git.show([`${commit}^:${parentPath}`]);
@@ -15508,13 +15581,13 @@ var init_git_manager = __esm({
15508
15581
  }
15509
15582
  let modified2 = "";
15510
15583
  try {
15511
- modified2 = await this.git.show([`${commit}:${path64}`]);
15584
+ modified2 = await this.git.show([`${commit}:${path63}`]);
15512
15585
  } catch {
15513
15586
  modified2 = "";
15514
15587
  }
15515
15588
  return { original: original2, modified: modified2 };
15516
15589
  }
15517
- const headPath = from || path64;
15590
+ const headPath = from || path63;
15518
15591
  let original = "";
15519
15592
  try {
15520
15593
  original = await this.git.show([`HEAD:${headPath}`]);
@@ -15524,13 +15597,13 @@ var init_git_manager = __esm({
15524
15597
  let modified = "";
15525
15598
  if (staged) {
15526
15599
  try {
15527
- modified = await this.git.show([`:${path64}`]);
15600
+ modified = await this.git.show([`:${path63}`]);
15528
15601
  } catch {
15529
15602
  modified = "";
15530
15603
  }
15531
15604
  } else {
15532
15605
  try {
15533
- modified = await this.workspace.readFile(path64);
15606
+ modified = await this.workspace.readFile(path63);
15534
15607
  } catch {
15535
15608
  modified = "";
15536
15609
  }
@@ -15561,6 +15634,24 @@ function isInTempDir(filePath) {
15561
15634
  const tmp = tempDir();
15562
15635
  return resolved === tmp || resolved.startsWith(tmp + path32.sep);
15563
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
+ }
15564
15655
  function classifyMedia(filePath) {
15565
15656
  const ext = path32.extname(filePath).toLowerCase();
15566
15657
  if (IMAGE_EXTS3.has(ext))
@@ -15571,10 +15662,11 @@ function classifyMedia(filePath) {
15571
15662
  return "voice";
15572
15663
  return "other";
15573
15664
  }
15574
- var IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
15665
+ var MEDIA_MARKER_RE, IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
15575
15666
  var init_media = __esm({
15576
15667
  "packages/server/dist/channels/shared/media.js"() {
15577
15668
  "use strict";
15669
+ MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
15578
15670
  IMAGE_EXTS3 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
15579
15671
  VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".avi"]);
15580
15672
  VOICE_EXTS = /* @__PURE__ */ new Set([".ogg", ".oga", ".opus"]);
@@ -16486,12 +16578,20 @@ function createGitRoutes() {
16486
16578
  const git = new GitManager(new Workspace(projectPath));
16487
16579
  return { git, projectPath };
16488
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
+ }
16489
16589
  app2.get("/git/status", async (c) => {
16490
16590
  try {
16491
- const res = await getGit(c.req.query("projectId"));
16591
+ const res = await getGitForRead(c.req.query("projectId"));
16492
16592
  if ("error" in res)
16493
16593
  return c.json({ error: res.error }, res.status);
16494
- if (!await res.git.isRepoRoot())
16594
+ if ("notRepo" in res)
16495
16595
  return c.json({ isRepo: false });
16496
16596
  const status = await res.git.getStatus();
16497
16597
  return c.json({ isRepo: true, ...status });
@@ -16503,9 +16603,11 @@ function createGitRoutes() {
16503
16603
  });
16504
16604
  app2.get("/git/ignored", async (c) => {
16505
16605
  try {
16506
- const res = await getGit(c.req.query("projectId"));
16606
+ const res = await getGitForRead(c.req.query("projectId"));
16507
16607
  if ("error" in res)
16508
16608
  return c.json({ error: res.error }, res.status);
16609
+ if ("notRepo" in res)
16610
+ return c.json({ isRepo: false, ignored: [] });
16509
16611
  const ignored = await res.git.getIgnoredPaths();
16510
16612
  return c.json({ ignored });
16511
16613
  } catch (err) {
@@ -16520,11 +16622,13 @@ function createGitRoutes() {
16520
16622
  const staged = c.req.query("staged") === "1";
16521
16623
  const from = c.req.query("from") || void 0;
16522
16624
  const commit = c.req.query("commit") || void 0;
16523
- const res = await getGit(c.req.query("projectId"));
16625
+ const res = await getGitForRead(c.req.query("projectId"));
16524
16626
  if ("error" in res)
16525
16627
  return c.json({ error: res.error }, res.status);
16526
16628
  if (!filePath)
16527
16629
  return c.json({ error: "path is required" }, 400);
16630
+ if ("notRepo" in res)
16631
+ return c.json({ path: filePath, original: "", modified: "" });
16528
16632
  if (!validatePath(filePath, res.projectPath)) {
16529
16633
  return c.json({ error: "Path traversal not allowed" }, 403);
16530
16634
  }
@@ -16538,9 +16642,11 @@ function createGitRoutes() {
16538
16642
  });
16539
16643
  app2.get("/git/log", async (c) => {
16540
16644
  try {
16541
- const res = await getGit(c.req.query("projectId"));
16645
+ const res = await getGitForRead(c.req.query("projectId"));
16542
16646
  if ("error" in res)
16543
16647
  return c.json({ error: res.error }, res.status);
16648
+ if ("notRepo" in res)
16649
+ return c.json({ commits: [] });
16544
16650
  const limitRaw = Number(c.req.query("limit"));
16545
16651
  const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 2e3) : 50;
16546
16652
  const commits = await res.git.getLog(limit);
@@ -16554,11 +16660,13 @@ function createGitRoutes() {
16554
16660
  app2.get("/git/commit-files", async (c) => {
16555
16661
  try {
16556
16662
  const hash = c.req.query("hash");
16557
- const res = await getGit(c.req.query("projectId"));
16663
+ const res = await getGitForRead(c.req.query("projectId"));
16558
16664
  if ("error" in res)
16559
16665
  return c.json({ error: res.error }, res.status);
16560
16666
  if (!hash)
16561
16667
  return c.json({ error: "hash is required" }, 400);
16668
+ if ("notRepo" in res)
16669
+ return c.json({ files: [] });
16562
16670
  const files = await res.git.getCommitFiles(hash);
16563
16671
  return c.json({ files });
16564
16672
  } catch (err) {
@@ -16667,9 +16775,11 @@ function createGitRoutes() {
16667
16775
  });
16668
16776
  app2.get("/git/remotes", async (c) => {
16669
16777
  try {
16670
- const res = await getGit(c.req.query("projectId"));
16778
+ const res = await getGitForRead(c.req.query("projectId"));
16671
16779
  if ("error" in res)
16672
16780
  return c.json({ error: res.error }, res.status);
16781
+ if ("notRepo" in res)
16782
+ return c.json({ remotes: [] });
16673
16783
  const remotes = await res.git.getRemotes();
16674
16784
  return c.json({ remotes });
16675
16785
  } catch (err) {
@@ -16792,9 +16902,11 @@ function createGitRoutes() {
16792
16902
  });
16793
16903
  app2.get("/git/remote/protocol", async (c) => {
16794
16904
  try {
16795
- const res = await getGit(c.req.query("projectId"));
16905
+ const res = await getGitForRead(c.req.query("projectId"));
16796
16906
  if ("error" in res)
16797
16907
  return c.json({ error: res.error }, res.status);
16908
+ if ("notRepo" in res)
16909
+ return c.json({ url: "", protocol: "other" });
16798
16910
  return c.json(getRemoteProtocol(res.projectPath));
16799
16911
  } catch (err) {
16800
16912
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -17657,8 +17769,8 @@ function detectOrphans(tree, declared) {
17657
17769
  if (!block || typeof block !== "object")
17658
17770
  continue;
17659
17771
  for (const k of Object.keys(block)) {
17660
- const path64 = `${namespaceId}.${kindWord}.${k}`;
17661
- if (!declaredMap.has(path64)) {
17772
+ const path63 = `${namespaceId}.${kindWord}.${k}`;
17773
+ if (!declaredMap.has(path63)) {
17662
17774
  out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
17663
17775
  }
17664
17776
  }
@@ -20459,6 +20571,17 @@ function setupWebSocketHandler(deps) {
20459
20571
  });
20460
20572
  };
20461
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
+ }
20462
20585
  wss2.on("connection", (ws) => {
20463
20586
  const fileWatcher = new WorkspaceWatcher();
20464
20587
  const gitDirWatcher = new GitDirWatcher();
@@ -20473,7 +20596,8 @@ function setupWebSocketHandler(deps) {
20473
20596
  gitDirWatcher,
20474
20597
  terminalManager,
20475
20598
  backgroundSaves: /* @__PURE__ */ new Map(),
20476
- unsubscribeEvents: null
20599
+ unsubscribeEvents: null,
20600
+ lastClientPingAt: Date.now()
20477
20601
  };
20478
20602
  fileWatcher.setCallback((evt) => {
20479
20603
  sendJson(ws, { type: "file:changed", path: evt.path, action: evt.action });
@@ -20488,6 +20612,7 @@ function setupWebSocketHandler(deps) {
20488
20612
  missedPongs = 0;
20489
20613
  });
20490
20614
  const keepaliveTimer = setInterval(() => {
20615
+ reclaimIfAbandoned(client);
20491
20616
  if (missedPongs >= 2) {
20492
20617
  ws.terminate();
20493
20618
  return;
@@ -20501,6 +20626,7 @@ function setupWebSocketHandler(deps) {
20501
20626
  sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], maxContextTokens: config.model.maxContextTokens } });
20502
20627
  let messageQueue = Promise.resolve();
20503
20628
  ws.on("message", (raw) => {
20629
+ client.lastClientPingAt = Date.now();
20504
20630
  let msg;
20505
20631
  try {
20506
20632
  const text5 = typeof raw === "string" ? raw : raw.toString("utf-8");
@@ -20678,7 +20804,7 @@ function setupWebSocketHandler(deps) {
20678
20804
  client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
20679
20805
  const ctxConfig = await sm.getContextConfig(client2.sessionId);
20680
20806
  sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], sessionId: client2.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId } });
20681
- } else if (client2.sessionId !== msg.sessionId) {
20807
+ } else if (client2.sessionId !== msg.sessionId || !client2.unsubscribeEvents) {
20682
20808
  client2.unsubscribeEvents?.();
20683
20809
  client2.sessionId = msg.sessionId;
20684
20810
  client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
@@ -20802,7 +20928,7 @@ ${msg.message}` : markers.join("\n");
20802
20928
  if (client2.sessionId && client2.sessionId !== msg.sessionId) {
20803
20929
  saveSession(client2);
20804
20930
  }
20805
- if (client2.sessionId && client2.sessionManager) {
20931
+ if (client2.sessionId) {
20806
20932
  client2.unsubscribeEvents?.();
20807
20933
  client2.unsubscribeEvents = null;
20808
20934
  client2.sessionId = null;
@@ -20911,7 +21037,7 @@ ${msg.message}` : markers.join("\n");
20911
21037
  });
20912
21038
  console.log("[WS] WebSocket handler initialized");
20913
21039
  }
20914
- var detachedSessions;
21040
+ var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
20915
21041
  var init_handler = __esm({
20916
21042
  "packages/server/dist/ws/handler.js"() {
20917
21043
  "use strict";
@@ -20924,6 +21050,7 @@ var init_handler = __esm({
20924
21050
  init_terminal_manager();
20925
21051
  init_commands2();
20926
21052
  init_goal_mode();
21053
+ CLIENT_SILENCE_LIMIT_MS = 3 * 6e4;
20927
21054
  detachedSessions = /* @__PURE__ */ new Map();
20928
21055
  }
20929
21056
  });
@@ -21078,7 +21205,9 @@ function listAllCronTargets() {
21078
21205
  }
21079
21206
  return out;
21080
21207
  }
21081
- 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;
21082
21211
  const out = [];
21083
21212
  for (const t2 of targets) {
21084
21213
  const handler = _registry.get(t2.channelType);
@@ -21093,7 +21222,7 @@ async function dispatchToTargets(text5, targets) {
21093
21222
  continue;
21094
21223
  }
21095
21224
  try {
21096
- 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);
21097
21226
  for (const r of results)
21098
21227
  out.push(r);
21099
21228
  } catch (err) {
@@ -21112,6 +21241,7 @@ var _registry;
21112
21241
  var init_dispatcher = __esm({
21113
21242
  "packages/server/dist/cron/dispatcher.js"() {
21114
21243
  "use strict";
21244
+ init_media();
21115
21245
  _registry = /* @__PURE__ */ new Map();
21116
21246
  }
21117
21247
  });
@@ -21542,7 +21672,7 @@ async function runJob(jobId, triggerKind) {
21542
21672
  }
21543
21673
  let dispatchResults = [];
21544
21674
  if (status === "succeeded" && targets.length > 0) {
21545
- dispatchResults = await dispatchToTargets(trimmedOut, targets);
21675
+ dispatchResults = await dispatchToTargets(trimmedOut, targets, job.workspacePath);
21546
21676
  if (dispatchResults.length > 0 && dispatchResults.every((r) => !r.ok)) {
21547
21677
  status = "failed";
21548
21678
  failureReason = `all ${dispatchResults.length} target(s) failed: ${dispatchResults.map((r) => `${r.channelType}:${r.error ?? "?"}`).join("; ")}`;
@@ -22277,12 +22407,12 @@ var init_accounts3 = __esm({
22277
22407
  });
22278
22408
 
22279
22409
  // packages/server/dist/channels/telegram/event-adapter.js
22280
- var HARD_CHARS, MEDIA_MARKER_RE, TelegramResponder;
22410
+ var HARD_CHARS, TelegramResponder;
22281
22411
  var init_event_adapter = __esm({
22282
22412
  "packages/server/dist/channels/telegram/event-adapter.js"() {
22283
22413
  "use strict";
22414
+ init_media();
22284
22415
  HARD_CHARS = 4e3;
22285
- MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
22286
22416
  TelegramResponder = class {
22287
22417
  buffer = "";
22288
22418
  deps;
@@ -22355,12 +22485,7 @@ var init_event_adapter = __esm({
22355
22485
  return limit;
22356
22486
  }
22357
22487
  async dispatchChunk(chunk) {
22358
- const mediaPaths = [];
22359
- const text5 = chunk.replace(MEDIA_MARKER_RE, (_m, p) => {
22360
- if (p)
22361
- mediaPaths.push(p.trim());
22362
- return "";
22363
- }).replace(/\n{3,}/g, "\n\n").trim();
22488
+ const { text: text5, mediaPaths } = extractMediaMessage(chunk);
22364
22489
  if (text5) {
22365
22490
  try {
22366
22491
  await this.deps.sendText(text5);
@@ -22380,6 +22505,21 @@ var init_event_adapter = __esm({
22380
22505
  }
22381
22506
  });
22382
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
+
22383
22523
  // packages/server/dist/channels/shared/session-prefix.js
22384
22524
  function sessionPrefix(channel, userId) {
22385
22525
  return `${channel}_${userId}_`;
@@ -22392,7 +22532,6 @@ var init_session_prefix = __esm({
22392
22532
 
22393
22533
  // packages/server/dist/channels/telegram/handler.js
22394
22534
  import { Bot, InputFile } from "grammy";
22395
- import path52 from "node:path";
22396
22535
  function inferMediaKind(filePath) {
22397
22536
  const cls = classifyMedia(filePath);
22398
22537
  if (cls === "image")
@@ -22668,23 +22807,16 @@ async function handleUserMessage(args) {
22668
22807
  const sm = registry2.getOrCreate(workspace);
22669
22808
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
22670
22809
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession(sm, userId, activeOverrides2, accessLevel, workspace));
22671
- if (sm.isSessionCompacting(sessionId)) {
22672
- await ctx.reply(t("handler.compacting", lang));
22673
- return;
22674
- }
22675
- if (sm.isSessionRunning(sessionId)) {
22676
- await ctx.reply(t("handler.queued", lang));
22677
- }
22810
+ const hint = busyHint(sm, sessionId, lang);
22811
+ if (hint)
22812
+ await ctx.reply(hint);
22678
22813
  if (!unsubscribers.has(sessionId)) {
22679
22814
  const responder = new TelegramResponder({
22680
22815
  sendText: async (chunk) => {
22681
22816
  await bot.api.sendMessage(chatId, chunk, { parse_mode: void 0 });
22682
22817
  },
22683
22818
  sendMedia: async (filePath) => {
22684
- const resolved = path52.resolve(filePath);
22685
- const ws = path52.resolve(workspace);
22686
- const inWorkspace = resolved === ws || resolved.startsWith(ws + path52.sep);
22687
- if (!inWorkspace && !isInTempDir(resolved)) {
22819
+ if (!isMediaPathAllowed(filePath, workspace)) {
22688
22820
  console.log(`[telegram] sendMedia blocked: ${filePath} not under workspace`);
22689
22821
  return;
22690
22822
  }
@@ -22742,6 +22874,7 @@ var init_handler2 = __esm({
22742
22874
  init_commands2();
22743
22875
  init_goal_mode();
22744
22876
  init_i18n();
22877
+ init_busy_hint();
22745
22878
  init_commands();
22746
22879
  init_media();
22747
22880
  init_session_prefix();
@@ -22750,7 +22883,7 @@ var init_handler2 = __esm({
22750
22883
 
22751
22884
  // packages/server/dist/routes/telegram.js
22752
22885
  import { Hono as Hono13 } from "hono";
22753
- import path53 from "node:path";
22886
+ import path52 from "node:path";
22754
22887
  import { Bot as Bot2 } from "grammy";
22755
22888
  import fs47 from "node:fs";
22756
22889
  function createTelegramRoutes(deps) {
@@ -22780,7 +22913,7 @@ function createTelegramRoutes(deps) {
22780
22913
  return c.json({ error: "botToken required" }, 400);
22781
22914
  if (!body.workspacePath)
22782
22915
  return c.json({ error: "workspacePath required" }, 400);
22783
- if (!path53.isAbsolute(body.workspacePath))
22916
+ if (!path52.isAbsolute(body.workspacePath))
22784
22917
  return c.json({ error: "workspacePath must be absolute" }, 400);
22785
22918
  if (!fs47.existsSync(body.workspacePath))
22786
22919
  return c.json({ error: "workspace path not found" }, 400);
@@ -23269,12 +23402,12 @@ var init_accounts4 = __esm({
23269
23402
  });
23270
23403
 
23271
23404
  // packages/server/dist/channels/wechat/event-adapter.js
23272
- var HARD_CHARS2, MEDIA_MARKER_RE2, WeixinResponder;
23405
+ var HARD_CHARS2, WeixinResponder;
23273
23406
  var init_event_adapter2 = __esm({
23274
23407
  "packages/server/dist/channels/wechat/event-adapter.js"() {
23275
23408
  "use strict";
23409
+ init_media();
23276
23410
  HARD_CHARS2 = 3500;
23277
- MEDIA_MARKER_RE2 = /^MEDIA:\s*(\S.*?)\s*$/gm;
23278
23411
  WeixinResponder = class {
23279
23412
  buffer = "";
23280
23413
  deps;
@@ -23295,13 +23428,13 @@ var init_event_adapter2 = __esm({
23295
23428
  case "error":
23296
23429
  if (event.error) {
23297
23430
  this.flushAll();
23298
- void this.dispatchChunk(`[\u9519\u8BEF] ${event.error}`);
23431
+ void this.dispatchChunk(`\u274C ${event.error}`);
23299
23432
  }
23300
23433
  break;
23301
23434
  case "system":
23302
23435
  if (event.text) {
23303
23436
  this.flushAll();
23304
- void this.dispatchChunk(`[\u7CFB\u7EDF] ${event.text}`);
23437
+ void this.dispatchChunk(`\u2139\uFE0F ${event.text}`);
23305
23438
  }
23306
23439
  break;
23307
23440
  case "complete":
@@ -23355,12 +23488,7 @@ var init_event_adapter2 = __esm({
23355
23488
  * goes out as a WeChat message (if non-empty after trim).
23356
23489
  */
23357
23490
  async dispatchChunk(chunk) {
23358
- const mediaPaths = [];
23359
- const text5 = chunk.replace(MEDIA_MARKER_RE2, (_m, p) => {
23360
- if (p)
23361
- mediaPaths.push(p.trim());
23362
- return "";
23363
- }).replace(/\n{3,}/g, "\n\n").trim();
23491
+ const { text: text5, mediaPaths } = extractMediaMessage(chunk);
23364
23492
  if (text5) {
23365
23493
  try {
23366
23494
  await this.deps.sendText(text5);
@@ -23488,7 +23616,7 @@ var init_cdn = __esm({
23488
23616
 
23489
23617
  // packages/server/dist/channels/wechat/send-media.js
23490
23618
  import fs48 from "node:fs/promises";
23491
- import path54 from "node:path";
23619
+ import path53 from "node:path";
23492
23620
  function inferKind(filePath) {
23493
23621
  const cls = classifyMedia(filePath);
23494
23622
  if (cls === "image")
@@ -23538,7 +23666,7 @@ async function sendMediaFile(params) {
23538
23666
  aesKeyBase64,
23539
23667
  fileSizeCiphertext: prep.filesize,
23540
23668
  fileSize: prep.rawsize,
23541
- fileName: path54.basename(filePath)
23669
+ fileName: path53.basename(filePath)
23542
23670
  });
23543
23671
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
23544
23672
  const body = {
@@ -23702,12 +23830,8 @@ var init_login = __esm({
23702
23830
 
23703
23831
  // packages/server/dist/channels/wechat/handler.js
23704
23832
  import fs49 from "node:fs";
23705
- import path55 from "node:path";
23833
+ import path54 from "node:path";
23706
23834
  import QRCode from "qrcode";
23707
- function isPathAllowed(filePath, workspacePath) {
23708
- const resolved = path55.resolve(filePath);
23709
- return resolved.startsWith(path55.resolve(workspacePath)) || isInTempDir(resolved);
23710
- }
23711
23835
  function startWeixinChannel(deps) {
23712
23836
  const { registry: registry2, db } = deps;
23713
23837
  const runners = /* @__PURE__ */ new Map();
@@ -23991,21 +24115,13 @@ async function handleInbound(args) {
23991
24115
  const sm = registry2.getOrCreate(account.workspacePath);
23992
24116
  const sessionAccessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
23993
24117
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession2(sm, fromUserId, activeOverrides2, sessionAccessLevel, account.workspacePath));
23994
- if (sm.isSessionCompacting(sessionId)) {
24118
+ const hint = busyHint(sm, sessionId, lang);
24119
+ if (hint) {
23995
24120
  await sendToUser({
23996
24121
  account,
23997
24122
  toUserId: fromUserId,
23998
24123
  contextToken: msg.context_token,
23999
- text: t("handler.compacting", lang)
24000
- });
24001
- return;
24002
- }
24003
- if (sm.isSessionRunning(sessionId)) {
24004
- await sendToUser({
24005
- account,
24006
- toUserId: fromUserId,
24007
- contextToken: msg.context_token,
24008
- text: t("handler.queued", lang)
24124
+ text: hint
24009
24125
  });
24010
24126
  }
24011
24127
  if (msg.context_token)
@@ -24019,7 +24135,7 @@ async function handleInbound(args) {
24019
24135
  contextToken: sessionContextTokens.get(sessionId)
24020
24136
  }),
24021
24137
  sendMedia: async (filePath) => {
24022
- if (!isPathAllowed(filePath, account.workspacePath)) {
24138
+ if (!isMediaPathAllowed(filePath, account.workspacePath)) {
24023
24139
  console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
24024
24140
  return;
24025
24141
  }
@@ -24061,7 +24177,7 @@ async function handleSlashCommand(args) {
24061
24177
  contextToken: sessionContextTokens.get(sessionId)
24062
24178
  }),
24063
24179
  sendMedia: async (filePath) => {
24064
- if (!isPathAllowed(filePath, account.workspacePath)) {
24180
+ if (!isMediaPathAllowed(filePath, account.workspacePath)) {
24065
24181
  console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
24066
24182
  return;
24067
24183
  }
@@ -24143,7 +24259,7 @@ async function handleSlashCommand(args) {
24143
24259
  await reply(login.message);
24144
24260
  return true;
24145
24261
  }
24146
- const tmpPath = path55.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
24262
+ const tmpPath = path54.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
24147
24263
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
24148
24264
  await sendMediaFile({
24149
24265
  baseUrl: account.baseUrl,
@@ -24223,6 +24339,7 @@ var init_handler3 = __esm({
24223
24339
  init_commands2();
24224
24340
  init_goal_mode();
24225
24341
  init_i18n();
24342
+ init_busy_hint();
24226
24343
  init_session_prefix();
24227
24344
  MAX_CONSECUTIVE_FAILURES = 3;
24228
24345
  BACKOFF_DELAY_MS = 3e4;
@@ -24232,7 +24349,7 @@ var init_handler3 = __esm({
24232
24349
 
24233
24350
  // packages/server/dist/routes/weixin.js
24234
24351
  import { Hono as Hono14 } from "hono";
24235
- import path56 from "node:path";
24352
+ import path55 from "node:path";
24236
24353
  import fs50 from "node:fs";
24237
24354
  function createWeixinRoutes(deps) {
24238
24355
  const { db, channel } = deps;
@@ -24258,7 +24375,7 @@ function createWeixinRoutes(deps) {
24258
24375
  return c.json({ error: "sessionKey required" }, 400);
24259
24376
  if (!body.workspacePath)
24260
24377
  return c.json({ error: "workspacePath required" }, 400);
24261
- if (!path56.isAbsolute(body.workspacePath))
24378
+ if (!path55.isAbsolute(body.workspacePath))
24262
24379
  return c.json({ error: "workspacePath must be absolute" }, 400);
24263
24380
  const accessLevel = body.accessLevel === "full" ? "full" : body.accessLevel === "workspace" ? "workspace" : "readonly";
24264
24381
  const result = await waitLogin({ sessionKey: body.sessionKey, timeoutMs: body.timeoutMs });
@@ -24303,7 +24420,7 @@ function createWeixinRoutes(deps) {
24303
24420
  if (!existing)
24304
24421
  return c.json({ error: "not found" }, 404);
24305
24422
  const body = await c.req.json().catch(() => ({}));
24306
- if (body.workspacePath !== void 0 && !path56.isAbsolute(body.workspacePath)) {
24423
+ if (body.workspacePath !== void 0 && !path55.isAbsolute(body.workspacePath)) {
24307
24424
  return c.json({ error: "workspacePath must be absolute" }, 400);
24308
24425
  }
24309
24426
  if (body.accessLevel !== void 0 && body.accessLevel !== "full" && body.accessLevel !== "workspace" && body.accessLevel !== "readonly") {
@@ -24361,7 +24478,7 @@ function readLastActiveChatId(accountId) {
24361
24478
  const v = acct.config?.lastActiveChatId;
24362
24479
  return typeof v === "string" && v.length > 0 ? v : null;
24363
24480
  }
24364
- async function dispatch2(accountId, text5, explicitChatId) {
24481
+ async function dispatch2(accountId, text5, explicitChatId, media) {
24365
24482
  const acct = getAccount4(getChannelDb(), accountId);
24366
24483
  if (!acct)
24367
24484
  throw new Error(`wechat account ${accountId} not found`);
@@ -24371,8 +24488,32 @@ async function dispatch2(accountId, text5, explicitChatId) {
24371
24488
  if (!chatId) {
24372
24489
  throw new Error("no wechat target \u2014 bind the account first (the QR-login owner becomes the default cron recipient)");
24373
24490
  }
24374
- await sendToUser({ account: acct, toUserId: chatId, text: text5 });
24375
- 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;
24376
24517
  }
24377
24518
  function listTargets2() {
24378
24519
  const cdb = getChannelDb();
@@ -24393,7 +24534,7 @@ function listTargets2() {
24393
24534
  return out;
24394
24535
  }
24395
24536
  function registerWechatCronDispatcher() {
24396
- registerCronDispatcher({ channelType: "wechat", dispatch: dispatch2, listTargets: listTargets2 });
24537
+ registerCronDispatcher({ channelType: "wechat", supportsMedia: true, dispatch: dispatch2, listTargets: listTargets2 });
24397
24538
  }
24398
24539
  var init_cron_dispatcher2 = __esm({
24399
24540
  "packages/server/dist/channels/wechat/cron-dispatcher.js"() {
@@ -24402,6 +24543,8 @@ var init_cron_dispatcher2 = __esm({
24402
24543
  init_accounts();
24403
24544
  init_accounts4();
24404
24545
  init_handler3();
24546
+ init_send_media();
24547
+ init_media();
24405
24548
  init_dispatcher();
24406
24549
  }
24407
24550
  });
@@ -24436,14 +24579,14 @@ function createWebChannel(deps) {
24436
24579
  return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
24437
24580
  }
24438
24581
  function resolveWorkspace(account, override) {
24439
- let path64 = account.workspacePath;
24582
+ let path63 = account.workspacePath;
24440
24583
  if (override && override !== account.workspacePath) {
24441
24584
  if (account.accessLevel !== "full") {
24442
24585
  return { ok: false, error: "workspace override requires a full-access token" };
24443
24586
  }
24444
- path64 = override;
24587
+ path63 = override;
24445
24588
  }
24446
- const resolved = resolveAccountWorkspace({ ...account, workspacePath: path64 });
24589
+ const resolved = resolveAccountWorkspace({ ...account, workspacePath: path63 });
24447
24590
  if (!resolved)
24448
24591
  return { ok: false, error: "workspace not found" };
24449
24592
  return { ok: true, path: resolved };
@@ -24744,15 +24887,11 @@ function sseData(obj) {
24744
24887
  `;
24745
24888
  }
24746
24889
  function flushText(text5) {
24747
- const files = [];
24748
- const cleaned = text5.replace(MEDIA_MARKER_RE3, (_match, p) => {
24749
- files.push(p);
24750
- return "";
24751
- });
24890
+ const { text: cleaned, mediaPaths } = extractMediaPaths(text5);
24752
24891
  let out = "";
24753
24892
  if (cleaned)
24754
24893
  out += sseData({ type: "stream", text: cleaned });
24755
- for (const filePath of files)
24894
+ for (const filePath of mediaPaths)
24756
24895
  out += sseData({ type: "file", path: filePath });
24757
24896
  return out;
24758
24897
  }
@@ -24810,7 +24949,6 @@ function createMediaBuffer() {
24810
24949
  }
24811
24950
  };
24812
24951
  }
24813
- var MEDIA_MARKER_RE3;
24814
24952
  var init_handler4 = __esm({
24815
24953
  "packages/server/dist/channels/web/handler.js"() {
24816
24954
  "use strict";
@@ -24818,12 +24956,12 @@ var init_handler4 = __esm({
24818
24956
  init_accounts2();
24819
24957
  init_accounts2();
24820
24958
  init_media_store();
24959
+ init_media();
24821
24960
  init_accounts();
24822
24961
  init_commands2();
24823
24962
  init_goal_mode();
24824
24963
  init_i18n();
24825
24964
  init_session_prefix();
24826
- MEDIA_MARKER_RE3 = /^MEDIA:\s*(\S.*?)\s*$/gm;
24827
24965
  }
24828
24966
  });
24829
24967
 
@@ -24831,7 +24969,7 @@ var init_handler4 = __esm({
24831
24969
  import { Hono as Hono15 } from "hono";
24832
24970
  import { streamSSE } from "hono/streaming";
24833
24971
  import crypto4 from "node:crypto";
24834
- import path57 from "node:path";
24972
+ import path56 from "node:path";
24835
24973
  import fs51 from "node:fs";
24836
24974
  function createWebRoutes(deps) {
24837
24975
  const { db, channel } = deps;
@@ -24857,7 +24995,7 @@ function createWebRoutes(deps) {
24857
24995
  const body = await c.req.json().catch(() => ({}));
24858
24996
  if (!body.workspacePath)
24859
24997
  return c.json({ error: "workspacePath required" }, 400);
24860
- if (!path57.isAbsolute(body.workspacePath))
24998
+ if (!path56.isAbsolute(body.workspacePath))
24861
24999
  return c.json({ error: "workspacePath must be absolute" }, 400);
24862
25000
  if (!fs51.existsSync(body.workspacePath))
24863
25001
  return c.json({ error: "workspace path not found" }, 400);
@@ -24995,10 +25133,10 @@ function createWebRoutes(deps) {
24995
25133
  const filePath = c.req.query("path");
24996
25134
  if (!filePath)
24997
25135
  return c.json({ error: "path required" }, 400);
24998
- const path64 = await import("node:path");
24999
- const resolved = path64.default.resolve(account.workspacePath, filePath);
25000
- const root = path64.default.resolve(account.workspacePath);
25001
- 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)) {
25002
25140
  return c.json({ error: "path traversal not allowed" }, 403);
25003
25141
  }
25004
25142
  let real;
@@ -25009,13 +25147,13 @@ function createWebRoutes(deps) {
25009
25147
  } catch {
25010
25148
  return c.json({ error: "file not found" }, 404);
25011
25149
  }
25012
- if (real !== realRoot && !real.startsWith(realRoot + path64.default.sep)) {
25150
+ if (real !== realRoot && !real.startsWith(realRoot + path63.default.sep)) {
25013
25151
  return c.json({ error: "path traversal not allowed" }, 403);
25014
25152
  }
25015
25153
  const stat = fs51.statSync(real);
25016
25154
  if (stat.isDirectory())
25017
25155
  return c.json({ error: "cannot serve directory" }, 400);
25018
- const ext = path64.default.extname(real).toLowerCase();
25156
+ const ext = path63.default.extname(real).toLowerCase();
25019
25157
  const mimeMap = {
25020
25158
  ".png": "image/png",
25021
25159
  ".jpg": "image/jpeg",
@@ -25029,7 +25167,7 @@ function createWebRoutes(deps) {
25029
25167
  ".txt": "text/plain"
25030
25168
  };
25031
25169
  const contentType = mimeMap[ext] || "application/octet-stream";
25032
- const fileName = path64.default.basename(resolved);
25170
+ const fileName = path63.default.basename(resolved);
25033
25171
  const data = fs51.readFileSync(real);
25034
25172
  c.header("content-type", contentType);
25035
25173
  c.header("content-disposition", `inline; filename="${fileName}"`);
@@ -25235,13 +25373,13 @@ var init_markdown = __esm({
25235
25373
  });
25236
25374
 
25237
25375
  // packages/server/dist/channels/slack/event-adapter.js
25238
- var HARD_CHARS3, MEDIA_MARKER_RE4, SlackResponder;
25376
+ var HARD_CHARS3, SlackResponder;
25239
25377
  var init_event_adapter3 = __esm({
25240
25378
  "packages/server/dist/channels/slack/event-adapter.js"() {
25241
25379
  "use strict";
25242
25380
  init_markdown();
25381
+ init_media();
25243
25382
  HARD_CHARS3 = 35e3;
25244
- MEDIA_MARKER_RE4 = /^MEDIA:\s*(\S.*?)\s*$/gm;
25245
25383
  SlackResponder = class {
25246
25384
  buffer = "";
25247
25385
  deps;
@@ -25305,12 +25443,7 @@ var init_event_adapter3 = __esm({
25305
25443
  return limit;
25306
25444
  }
25307
25445
  async dispatchChunk(chunk) {
25308
- const mediaPaths = [];
25309
- const stripped = chunk.replace(MEDIA_MARKER_RE4, (_m, p) => {
25310
- if (p)
25311
- mediaPaths.push(p.trim());
25312
- return "";
25313
- }).replace(/\n{3,}/g, "\n\n").trim();
25446
+ const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
25314
25447
  const text5 = formatForSlack(stripped);
25315
25448
  if (text5) {
25316
25449
  try {
@@ -25361,8 +25494,8 @@ async function authTest(botToken) {
25361
25494
  }
25362
25495
  async function uploadFile(args) {
25363
25496
  const fs56 = await import("node:fs");
25364
- const path64 = await import("node:path");
25365
- const filename = args.filename ?? path64.basename(args.filePath);
25497
+ const path63 = await import("node:path");
25498
+ const filename = args.filename ?? path63.basename(args.filePath);
25366
25499
  const stat = fs56.statSync(args.filePath);
25367
25500
  if (!stat.isFile())
25368
25501
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
@@ -25526,7 +25659,7 @@ var init_api2 = __esm({
25526
25659
  });
25527
25660
 
25528
25661
  // packages/server/dist/channels/slack/handler.js
25529
- import path58 from "node:path";
25662
+ import path57 from "node:path";
25530
25663
  import { WebSocket } from "ws";
25531
25664
  function slashToBang(text5, commandNames) {
25532
25665
  if (commandNames.length === 0)
@@ -25535,11 +25668,6 @@ function slashToBang(text5, commandNames) {
25535
25668
  const re = new RegExp(`(^|[\\s(\\[,\uFF0C\u3001])\\/(${alternation})\\b`, "g");
25536
25669
  return text5.replace(re, "$1!$2");
25537
25670
  }
25538
- function isPathAllowed2(filePath, workspacePath) {
25539
- const resolved = path58.resolve(filePath);
25540
- const wsResolved = path58.resolve(workspacePath);
25541
- return resolved.startsWith(wsResolved + path58.sep) || resolved === wsResolved || isInTempDir(resolved);
25542
- }
25543
25671
  function rememberEventId(id) {
25544
25672
  if (SEEN_EVENT_IDS.has(id))
25545
25673
  return true;
@@ -25797,12 +25925,9 @@ async function handleInbound2(args) {
25797
25925
  const sm = registry2.getOrCreate(workspace);
25798
25926
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
25799
25927
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread({ sm, channelId, rootTs, userId, accessLevel, state, workspacePath: workspace }));
25800
- if (sm.isSessionCompacting(sessionId)) {
25801
- 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" });
25802
- return;
25803
- }
25804
- if (sm.isSessionRunning(sessionId)) {
25805
- 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 });
25806
25931
  }
25807
25932
  if (!state.unsubscribers.has(sessionId)) {
25808
25933
  const responder = new SlackResponder({
@@ -25810,8 +25935,8 @@ async function handleInbound2(args) {
25810
25935
  await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: chunk });
25811
25936
  },
25812
25937
  sendMedia: async (filePath) => {
25813
- const resolved = path58.resolve(filePath);
25814
- if (!isPathAllowed2(resolved, workspace)) {
25938
+ const resolved = path57.resolve(filePath);
25939
+ if (!isMediaPathAllowed(resolved, workspace)) {
25815
25940
  console.log(`[slack] sendMedia blocked: ${filePath} not under workspace`);
25816
25941
  return;
25817
25942
  }
@@ -25828,7 +25953,10 @@ async function handleInbound2(args) {
25828
25953
  botToken: account.botToken,
25829
25954
  channel: channelId,
25830
25955
  threadTs: replyTs,
25831
- 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
+ })
25832
25960
  }).catch(() => {
25833
25961
  });
25834
25962
  }
@@ -25898,6 +26026,7 @@ var init_handler5 = __esm({
25898
26026
  init_session_prefix();
25899
26027
  init_commands();
25900
26028
  init_i18n();
26029
+ init_busy_hint();
25901
26030
  SLACK_EXTRA_COMMANDS = ["qr"];
25902
26031
  SEEN_EVENT_IDS = /* @__PURE__ */ new Set();
25903
26032
  SEEN_EVENT_IDS_MAX = 2e3;
@@ -25907,7 +26036,7 @@ var init_handler5 = __esm({
25907
26036
  // packages/server/dist/routes/slack.js
25908
26037
  import { Hono as Hono16 } from "hono";
25909
26038
  import fs52 from "node:fs";
25910
- import path59 from "node:path";
26039
+ import path58 from "node:path";
25911
26040
  function createSlackRoutes(deps) {
25912
26041
  const { db, channel } = deps;
25913
26042
  const app2 = new Hono16();
@@ -25950,7 +26079,7 @@ function createSlackRoutes(deps) {
25950
26079
  return c.json({ error: "appToken must start with xapp-" }, 400);
25951
26080
  if (!body.workspacePath)
25952
26081
  return c.json({ error: "workspacePath required" }, 400);
25953
- if (!path59.isAbsolute(body.workspacePath))
26082
+ if (!path58.isAbsolute(body.workspacePath))
25954
26083
  return c.json({ error: "workspacePath must be absolute" }, 400);
25955
26084
  if (!fs52.existsSync(body.workspacePath))
25956
26085
  return c.json({ error: "workspace path not found" }, 400);
@@ -26050,7 +26179,7 @@ function parseChatId(raw) {
26050
26179
  return { channel: raw };
26051
26180
  return { channel: raw.slice(0, idx), threadTs: raw.slice(idx + 1) || void 0 };
26052
26181
  }
26053
- async function dispatch3(accountId, text5, explicitChatId) {
26182
+ async function dispatch3(accountId, text5, explicitChatId, media) {
26054
26183
  const acct = getAccount5(getChannelDb(), accountId);
26055
26184
  if (!acct)
26056
26185
  throw new Error(`slack account ${accountId} not found`);
@@ -26060,18 +26189,37 @@ async function dispatch3(accountId, text5, explicitChatId) {
26060
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>.');
26061
26190
  }
26062
26191
  const { channel, threadTs } = parseChatId(explicitChatId);
26063
- try {
26064
- await postMessage({ botToken: acct.botToken, channel, threadTs, text: text5 });
26065
- return [{ channelType: "slack", accountId, chatId: explicitChatId, ok: true }];
26066
- } catch (err) {
26067
- return [{
26068
- channelType: "slack",
26069
- accountId,
26070
- chatId: explicitChatId,
26071
- ok: false,
26072
- error: err instanceof Error ? err.message : String(err)
26073
- }];
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
+ }
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
+ }
26074
26221
  }
26222
+ return out;
26075
26223
  }
26076
26224
  function listTargets3() {
26077
26225
  const cdb = getChannelDb();
@@ -26085,7 +26233,7 @@ function listTargets3() {
26085
26233
  }));
26086
26234
  }
26087
26235
  function registerSlackCronDispatcher() {
26088
- registerCronDispatcher({ channelType: "slack", dispatch: dispatch3, listTargets: listTargets3 });
26236
+ registerCronDispatcher({ channelType: "slack", supportsMedia: true, dispatch: dispatch3, listTargets: listTargets3 });
26089
26237
  }
26090
26238
  var init_cron_dispatcher3 = __esm({
26091
26239
  "packages/server/dist/channels/slack/cron-dispatcher.js"() {
@@ -26093,6 +26241,7 @@ var init_cron_dispatcher3 = __esm({
26093
26241
  init_channel_db();
26094
26242
  init_accounts5();
26095
26243
  init_api2();
26244
+ init_media();
26096
26245
  init_dispatcher();
26097
26246
  }
26098
26247
  });
@@ -26201,13 +26350,13 @@ var init_accounts6 = __esm({
26201
26350
  });
26202
26351
 
26203
26352
  // packages/server/dist/channels/feishu/event-adapter.js
26204
- var HARD_CHARS4, MEDIA_MARKER_RE5, FeishuResponder;
26353
+ var HARD_CHARS4, FeishuResponder;
26205
26354
  var init_event_adapter4 = __esm({
26206
26355
  "packages/server/dist/channels/feishu/event-adapter.js"() {
26207
26356
  "use strict";
26208
26357
  init_markdown();
26358
+ init_media();
26209
26359
  HARD_CHARS4 = 4500;
26210
- MEDIA_MARKER_RE5 = /^MEDIA:\s*(\S.*?)\s*$/gm;
26211
26360
  FeishuResponder = class {
26212
26361
  buffer = "";
26213
26362
  deps;
@@ -26271,12 +26420,7 @@ var init_event_adapter4 = __esm({
26271
26420
  return limit;
26272
26421
  }
26273
26422
  async dispatchChunk(chunk) {
26274
- const mediaPaths = [];
26275
- const stripped = chunk.replace(MEDIA_MARKER_RE5, (_m, p) => {
26276
- if (p)
26277
- mediaPaths.push(p.trim());
26278
- return "";
26279
- }).replace(/\n{3,}/g, "\n\n").trim();
26423
+ const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
26280
26424
  const text5 = formatForFeishu(stripped);
26281
26425
  if (text5) {
26282
26426
  try {
@@ -26427,9 +26571,9 @@ async function replyMessage(args) {
26427
26571
  }
26428
26572
  async function uploadImage(args) {
26429
26573
  const fs56 = await import("node:fs");
26430
- const path64 = await import("node:path");
26574
+ const path63 = await import("node:path");
26431
26575
  const buf = fs56.readFileSync(args.filePath);
26432
- const filename = path64.basename(args.filePath);
26576
+ const filename = path63.basename(args.filePath);
26433
26577
  const send = async (token2) => {
26434
26578
  const form = new FormData();
26435
26579
  form.append("image_type", "message");
@@ -26461,9 +26605,9 @@ async function uploadImage(args) {
26461
26605
  }
26462
26606
  async function uploadFile2(args) {
26463
26607
  const fs56 = await import("node:fs");
26464
- const path64 = await import("node:path");
26608
+ const path63 = await import("node:path");
26465
26609
  const buf = fs56.readFileSync(args.filePath);
26466
- const filename = args.filename ?? path64.basename(args.filePath);
26610
+ const filename = args.filename ?? path63.basename(args.filePath);
26467
26611
  const send = async (token2) => {
26468
26612
  const form = new FormData();
26469
26613
  form.append("file_type", args.fileType);
@@ -26569,13 +26713,8 @@ var init_api3 = __esm({
26569
26713
  });
26570
26714
 
26571
26715
  // packages/server/dist/channels/feishu/handler.js
26572
- import path60 from "node:path";
26716
+ import path59 from "node:path";
26573
26717
  import * as Lark from "@larksuiteoapi/node-sdk";
26574
- function isPathAllowed3(filePath, workspacePath) {
26575
- const resolved = path60.resolve(filePath);
26576
- const wsResolved = path60.resolve(workspacePath);
26577
- return resolved.startsWith(wsResolved + path60.sep) || resolved === wsResolved || isInTempDir(resolved);
26578
- }
26579
26718
  function newAccountState2() {
26580
26719
  return {
26581
26720
  wsClient: null,
@@ -26803,12 +26942,9 @@ async function handleInbound3(args) {
26803
26942
  const sm = registry2.getOrCreate(workspace);
26804
26943
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
26805
26944
  const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread2({ sm, chatId, rootId, userId, accessLevel, state, workspacePath: workspace }));
26806
- if (sm.isSessionCompacting(sessionId)) {
26807
- 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" });
26808
- return;
26809
- }
26810
- if (sm.isSessionRunning(sessionId)) {
26811
- 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 });
26812
26948
  }
26813
26949
  if (!state.unsubscribers.has(sessionId)) {
26814
26950
  const responder = new FeishuResponder({
@@ -26816,8 +26952,8 @@ async function handleInbound3(args) {
26816
26952
  await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: chunk });
26817
26953
  },
26818
26954
  sendMedia: async (filePath) => {
26819
- const resolved = path60.resolve(filePath);
26820
- if (!isPathAllowed3(resolved, workspace)) {
26955
+ const resolved = path59.resolve(filePath);
26956
+ if (!isMediaPathAllowed(resolved, workspace)) {
26821
26957
  console.log(`[feishu] sendMedia blocked: ${filePath} not under workspace`);
26822
26958
  return;
26823
26959
  }
@@ -26836,7 +26972,10 @@ async function handleInbound3(args) {
26836
26972
  inboundMessageId,
26837
26973
  isP2P,
26838
26974
  chatId,
26839
- 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
+ })
26840
26979
  }).catch(() => {
26841
26980
  });
26842
26981
  }
@@ -26866,7 +27005,7 @@ async function replyToInbound(args) {
26866
27005
  async function sendFeishuMedia(args) {
26867
27006
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
26868
27007
  const cls = classifyMedia(filePath);
26869
- const ext = path60.extname(filePath).toLowerCase();
27008
+ const ext = path59.extname(filePath).toLowerCase();
26870
27009
  if (cls === "image") {
26871
27010
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
26872
27011
  await replyToInboundRaw({
@@ -27001,13 +27140,14 @@ var init_handler6 = __esm({
27001
27140
  init_goal_mode();
27002
27141
  init_session_prefix();
27003
27142
  init_i18n();
27143
+ init_busy_hint();
27004
27144
  }
27005
27145
  });
27006
27146
 
27007
27147
  // packages/server/dist/routes/feishu.js
27008
27148
  import { Hono as Hono17 } from "hono";
27009
27149
  import fs53 from "node:fs";
27010
- import path61 from "node:path";
27150
+ import path60 from "node:path";
27011
27151
  function createFeishuRoutes(deps) {
27012
27152
  const { db, channel } = deps;
27013
27153
  const app2 = new Hono17();
@@ -27049,7 +27189,7 @@ function createFeishuRoutes(deps) {
27049
27189
  return c.json({ error: "appSecret required" }, 400);
27050
27190
  if (!body.workspacePath)
27051
27191
  return c.json({ error: "workspacePath required" }, 400);
27052
- if (!path61.isAbsolute(body.workspacePath))
27192
+ if (!path60.isAbsolute(body.workspacePath))
27053
27193
  return c.json({ error: "workspacePath must be absolute" }, 400);
27054
27194
  if (!fs53.existsSync(body.workspacePath))
27055
27195
  return c.json({ error: "workspace path not found" }, 400);
@@ -27383,18 +27523,46 @@ function createAuthRoutes() {
27383
27523
  setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
27384
27524
  return c.json({ ok: true });
27385
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
+ });
27386
27545
  return app2;
27387
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
+ }
27388
27556
  function authMiddleware() {
27389
27557
  return async (c, next) => {
27390
- const path64 = c.req.path;
27391
- 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 + "?"))) {
27392
27560
  return next();
27393
27561
  }
27394
27562
  const token = getCookie(c, COOKIE_NAME);
27395
27563
  const payload = validateToken(token);
27396
27564
  if (!payload) {
27397
- if (!path64.startsWith("/api/")) {
27565
+ if (!path63.startsWith("/api/")) {
27398
27566
  return next();
27399
27567
  }
27400
27568
  return c.json({ error: "Unauthorized" }, 401);
@@ -27412,6 +27580,7 @@ var init_auth = __esm({
27412
27580
  "use strict";
27413
27581
  init_config();
27414
27582
  init_password_hash();
27583
+ init_setup_config();
27415
27584
  init_brute_force();
27416
27585
  TOKEN_MAX_AGE = config.auth.tokenMaxAge;
27417
27586
  REFRESH_AFTER = config.auth.refreshAfter;
@@ -27431,7 +27600,7 @@ import { cors } from "hono/cors";
27431
27600
  import { serve } from "@hono/node-server";
27432
27601
  import { serveStatic } from "@hono/node-server/serve-static";
27433
27602
  import { WebSocketServer } from "ws";
27434
- import path62 from "node:path";
27603
+ import path61 from "node:path";
27435
27604
  import fs54 from "node:fs";
27436
27605
  import { spawnSync as spawnSync4 } from "node:child_process";
27437
27606
  import { homedir as homedir24 } from "node:os";
@@ -27439,9 +27608,9 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
27439
27608
  function findProjectRoot() {
27440
27609
  let dir = process.cwd();
27441
27610
  for (let i = 0; i < 32; i++) {
27442
- if (fs54.existsSync(path62.join(dir, "pnpm-workspace.yaml")))
27611
+ if (fs54.existsSync(path61.join(dir, "pnpm-workspace.yaml")))
27443
27612
  return dir;
27444
- const parent = path62.dirname(dir);
27613
+ const parent = path61.dirname(dir);
27445
27614
  if (parent === dir)
27446
27615
  break;
27447
27616
  dir = parent;
@@ -27450,7 +27619,7 @@ function findProjectRoot() {
27450
27619
  return process.cwd();
27451
27620
  }
27452
27621
  function acquireSingleInstanceLock(lockFile) {
27453
- fs54.mkdirSync(path62.dirname(lockFile), { recursive: true });
27622
+ fs54.mkdirSync(path61.dirname(lockFile), { recursive: true });
27454
27623
  const fd = fs54.openSync(lockFile, fs54.constants.O_RDWR | fs54.constants.O_CREAT, 420);
27455
27624
  const result = spawnSync4("flock", ["-n", "3"], {
27456
27625
  stdio: ["ignore", "ignore", "ignore", fd]
@@ -27507,7 +27676,7 @@ function isProcessAlive(pid) {
27507
27676
  }
27508
27677
  }
27509
27678
  function readGitSha() {
27510
- 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")))
27511
27680
  return null;
27512
27681
  try {
27513
27682
  const rev = spawnSync4("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
@@ -27524,14 +27693,14 @@ function readGitSha() {
27524
27693
  }
27525
27694
  function resolveFrontendDir() {
27526
27695
  if (process.env.HALO_FRONTEND_DIR)
27527
- return path62.resolve(process.env.HALO_FRONTEND_DIR);
27528
- const bundled = path62.resolve(__dirname3, "..", "admin-out");
27529
- 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")))
27530
27699
  return bundled;
27531
- const sibling = path62.resolve(__dirname3, "..", "..", "admin", "out");
27532
- 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")))
27533
27702
  return sibling;
27534
- return path62.resolve(PROJECT_ROOT, "packages", "admin", "out");
27703
+ return path61.resolve(PROJECT_ROOT, "packages", "admin", "out");
27535
27704
  }
27536
27705
  async function gracefulShutdown(signal) {
27537
27706
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
@@ -27584,13 +27753,13 @@ var init_dist2 = __esm({
27584
27753
  init_sandbox();
27585
27754
  init_init();
27586
27755
  init_git_ssh();
27587
- __dirname3 = path62.dirname(fileURLToPath3(import.meta.url));
27756
+ __dirname3 = path61.dirname(fileURLToPath3(import.meta.url));
27588
27757
  PORT = config.server.port;
27589
- HALO_VERSION = "1.0.2";
27758
+ HALO_VERSION = "1.0.3";
27590
27759
  PROJECT_ROOT = findProjectRoot();
27591
- HALO_HOME4 = path62.join(homedir24(), ".halo");
27760
+ HALO_HOME4 = path61.join(homedir24(), ".halo");
27592
27761
  GIT_SHA = readGitSha();
27593
- if (!fs54.existsSync(path62.join(HALO_HOME4, "global", ".template-version"))) {
27762
+ if (!fs54.existsSync(path61.join(HALO_HOME4, "global", ".template-version"))) {
27594
27763
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
27595
27764
  process.exit(1);
27596
27765
  }
@@ -27611,7 +27780,7 @@ var init_dist2 = __esm({
27611
27780
  process.exit(1);
27612
27781
  }
27613
27782
  if (!AGENTCORE)
27614
- acquireSingleInstanceLock(path62.join(HALO_HOME4, "global", "server.lock"));
27783
+ acquireSingleInstanceLock(path61.join(HALO_HOME4, "global", "server.lock"));
27615
27784
  {
27616
27785
  const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
27617
27786
  const dispatched = new Set(DISPATCH_COMMANDS);
@@ -27677,10 +27846,10 @@ var init_dist2 = __esm({
27677
27846
  app.route("/api", evolutionRoutes);
27678
27847
  cronRoutes = createCronRoutes();
27679
27848
  app.route("/api", cronRoutes);
27680
- channelDb = createChannelDb(path62.join(HALO_HOME4, "secrets"));
27849
+ channelDb = createChannelDb(path61.join(HALO_HOME4, "secrets"));
27681
27850
  setChannelDb(channelDb);
27682
- setEvoDb(createEvoDb(path62.join(HALO_HOME4, "global")));
27683
- setCronDb(createCronDb(path62.join(HALO_HOME4, "global")));
27851
+ setEvoDb(createEvoDb(path61.join(HALO_HOME4, "global")));
27852
+ setCronDb(createCronDb(path61.join(HALO_HOME4, "global")));
27684
27853
  setEvoSpawner(realEvoSpawner);
27685
27854
  if (!AGENTCORE) {
27686
27855
  startEvoTicker();
@@ -27704,9 +27873,9 @@ var init_dist2 = __esm({
27704
27873
  console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
27705
27874
  }
27706
27875
  FRONTEND_DIR = resolveFrontendDir();
27707
- app.use("/*", serveStatic({ root: path62.relative(process.cwd(), FRONTEND_DIR) }));
27876
+ app.use("/*", serveStatic({ root: path61.relative(process.cwd(), FRONTEND_DIR) }));
27708
27877
  app.get("/*", (c) => {
27709
- const indexPath = path62.join(FRONTEND_DIR, "index.html");
27878
+ const indexPath = path61.join(FRONTEND_DIR, "index.html");
27710
27879
  try {
27711
27880
  const html = fs54.readFileSync(indexPath, "utf-8");
27712
27881
  return c.html(html);
@@ -28477,11 +28646,11 @@ process.emitWarning = (warning, ...args) => {
28477
28646
  };
28478
28647
 
28479
28648
  // packages/cli/src/index.ts
28480
- import path63 from "node:path";
28649
+ import path62 from "node:path";
28481
28650
  import fs55 from "node:fs";
28482
28651
  import { homedir as homedir25 } from "node:os";
28483
28652
  import { parseArgs as parseArgs2 } from "node:util";
28484
- var VERSION = "1.0.2";
28653
+ var VERSION = "1.0.3";
28485
28654
  var HELP_TOP = `Usage: halo <command> [options]
28486
28655
 
28487
28656
  Commands:
@@ -28734,8 +28903,8 @@ async function cmdSetup(argv = []) {
28734
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));
28735
28904
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
28736
28905
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
28737
- const haloHome2 = path63.join(homedir25(), ".halo");
28738
- const globalDir2 = path63.join(haloHome2, "global");
28906
+ const haloHome2 = path62.join(homedir25(), ".halo");
28907
+ const globalDir2 = path62.join(haloHome2, "global");
28739
28908
  ensureHaloHome2(haloHome2);
28740
28909
  process.stderr.write(`[setup] ${haloHome2}/ ready
28741
28910
  `);
@@ -29079,7 +29248,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
29079
29248
  async function cmdAgents(flags) {
29080
29249
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
29081
29250
  await initRuntime2();
29082
- const workspace = path63.resolve(flags.workspace ?? process.cwd());
29251
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
29083
29252
  const agents = await listAgents2(workspace);
29084
29253
  if (agents.length === 0) {
29085
29254
  process.stderr.write("No agents found.\n");
@@ -29095,7 +29264,7 @@ async function cmdAgents(flags) {
29095
29264
  async function cmdSessions(flags) {
29096
29265
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
29097
29266
  await initRuntime2();
29098
- const workspace = path63.resolve(flags.workspace ?? process.cwd());
29267
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
29099
29268
  const sessions2 = listSessions2(workspace);
29100
29269
  if (sessions2.length === 0) {
29101
29270
  process.stderr.write("No sessions found.\n");
@@ -29197,8 +29366,8 @@ ${stdinContent}` : stdinContent;
29197
29366
  harness.destroy();
29198
29367
  }
29199
29368
  }
29200
- var SERVER_LOCK_PATH = path63.join(homedir25(), ".halo", "global", "server.lock");
29201
- 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");
29202
29371
  function readServerPid() {
29203
29372
  try {
29204
29373
  const txt = fs55.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
@@ -29283,7 +29452,7 @@ async function cmdServerStart(argv, _unused) {
29283
29452
  return;
29284
29453
  }
29285
29454
  const { spawn: spawn4 } = await import("node:child_process");
29286
- fs55.mkdirSync(path63.dirname(SERVER_LOG_PATH), { recursive: true });
29455
+ fs55.mkdirSync(path62.dirname(SERVER_LOG_PATH), { recursive: true });
29287
29456
  const out = fs55.openSync(SERVER_LOG_PATH, "a");
29288
29457
  const child = spawn4(process.argv[0], [process.argv[1], "server", "start"], {
29289
29458
  detached: true,
@@ -29402,7 +29571,7 @@ function exitMissingSetup() {
29402
29571
  process.exit(1);
29403
29572
  }
29404
29573
  function isHaloHomeReady() {
29405
- return fs55.existsSync(path63.join(homedir25(), ".halo", "global", ".template-version"));
29574
+ return fs55.existsSync(path62.join(homedir25(), ".halo", "global", ".template-version"));
29406
29575
  }
29407
29576
  async function main2() {
29408
29577
  const argv = process.argv.slice(2);