@turmind/halo 1.6.4 → 1.6.6

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 (35) hide show
  1. package/README.md +18 -9
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/87-6a2716c98a819c4e.js +1 -0
  4. package/admin-out/_next/static/chunks/989-066a874961bd519f.js +1 -0
  5. package/admin-out/_next/static/chunks/app/page-812bcbb46db628b6.js +1 -0
  6. package/admin-out/_next/static/css/e890aa0d13e94290.css +1 -0
  7. package/admin-out/index.html +1 -1
  8. package/admin-out/index.txt +5 -5
  9. package/bundled-docs/design/a2a.md +1 -1
  10. package/bundled-docs/design/canvas-extensions.md +38 -7
  11. package/bundled-docs/design/cron.md +13 -5
  12. package/bundled-docs/design/feishu.md +2 -0
  13. package/bundled-docs/design/halo-city.md +18 -149
  14. package/bundled-docs/design/slack.md +2 -2
  15. package/bundled-docs/design/storage.md +6 -2
  16. package/bundled-docs/design/stt-local.md +106 -0
  17. package/bundled-docs/design/web.md +1 -1
  18. package/bundled-docs/design/wecom.md +1 -1
  19. package/bundled-docs/dev/add-model-provider.md +1 -1
  20. package/bundled-docs/dev/api.md +13 -10
  21. package/bundled-docs/dev/desktop-packaging.md +37 -9
  22. package/bundled-docs/dev/env.md +1 -1
  23. package/bundled-docs/requirements/chat.md +8 -0
  24. package/bundled-docs/requirements/workspace.md +4 -0
  25. package/dist/index.js +1509 -604
  26. package/package.json +2 -1
  27. package/templates/models/mimo-token-plan-china.yaml +3 -55
  28. package/templates/skills/cron/SKILL.md +2 -1
  29. package/templates/skills/cron/manage-cron.py +2 -1
  30. package/admin-out/_next/static/chunks/147-d4200c70445aabb6.js +0 -1
  31. package/admin-out/_next/static/chunks/989-5d7743ff56d00dd7.js +0 -1
  32. package/admin-out/_next/static/chunks/app/page-8881b4f87a3d5ae8.js +0 -1
  33. package/admin-out/_next/static/css/3b4dcd0ac6b8eae8.css +0 -1
  34. /package/admin-out/_next/static/{kfp1cMc36J6hjZ06L9mm- → Yc05ORrVk5IE9P1Gd8Ojh}/_buildManifest.js +0 -0
  35. /package/admin-out/_next/static/{kfp1cMc36J6hjZ06L9mm- → Yc05ORrVk5IE9P1Gd8Ojh}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1381,6 +1381,9 @@ function globalHubModelsDir() {
1381
1381
  function globalExtensionsDir() {
1382
1382
  return path6.join(globalDir(), "extensions");
1383
1383
  }
1384
+ function globalSttModelsDir() {
1385
+ return path6.join(globalDir(), "stt-models");
1386
+ }
1384
1387
  function globalLogsDir() {
1385
1388
  return path6.join(globalDir(), "logs");
1386
1389
  }
@@ -1566,7 +1569,79 @@ function parseManifest(raw, dirName, entryExists) {
1566
1569
  return { error: "version must be semver (x.y.z[-pre])" };
1567
1570
  if (m.description !== void 0 && (typeof m.description !== "string" || m.description.length > 200))
1568
1571
  return { error: "description must be a string (\u2264 200 chars)" };
1569
- if (!Array.isArray(m.extensions) || m.extensions.length === 0)
1572
+ let activity;
1573
+ if (m.activity !== void 0) {
1574
+ if (typeof m.activity !== "object" || m.activity === null || Array.isArray(m.activity))
1575
+ return { error: "activity must be an object" };
1576
+ const a = m.activity;
1577
+ activity = { order: 100 };
1578
+ if (a.icon !== void 0) {
1579
+ if (typeof a.icon !== "string" || a.icon === "" || a.icon.startsWith("/") || a.icon.split(/[\\/]/).includes("..") || !/\.(svg|png)$/i.test(a.icon)) {
1580
+ return { error: "activity.icon must be a relative .svg or .png path inside the extension directory" };
1581
+ }
1582
+ if (!entryExists(a.icon))
1583
+ return { error: `activity icon not found: ${a.icon}` };
1584
+ activity.icon = a.icon;
1585
+ }
1586
+ for (const k of ["title", "title_zh"]) {
1587
+ const v = a[k];
1588
+ if (v === void 0)
1589
+ continue;
1590
+ if (typeof v !== "string" || v.trim() === "" || v.length > 64)
1591
+ return { error: `activity.${k} must be a non-empty string (\u2264 64 chars)` };
1592
+ activity[k] = v;
1593
+ }
1594
+ for (const k of ["description", "description_zh"]) {
1595
+ const v = a[k];
1596
+ if (v === void 0)
1597
+ continue;
1598
+ if (typeof v !== "string" || v.length > 200)
1599
+ return { error: `activity.${k} must be a string (\u2264 200 chars)` };
1600
+ activity[k] = v;
1601
+ }
1602
+ if (a.order !== void 0) {
1603
+ if (typeof a.order !== "number" || !Number.isInteger(a.order) || a.order < 0 || a.order > 1e3)
1604
+ return { error: "activity.order must be an integer 0\u20131000" };
1605
+ activity.order = a.order;
1606
+ }
1607
+ }
1608
+ let chatbar;
1609
+ if (m.chatbar !== void 0) {
1610
+ if (typeof m.chatbar !== "object" || m.chatbar === null || Array.isArray(m.chatbar))
1611
+ return { error: "chatbar must be an object" };
1612
+ const c = m.chatbar;
1613
+ chatbar = { height: 160, order: 100 };
1614
+ if (c.icon !== void 0) {
1615
+ if (typeof c.icon !== "string" || c.icon === "" || c.icon.startsWith("/") || c.icon.split(/[\\/]/).includes("..") || !/\.(svg|png)$/i.test(c.icon)) {
1616
+ return { error: "chatbar.icon must be a relative .svg or .png path inside the extension directory" };
1617
+ }
1618
+ if (!entryExists(c.icon))
1619
+ return { error: `chatbar icon not found: ${c.icon}` };
1620
+ chatbar.icon = c.icon;
1621
+ }
1622
+ for (const k of ["title", "title_zh"]) {
1623
+ const v = c[k];
1624
+ if (v === void 0)
1625
+ continue;
1626
+ if (typeof v !== "string" || v.trim() === "" || v.length > 64)
1627
+ return { error: `chatbar.${k} must be a non-empty string (\u2264 64 chars)` };
1628
+ chatbar[k] = v;
1629
+ }
1630
+ if (c.height !== void 0) {
1631
+ if (typeof c.height !== "number" || !Number.isInteger(c.height) || c.height < 80 || c.height > 480)
1632
+ return { error: "chatbar.height must be an integer 80\u2013480" };
1633
+ chatbar.height = c.height;
1634
+ }
1635
+ if (c.order !== void 0) {
1636
+ if (typeof c.order !== "number" || !Number.isInteger(c.order) || c.order < 0 || c.order > 1e3)
1637
+ return { error: "chatbar.order must be an integer 0\u20131000" };
1638
+ chatbar.order = c.order;
1639
+ }
1640
+ }
1641
+ if (activity || chatbar) {
1642
+ if (!Array.isArray(m.extensions))
1643
+ return { error: "extensions must be an array (empty is fine with activity or chatbar)" };
1644
+ } else if (!Array.isArray(m.extensions) || m.extensions.length === 0)
1570
1645
  return { error: "extensions must be a non-empty array" };
1571
1646
  for (const e of m.extensions) {
1572
1647
  if (typeof e !== "string" || !FILE_EXT_RE.test(e))
@@ -1635,6 +1710,8 @@ function parseManifest(raw, dirName, entryExists) {
1635
1710
  bundle,
1636
1711
  ...platforms ? { platforms } : {},
1637
1712
  ...settings ? { settings } : {},
1713
+ ...activity ? { activity } : {},
1714
+ ...chatbar ? { chatbar } : {},
1638
1715
  ...m.homepage !== void 0 ? { homepage: m.homepage } : {},
1639
1716
  ...m.license !== void 0 ? { license: m.license } : {}
1640
1717
  };
@@ -1711,7 +1788,7 @@ var init_registry2 = __esm({
1711
1788
  ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1712
1789
  VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
1713
1790
  FILE_EXT_RE = /^\.[a-z0-9]+$/;
1714
- CAPABILITIES = /* @__PURE__ */ new Set(["save", "media", "transcribe", "fs-read"]);
1791
+ CAPABILITIES = /* @__PURE__ */ new Set(["save", "media", "transcribe", "fs-read", "stt-local"]);
1715
1792
  PLATFORMS = /* @__PURE__ */ new Set(["web", "desktop-mac", "desktop-win", "desktop-linux"]);
1716
1793
  SETTING_KEY_RE = /^[a-z][a-z0-9_]{0,63}$/;
1717
1794
  SETTING_TYPES = /* @__PURE__ */ new Set(["string", "int", "float", "boolean", "enum"]);
@@ -4072,7 +4149,7 @@ var init_init = __esm({
4072
4149
  path14.resolve(__dirname, "..", "bundled-docs"),
4073
4150
  path14.resolve(__dirname, "..", "..", "..", ".halo", "docs")
4074
4151
  ];
4075
- TEMPLATE_VERSION = 101;
4152
+ TEMPLATE_VERSION = 103;
4076
4153
  VERSION_FILE = ".template-version";
4077
4154
  SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
4078
4155
  BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
@@ -7849,27 +7926,27 @@ var init_workspace = __esm({
7849
7926
  constructor(projectRoot) {
7850
7927
  this.projectRoot = projectRoot;
7851
7928
  }
7852
- async readFile(path89) {
7853
- const fullPath = this.validatePath(path89);
7929
+ async readFile(path90) {
7930
+ const fullPath = this.validatePath(path90);
7854
7931
  return readFile(fullPath, "utf-8");
7855
7932
  }
7856
- async writeFile(path89, content) {
7857
- const fullPath = this.validatePath(path89);
7933
+ async writeFile(path90, content) {
7934
+ const fullPath = this.validatePath(path90);
7858
7935
  const dir = resolve(fullPath, "..");
7859
7936
  await mkdir(dir, { recursive: true });
7860
7937
  await writeFile(fullPath, content, "utf-8");
7861
7938
  }
7862
- validatePath(path89) {
7863
- const fullPath = isAbsolute(path89) ? resolve(path89) : resolve(this.projectRoot, path89);
7939
+ validatePath(path90) {
7940
+ const fullPath = isAbsolute(path90) ? resolve(path90) : resolve(this.projectRoot, path90);
7864
7941
  const root = resolve(this.projectRoot);
7865
7942
  if (fullPath !== root && !fullPath.startsWith(root + sep)) {
7866
- throw new WorkspaceError(`Path "${path89}" is outside the workspace directory "${this.projectRoot}"`);
7943
+ throw new WorkspaceError(`Path "${path90}" is outside the workspace directory "${this.projectRoot}"`);
7867
7944
  }
7868
7945
  try {
7869
7946
  const real = realpathSync2(fullPath);
7870
7947
  const realRoot = realpathSync2(root);
7871
7948
  if (real !== realRoot && !real.startsWith(realRoot + sep)) {
7872
- throw new WorkspaceError(`Path "${path89}" is outside the workspace directory "${this.projectRoot}"`);
7949
+ throw new WorkspaceError(`Path "${path90}" is outside the workspace directory "${this.projectRoot}"`);
7873
7950
  }
7874
7951
  } catch (err) {
7875
7952
  if (err instanceof WorkspaceError)
@@ -8056,12 +8133,12 @@ var init_git_manager = __esm({
8056
8133
  const code = parts[0];
8057
8134
  const status = code[0];
8058
8135
  const isRenameOrCopy = status === "R" || status === "C";
8059
- const path89 = isRenameOrCopy ? parts[2] : parts[1];
8060
- if (!path89 || seen.has(path89))
8136
+ const path90 = isRenameOrCopy ? parts[2] : parts[1];
8137
+ if (!path90 || seen.has(path90))
8061
8138
  continue;
8062
- seen.add(path89);
8139
+ seen.add(path90);
8063
8140
  files.push({
8064
- path: path89,
8141
+ path: path90,
8065
8142
  status,
8066
8143
  from: isRenameOrCopy ? parts[1] : void 0
8067
8144
  });
@@ -8209,9 +8286,9 @@ var init_git_manager = __esm({
8209
8286
  * empty for an added file or the root commit) and `modified` is the version
8210
8287
  * at `commit` (empty for a deletion). The working-tree/staged path above is
8211
8288
  * left entirely untouched (commit === undefined preserves existing behavior). */
8212
- async getFileDiff(path89, staged, from, commit) {
8289
+ async getFileDiff(path90, staged, from, commit) {
8213
8290
  if (commit) {
8214
- const parentPath = from || path89;
8291
+ const parentPath = from || path90;
8215
8292
  let original2 = "";
8216
8293
  try {
8217
8294
  original2 = await this.git.show([`${commit}^:${parentPath}`]);
@@ -8220,13 +8297,13 @@ var init_git_manager = __esm({
8220
8297
  }
8221
8298
  let modified2 = "";
8222
8299
  try {
8223
- modified2 = await this.git.show([`${commit}:${path89}`]);
8300
+ modified2 = await this.git.show([`${commit}:${path90}`]);
8224
8301
  } catch {
8225
8302
  modified2 = "";
8226
8303
  }
8227
8304
  return { original: original2, modified: modified2 };
8228
8305
  }
8229
- const headPath = from || path89;
8306
+ const headPath = from || path90;
8230
8307
  let original = "";
8231
8308
  try {
8232
8309
  original = await this.git.show([`HEAD:${headPath}`]);
@@ -8236,13 +8313,13 @@ var init_git_manager = __esm({
8236
8313
  let modified = "";
8237
8314
  if (staged) {
8238
8315
  try {
8239
- modified = await this.git.show([`:${path89}`]);
8316
+ modified = await this.git.show([`:${path90}`]);
8240
8317
  } catch {
8241
8318
  modified = "";
8242
8319
  }
8243
8320
  } else {
8244
8321
  try {
8245
- modified = await this.workspace.readFile(path89);
8322
+ modified = await this.workspace.readFile(path90);
8246
8323
  } catch {
8247
8324
  modified = "";
8248
8325
  }
@@ -14226,7 +14303,7 @@ async function initObservability(haloProcess = "server") {
14226
14303
  return true;
14227
14304
  }
14228
14305
  const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
14229
- providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.4", haloProcess);
14306
+ providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.6", haloProcess);
14230
14307
  console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
14231
14308
  return true;
14232
14309
  }
@@ -15747,8 +15824,8 @@ ${q.text}` : q.text).join("\n\n");
15747
15824
  try {
15748
15825
  await this.runAgentTurn(session, input);
15749
15826
  } catch (err) {
15750
- const errMsg3 = err instanceof Error ? err.message : String(err);
15751
- console.error(`[SessionManager] Drain run error for ${session.id}: ${errMsg3}`);
15827
+ const errMsg4 = err instanceof Error ? err.message : String(err);
15828
+ console.error(`[SessionManager] Drain run error for ${session.id}: ${errMsg4}`);
15752
15829
  throw err;
15753
15830
  }
15754
15831
  const nextTurnFollows = session.selfKick && !session.interruptRequested || session.messageQueue.length > 0;
@@ -16044,8 +16121,8 @@ ${q.text}` : q.text);
16044
16121
  }
16045
16122
  console.debug(`[SessionManager] querySession: ${targetSessionId} is IDLE \u2014 draining now`);
16046
16123
  this.runSession(targetSessionId, "").catch((err) => {
16047
- const errMsg3 = err instanceof Error ? err.message : String(err);
16048
- console.error(`[SessionManager] Query run error for ${targetSessionId}: ${errMsg3}`);
16124
+ const errMsg4 = err instanceof Error ? err.message : String(err);
16125
+ console.error(`[SessionManager] Query run error for ${targetSessionId}: ${errMsg4}`);
16049
16126
  });
16050
16127
  return JSON.stringify({ code: 0, message: `Message sent to session ${targetSessionId}.` });
16051
16128
  }
@@ -18950,8 +19027,8 @@ function applyPathPick(value, ref, pick) {
18950
19027
  const before = value.slice(0, markerStart);
18951
19028
  const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
18952
19029
  const needsQuote = pick.insertPath.includes(" ");
18953
- const path89 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
18954
- return `${before}@${kind} ${path89}`;
19030
+ const path90 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
19031
+ return `${before}@${kind} ${path90}`;
18955
19032
  }
18956
19033
  var init_path_suggest_engine = __esm({
18957
19034
  "src/tui/path-suggest-engine.ts"() {
@@ -23737,8 +23814,8 @@ function detectOrphans(tree, declared) {
23737
23814
  if (!block || typeof block !== "object")
23738
23815
  continue;
23739
23816
  for (const k of Object.keys(block)) {
23740
- const path89 = `${namespaceId}.${kindWord}.${k}`;
23741
- if (!declaredMap.has(path89)) {
23817
+ const path90 = `${namespaceId}.${kindWord}.${k}`;
23818
+ if (!declaredMap.has(path90)) {
23742
23819
  out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
23743
23820
  }
23744
23821
  }
@@ -24772,11 +24849,14 @@ var init_brute_force = __esm({
24772
24849
  });
24773
24850
 
24774
24851
  // ../server/dist/middleware/web-token.js
24852
+ function requestToken(c, opts) {
24853
+ return opts?.headerOnly ? /^Bearer\s+(.+)$/i.exec(c.req.header("authorization") ?? "")?.[1]?.trim() || c.req.header("x-token") : c.req.header("x-token") || c.req.query("token");
24854
+ }
24775
24855
  function resolveTokenAuth(c, db3, opts) {
24776
24856
  const ip = getClientIp(c);
24777
24857
  if (isLockedOut(TOKEN_BUCKET, ip))
24778
24858
  return { ok: false, reason: "locked_out" };
24779
- const token = opts?.headerOnly ? /^Bearer\s+(.+)$/i.exec(c.req.header("authorization") ?? "")?.[1]?.trim() || c.req.header("x-token") : c.req.header("x-token") || c.req.query("token");
24859
+ const token = requestToken(c, opts);
24780
24860
  if (!token) {
24781
24861
  return { ok: false, reason: "missing_token" };
24782
24862
  }
@@ -24811,6 +24891,233 @@ var init_web_token = __esm({
24811
24891
  }
24812
24892
  });
24813
24893
 
24894
+ // ../server/dist/middleware/auth.js
24895
+ import { Hono as Hono11 } from "hono";
24896
+ import { getCookie, setCookie } from "hono/cookie";
24897
+ import crypto5 from "node:crypto";
24898
+ function passwordHash() {
24899
+ return config.server.password ?? "";
24900
+ }
24901
+ function passwordEnvPlaintext() {
24902
+ return config.server.passwordEnvPlaintext;
24903
+ }
24904
+ function jwtSecret() {
24905
+ const secret = config.server.jwtSecret ?? "";
24906
+ if (!secret) {
24907
+ throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
24908
+ }
24909
+ return Buffer.from(secret, "base64");
24910
+ }
24911
+ function base64url(data) {
24912
+ const buf = typeof data === "string" ? Buffer.from(data) : data;
24913
+ return buf.toString("base64url");
24914
+ }
24915
+ function jwtSign(payload) {
24916
+ const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
24917
+ const body = base64url(JSON.stringify(payload));
24918
+ const sig = crypto5.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
24919
+ return `${header}.${body}.${base64url(sig)}`;
24920
+ }
24921
+ function jwtVerify(token) {
24922
+ const parts = token.split(".");
24923
+ if (parts.length !== 3)
24924
+ return null;
24925
+ const [header, body, sig] = parts;
24926
+ let secret;
24927
+ try {
24928
+ secret = jwtSecret();
24929
+ } catch {
24930
+ return null;
24931
+ }
24932
+ const expected = base64url(crypto5.createHmac("sha256", secret).update(`${header}.${body}`).digest());
24933
+ if (sig !== expected)
24934
+ return null;
24935
+ try {
24936
+ const payload = JSON.parse(Buffer.from(body, "base64url").toString());
24937
+ if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
24938
+ return null;
24939
+ return payload;
24940
+ } catch {
24941
+ return null;
24942
+ }
24943
+ }
24944
+ function createToken() {
24945
+ const now = Math.floor(Date.now() / 1e3);
24946
+ return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
24947
+ }
24948
+ function constantTimeEqualString(a, b) {
24949
+ const ab = Buffer.from(a, "utf-8");
24950
+ const bb = Buffer.from(b, "utf-8");
24951
+ const max = Math.max(ab.length, bb.length);
24952
+ const ap = Buffer.alloc(max);
24953
+ ab.copy(ap);
24954
+ const bp = Buffer.alloc(max);
24955
+ bb.copy(bp);
24956
+ const eq17 = crypto5.timingSafeEqual(ap, bp);
24957
+ return eq17 && ab.length === bb.length;
24958
+ }
24959
+ function validateToken(token) {
24960
+ if (!token)
24961
+ return null;
24962
+ const payload = jwtVerify(token);
24963
+ if (payload?.scope)
24964
+ return null;
24965
+ return payload;
24966
+ }
24967
+ function mintScopedToken(scope, maxAgeSec) {
24968
+ const now = Math.floor(Date.now() / 1e3);
24969
+ return jwtSign({ scope, iat: now, exp: now + maxAgeSec });
24970
+ }
24971
+ function verifyScopedToken(token, scope) {
24972
+ if (!token)
24973
+ return false;
24974
+ const payload = jwtVerify(token);
24975
+ return payload !== null && payload.scope === scope;
24976
+ }
24977
+ function shouldRefresh(payload) {
24978
+ const iat = payload.iat;
24979
+ if (!iat)
24980
+ return true;
24981
+ return Date.now() / 1e3 - iat > REFRESH_AFTER;
24982
+ }
24983
+ function isAuthDisabled() {
24984
+ return passwordHash().length === 0 && passwordEnvPlaintext() == null;
24985
+ }
24986
+ function isAuthenticated(cookie) {
24987
+ if (isAuthDisabled())
24988
+ return true;
24989
+ return validateToken(cookie) !== null;
24990
+ }
24991
+ function getTokenFromCookieHeader(cookieHeader) {
24992
+ if (!cookieHeader)
24993
+ return void 0;
24994
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
24995
+ return match?.[1];
24996
+ }
24997
+ function setTokenCookie(c, token) {
24998
+ setCookie(c, COOKIE_NAME, token, {
24999
+ path: "/",
25000
+ // WS upgrade requests carry cookies on the underlying HTTP handshake;
25001
+ // the server reads them via the Cookie header, never via JS — so the
25002
+ // cookie can stay httpOnly to avoid XSS exfiltration.
25003
+ httpOnly: true,
25004
+ maxAge: TOKEN_MAX_AGE,
25005
+ sameSite: "Lax"
25006
+ });
25007
+ }
25008
+ function createAuthRoutes() {
25009
+ const app2 = new Hono11();
25010
+ app2.post("/auth/login", async (c) => {
25011
+ const ip = getClientIp(c);
25012
+ if (isLockedOut2(ip)) {
25013
+ return c.json({ error: "Too many attempts, try again later" }, 429);
25014
+ }
25015
+ const body = await c.req.json().catch(() => ({ password: "" }));
25016
+ if (!body.password) {
25017
+ recordFailure2(ip);
25018
+ return c.json({ error: "Invalid password" }, 401);
25019
+ }
25020
+ const envPlain = passwordEnvPlaintext();
25021
+ const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
25022
+ if (!ok) {
25023
+ recordFailure2(ip);
25024
+ return c.json({ error: "Invalid password" }, 401);
25025
+ }
25026
+ clearFailures2(ip);
25027
+ const token = createToken();
25028
+ setTokenCookie(c, token);
25029
+ return c.json({ ok: true });
25030
+ });
25031
+ app2.get("/auth/check", (c) => {
25032
+ const token = getCookie(c, COOKIE_NAME);
25033
+ const payload = validateToken(token);
25034
+ if (payload) {
25035
+ if (shouldRefresh(payload)) {
25036
+ const newToken = createToken();
25037
+ setTokenCookie(c, newToken);
25038
+ }
25039
+ return c.json({ authenticated: true, badge: config.server.badge, goalModeEnabled: config.goalModeEnabled });
25040
+ }
25041
+ return c.json({ authenticated: false, badge: config.server.badge }, 401);
25042
+ });
25043
+ app2.post("/auth/logout", (c) => {
25044
+ setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
25045
+ return c.json({ ok: true });
25046
+ });
25047
+ app2.post("/auth/change-password", async (c) => {
25048
+ const body = await c.req.json().catch(() => ({}));
25049
+ const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
25050
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
25051
+ if (passwordEnvPlaintext() != null) {
25052
+ return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
25053
+ }
25054
+ if (!await verifyPassword(oldPassword, passwordHash())) {
25055
+ return c.json({ error: "Incorrect current password" }, 401);
25056
+ }
25057
+ const weak = passwordStrengthError(newPassword);
25058
+ if (weak)
25059
+ return c.json({ error: weak }, 400);
25060
+ if (newPassword === oldPassword) {
25061
+ return c.json({ error: "New password must differ from the current password" }, 400);
25062
+ }
25063
+ updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
25064
+ return c.json({ ok: true });
25065
+ });
25066
+ return app2;
25067
+ }
25068
+ function passwordStrengthError(pw) {
25069
+ if (pw.length < 8)
25070
+ return "Password must be at least 8 characters";
25071
+ if (!/[A-Za-z]/.test(pw))
25072
+ return "Password must contain at least one letter";
25073
+ if (!/[0-9]/.test(pw))
25074
+ return "Password must contain at least one digit";
25075
+ return null;
25076
+ }
25077
+ function authMiddleware() {
25078
+ return async (c, next) => {
25079
+ const path90 = c.req.path;
25080
+ if (PUBLIC_PATHS.some((p) => path90 === p || path90.startsWith(p + "?"))) {
25081
+ return next();
25082
+ }
25083
+ if (EXTENSION_ASSET_PATH.test(path90)) {
25084
+ return next();
25085
+ }
25086
+ const token = getCookie(c, COOKIE_NAME);
25087
+ const payload = validateToken(token);
25088
+ if (!payload) {
25089
+ if (!path90.startsWith("/api/")) {
25090
+ return next();
25091
+ }
25092
+ return c.json({ error: "Unauthorized" }, 401);
25093
+ }
25094
+ if (shouldRefresh(payload)) {
25095
+ const newToken = createToken();
25096
+ setTokenCookie(c, newToken);
25097
+ }
25098
+ return next();
25099
+ };
25100
+ }
25101
+ var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS, EXTENSION_ASSET_PATH;
25102
+ var init_auth = __esm({
25103
+ "../server/dist/middleware/auth.js"() {
25104
+ "use strict";
25105
+ init_config();
25106
+ init_password_hash();
25107
+ init_setup_config();
25108
+ init_brute_force();
25109
+ TOKEN_MAX_AGE = config.auth.tokenMaxAge;
25110
+ REFRESH_AFTER = config.auth.refreshAfter;
25111
+ COOKIE_NAME = "halo_token";
25112
+ ADMIN_LOGIN_BUCKET = "admin-login";
25113
+ isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
25114
+ recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
25115
+ clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
25116
+ PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
25117
+ EXTENSION_ASSET_PATH = /^\/api\/extensions\/[^/]+\/[^/]+\/[^/]+\/./;
25118
+ }
25119
+ });
25120
+
24814
25121
  // ../server/dist/channels/web/handler.js
24815
25122
  import fs58 from "node:fs";
24816
25123
  function buildWebSessionPrefix(accountId) {
@@ -24833,14 +25140,14 @@ function createWebChannel(deps) {
24833
25140
  return findActiveSessionId(sm, accountId, prefix, activeOverrides, "full") ?? void 0;
24834
25141
  }
24835
25142
  function resolveWorkspace(account, override, scaffold = true) {
24836
- let path89 = account.workspacePath;
25143
+ let path90 = account.workspacePath;
24837
25144
  if (override && override !== account.workspacePath) {
24838
25145
  if (account.accessLevel !== "full") {
24839
25146
  return { ok: false, error: "workspace override requires a full-access token" };
24840
25147
  }
24841
- path89 = override;
25148
+ path90 = override;
24842
25149
  }
24843
- const resolved = scaffold ? resolveAccountWorkspace({ ...account, workspacePath: path89 }) : fs58.existsSync(path89) ? path89 : null;
25150
+ const resolved = scaffold ? resolveAccountWorkspace({ ...account, workspacePath: path90 }) : fs58.existsSync(path90) ? path90 : null;
24844
25151
  if (!resolved)
24845
25152
  return { ok: false, error: "workspace not found" };
24846
25153
  return { ok: true, path: resolved };
@@ -25249,10 +25556,27 @@ var init_session_types = __esm({
25249
25556
  }
25250
25557
  });
25251
25558
 
25559
+ // ../server/dist/routes/etag.js
25560
+ function etagMatches(header, etag) {
25561
+ if (!header)
25562
+ return false;
25563
+ const bare = etag.replace(/^W\//, "");
25564
+ return header.split(",").some((t2) => {
25565
+ const tag = t2.trim();
25566
+ return tag === "*" || tag.replace(/^W\//, "") === bare;
25567
+ });
25568
+ }
25569
+ var init_etag = __esm({
25570
+ "../server/dist/routes/etag.js"() {
25571
+ "use strict";
25572
+ }
25573
+ });
25574
+
25252
25575
  // ../server/dist/routes/halo-city.js
25253
- import { Hono as Hono11 } from "hono";
25576
+ import { Hono as Hono12 } from "hono";
25254
25577
  import fs59 from "node:fs";
25255
25578
  import path67 from "node:path";
25579
+ import crypto6 from "node:crypto";
25256
25580
  import Database7 from "better-sqlite3";
25257
25581
  function scanSkills2(dir) {
25258
25582
  let names;
@@ -25536,21 +25860,30 @@ function discoverWorkspaces(registry2) {
25536
25860
  }
25537
25861
  return out;
25538
25862
  }
25863
+ function stateEtag(accessLevel, skills, workspaces, uptime) {
25864
+ const json = JSON.stringify({ accessLevel, skills, workspaces, uptimeMin: Math.floor(uptime / 60) });
25865
+ return `W/"${crypto6.createHash("sha1").update(json).digest("base64url")}"`;
25866
+ }
25539
25867
  function createShowRoutes(registry2) {
25540
- const app2 = new Hono11();
25868
+ const app2 = new Hono12();
25541
25869
  function auth(c) {
25870
+ const cookie = getTokenFromCookieHeader(c.req.header("cookie"));
25871
+ if (!requestToken(c) && cookie && isAuthenticated(cookie)) {
25872
+ return { ok: true, accessLevel: "full", scoped: null };
25873
+ }
25542
25874
  const a = resolveTokenAuth(c, getChannelDb());
25543
25875
  if (!a.ok)
25544
25876
  return { ok: false, response: tokenAuthJsonError(c, a.reason) };
25545
- return { ok: true, account: a.account };
25877
+ const { account } = a;
25878
+ const globalView = account.accessLevel === "full" || account.accessLevel === "observer";
25879
+ return { ok: true, accessLevel: account.accessLevel, scoped: globalView ? null : account };
25546
25880
  }
25547
25881
  app2.get("/show/state", (c) => {
25548
25882
  const a = auth(c);
25549
25883
  if (!a.ok)
25550
25884
  return a.response;
25551
- const { account } = a;
25552
- const globalView = account.accessLevel === "full" || account.accessLevel === "observer";
25553
- const targets = globalView ? discoverWorkspaces(registry2) : /* @__PURE__ */ new Map([[account.workspacePath, path67.basename(account.workspacePath)]]);
25885
+ const { scoped } = a;
25886
+ const targets = scoped ? /* @__PURE__ */ new Map([[scoped.workspacePath, path67.basename(scoped.workspacePath)]]) : discoverWorkspaces(registry2);
25554
25887
  const workspaces = [];
25555
25888
  for (const [wsPath, label] of targets) {
25556
25889
  try {
@@ -25563,11 +25896,18 @@ function createShowRoutes(registry2) {
25563
25896
  }
25564
25897
  }
25565
25898
  workspaces.sort((x, y) => workspaceBirth(x.path) - workspaceBirth(y.path) || x.key.localeCompare(y.key));
25899
+ const uptime = process.uptime();
25900
+ const skills = scanSkills2(GLOBAL_SKILLS_DIR);
25901
+ const etag = stateEtag(a.accessLevel, skills, workspaces, uptime);
25902
+ c.header("ETag", etag);
25903
+ c.header("Cache-Control", "private, no-cache");
25904
+ if (etagMatches(c.req.header("if-none-match"), etag))
25905
+ return c.body(null, 304);
25566
25906
  return c.json({
25567
25907
  serverTime: Date.now(),
25568
- uptime: process.uptime(),
25569
- accessLevel: account.accessLevel,
25570
- skills: scanSkills2(GLOBAL_SKILLS_DIR),
25908
+ uptime,
25909
+ accessLevel: a.accessLevel,
25910
+ skills,
25571
25911
  workspaces
25572
25912
  });
25573
25913
  });
@@ -25575,19 +25915,18 @@ function createShowRoutes(registry2) {
25575
25915
  const a = auth(c);
25576
25916
  if (!a.ok)
25577
25917
  return a.response;
25578
- const { account } = a;
25918
+ const { scoped } = a;
25579
25919
  const wsPath = c.req.query("ws") ?? "";
25580
25920
  const id = c.req.query("id") ?? "";
25581
25921
  if (!wsPath || !id)
25582
25922
  return c.json({ error: "ws and id required" }, 400);
25583
- const globalView = account.accessLevel === "full" || account.accessLevel === "observer";
25584
- if (!globalView) {
25923
+ if (scoped) {
25585
25924
  let allowed = false;
25586
25925
  try {
25587
- allowed = fs59.realpathSync(wsPath) === fs59.realpathSync(account.workspacePath);
25926
+ allowed = fs59.realpathSync(wsPath) === fs59.realpathSync(scoped.workspacePath);
25588
25927
  } catch {
25589
25928
  }
25590
- if (!allowed || !canAddressSession(account, id))
25929
+ if (!allowed || !canAddressSession(scoped, id))
25591
25930
  return c.json({ error: "forbidden" }, 403);
25592
25931
  }
25593
25932
  try {
@@ -25640,11 +25979,13 @@ var init_halo_city = __esm({
25640
25979
  "use strict";
25641
25980
  init_channel_db();
25642
25981
  init_web_token();
25982
+ init_auth();
25643
25983
  init_handler();
25644
25984
  init_agent_loader();
25645
25985
  init_session_store();
25646
25986
  init_session_types();
25647
25987
  init_mtime_cache();
25988
+ init_etag();
25648
25989
  SESSIONS_PER_WS = 80;
25649
25990
  _skillCache = createMtimeCache();
25650
25991
  _metaCache = /* @__PURE__ */ new Map();
@@ -25655,7 +25996,7 @@ var init_halo_city = __esm({
25655
25996
  });
25656
25997
 
25657
25998
  // ../server/dist/routes/metrics.js
25658
- import { Hono as Hono12 } from "hono";
25999
+ import { Hono as Hono13 } from "hono";
25659
26000
  function family(name, type, help, samples) {
25660
26001
  const lines = [`# HELP ${name} ${help}`, `# TYPE ${name} ${type}`];
25661
26002
  for (const s of samples)
@@ -25663,7 +26004,7 @@ function family(name, type, help, samples) {
25663
26004
  return lines.join("\n");
25664
26005
  }
25665
26006
  function createMetricsRoutes(registry2) {
25666
- const app2 = new Hono12();
26007
+ const app2 = new Hono13();
25667
26008
  function auth(c) {
25668
26009
  const a = resolveTokenAuth(c, getChannelDb());
25669
26010
  if (!a.ok) {
@@ -25748,9 +26089,9 @@ var init_metrics = __esm({
25748
26089
  });
25749
26090
 
25750
26091
  // ../server/dist/routes/commands.js
25751
- import { Hono as Hono13 } from "hono";
26092
+ import { Hono as Hono14 } from "hono";
25752
26093
  function createCommandRoutes(registry2, smRegistry) {
25753
- const app2 = new Hono13();
26094
+ const app2 = new Hono14();
25754
26095
  app2.get("/commands", async (c) => {
25755
26096
  const projectId = c.req.query("projectId");
25756
26097
  const sessionId = c.req.query("sessionId");
@@ -25777,233 +26118,6 @@ var init_commands3 = __esm({
25777
26118
  }
25778
26119
  });
25779
26120
 
25780
- // ../server/dist/middleware/auth.js
25781
- import { Hono as Hono14 } from "hono";
25782
- import { getCookie, setCookie } from "hono/cookie";
25783
- import crypto5 from "node:crypto";
25784
- function passwordHash() {
25785
- return config.server.password ?? "";
25786
- }
25787
- function passwordEnvPlaintext() {
25788
- return config.server.passwordEnvPlaintext;
25789
- }
25790
- function jwtSecret() {
25791
- const secret = config.server.jwtSecret ?? "";
25792
- if (!secret) {
25793
- throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
25794
- }
25795
- return Buffer.from(secret, "base64");
25796
- }
25797
- function base64url(data) {
25798
- const buf = typeof data === "string" ? Buffer.from(data) : data;
25799
- return buf.toString("base64url");
25800
- }
25801
- function jwtSign(payload) {
25802
- const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
25803
- const body = base64url(JSON.stringify(payload));
25804
- const sig = crypto5.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
25805
- return `${header}.${body}.${base64url(sig)}`;
25806
- }
25807
- function jwtVerify(token) {
25808
- const parts = token.split(".");
25809
- if (parts.length !== 3)
25810
- return null;
25811
- const [header, body, sig] = parts;
25812
- let secret;
25813
- try {
25814
- secret = jwtSecret();
25815
- } catch {
25816
- return null;
25817
- }
25818
- const expected = base64url(crypto5.createHmac("sha256", secret).update(`${header}.${body}`).digest());
25819
- if (sig !== expected)
25820
- return null;
25821
- try {
25822
- const payload = JSON.parse(Buffer.from(body, "base64url").toString());
25823
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
25824
- return null;
25825
- return payload;
25826
- } catch {
25827
- return null;
25828
- }
25829
- }
25830
- function createToken() {
25831
- const now = Math.floor(Date.now() / 1e3);
25832
- return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
25833
- }
25834
- function constantTimeEqualString(a, b) {
25835
- const ab = Buffer.from(a, "utf-8");
25836
- const bb = Buffer.from(b, "utf-8");
25837
- const max = Math.max(ab.length, bb.length);
25838
- const ap = Buffer.alloc(max);
25839
- ab.copy(ap);
25840
- const bp = Buffer.alloc(max);
25841
- bb.copy(bp);
25842
- const eq17 = crypto5.timingSafeEqual(ap, bp);
25843
- return eq17 && ab.length === bb.length;
25844
- }
25845
- function validateToken(token) {
25846
- if (!token)
25847
- return null;
25848
- const payload = jwtVerify(token);
25849
- if (payload?.scope)
25850
- return null;
25851
- return payload;
25852
- }
25853
- function mintScopedToken(scope, maxAgeSec) {
25854
- const now = Math.floor(Date.now() / 1e3);
25855
- return jwtSign({ scope, iat: now, exp: now + maxAgeSec });
25856
- }
25857
- function verifyScopedToken(token, scope) {
25858
- if (!token)
25859
- return false;
25860
- const payload = jwtVerify(token);
25861
- return payload !== null && payload.scope === scope;
25862
- }
25863
- function shouldRefresh(payload) {
25864
- const iat = payload.iat;
25865
- if (!iat)
25866
- return true;
25867
- return Date.now() / 1e3 - iat > REFRESH_AFTER;
25868
- }
25869
- function isAuthDisabled() {
25870
- return passwordHash().length === 0 && passwordEnvPlaintext() == null;
25871
- }
25872
- function isAuthenticated(cookie) {
25873
- if (isAuthDisabled())
25874
- return true;
25875
- return validateToken(cookie) !== null;
25876
- }
25877
- function getTokenFromCookieHeader(cookieHeader) {
25878
- if (!cookieHeader)
25879
- return void 0;
25880
- const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
25881
- return match?.[1];
25882
- }
25883
- function setTokenCookie(c, token) {
25884
- setCookie(c, COOKIE_NAME, token, {
25885
- path: "/",
25886
- // WS upgrade requests carry cookies on the underlying HTTP handshake;
25887
- // the server reads them via the Cookie header, never via JS — so the
25888
- // cookie can stay httpOnly to avoid XSS exfiltration.
25889
- httpOnly: true,
25890
- maxAge: TOKEN_MAX_AGE,
25891
- sameSite: "Lax"
25892
- });
25893
- }
25894
- function createAuthRoutes() {
25895
- const app2 = new Hono14();
25896
- app2.post("/auth/login", async (c) => {
25897
- const ip = getClientIp(c);
25898
- if (isLockedOut2(ip)) {
25899
- return c.json({ error: "Too many attempts, try again later" }, 429);
25900
- }
25901
- const body = await c.req.json().catch(() => ({ password: "" }));
25902
- if (!body.password) {
25903
- recordFailure2(ip);
25904
- return c.json({ error: "Invalid password" }, 401);
25905
- }
25906
- const envPlain = passwordEnvPlaintext();
25907
- const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
25908
- if (!ok) {
25909
- recordFailure2(ip);
25910
- return c.json({ error: "Invalid password" }, 401);
25911
- }
25912
- clearFailures2(ip);
25913
- const token = createToken();
25914
- setTokenCookie(c, token);
25915
- return c.json({ ok: true });
25916
- });
25917
- app2.get("/auth/check", (c) => {
25918
- const token = getCookie(c, COOKIE_NAME);
25919
- const payload = validateToken(token);
25920
- if (payload) {
25921
- if (shouldRefresh(payload)) {
25922
- const newToken = createToken();
25923
- setTokenCookie(c, newToken);
25924
- }
25925
- return c.json({ authenticated: true, badge: config.server.badge, goalModeEnabled: config.goalModeEnabled });
25926
- }
25927
- return c.json({ authenticated: false, badge: config.server.badge }, 401);
25928
- });
25929
- app2.post("/auth/logout", (c) => {
25930
- setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
25931
- return c.json({ ok: true });
25932
- });
25933
- app2.post("/auth/change-password", async (c) => {
25934
- const body = await c.req.json().catch(() => ({}));
25935
- const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
25936
- const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
25937
- if (passwordEnvPlaintext() != null) {
25938
- return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
25939
- }
25940
- if (!await verifyPassword(oldPassword, passwordHash())) {
25941
- return c.json({ error: "Incorrect current password" }, 401);
25942
- }
25943
- const weak = passwordStrengthError(newPassword);
25944
- if (weak)
25945
- return c.json({ error: weak }, 400);
25946
- if (newPassword === oldPassword) {
25947
- return c.json({ error: "New password must differ from the current password" }, 400);
25948
- }
25949
- updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
25950
- return c.json({ ok: true });
25951
- });
25952
- return app2;
25953
- }
25954
- function passwordStrengthError(pw) {
25955
- if (pw.length < 8)
25956
- return "Password must be at least 8 characters";
25957
- if (!/[A-Za-z]/.test(pw))
25958
- return "Password must contain at least one letter";
25959
- if (!/[0-9]/.test(pw))
25960
- return "Password must contain at least one digit";
25961
- return null;
25962
- }
25963
- function authMiddleware() {
25964
- return async (c, next) => {
25965
- const path89 = c.req.path;
25966
- if (PUBLIC_PATHS.some((p) => path89 === p || path89.startsWith(p + "?"))) {
25967
- return next();
25968
- }
25969
- if (EXTENSION_ASSET_PATH.test(path89)) {
25970
- return next();
25971
- }
25972
- const token = getCookie(c, COOKIE_NAME);
25973
- const payload = validateToken(token);
25974
- if (!payload) {
25975
- if (!path89.startsWith("/api/")) {
25976
- return next();
25977
- }
25978
- return c.json({ error: "Unauthorized" }, 401);
25979
- }
25980
- if (shouldRefresh(payload)) {
25981
- const newToken = createToken();
25982
- setTokenCookie(c, newToken);
25983
- }
25984
- return next();
25985
- };
25986
- }
25987
- var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS, EXTENSION_ASSET_PATH;
25988
- var init_auth = __esm({
25989
- "../server/dist/middleware/auth.js"() {
25990
- "use strict";
25991
- init_config();
25992
- init_password_hash();
25993
- init_setup_config();
25994
- init_brute_force();
25995
- TOKEN_MAX_AGE = config.auth.tokenMaxAge;
25996
- REFRESH_AFTER = config.auth.refreshAfter;
25997
- COOKIE_NAME = "halo_token";
25998
- ADMIN_LOGIN_BUCKET = "admin-login";
25999
- isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
26000
- recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
26001
- clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
26002
- PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
26003
- EXTENSION_ASSET_PATH = /^\/api\/extensions\/[^/]+\/[^/]+\/[^/]+\/./;
26004
- }
26005
- });
26006
-
26007
26121
  // ../server/dist/routes/transcribe-ws.js
26008
26122
  import { WebSocketServer } from "ws";
26009
26123
  import { StartStreamTranscriptionCommand, TranscribeStreamingClient } from "@aws-sdk/client-transcribe-streaming";
@@ -26217,19 +26331,688 @@ var init_transcribe_ws = __esm({
26217
26331
  }
26218
26332
  });
26219
26333
 
26334
+ // ../server/dist/stt/models.js
26335
+ import { createHash as createHash2 } from "node:crypto";
26336
+ import fs60 from "node:fs";
26337
+ import fsp from "node:fs/promises";
26338
+ import http2 from "node:http";
26339
+ import https2 from "node:https";
26340
+ import path68 from "node:path";
26341
+ function httpGet(url, headers, signal, hops = 5) {
26342
+ return new Promise((resolve2, reject) => {
26343
+ const u = new URL(url);
26344
+ const req = (u.protocol === "https:" ? https2 : http2).get(u, { headers: { "User-Agent": "halo-stt", ...headers }, signal }, (res) => {
26345
+ res.on("error", () => {
26346
+ });
26347
+ const location = res.headers.location;
26348
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && location) {
26349
+ res.resume();
26350
+ if (hops === 0)
26351
+ reject(new Error("too many redirects"));
26352
+ else
26353
+ resolve2(httpGet(new URL(location, u).toString(), headers, signal, hops - 1));
26354
+ return;
26355
+ }
26356
+ resolve2(res);
26357
+ });
26358
+ req.on("error", reject);
26359
+ });
26360
+ }
26361
+ function createSttModelManager(opts = {}) {
26362
+ const files = opts.files ?? STT_MODEL_FILES;
26363
+ const connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
26364
+ const stallTimeoutMs = opts.stallTimeoutMs ?? 3e4;
26365
+ const progressIntervalMs = opts.progressIntervalMs ?? 500;
26366
+ const filePath = (rel) => path68.join(opts.dir ?? globalSttModelsDir(), rel);
26367
+ const total = files.reduce((n, f) => n + f.size, 0);
26368
+ const listeners2 = /* @__PURE__ */ new Set();
26369
+ let inflight = null;
26370
+ let received = 0;
26371
+ let lastError = "";
26372
+ let lastEmit = 0;
26373
+ let current = null;
26374
+ let closed = false;
26375
+ const fileReady = (f) => {
26376
+ try {
26377
+ return fs60.statSync(filePath(f.path)).size === f.size;
26378
+ } catch {
26379
+ return false;
26380
+ }
26381
+ };
26382
+ function status() {
26383
+ if (inflight)
26384
+ return { state: "downloading", received, total };
26385
+ if (files.every(fileReady))
26386
+ return { state: "ready" };
26387
+ if (lastError)
26388
+ return { state: "error", message: lastError };
26389
+ return { state: "missing" };
26390
+ }
26391
+ function emit2() {
26392
+ lastEmit = Date.now();
26393
+ const s = status();
26394
+ for (const l of listeners2)
26395
+ l(s);
26396
+ }
26397
+ async function fetchFile(f, url, base) {
26398
+ const final = filePath(f.path);
26399
+ const part = `${final}.part`;
26400
+ fs60.mkdirSync(path68.dirname(final), { recursive: true });
26401
+ let have = 0;
26402
+ try {
26403
+ have = fs60.statSync(part).size;
26404
+ } catch {
26405
+ }
26406
+ if (have > f.size) {
26407
+ fs60.rmSync(part, { force: true });
26408
+ have = 0;
26409
+ }
26410
+ received = base + have;
26411
+ if (have === f.size) {
26412
+ const hash = createHash2("sha256");
26413
+ for await (const chunk of fs60.createReadStream(part))
26414
+ hash.update(chunk);
26415
+ if (hash.digest("hex") === f.sha256) {
26416
+ fs60.renameSync(part, final);
26417
+ return;
26418
+ }
26419
+ fs60.rmSync(part, { force: true });
26420
+ have = 0;
26421
+ }
26422
+ const ac = new AbortController();
26423
+ current = ac;
26424
+ let reason = "";
26425
+ let timer2 = null;
26426
+ const arm = (ms, why) => {
26427
+ if (timer2)
26428
+ clearTimeout(timer2);
26429
+ timer2 = setTimeout(() => {
26430
+ reason = why;
26431
+ ac.abort();
26432
+ }, ms);
26433
+ };
26434
+ arm(connectTimeoutMs, `no data within ${connectTimeoutMs / 1e3}s`);
26435
+ try {
26436
+ const res = await httpGet(url, have > 0 ? { Range: `bytes=${have}-` } : {}, ac.signal);
26437
+ if (res.statusCode === 416) {
26438
+ fs60.rmSync(part, { force: true });
26439
+ throw new Error("HTTP 416");
26440
+ }
26441
+ if (res.statusCode !== 200 && res.statusCode !== 206)
26442
+ throw new Error(`HTTP ${res.statusCode}`);
26443
+ const resume = res.statusCode === 206;
26444
+ if (resume && !(res.headers["content-range"] ?? "").startsWith(`bytes ${have}-`)) {
26445
+ throw new Error(`unexpected Content-Range ${res.headers["content-range"] ?? "(none)"}`);
26446
+ }
26447
+ if (have > 0)
26448
+ console.log(`[STT] ${f.path}: ${resume ? `resuming at ${have} bytes` : `source ignored Range, restarting from 0 (had ${have} bytes)`}`);
26449
+ const hash = createHash2("sha256");
26450
+ let got = 0;
26451
+ if (resume) {
26452
+ for await (const chunk of fs60.createReadStream(part))
26453
+ hash.update(chunk);
26454
+ got = have;
26455
+ }
26456
+ const fh = await fsp.open(part, resume ? "a" : "w");
26457
+ try {
26458
+ for await (const value of res) {
26459
+ arm(stallTimeoutMs, `stalled for ${stallTimeoutMs / 1e3}s`);
26460
+ got += value.length;
26461
+ if (got > f.size)
26462
+ break;
26463
+ hash.update(value);
26464
+ await fh.write(value);
26465
+ received = base + got;
26466
+ if (Date.now() - lastEmit >= progressIntervalMs)
26467
+ emit2();
26468
+ }
26469
+ } finally {
26470
+ await fh.close();
26471
+ }
26472
+ if (got < f.size)
26473
+ throw new Error(`truncated (${got} of ${f.size} bytes)`);
26474
+ if (got > f.size || hash.digest("hex") !== f.sha256) {
26475
+ fs60.rmSync(part, { force: true });
26476
+ throw new Error(got > f.size ? `size mismatch (more than ${f.size} bytes)` : "sha256 mismatch");
26477
+ }
26478
+ fs60.renameSync(part, final);
26479
+ } catch (err) {
26480
+ if (reason)
26481
+ throw new Error(reason);
26482
+ throw err;
26483
+ } finally {
26484
+ if (timer2)
26485
+ clearTimeout(timer2);
26486
+ ac.abort();
26487
+ current = null;
26488
+ }
26489
+ }
26490
+ async function run() {
26491
+ let base = files.filter(fileReady).reduce((n, f) => n + f.size, 0);
26492
+ received = base;
26493
+ for (const f of files) {
26494
+ if (fileReady(f))
26495
+ continue;
26496
+ const failures = [];
26497
+ for (const src of f.sources) {
26498
+ if (closed)
26499
+ return;
26500
+ const t0 = Date.now();
26501
+ try {
26502
+ await fetchFile(f, src.url, base);
26503
+ console.log(`[STT] downloaded ${f.path} from ${src.name} (${(f.size / 1048576).toFixed(1)} MB in ${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
26504
+ failures.length = 0;
26505
+ break;
26506
+ } catch (err) {
26507
+ if (closed)
26508
+ return;
26509
+ const msg = err instanceof Error ? err.message : String(err);
26510
+ failures.push(`${src.name}: ${msg}`);
26511
+ console.log(`[STT] download ${f.path} from ${src.name} failed: ${msg}`);
26512
+ }
26513
+ }
26514
+ if (failures.length > 0) {
26515
+ lastError = `${path68.basename(f.path)}: all sources failed (${failures.join("; ")})`;
26516
+ return;
26517
+ }
26518
+ base += f.size;
26519
+ received = base;
26520
+ }
26521
+ }
26522
+ function ensure() {
26523
+ if (inflight)
26524
+ return inflight;
26525
+ if (closed || files.every(fileReady))
26526
+ return Promise.resolve();
26527
+ lastError = "";
26528
+ console.log("[STT] model download start");
26529
+ inflight = run().finally(() => {
26530
+ inflight = null;
26531
+ emit2();
26532
+ });
26533
+ emit2();
26534
+ return inflight;
26535
+ }
26536
+ return {
26537
+ status,
26538
+ ensure,
26539
+ subscribe(listener) {
26540
+ listeners2.add(listener);
26541
+ return () => {
26542
+ listeners2.delete(listener);
26543
+ };
26544
+ },
26545
+ filePath,
26546
+ close() {
26547
+ closed = true;
26548
+ current?.abort();
26549
+ }
26550
+ };
26551
+ }
26552
+ var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
26553
+ var init_models = __esm({
26554
+ "../server/dist/stt/models.js"() {
26555
+ "use strict";
26556
+ init_paths();
26557
+ SENSE_VOICE_MODEL = "sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx";
26558
+ SENSE_VOICE_TOKENS = "sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt";
26559
+ SILERO_VAD_MODEL = "silero-vad-v4/silero_vad.onnx";
26560
+ HF_SENSE_VOICE = "https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07";
26561
+ MS_SENSE_VOICE = "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/repo?Revision=master&FilePath=";
26562
+ STT_MODEL_FILES = [
26563
+ {
26564
+ path: SENSE_VOICE_MODEL,
26565
+ size: 239233841,
26566
+ sha256: "c71f0ce00bec95b07744e116345e33d8cbbe08cef896382cf907bf4b51a2cd51",
26567
+ sources: [
26568
+ { name: "huggingface", url: `${HF_SENSE_VOICE}/model.int8.onnx` },
26569
+ { name: "modelscope", url: `${MS_SENSE_VOICE}model.int8.onnx` }
26570
+ ]
26571
+ },
26572
+ {
26573
+ path: SENSE_VOICE_TOKENS,
26574
+ size: 315894,
26575
+ sha256: "f449eb28dc567533d7fa59be34e2abca8784f771850c78a47fb731a31429a1dc",
26576
+ sources: [
26577
+ { name: "huggingface", url: `${HF_SENSE_VOICE}/tokens.txt` },
26578
+ { name: "modelscope", url: `${MS_SENSE_VOICE}tokens.txt` }
26579
+ ]
26580
+ },
26581
+ {
26582
+ path: SILERO_VAD_MODEL,
26583
+ size: 1807522,
26584
+ sha256: "a35ebf52fd3ce5f1469b2a36158dba761bc47b973ea3382b3186ca15b1f5af28",
26585
+ sources: [
26586
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/vad/resolve/fba88cd2e921609e7675c3aaf51e0b9b295da4bc/silero_vad.onnx" },
26587
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/silero-vad/repo?Revision=master&FilePath=v4/silero_vad.onnx" }
26588
+ ]
26589
+ }
26590
+ ];
26591
+ sttModels = createSttModelManager();
26592
+ }
26593
+ });
26594
+
26595
+ // ../server/dist/stt/engine.js
26596
+ import os8 from "node:os";
26597
+ function createSttEngine(deps = {}) {
26598
+ const loadSherpa = deps.loadSherpa ?? defaultLoadSherpa;
26599
+ const models = deps.models ?? sttModels;
26600
+ let sherpa = null;
26601
+ let recognizer = null;
26602
+ let loading = null;
26603
+ const queue = [];
26604
+ let decoding = false;
26605
+ function load() {
26606
+ if (recognizer)
26607
+ return Promise.resolve();
26608
+ loading ??= doLoad().finally(() => {
26609
+ loading = null;
26610
+ });
26611
+ return loading;
26612
+ }
26613
+ async function doLoad() {
26614
+ const t0 = Date.now();
26615
+ try {
26616
+ const m = await loadSherpa();
26617
+ recognizer = await m.OfflineRecognizer.createAsync({
26618
+ featConfig: { sampleRate: SAMPLE_RATE, featureDim: 80 },
26619
+ modelConfig: {
26620
+ // language '' = auto: setting one barely changed results in testing.
26621
+ senseVoice: { model: models.filePath(SENSE_VOICE_MODEL), language: "", useInverseTextNormalization: 1 },
26622
+ tokens: models.filePath(SENSE_VOICE_TOKENS),
26623
+ numThreads: NUM_THREADS,
26624
+ provider: "cpu",
26625
+ debug: 0
26626
+ }
26627
+ });
26628
+ sherpa = m;
26629
+ console.log(`[STT] engine loaded in ${Date.now() - t0}ms (threads=${NUM_THREADS})`);
26630
+ } catch (err) {
26631
+ console.log(`[STT] engine load failed: ${errMsg2(err)}`);
26632
+ throw new SttError("engine", `speech engine unavailable: ${errMsg2(err)}`);
26633
+ }
26634
+ }
26635
+ async function pump() {
26636
+ if (decoding || !recognizer)
26637
+ return;
26638
+ decoding = true;
26639
+ while (queue.length > 0) {
26640
+ const job = queue.shift();
26641
+ try {
26642
+ const stream = recognizer.createStream();
26643
+ stream.acceptWaveform({ samples: job.samples, sampleRate: SAMPLE_RATE });
26644
+ await recognizer.decodeAsync(stream);
26645
+ job.owner.done(job, recognizer.getResult(stream));
26646
+ } catch (err) {
26647
+ job.owner.fail(new SttError("engine", `decode failed: ${errMsg2(err)}`));
26648
+ }
26649
+ }
26650
+ decoding = false;
26651
+ }
26652
+ function open(handlers) {
26653
+ if (!sherpa)
26654
+ throw new SttError("engine", "speech engine not loaded");
26655
+ let vad;
26656
+ try {
26657
+ vad = new sherpa.Vad({
26658
+ sileroVad: {
26659
+ model: models.filePath(SILERO_VAD_MODEL),
26660
+ threshold: VAD_THRESHOLD,
26661
+ minSilenceDuration: VAD_MIN_SILENCE_S,
26662
+ minSpeechDuration: VAD_MIN_SPEECH_S,
26663
+ maxSpeechDuration: VAD_MAX_SPEECH_S,
26664
+ windowSize: VAD_WINDOW
26665
+ },
26666
+ sampleRate: SAMPLE_RATE,
26667
+ numThreads: 1,
26668
+ debug: false
26669
+ }, RING_S);
26670
+ } catch (err) {
26671
+ throw new SttError("engine", `VAD unavailable: ${errMsg2(err)}`);
26672
+ }
26673
+ const ringLen = RING_S * SAMPLE_RATE;
26674
+ const ring = new Float32Array(ringLen);
26675
+ const win = new Float32Array(VAD_WINDOW);
26676
+ let winFill = 0;
26677
+ let total = 0;
26678
+ let carry = null;
26679
+ let prevEnd = 0;
26680
+ let pendingSamples = 0;
26681
+ let inFlight2 = 0;
26682
+ let segments = 0;
26683
+ let decodedSamples = 0;
26684
+ let stopped = false;
26685
+ let drained = null;
26686
+ const checkDrain = () => {
26687
+ if (drained && (stopped || inFlight2 === 0)) {
26688
+ const d = drained;
26689
+ drained = null;
26690
+ d();
26691
+ }
26692
+ };
26693
+ const dropQueued = () => {
26694
+ for (let i = queue.length - 1; i >= 0; i--) {
26695
+ if (queue[i].owner !== owner)
26696
+ continue;
26697
+ pendingSamples -= queue[i].samples.length;
26698
+ inFlight2--;
26699
+ queue.splice(i, 1);
26700
+ }
26701
+ };
26702
+ function failStream(err) {
26703
+ if (stopped)
26704
+ return;
26705
+ stopped = true;
26706
+ dropQueued();
26707
+ handlers.onError(err);
26708
+ checkDrain();
26709
+ }
26710
+ const owner = {
26711
+ done(job, result) {
26712
+ pendingSamples -= job.samples.length;
26713
+ inFlight2--;
26714
+ if (!stopped) {
26715
+ segments++;
26716
+ decodedSamples += job.samples.length;
26717
+ const text6 = (result.text ?? "").trim();
26718
+ if (text6)
26719
+ handlers.onFinal({ start: round3(job.start / SAMPLE_RATE), end: round3(job.end / SAMPLE_RATE), text: text6, lang: (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1") });
26720
+ }
26721
+ checkDrain();
26722
+ },
26723
+ fail(err) {
26724
+ inFlight2--;
26725
+ failStream(err);
26726
+ checkDrain();
26727
+ }
26728
+ };
26729
+ function enqueue(segStart, segEnd) {
26730
+ const start3 = Math.max(segStart - PAD_BEFORE_S * SAMPLE_RATE, prevEnd, total - ringLen, 0);
26731
+ const end = Math.min(segEnd + PAD_AFTER_S * SAMPLE_RATE, total);
26732
+ if (end <= start3)
26733
+ return;
26734
+ prevEnd = end;
26735
+ if (queue.length >= MAX_QUEUED_SEGMENTS) {
26736
+ failStream(new SttError("busy", `decode queue full (${MAX_QUEUED_SEGMENTS} segments waiting)`));
26737
+ return;
26738
+ }
26739
+ if ((pendingSamples + end - start3) / SAMPLE_RATE > MAX_PENDING_AUDIO_S) {
26740
+ failStream(new SttError("busy", `more than ${MAX_PENDING_AUDIO_S}s of audio waiting to be decoded`));
26741
+ return;
26742
+ }
26743
+ const samples = new Float32Array(end - start3);
26744
+ for (let i = 0; i < samples.length; i++)
26745
+ samples[i] = ring[(start3 + i) % ringLen];
26746
+ pendingSamples += samples.length;
26747
+ inFlight2++;
26748
+ queue.push({ owner, samples, start: start3, end });
26749
+ void pump();
26750
+ }
26751
+ function drainVad() {
26752
+ while (!stopped && !vad.isEmpty()) {
26753
+ const seg = vad.front(false);
26754
+ vad.pop();
26755
+ enqueue(seg.start, seg.start + seg.samples.length);
26756
+ }
26757
+ }
26758
+ function feed(samples) {
26759
+ vad.acceptWaveform(samples);
26760
+ drainVad();
26761
+ }
26762
+ return {
26763
+ write(pcm) {
26764
+ if (stopped)
26765
+ return;
26766
+ try {
26767
+ const bytes = carry ? Buffer.concat([carry, pcm]) : pcm;
26768
+ const n = bytes.length >> 1;
26769
+ carry = bytes.length & 1 ? Buffer.from(bytes.subarray(bytes.length - 1)) : null;
26770
+ for (let i = 0; i < n && !stopped; i++) {
26771
+ const f = bytes.readInt16LE(i * 2) / 32768;
26772
+ ring[total % ringLen] = f;
26773
+ total++;
26774
+ win[winFill++] = f;
26775
+ if (winFill === VAD_WINDOW) {
26776
+ winFill = 0;
26777
+ feed(win);
26778
+ }
26779
+ }
26780
+ } catch (err) {
26781
+ failStream(new SttError("engine", `VAD failed: ${errMsg2(err)}`));
26782
+ }
26783
+ },
26784
+ end() {
26785
+ if (!stopped) {
26786
+ try {
26787
+ if (winFill > 0) {
26788
+ win.fill(0, winFill);
26789
+ winFill = 0;
26790
+ feed(win);
26791
+ }
26792
+ vad.flush();
26793
+ drainVad();
26794
+ } catch (err) {
26795
+ failStream(new SttError("engine", `VAD failed: ${errMsg2(err)}`));
26796
+ }
26797
+ }
26798
+ return new Promise((resolve2) => {
26799
+ drained = resolve2;
26800
+ checkDrain();
26801
+ });
26802
+ },
26803
+ abort() {
26804
+ stopped = true;
26805
+ dropQueued();
26806
+ checkDrain();
26807
+ },
26808
+ stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE) })
26809
+ };
26810
+ }
26811
+ return { load, open };
26812
+ }
26813
+ var SAMPLE_RATE, VAD_WINDOW, VAD_THRESHOLD, VAD_MIN_SILENCE_S, VAD_MIN_SPEECH_S, VAD_MAX_SPEECH_S, PAD_BEFORE_S, PAD_AFTER_S, RING_S, MAX_QUEUED_SEGMENTS, MAX_PENDING_AUDIO_S, NUM_THREADS, SttError, defaultLoadSherpa, errMsg2, round3, sttEngine;
26814
+ var init_engine = __esm({
26815
+ "../server/dist/stt/engine.js"() {
26816
+ "use strict";
26817
+ init_models();
26818
+ SAMPLE_RATE = 16e3;
26819
+ VAD_WINDOW = 512;
26820
+ VAD_THRESHOLD = 0.5;
26821
+ VAD_MIN_SILENCE_S = 0.5;
26822
+ VAD_MIN_SPEECH_S = 0.25;
26823
+ VAD_MAX_SPEECH_S = 20;
26824
+ PAD_BEFORE_S = 0.3;
26825
+ PAD_AFTER_S = 0.2;
26826
+ RING_S = 30;
26827
+ MAX_QUEUED_SEGMENTS = 32;
26828
+ MAX_PENDING_AUDIO_S = 60;
26829
+ NUM_THREADS = Math.min(4, Math.max(1, Math.floor(os8.availableParallelism() / 2)));
26830
+ SttError = class extends Error {
26831
+ code;
26832
+ constructor(code, message) {
26833
+ super(message);
26834
+ this.code = code;
26835
+ this.name = "SttError";
26836
+ }
26837
+ };
26838
+ defaultLoadSherpa = async () => (await import("sherpa-onnx-node")).default;
26839
+ errMsg2 = (err) => err instanceof Error ? err.message : String(err);
26840
+ round3 = (s) => Math.round(s * 1e3) / 1e3;
26841
+ sttEngine = createSttEngine();
26842
+ }
26843
+ });
26844
+
26845
+ // ../server/dist/routes/stt-ws.js
26846
+ import { WebSocketServer as WebSocketServer2 } from "ws";
26847
+ function parseQuery2(req) {
26848
+ const url = req.url ?? "";
26849
+ const i = url.indexOf("?");
26850
+ return new URLSearchParams(i < 0 ? "" : url.slice(i + 1));
26851
+ }
26852
+ function createSttProxy(deps = {}) {
26853
+ const models = deps.models ?? sttModels;
26854
+ const engine = deps.engine ?? sttEngine;
26855
+ const wss2 = new WebSocketServer2({
26856
+ noServer: true,
26857
+ maxPayload: MAX_FRAME_BYTES2,
26858
+ // Same cookie gate as /ws; the extension iframe is same-origin with the
26859
+ // admin, so its WS handshake carries the login cookie.
26860
+ verifyClient: (info, callback) => {
26861
+ if (!isAuthenticated(getTokenFromCookieHeader(info.req.headers.cookie)))
26862
+ return callback(false, 401, "Unauthorized");
26863
+ const ext = getExtension(parseQuery2(info.req).get("ext") ?? "");
26864
+ if (!ext || !ext.capabilities.includes("stt-local"))
26865
+ return callback(false, 403, "Forbidden");
26866
+ callback(true);
26867
+ }
26868
+ });
26869
+ wss2.on("connection", (ws, req) => {
26870
+ const extId = parseQuery2(req).get("ext");
26871
+ const openedAt = Date.now();
26872
+ let closed = false;
26873
+ let failed = false;
26874
+ let ended = false;
26875
+ let stream = null;
26876
+ let unsubscribe = null;
26877
+ ws.on("error", (err) => {
26878
+ console.log(`[STT] client error ext=${extId}: ${err.message}`);
26879
+ });
26880
+ const send2 = (frame) => {
26881
+ if (ws.readyState === ws.OPEN)
26882
+ ws.send(JSON.stringify(frame));
26883
+ };
26884
+ const fail2 = (code, message) => {
26885
+ if (failed || closed)
26886
+ return;
26887
+ failed = true;
26888
+ console.log(`[STT] stream error ext=${extId} code=${code}`);
26889
+ send2({ type: "error", code, message });
26890
+ ws.close(1011, code);
26891
+ };
26892
+ async function startEngine() {
26893
+ try {
26894
+ await engine.load();
26895
+ if (closed || failed || ended)
26896
+ return;
26897
+ stream = engine.open({
26898
+ // Recognized text goes to the client only — never to the log.
26899
+ onFinal: (f) => send2({ type: "final", ...f }),
26900
+ onError: (err) => fail2(err.code, err.message)
26901
+ });
26902
+ } catch (err) {
26903
+ if (err instanceof SttError)
26904
+ fail2(err.code, err.message);
26905
+ else
26906
+ fail2("io", err instanceof Error ? err.message : String(err));
26907
+ return;
26908
+ }
26909
+ if (closed || failed)
26910
+ return;
26911
+ send2({ type: "ready" });
26912
+ }
26913
+ const onModel = (s) => {
26914
+ if (closed || failed)
26915
+ return;
26916
+ send2({ type: "model", ...s });
26917
+ if (s.state === "ready" || s.state === "error") {
26918
+ unsubscribe?.();
26919
+ unsubscribe = null;
26920
+ }
26921
+ if (s.state === "ready")
26922
+ void startEngine();
26923
+ else if (s.state === "error")
26924
+ fail2("model", s.message ?? "model download failed");
26925
+ };
26926
+ console.log(`[STT] stream open ext=${extId}`);
26927
+ ws.on("message", (data, isBinary) => {
26928
+ if (ended || failed)
26929
+ return;
26930
+ if (isBinary) {
26931
+ if (!stream)
26932
+ return;
26933
+ stream.write(Buffer.isBuffer(data) ? data : Buffer.concat(Array.isArray(data) ? data : [Buffer.from(data)]));
26934
+ return;
26935
+ }
26936
+ let msg;
26937
+ try {
26938
+ msg = JSON.parse(data.toString());
26939
+ } catch {
26940
+ msg = null;
26941
+ }
26942
+ if (msg?.type !== "end") {
26943
+ fail2("bad-request", 'expected binary PCM or {"type":"end"}');
26944
+ return;
26945
+ }
26946
+ ended = true;
26947
+ if (!stream) {
26948
+ ws.close(1e3);
26949
+ return;
26950
+ }
26951
+ void stream.end().then(() => {
26952
+ if (!closed && !failed)
26953
+ ws.close(1e3);
26954
+ });
26955
+ });
26956
+ ws.on("close", () => {
26957
+ closed = true;
26958
+ unsubscribe?.();
26959
+ unsubscribe = null;
26960
+ stream?.abort();
26961
+ const st = stream?.stats();
26962
+ console.log(`[STT] stream closed after ${((Date.now() - openedAt) / 1e3).toFixed(1)}s ext=${extId}${st ? ` segments=${st.segments} audio=${st.audioSeconds.toFixed(1)}s` : ""}`);
26963
+ });
26964
+ const initial = models.status();
26965
+ if (initial.state === "ready") {
26966
+ onModel(initial);
26967
+ return;
26968
+ }
26969
+ let pushed = false;
26970
+ unsubscribe = models.subscribe((s) => {
26971
+ pushed = true;
26972
+ onModel(s);
26973
+ });
26974
+ void models.ensure();
26975
+ if (!pushed)
26976
+ onModel(models.status());
26977
+ });
26978
+ return {
26979
+ handleUpgrade(req, socket, head) {
26980
+ wss2.handleUpgrade(req, socket, head, (ws) => wss2.emit("connection", ws, req));
26981
+ },
26982
+ close() {
26983
+ models.close();
26984
+ for (const client of wss2.clients)
26985
+ client.close(1001, "Server shutting down");
26986
+ wss2.close();
26987
+ }
26988
+ };
26989
+ }
26990
+ var STT_PATH, MAX_FRAME_BYTES2;
26991
+ var init_stt_ws = __esm({
26992
+ "../server/dist/routes/stt-ws.js"() {
26993
+ "use strict";
26994
+ init_auth();
26995
+ init_registry2();
26996
+ init_models();
26997
+ init_engine();
26998
+ STT_PATH = "/api/stt/stream";
26999
+ MAX_FRAME_BYTES2 = 1024 * 1024;
27000
+ }
27001
+ });
27002
+
26220
27003
  // ../server/dist/jupyter/manager.js
26221
27004
  import { spawn as nodeSpawn } from "node:child_process";
26222
27005
  import { randomBytes as randomBytes2 } from "node:crypto";
26223
- import fs60 from "node:fs";
27006
+ import fs61 from "node:fs";
26224
27007
  import net from "node:net";
26225
- import os8 from "node:os";
26226
- import path68 from "node:path";
27008
+ import os9 from "node:os";
27009
+ import path69 from "node:path";
26227
27010
  import { WebSocket } from "ws";
26228
27011
  function resolveExecutable(configured) {
26229
27012
  const isExe = (p) => {
26230
27013
  try {
26231
- fs60.accessSync(p, fs60.constants.X_OK);
26232
- return fs60.statSync(p).isFile();
27014
+ fs61.accessSync(p, fs61.constants.X_OK);
27015
+ return fs61.statSync(p).isFile();
26233
27016
  } catch {
26234
27017
  return false;
26235
27018
  }
@@ -26237,9 +27020,9 @@ function resolveExecutable(configured) {
26237
27020
  if (configured)
26238
27021
  return isExe(configured) ? configured : null;
26239
27022
  const name = process.platform === "win32" ? "jupyter.exe" : "jupyter";
26240
- for (const dir of (process.env.PATH ?? "").split(path68.delimiter)) {
26241
- if (dir && isExe(path68.join(dir, name)))
26242
- return path68.join(dir, name);
27023
+ for (const dir of (process.env.PATH ?? "").split(path69.delimiter)) {
27024
+ if (dir && isExe(path69.join(dir, name)))
27025
+ return path69.join(dir, name);
26243
27026
  }
26244
27027
  return null;
26245
27028
  }
@@ -26267,9 +27050,9 @@ function createJupyterManager(deps = {}) {
26267
27050
  let err = "";
26268
27051
  let child;
26269
27052
  try {
26270
- child = spawnFn(exe, ["server", "--version"], { cwd: os8.homedir(), env: cleanChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
27053
+ child = spawnFn(exe, ["server", "--version"], { cwd: os9.homedir(), env: cleanChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
26271
27054
  } catch (e) {
26272
- resolve2({ ok: false, error: errMsg2(e) });
27055
+ resolve2({ ok: false, error: errMsg3(e) });
26273
27056
  return;
26274
27057
  }
26275
27058
  const timer2 = setTimeout(() => child.kill(), PROBE_TIMEOUT_MS);
@@ -26350,7 +27133,7 @@ function createJupyterManager(deps = {}) {
26350
27133
  "--ServerApp.ip=127.0.0.1",
26351
27134
  `--ServerApp.port=${port}`,
26352
27135
  "--ServerApp.port_retries=0",
26353
- `--ServerApp.root_dir=${path68.parse(process.cwd()).root}`,
27136
+ `--ServerApp.root_dir=${path69.parse(process.cwd()).root}`,
26354
27137
  "--ServerApp.terminals_enabled=False",
26355
27138
  "--ServerApp.shutdown_no_activity_timeout=600",
26356
27139
  "--MappingKernelManager.cull_idle_timeout=3600",
@@ -26360,7 +27143,7 @@ function createJupyterManager(deps = {}) {
26360
27143
  // its terminal does too, and kernels match the terminal.
26361
27144
  ...process.getuid?.() === 0 ? ["--ServerApp.allow_root=True"] : []
26362
27145
  ];
26363
- const child = spawnFn(exe, args, { cwd: os8.homedir(), env: { ...cleanChildEnv(), JUPYTER_TOKEN: token }, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
27146
+ const child = spawnFn(exe, args, { cwd: os9.homedir(), env: { ...cleanChildEnv(), JUPYTER_TOKEN: token }, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
26364
27147
  proc = child;
26365
27148
  let tail = "";
26366
27149
  child.stderr?.on("data", (d) => {
@@ -26428,7 +27211,7 @@ function createJupyterManager(deps = {}) {
26428
27211
  if (kernels.get(id) !== k || k.sockets.size > 0)
26429
27212
  return;
26430
27213
  console.log(`[Jupyter] kernel ${id}: no client for ${KERNEL_GRACE_MS / 1e3}s, shutting it down`);
26431
- kernelOp(id, "delete").catch((e) => console.log(`[Jupyter] kernel ${id} shutdown failed: ${errMsg2(e)}`));
27214
+ kernelOp(id, "delete").catch((e) => console.log(`[Jupyter] kernel ${id} shutdown failed: ${errMsg3(e)}`));
26432
27215
  }, KERNEL_GRACE_MS);
26433
27216
  k.grace.unref?.();
26434
27217
  }
@@ -26538,7 +27321,7 @@ function createJupyterManager(deps = {}) {
26538
27321
  }
26539
27322
  };
26540
27323
  }
26541
- var JUPYTER_HINT, KERNEL_GRACE_MS, START_TIMEOUT_MS, PROBE_TIMEOUT_MS, STOP_TIMEOUT_MS, JupyterError, errMsg2, lastLine;
27324
+ var JUPYTER_HINT, KERNEL_GRACE_MS, START_TIMEOUT_MS, PROBE_TIMEOUT_MS, STOP_TIMEOUT_MS, JupyterError, errMsg3, lastLine;
26542
27325
  var init_manager = __esm({
26543
27326
  "../server/dist/jupyter/manager.js"() {
26544
27327
  "use strict";
@@ -26558,16 +27341,16 @@ var init_manager = __esm({
26558
27341
  this.hint = hint;
26559
27342
  }
26560
27343
  };
26561
- errMsg2 = (err) => err instanceof Error ? err.message : String(err);
27344
+ errMsg3 = (err) => err instanceof Error ? err.message : String(err);
26562
27345
  lastLine = (s) => s.trim().split("\n").pop()?.trim() ?? "";
26563
27346
  }
26564
27347
  });
26565
27348
 
26566
27349
  // ../server/dist/routes/jupyter.js
26567
- import fs61 from "node:fs/promises";
26568
- import path69 from "node:path";
27350
+ import fs62 from "node:fs/promises";
27351
+ import path70 from "node:path";
26569
27352
  import { Hono as Hono15 } from "hono";
26570
- import { WebSocketServer as WebSocketServer2 } from "ws";
27353
+ import { WebSocketServer as WebSocketServer3 } from "ws";
26571
27354
  function fail(c, err) {
26572
27355
  if (err instanceof JupyterError)
26573
27356
  return c.json({ error: err.message, ...err.hint ? { hint: err.hint } : {} }, err.status);
@@ -26596,8 +27379,8 @@ function createJupyterRoutes(jm) {
26596
27379
  return c.json({ error: "Project not found" }, 400);
26597
27380
  if (!validatePath(nbPath, projectPath))
26598
27381
  return c.json({ error: "Path outside the workspace" }, 400);
26599
- const cwd = await fs61.realpath(path69.dirname(path69.resolve(projectPath, nbPath))).catch(() => null);
26600
- if (!cwd || !(await fs61.stat(cwd)).isDirectory())
27382
+ const cwd = await fs62.realpath(path70.dirname(path70.resolve(projectPath, nbPath))).catch(() => null);
27383
+ if (!cwd || !(await fs62.stat(cwd)).isDirectory())
26601
27384
  return c.json({ error: "Notebook directory not found" }, 400);
26602
27385
  try {
26603
27386
  return c.json(await jm.startKernel(name, cwd));
@@ -26641,9 +27424,9 @@ function queryOf(req) {
26641
27424
  return new URLSearchParams(i < 0 ? "" : url.slice(i + 1));
26642
27425
  }
26643
27426
  function createJupyterChannels(jm) {
26644
- const wss2 = new WebSocketServer2({
27427
+ const wss2 = new WebSocketServer3({
26645
27428
  noServer: true,
26646
- maxPayload: MAX_FRAME_BYTES2,
27429
+ maxPayload: MAX_FRAME_BYTES3,
26647
27430
  // The client speaks the legacy JSON protocol; never echo a subprotocol.
26648
27431
  handleProtocols: () => false,
26649
27432
  // Same cookie gate as /ws and the transcribe proxy.
@@ -26737,7 +27520,7 @@ function createJupyterChannels(jm) {
26737
27520
  }
26738
27521
  };
26739
27522
  }
26740
- var JUPYTER_CHANNELS_PATH, KERNEL_NAME_RE, SESSION_ID_RE, MAX_FRAME_BYTES2;
27523
+ var JUPYTER_CHANNELS_PATH, KERNEL_NAME_RE, SESSION_ID_RE, MAX_FRAME_BYTES3;
26741
27524
  var init_jupyter = __esm({
26742
27525
  "../server/dist/routes/jupyter.js"() {
26743
27526
  "use strict";
@@ -26747,7 +27530,7 @@ var init_jupyter = __esm({
26747
27530
  JUPYTER_CHANNELS_PATH = "/api/jupyter/channels";
26748
27531
  KERNEL_NAME_RE = /^[\w.-]{1,128}$/;
26749
27532
  SESSION_ID_RE = /^[\w-]{1,128}$/;
26750
- MAX_FRAME_BYTES2 = 64 * 1024 * 1024;
27533
+ MAX_FRAME_BYTES3 = 64 * 1024 * 1024;
26751
27534
  }
26752
27535
  });
26753
27536
 
@@ -26768,8 +27551,8 @@ var init_upgrade_router = __esm({
26768
27551
  });
26769
27552
 
26770
27553
  // ../server/dist/ws/git-dir-watcher.js
26771
- import fs62 from "node:fs";
26772
- import path70 from "node:path";
27554
+ import fs63 from "node:fs";
27555
+ import path71 from "node:path";
26773
27556
  var GitDirWatcher;
26774
27557
  var init_git_dir_watcher = __esm({
26775
27558
  "../server/dist/ws/git-dir-watcher.js"() {
@@ -26793,8 +27576,8 @@ var init_git_dir_watcher = __esm({
26793
27576
  return;
26794
27577
  this.stop();
26795
27578
  this.workspaceRoot = workspaceRoot;
26796
- const gitDir = path70.join(workspaceRoot, ".git");
26797
- if (fs62.existsSync(gitDir)) {
27579
+ const gitDir = path71.join(workspaceRoot, ".git");
27580
+ if (fs63.existsSync(gitDir)) {
26798
27581
  this.watchGitDir(gitDir);
26799
27582
  } else {
26800
27583
  this.watchParentForGit(workspaceRoot);
@@ -26803,7 +27586,7 @@ var init_git_dir_watcher = __esm({
26803
27586
  /** Watch the `.git` dir for index/HEAD changes (steady state). */
26804
27587
  watchGitDir(gitDir) {
26805
27588
  try {
26806
- this.watcher = fs62.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
27589
+ this.watcher = fs63.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
26807
27590
  this.handleGitEvent(filename);
26808
27591
  });
26809
27592
  this.watcher.on("error", (err) => {
@@ -26822,7 +27605,7 @@ var init_git_dir_watcher = __esm({
26822
27605
  * Exists solely during the non-repo window; upgraded away on first `.git`. */
26823
27606
  watchParentForGit(workspaceRoot) {
26824
27607
  try {
26825
- this.watcher = fs62.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
27608
+ this.watcher = fs63.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
26826
27609
  this.handleParentEvent(filename);
26827
27610
  });
26828
27611
  this.watcher.on("error", (err) => {
@@ -26858,8 +27641,8 @@ var init_git_dir_watcher = __esm({
26858
27641
  const root = this.workspaceRoot;
26859
27642
  if (!root)
26860
27643
  return;
26861
- const gitDir = path70.join(root, ".git");
26862
- if (!fs62.existsSync(gitDir))
27644
+ const gitDir = path71.join(root, ".git");
27645
+ if (!fs63.existsSync(gitDir))
26863
27646
  return;
26864
27647
  this.closeWatcher();
26865
27648
  this.watchGitDir(gitDir);
@@ -27277,7 +28060,7 @@ var init_terminal_manager = __esm({
27277
28060
  });
27278
28061
 
27279
28062
  // ../server/dist/ws/handler.js
27280
- import path71 from "node:path";
28063
+ import path72 from "node:path";
27281
28064
  function setupWebSocketHandler(deps) {
27282
28065
  const { wss: wss2, registry: registry2 } = deps;
27283
28066
  const clients = /* @__PURE__ */ new Set();
@@ -27300,7 +28083,7 @@ function setupWebSocketHandler(deps) {
27300
28083
  ackedChatIds.add(id);
27301
28084
  }
27302
28085
  function resolveProjectPath2(projectId) {
27303
- if (path71.isAbsolute(projectId))
28086
+ if (path72.isAbsolute(projectId))
27304
28087
  return projectId;
27305
28088
  return null;
27306
28089
  }
@@ -27901,11 +28684,11 @@ var init_handler2 = __esm({
27901
28684
  import Database8 from "better-sqlite3";
27902
28685
  import { drizzle as drizzle5 } from "drizzle-orm/better-sqlite3";
27903
28686
  import { sqliteTable as sqliteTable5, text as text5, integer as integer5 } from "drizzle-orm/sqlite-core";
27904
- import path72 from "node:path";
27905
- import fs63 from "node:fs";
28687
+ import path73 from "node:path";
28688
+ import fs64 from "node:fs";
27906
28689
  function createCronDb(globalDir2) {
27907
- fs63.mkdirSync(globalDir2, { recursive: true });
27908
- const dbPath = path72.join(globalDir2, "cron.db");
28690
+ fs64.mkdirSync(globalDir2, { recursive: true });
28691
+ const dbPath = path73.join(globalDir2, "cron.db");
27909
28692
  const sqlite = new Database8(dbPath);
27910
28693
  sqlite.pragma("journal_mode = WAL");
27911
28694
  sqlite.exec(CREATE_SQL5);
@@ -28015,7 +28798,12 @@ var init_cron_db = __esm({
28015
28798
  /** Session this run's cli was spawned on (`-s`). The orphan sweep's
28016
28799
  * fingerprint — a stateless job's per-run id can't be re-derived from
28017
28800
  * the job row. NULL on skipped rows and rows from before the column. */
28018
- sessionId: text5("session_id")
28801
+ sessionId: text5("session_id"),
28802
+ /** Workspace the run fired against (the job's at fire time) — where a
28803
+ * stateless run's session lives, even after the job moves workspace or is
28804
+ * deleted. NULL on rows from before the column whose job was already gone
28805
+ * (deleteRuns falls back to the job row). */
28806
+ workspacePath: text5("workspace_path")
28019
28807
  });
28020
28808
  CREATE_SQL5 = `
28021
28809
  CREATE TABLE IF NOT EXISTS cron_jobs (
@@ -28057,7 +28845,8 @@ CREATE TABLE IF NOT EXISTS cron_runs (
28057
28845
  log_path TEXT,
28058
28846
  dispatch_results TEXT,
28059
28847
  pid INTEGER,
28060
- session_id TEXT
28848
+ session_id TEXT,
28849
+ workspace_path TEXT
28061
28850
  );
28062
28851
  CREATE INDEX IF NOT EXISTS idx_cron_runs_job ON cron_runs(job_id, started_at);
28063
28852
  CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
@@ -28098,6 +28887,15 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
28098
28887
  addColumnIfMissing(s, "cron_jobs", "working_dir", "TEXT");
28099
28888
  addColumnIfMissing(s, "cron_jobs", "stateless", "INTEGER NOT NULL DEFAULT 0");
28100
28889
  addColumnIfMissing(s, "cron_runs", "session_id", "TEXT");
28890
+ },
28891
+ // v6: `cron_runs.workspace_path` — the workspace a run's session is in.
28892
+ // Backfilled from the job row; runs of an already-deleted job stay NULL.
28893
+ (s) => {
28894
+ addColumnIfMissing(s, "cron_runs", "workspace_path", "TEXT");
28895
+ s.exec(`
28896
+ UPDATE cron_runs SET workspace_path = (SELECT workspace_path FROM cron_jobs WHERE cron_jobs.id = cron_runs.job_id)
28897
+ WHERE workspace_path IS NULL
28898
+ `);
28101
28899
  }
28102
28900
  ];
28103
28901
  _cronDb = null;
@@ -28105,9 +28903,9 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
28105
28903
  });
28106
28904
 
28107
28905
  // ../server/dist/a2a/exposure.js
28108
- import fs64 from "node:fs";
28109
- import path73 from "node:path";
28110
- import crypto6 from "node:crypto";
28906
+ import fs65 from "node:fs";
28907
+ import path74 from "node:path";
28908
+ import crypto7 from "node:crypto";
28111
28909
  function samePath(a, b, platform = process.platform) {
28112
28910
  if (platform !== "win32")
28113
28911
  return a === b;
@@ -28119,7 +28917,7 @@ function realBase() {
28119
28917
  if (baseCache?.raw !== raw) {
28120
28918
  let real = null;
28121
28919
  try {
28122
- real = fs64.realpathSync(raw);
28920
+ real = fs65.realpathSync(raw);
28123
28921
  } catch {
28124
28922
  console.warn(`[A2A] base dir ${raw} not found \u2014 nothing is exposed`);
28125
28923
  }
@@ -28144,14 +28942,14 @@ function resolveExposedWorkspace(base, rel) {
28144
28942
  }
28145
28943
  let real;
28146
28944
  try {
28147
- real = fs64.realpathSync(path73.join(base, ...segs));
28945
+ real = fs65.realpathSync(path74.join(base, ...segs));
28148
28946
  } catch {
28149
28947
  return null;
28150
28948
  }
28151
- if (!real.startsWith(base.endsWith(path73.sep) ? base : base + path73.sep))
28949
+ if (!real.startsWith(base.endsWith(path74.sep) ? base : base + path74.sep))
28152
28950
  return null;
28153
28951
  try {
28154
- if (!fs64.statSync(path73.join(real, CARD_FILE)).isFile())
28952
+ if (!fs65.statSync(path74.join(real, CARD_FILE)).isFile())
28155
28953
  return null;
28156
28954
  } catch {
28157
28955
  return null;
@@ -28166,14 +28964,14 @@ function publicOrigin(c) {
28166
28964
  return `${proto}://${c.req.header("host") ?? url.host}`;
28167
28965
  }
28168
28966
  function buildCard(workspace, interfaceUrl, caps) {
28169
- const raw = JSON.parse(fs64.readFileSync(path73.join(workspace, CARD_FILE), "utf8"));
28967
+ const raw = JSON.parse(fs65.readFileSync(path74.join(workspace, CARD_FILE), "utf8"));
28170
28968
  if (typeof raw.name !== "string" || typeof raw.description !== "string" || !Array.isArray(raw.skills)) {
28171
28969
  throw new Error('agent-card.json needs string "name", string "description" and array "skills"');
28172
28970
  }
28173
28971
  return {
28174
28972
  name: raw.name,
28175
28973
  description: raw.description,
28176
- version: typeof raw.version === "string" ? raw.version : "1.6.4",
28974
+ version: typeof raw.version === "string" ? raw.version : "1.6.6",
28177
28975
  supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
28178
28976
  capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
28179
28977
  ...caps.tokenAuth === false ? {} : {
@@ -28189,7 +28987,7 @@ function buildCard(workspace, interfaceUrl, caps) {
28189
28987
  };
28190
28988
  }
28191
28989
  function etagOf(body) {
28192
- return `"${crypto6.createHash("sha1").update(body).digest("base64url")}"`;
28990
+ return `W/"${crypto7.createHash("sha1").update(body).digest("base64url")}"`;
28193
28991
  }
28194
28992
  var CARD_FILE, IO_MODES, baseCache, homeStrategies;
28195
28993
  var init_exposure = __esm({
@@ -28200,7 +28998,7 @@ var init_exposure = __esm({
28200
28998
  init_channel_db();
28201
28999
  init_media_store();
28202
29000
  init_wire();
28203
- CARD_FILE = path73.join(".halo", "agent-card.json");
29001
+ CARD_FILE = path74.join(".halo", "agent-card.json");
28204
29002
  IO_MODES = ["text/plain", ...VISION_IMAGE_MIME_TYPES];
28205
29003
  baseCache = null;
28206
29004
  homeStrategies = {
@@ -28214,7 +29012,7 @@ var init_exposure = __esm({
28214
29012
  return { ok: false, status: 404 };
28215
29013
  let bound;
28216
29014
  try {
28217
- bound = fs64.realpathSync(auth.account.workspacePath);
29015
+ bound = fs65.realpathSync(auth.account.workspacePath);
28218
29016
  } catch {
28219
29017
  return { ok: false, status: 404 };
28220
29018
  }
@@ -28232,7 +29030,7 @@ var init_exposure = __esm({
28232
29030
  // ../server/dist/a2a/routes.js
28233
29031
  import { Hono as Hono16 } from "hono";
28234
29032
  import { streamSSE } from "hono/streaming";
28235
- import crypto7 from "node:crypto";
29033
+ import crypto8 from "node:crypto";
28236
29034
  function str(v) {
28237
29035
  return typeof v === "string" ? v : "";
28238
29036
  }
@@ -28342,7 +29140,7 @@ async function dispatchMessage(x, p) {
28342
29140
  }
28343
29141
  const newSession = !sessionId;
28344
29142
  if (newSession)
28345
- sessionId = `${sessionPrefixFor(x.caller)}${Date.now().toString(36)}${crypto7.randomBytes(3).toString("hex")}`;
29143
+ sessionId = `${sessionPrefixFor(x.caller)}${Date.now().toString(36)}${crypto8.randomBytes(3).toString("hex")}`;
28346
29144
  const followUp = task !== null;
28347
29145
  if (!task) {
28348
29146
  try {
@@ -28546,7 +29344,7 @@ function createA2ARoutes(deps) {
28546
29344
  const etag = etagOf(body);
28547
29345
  c.header("Cache-Control", "private, max-age=300");
28548
29346
  c.header("ETag", etag);
28549
- if (c.req.header("if-none-match") === etag)
29347
+ if (etagMatches(c.req.header("if-none-match"), etag))
28550
29348
  return c.body(null, 304);
28551
29349
  return c.body(body, 200, { "Content-Type": "application/json" });
28552
29350
  });
@@ -28621,6 +29419,7 @@ var init_routes = __esm({
28621
29419
  init_commands2();
28622
29420
  init_accounts();
28623
29421
  init_session_prefix();
29422
+ init_etag();
28624
29423
  init_url_policy();
28625
29424
  init_files();
28626
29425
  init_exposure();
@@ -28656,17 +29455,17 @@ var init_routes = __esm({
28656
29455
  });
28657
29456
 
28658
29457
  // ../server/dist/a2a/agentcore.js
28659
- import fs65 from "node:fs";
28660
- import os9 from "node:os";
28661
- import path74 from "node:path";
28662
- import crypto8 from "node:crypto";
29458
+ import fs66 from "node:fs";
29459
+ import os10 from "node:os";
29460
+ import path75 from "node:path";
29461
+ import crypto9 from "node:crypto";
28663
29462
  import { Hono as Hono17 } from "hono";
28664
29463
  function readLease(file) {
28665
29464
  let raw;
28666
29465
  let mtimeMs;
28667
29466
  try {
28668
- raw = fs65.readFileSync(file, "utf8");
28669
- mtimeMs = fs65.statSync(file).mtimeMs;
29467
+ raw = fs66.readFileSync(file, "utf8");
29468
+ mtimeMs = fs66.statSync(file).mtimeMs;
28670
29469
  } catch (err) {
28671
29470
  if (err.code === "ENOENT")
28672
29471
  return null;
@@ -28680,16 +29479,16 @@ function readLease(file) {
28680
29479
  }
28681
29480
  }
28682
29481
  function leaseJson(owner, now) {
28683
- return JSON.stringify({ owner, host: os9.hostname(), pid: process.pid, heartbeatAt: now });
29482
+ return JSON.stringify({ owner, host: os10.hostname(), pid: process.pid, heartbeatAt: now });
28684
29483
  }
28685
29484
  function writeLease(file, owner, now) {
28686
29485
  const tmp = `${file}.${owner}.tmp`;
28687
- fs65.writeFileSync(tmp, leaseJson(owner, now));
28688
- fs65.renameSync(tmp, file);
29486
+ fs66.writeFileSync(tmp, leaseJson(owner, now));
29487
+ fs66.renameSync(tmp, file);
28689
29488
  }
28690
29489
  function tryAcquireLease(file, owner, now = Date.now()) {
28691
29490
  try {
28692
- fs65.writeFileSync(file, leaseJson(owner, now), { flag: "wx" });
29491
+ fs66.writeFileSync(file, leaseJson(owner, now), { flag: "wx" });
28693
29492
  return "held";
28694
29493
  } catch (err) {
28695
29494
  if (err.code !== "EEXIST")
@@ -28717,7 +29516,7 @@ function heartbeatLease(file, owner, now = Date.now()) {
28717
29516
  function releaseLease(file, owner) {
28718
29517
  try {
28719
29518
  if (readLease(file)?.body?.owner === owner)
28720
- fs65.unlinkSync(file);
29519
+ fs66.unlinkSync(file);
28721
29520
  } catch {
28722
29521
  }
28723
29522
  }
@@ -28739,7 +29538,7 @@ function upstreamStrategies(workspace, accessLevel) {
28739
29538
  function seedAgentCard(workspace) {
28740
29539
  const card = { name: "Halo", description: "Halo multi-agent workspace on Amazon Bedrock AgentCore. Send a task as plain text.", skills: [] };
28741
29540
  try {
28742
- fs65.writeFileSync(path74.join(workspace, CARD_FILE), `${JSON.stringify(card, null, 2)}
29541
+ fs66.writeFileSync(path75.join(workspace, CARD_FILE), `${JSON.stringify(card, null, 2)}
28743
29542
  `, { flag: "wx" });
28744
29543
  return true;
28745
29544
  } catch (err) {
@@ -28787,16 +29586,16 @@ function createAgentCoreA2ARoutes(deps) {
28787
29586
  return app2;
28788
29587
  }
28789
29588
  function createAgentCoreA2A(registry2, rawWorkspace, onLost) {
28790
- const workspace = fs65.realpathSync(rawWorkspace);
28791
- const haloDir = path74.join(workspace, ".halo");
29589
+ const workspace = fs66.realpathSync(rawWorkspace);
29590
+ const haloDir = path75.join(workspace, ".halo");
28792
29591
  if (!config.a2a.publicUrl)
28793
29592
  console.warn("[AgentCoreA2A] HALO_A2A_PUBLIC_URL is not set \u2014 the card advertises the container's own URL, not the AgentCore invoke URL");
28794
29593
  const accessLevel = upstreamAccessLevel();
28795
- const lease = new WorkspaceLease(path74.join(haloDir, LEASE_FILE), {
29594
+ const lease = new WorkspaceLease(path75.join(haloDir, LEASE_FILE), {
28796
29595
  onAcquired() {
28797
29596
  setA2ADb(createA2ADb(haloDir));
28798
29597
  setRunsDb(createRunsDb(haloDir));
28799
- fs65.rmSync(path74.join(haloDir, RUNTIME_LOCK_FILE), { force: true });
29598
+ fs66.rmSync(path75.join(haloDir, RUNTIME_LOCK_FILE), { force: true });
28800
29599
  registry2.getOrCreate(workspace);
28801
29600
  startPushSender({ maxAgeMs: PUSH_MAX_AGE_MS });
28802
29601
  reconcileOpenDispatches();
@@ -28810,9 +29609,9 @@ function createAgentCoreA2A(registry2, rawWorkspace, onLost) {
28810
29609
  const activate = () => {
28811
29610
  if (active)
28812
29611
  return;
28813
- fs65.mkdirSync(haloDir, { recursive: true });
29612
+ fs66.mkdirSync(haloDir, { recursive: true });
28814
29613
  if (seedAgentCard(workspace))
28815
- console.log(`[AgentCoreA2A] seeded ${path74.join(workspace, CARD_FILE)}`);
29614
+ console.log(`[AgentCoreA2A] seeded ${path75.join(workspace, CARD_FILE)}`);
28816
29615
  active = true;
28817
29616
  };
28818
29617
  return { app: createAgentCoreA2ARoutes({ registry: registry2, workspace, accessLevel, lease, activate }), lease, workspace };
@@ -28842,7 +29641,7 @@ var init_agentcore = __esm({
28842
29641
  WorkspaceLease = class {
28843
29642
  file;
28844
29643
  hooks;
28845
- owner = crypto8.randomUUID();
29644
+ owner = crypto9.randomUUID();
28846
29645
  _held = false;
28847
29646
  timer = null;
28848
29647
  waitLogged = false;
@@ -28977,12 +29776,12 @@ var init_dispatcher = __esm({
28977
29776
 
28978
29777
  // ../server/dist/cron/runner.js
28979
29778
  import { spawn as spawn3, execFileSync } from "node:child_process";
28980
- import fs66 from "node:fs";
28981
- import path75 from "node:path";
29779
+ import fs67 from "node:fs";
29780
+ import path76 from "node:path";
28982
29781
  import { homedir as homedir24 } from "node:os";
28983
29782
  import { Cron } from "croner";
28984
29783
  import Database9 from "better-sqlite3";
28985
- import { and as and11, eq as eq14, gte as gte3, inArray as inArray2, lt as lt5, or as or2 } from "drizzle-orm";
29784
+ import { and as and11, eq as eq14, gte as gte3, inArray as inArray2, lt as lt5, notInArray, or as or2 } from "drizzle-orm";
28986
29785
  function resolveHaloCli() {
28987
29786
  if (process.env.HALO_CLI)
28988
29787
  return process.env.HALO_CLI;
@@ -29083,9 +29882,9 @@ function jobChangedFields(job) {
29083
29882
  function sessionKeyFor(workspacePath, sessionId) {
29084
29883
  let ws;
29085
29884
  try {
29086
- ws = fs66.realpathSync(workspacePath);
29885
+ ws = fs67.realpathSync(workspacePath);
29087
29886
  } catch {
29088
- ws = path75.resolve(workspacePath);
29887
+ ws = path76.resolve(workspacePath);
29089
29888
  }
29090
29889
  return `${ws}
29091
29890
  ${sessionId}`;
@@ -29094,10 +29893,10 @@ function setCronSessionRegistry(r) {
29094
29893
  _sessionRegistry = r;
29095
29894
  }
29096
29895
  function logsDir() {
29097
- return path75.join(homedir24(), ".halo", "global", "logs", "cron");
29896
+ return path76.join(homedir24(), ".halo", "global", "logs", "cron");
29098
29897
  }
29099
29898
  function logPathFor(runId) {
29100
- return path75.join(logsDir(), `${runId}.log`);
29899
+ return path76.join(logsDir(), `${runId}.log`);
29101
29900
  }
29102
29901
  function newRunId() {
29103
29902
  return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`;
@@ -29158,7 +29957,7 @@ function sweepOrphanRuns(opts) {
29158
29957
  }
29159
29958
  }
29160
29959
  function startCronDaemon() {
29161
- fs66.mkdirSync(logsDir(), { recursive: true });
29960
+ fs67.mkdirSync(logsDir(), { recursive: true });
29162
29961
  sweepOrphanRuns();
29163
29962
  reloadAll();
29164
29963
  _daemonTimers.push(setInterval(() => {
@@ -29288,6 +30087,7 @@ function reconcileFromDb() {
29288
30087
  if (dv !== null && _lastCronDataVersion === dv)
29289
30088
  return;
29290
30089
  _lastCronDataVersion = dv;
30090
+ deleteOrphanRuns().catch((err) => console.warn(`[Cron] deleting runs of deleted jobs failed: ${err instanceof Error ? err.message : String(err)}`));
29291
30091
  const db3 = getCronDb();
29292
30092
  const rows = db3.select().from(cronJobs).all();
29293
30093
  const liveIds = /* @__PURE__ */ new Set();
@@ -29342,7 +30142,7 @@ async function runJob(jobId, triggerKind) {
29342
30142
  const runId = newRunId();
29343
30143
  const startedAt = Date.now();
29344
30144
  const logPath = logPathFor(runId);
29345
- fs66.mkdirSync(logsDir(), { recursive: true });
30145
+ fs67.mkdirSync(logsDir(), { recursive: true });
29346
30146
  const sessionId = job.stateless ? perRunSessionId(job, startedAt) : effectiveSessionId(job);
29347
30147
  const sessionKey = sessionKeyFor(job.workspacePath, sessionId);
29348
30148
  const host = _sessionRegistry?.peek(job.workspacePath);
@@ -29359,7 +30159,8 @@ async function runJob(jobId, triggerKind) {
29359
30159
  exitCode: null,
29360
30160
  failureReason: skipReason,
29361
30161
  logPath: null,
29362
- dispatchResults: null
30162
+ dispatchResults: null,
30163
+ workspacePath: job.workspacePath
29363
30164
  }).run();
29364
30165
  broadcast({ type: "cron:run_changed", jobId, runId, status: "skipped" });
29365
30166
  return runId;
@@ -29379,11 +30180,12 @@ async function runJob(jobId, triggerKind) {
29379
30180
  failureReason: null,
29380
30181
  logPath,
29381
30182
  dispatchResults: null,
29382
- sessionId
30183
+ sessionId,
30184
+ workspacePath: job.workspacePath
29383
30185
  }).run();
29384
30186
  broadcast({ type: "cron:run_changed", jobId, runId, status: "running" });
29385
30187
  const targets = parseTargets(job.targets);
29386
- if (!fs66.existsSync(job.workspacePath)) {
30188
+ if (!fs67.existsSync(job.workspacePath)) {
29387
30189
  finalize(runId, jobId, {
29388
30190
  status: "failed",
29389
30191
  output: null,
@@ -29426,7 +30228,7 @@ async function runJob(jobId, triggerKind) {
29426
30228
  args.push("--access", accessLevel);
29427
30229
  const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
29428
30230
  const timeoutSec = job.timeoutSec ?? CLI_TIMEOUT_SEC;
29429
- const teeStream = fs66.createWriteStream(logPath, { flags: "a" });
30231
+ const teeStream = fs67.createWriteStream(logPath, { flags: "a" });
29430
30232
  teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
29431
30233
  `);
29432
30234
  teeStream.write(`agent=${job.agentId} session=${sessionId}${job.stateless ? " (stateless)" : ""}${accessLevel ? ` access=${accessLevel}` : ""} workspace=${job.workspacePath} dir=${job.workingDir ?? "."} schedule=${job.schedule}
@@ -29552,8 +30354,8 @@ function readSessionRow(workspacePath, sessionId) {
29552
30354
  try {
29553
30355
  let entry = _sessionRows.get(workspacePath);
29554
30356
  if (!entry) {
29555
- const file = path75.join(workspacePath, ".halo", "halo.db");
29556
- if (!fs66.existsSync(file))
30357
+ const file = path76.join(workspacePath, ".halo", "halo.db");
30358
+ if (!fs67.existsSync(file))
29557
30359
  return null;
29558
30360
  entry = { db: new Database9(file, { readonly: true, fileMustExist: true }), version: -1, rows: /* @__PURE__ */ new Map() };
29559
30361
  _sessionRows.set(workspacePath, entry);
@@ -29624,10 +30426,14 @@ function parseTargets(raw) {
29624
30426
  async function pruneOldRuns(jobId) {
29625
30427
  const db3 = getCronDb();
29626
30428
  const job = db3.select({ stateless: cronJobs.stateless }).from(cronJobs).where(eq14(cronJobs.id, jobId)).get();
29627
- const keep = job?.stateless ? STATELESS_RUNS_PER_JOB_KEEP : RUNS_PER_JOB_KEEP;
30429
+ if (!job) {
30430
+ await deleteJobRuns(jobId);
30431
+ return;
30432
+ }
30433
+ const keep = job.stateless ? STATELESS_RUNS_PER_JOB_KEEP : RUNS_PER_JOB_KEEP;
29628
30434
  const doomed = rawSqlite(db3).prepare(`
29629
30435
  SELECT id, job_id AS jobId, session_id AS sessionId, started_at AS startedAt,
29630
- completed_at AS completedAt, status
30436
+ completed_at AS completedAt, status, workspace_path AS workspacePath
29631
30437
  FROM cron_runs
29632
30438
  WHERE job_id = ?
29633
30439
  AND id NOT IN (
@@ -29642,31 +30448,44 @@ async function pruneOldRuns(jobId) {
29642
30448
  async function deleteJobRuns(jobId) {
29643
30449
  await deleteRuns(getCronDb().select(DOOMED_RUN_COLUMNS).from(cronRuns).where(eq14(cronRuns.jobId, jobId)).all());
29644
30450
  }
30451
+ async function deleteOrphanRuns() {
30452
+ const db3 = getCronDb();
30453
+ await deleteRuns(db3.select(DOOMED_RUN_COLUMNS).from(cronRuns).where(notInArray(cronRuns.jobId, db3.select({ id: cronJobs.id }).from(cronJobs))).all());
30454
+ }
29645
30455
  async function deleteRuns(runs) {
29646
30456
  if (runs.length === 0)
29647
30457
  return;
29648
30458
  const db3 = getCronDb();
29649
30459
  const workspaceOf = /* @__PURE__ */ new Map();
29650
- const failed = /* @__PURE__ */ new Set();
30460
+ const openWorkspace = (workspacePath) => {
30461
+ let ws = workspaceOf.get(workspacePath);
30462
+ if (ws === void 0) {
30463
+ ws = fs67.existsSync(path76.join(workspacePath, ".halo", "halo.db")) ? { workspacePath, db: getWorkspaceDb(workspacePath).db } : null;
30464
+ workspaceOf.set(workspacePath, ws);
30465
+ }
30466
+ return ws;
30467
+ };
30468
+ const keptRows = /* @__PURE__ */ new Set();
29651
30469
  let deleted = 0;
29652
30470
  for (const run of runs) {
29653
30471
  if (run.sessionId !== perRunSessionId({ id: run.jobId }, run.startedAt))
29654
30472
  continue;
30473
+ if (run.status === "running" || run.completedAt === null) {
30474
+ keptRows.add(run.id);
30475
+ console.log(`[Cron] run ${run.id}: still in progress, kept with its session ${run.sessionId}`);
30476
+ continue;
30477
+ }
29655
30478
  try {
29656
- let ws = workspaceOf.get(run.jobId);
29657
- if (ws === void 0) {
29658
- const workspacePath = db3.select({ ws: cronJobs.workspacePath }).from(cronJobs).where(eq14(cronJobs.id, run.jobId)).get()?.ws;
29659
- ws = workspacePath && fs66.existsSync(path75.join(workspacePath, ".halo", "halo.db")) ? { workspacePath, db: getWorkspaceDb(workspacePath).db } : null;
29660
- workspaceOf.set(run.jobId, ws);
29661
- }
30479
+ const workspacePath = run.workspacePath ?? db3.select({ ws: cronJobs.workspacePath }).from(cronJobs).where(eq14(cronJobs.id, run.jobId)).get()?.ws;
30480
+ const ws = workspacePath ? openWorkspace(workspacePath) : null;
29662
30481
  if (ws && await deleteRunSession(ws, run))
29663
30482
  deleted++;
29664
30483
  } catch (err) {
29665
- failed.add(run.id);
30484
+ keptRows.add(run.id);
29666
30485
  console.warn(`[Cron] run ${run.id}: deleting session ${run.sessionId} failed, run row kept for the next prune: ${err instanceof Error ? err.message : String(err)}`);
29667
30486
  }
29668
30487
  }
29669
- const ids = runs.map((r) => r.id).filter((id) => !failed.has(id));
30488
+ const ids = runs.map((r) => r.id).filter((id) => !keptRows.has(id));
29670
30489
  if (ids.length > 0)
29671
30490
  db3.delete(cronRuns).where(inArray2(cronRuns.id, ids)).run();
29672
30491
  if (deleted > 0) {
@@ -29680,8 +30499,6 @@ async function deleteRunSession({ workspacePath, db: db3 }, run) {
29680
30499
  console.log(`[Cron] run ${run.id}: kept session ${sid} (${why}); pruning the run row only`);
29681
30500
  return false;
29682
30501
  };
29683
- if (run.status === "running" || run.completedAt === null)
29684
- return keep("its run is still in progress");
29685
30502
  const host = _sessionRegistry?.peek(workspacePath);
29686
30503
  if (host?.hasActiveWorkInTree(sid))
29687
30504
  return keep("a turn is running in it");
@@ -29700,15 +30517,15 @@ async function deleteRunSession({ workspacePath, db: db3 }, run) {
29700
30517
  }
29701
30518
  async function pruneOldLogs() {
29702
30519
  const dir = logsDir();
29703
- if (!fs66.existsSync(dir))
30520
+ if (!fs67.existsSync(dir))
29704
30521
  return;
29705
30522
  const cutoff = Date.now() - LOG_RETENTION_DAYS * DAY_MS2;
29706
- for (const name of fs66.readdirSync(dir)) {
29707
- const p = path75.join(dir, name);
30523
+ for (const name of fs67.readdirSync(dir)) {
30524
+ const p = path76.join(dir, name);
29708
30525
  try {
29709
- const stat = fs66.statSync(p);
30526
+ const stat = fs67.statSync(p);
29710
30527
  if (stat.mtimeMs < cutoff)
29711
- fs66.rmSync(p, { force: true });
30528
+ fs67.rmSync(p, { force: true });
29712
30529
  } catch {
29713
30530
  }
29714
30531
  }
@@ -29751,7 +30568,8 @@ var init_runner = __esm({
29751
30568
  sessionId: cronRuns.sessionId,
29752
30569
  startedAt: cronRuns.startedAt,
29753
30570
  completedAt: cronRuns.completedAt,
29754
- status: cronRuns.status
30571
+ status: cronRuns.status,
30572
+ workspacePath: cronRuns.workspacePath
29755
30573
  };
29756
30574
  }
29757
30575
  });
@@ -29759,8 +30577,8 @@ var init_runner = __esm({
29759
30577
  // ../server/dist/routes/cron.js
29760
30578
  import { Hono as Hono18 } from "hono";
29761
30579
  import { eq as eq15, desc as desc3, lt as lt6, and as and12 } from "drizzle-orm";
29762
- import fs67 from "node:fs/promises";
29763
- import path76 from "node:path";
30580
+ import fs68 from "node:fs/promises";
30581
+ import path77 from "node:path";
29764
30582
  import { Cron as Cron2 } from "croner";
29765
30583
  function newJobId() {
29766
30584
  return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -29805,7 +30623,7 @@ async function parseWorkingDir(workspacePath, v) {
29805
30623
  return { workingDir: null };
29806
30624
  try {
29807
30625
  const abs = await resolveWorkingDir(workspacePath, s);
29808
- return { workingDir: path76.relative(path76.resolve(workspacePath), abs) || null };
30626
+ return { workingDir: path77.relative(path77.resolve(workspacePath), abs) || null };
29809
30627
  } catch (err) {
29810
30628
  return { error: `workingDir: ${err instanceof Error ? err.message : String(err)}` };
29811
30629
  }
@@ -30110,7 +30928,7 @@ function createCronRoutes() {
30110
30928
  return c.json({ log: null });
30111
30929
  let log2 = null;
30112
30930
  try {
30113
- log2 = await fs67.readFile(row.logPath, "utf-8");
30931
+ log2 = await fs68.readFile(row.logPath, "utf-8");
30114
30932
  } catch {
30115
30933
  }
30116
30934
  return c.json({ log: log2 });
@@ -30176,8 +30994,8 @@ var init_cron = __esm({
30176
30994
  });
30177
30995
 
30178
30996
  // ../server/dist/extensions/install.js
30179
- import fs68 from "node:fs";
30180
- import path77 from "node:path";
30997
+ import fs69 from "node:fs";
30998
+ import path78 from "node:path";
30181
30999
  import { randomBytes as randomBytes3 } from "node:crypto";
30182
31000
  import JSZip2 from "jszip";
30183
31001
  function safeEntryPath(name) {
@@ -30208,43 +31026,43 @@ async function unpackZip(zipBuf, dest) {
30208
31026
  const perm = entry.unixPermissions;
30209
31027
  if (typeof perm === "number" && (perm & S_IFMT) === S_IFLNK)
30210
31028
  throw new ExtensionInstallError(`zip entry is a symlink: ${entry.name}`);
30211
- const abs = path77.join(dest, rel);
31029
+ const abs = path78.join(dest, rel);
30212
31030
  if (entry.dir) {
30213
- fs68.mkdirSync(abs, { recursive: true });
31031
+ fs69.mkdirSync(abs, { recursive: true });
30214
31032
  continue;
30215
31033
  }
30216
- fs68.mkdirSync(path77.dirname(abs), { recursive: true });
30217
- fs68.writeFileSync(abs, await entry.async("nodebuffer"));
31034
+ fs69.mkdirSync(path78.dirname(abs), { recursive: true });
31035
+ fs69.writeFileSync(abs, await entry.async("nodebuffer"));
30218
31036
  }
30219
31037
  }
30220
31038
  function stripWrapperDir(dir) {
30221
- if (fs68.existsSync(path77.join(dir, MANIFEST_FILE)))
31039
+ if (fs69.existsSync(path78.join(dir, MANIFEST_FILE)))
30222
31040
  return;
30223
- const children = fs68.readdirSync(dir, { withFileTypes: true });
31041
+ const children = fs69.readdirSync(dir, { withFileTypes: true });
30224
31042
  if (children.length !== 1 || !children[0].isDirectory())
30225
31043
  return;
30226
- const inner = path77.join(dir, children[0].name);
30227
- if (!fs68.existsSync(path77.join(inner, MANIFEST_FILE)))
31044
+ const inner = path78.join(dir, children[0].name);
31045
+ if (!fs69.existsSync(path78.join(inner, MANIFEST_FILE)))
30228
31046
  return;
30229
- for (const name of fs68.readdirSync(inner)) {
30230
- fs68.renameSync(path77.join(inner, name), path77.join(dir, name));
31047
+ for (const name of fs69.readdirSync(inner)) {
31048
+ fs69.renameSync(path78.join(inner, name), path78.join(dir, name));
30231
31049
  }
30232
- fs68.rmdirSync(inner);
31050
+ fs69.rmdirSync(inner);
30233
31051
  }
30234
31052
  function rand() {
30235
31053
  return randomBytes3(3).toString("hex");
30236
31054
  }
30237
31055
  async function installExtensionZip(zipBuf, expectId) {
30238
31056
  const root = extensionsRoot();
30239
- fs68.mkdirSync(root, { recursive: true });
30240
- const stage = path77.join(root, `.tmp-unpack-${rand()}`);
30241
- fs68.mkdirSync(stage);
31057
+ fs69.mkdirSync(root, { recursive: true });
31058
+ const stage = path78.join(root, `.tmp-unpack-${rand()}`);
31059
+ fs69.mkdirSync(stage);
30242
31060
  try {
30243
31061
  await unpackZip(zipBuf, stage);
30244
31062
  stripWrapperDir(stage);
30245
31063
  let manifestId;
30246
31064
  try {
30247
- const raw = JSON.parse(fs68.readFileSync(path77.join(stage, MANIFEST_FILE), "utf-8"));
31065
+ const raw = JSON.parse(fs69.readFileSync(path78.join(stage, MANIFEST_FILE), "utf-8"));
30248
31066
  manifestId = typeof raw.id === "string" ? raw.id : "";
30249
31067
  } catch (err) {
30250
31068
  throw new ExtensionInstallError(`${MANIFEST_FILE} missing or unreadable at the zip root: ${err instanceof Error ? err.message : String(err)}`);
@@ -30256,36 +31074,36 @@ async function installExtensionZip(zipBuf, expectId) {
30256
31074
  const verdict2 = readExtensionDir(stage, manifestId);
30257
31075
  if ("error" in verdict2)
30258
31076
  throw new ExtensionInstallError(`invalid extension: ${verdict2.error}`);
30259
- const target = path77.join(root, manifestId);
30260
- const old = path77.join(root, `.old-${manifestId}-${rand()}`);
30261
- const hadOld = fs68.existsSync(target);
31077
+ const target = path78.join(root, manifestId);
31078
+ const old = path78.join(root, `.old-${manifestId}-${rand()}`);
31079
+ const hadOld = fs69.existsSync(target);
30262
31080
  if (hadOld)
30263
- fs68.renameSync(target, old);
31081
+ fs69.renameSync(target, old);
30264
31082
  try {
30265
- fs68.renameSync(stage, target);
31083
+ fs69.renameSync(stage, target);
30266
31084
  } catch (err) {
30267
31085
  if (hadOld)
30268
- fs68.renameSync(old, target);
31086
+ fs69.renameSync(old, target);
30269
31087
  throw err;
30270
31088
  }
30271
31089
  if (hadOld)
30272
- fs68.rmSync(old, { recursive: true, force: true });
31090
+ fs69.rmSync(old, { recursive: true, force: true });
30273
31091
  const info = readExtensionDir(target);
30274
31092
  if ("error" in info)
30275
31093
  throw new ExtensionInstallError(`installed but failed re-validation: ${info.error}`);
30276
31094
  console.log(`[Extensions] installed ${info.id}@${info.version}${hadOld ? " (replaced previous)" : ""}`);
30277
31095
  return info;
30278
31096
  } finally {
30279
- fs68.rmSync(stage, { recursive: true, force: true });
31097
+ fs69.rmSync(stage, { recursive: true, force: true });
30280
31098
  }
30281
31099
  }
30282
31100
  function uninstallExtension(id) {
30283
31101
  if (!isExtensionId(id))
30284
31102
  throw new ExtensionInstallError(`"${id}" is not a valid extension id`);
30285
- const dir = path77.join(extensionsRoot(), id);
30286
- if (!fs68.existsSync(dir))
31103
+ const dir = path78.join(extensionsRoot(), id);
31104
+ if (!fs69.existsSync(dir))
30287
31105
  return false;
30288
- fs68.rmSync(dir, { recursive: true, force: true });
31106
+ fs69.rmSync(dir, { recursive: true, force: true });
30289
31107
  console.log(`[Extensions] removed ${id}`);
30290
31108
  return true;
30291
31109
  }
@@ -30302,7 +31120,7 @@ var init_install2 = __esm({
30302
31120
  });
30303
31121
 
30304
31122
  // ../server/dist/extensions/watcher.js
30305
- import fs69 from "node:fs";
31123
+ import fs70 from "node:fs";
30306
31124
  function scheduleFlush() {
30307
31125
  if (flushTimer)
30308
31126
  return;
@@ -30313,15 +31131,23 @@ function scheduleFlush() {
30313
31131
  }
30314
31132
  function rescanAndBroadcast() {
30315
31133
  const snap = scanExtensions();
31134
+ reconcileSttModels(snap);
30316
31135
  const key = snapshotKey(snap);
30317
31136
  if (key === lastKey)
30318
31137
  return;
30319
31138
  lastKey = key;
30320
31139
  broadcast({ type: "extension:changed", ...snap });
30321
31140
  }
31141
+ function reconcileSttModels(snap) {
31142
+ if (!snap.extensions.some((e) => e.capabilities.includes("stt-local")))
31143
+ return;
31144
+ const { state } = sttModels.status();
31145
+ if (state !== "ready" && state !== "downloading")
31146
+ void sttModels.ensure();
31147
+ }
30322
31148
  function openWatcher(root) {
30323
31149
  try {
30324
- watcher = fs69.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
31150
+ watcher = fs70.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
30325
31151
  watcher.on("error", (err) => {
30326
31152
  console.warn(`[Extensions] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
30327
31153
  closeWatcher();
@@ -30347,11 +31173,13 @@ function start() {
30347
31173
  return;
30348
31174
  const root = extensionsRoot();
30349
31175
  try {
30350
- fs69.mkdirSync(root, { recursive: true });
31176
+ fs70.mkdirSync(root, { recursive: true });
30351
31177
  } catch (err) {
30352
31178
  console.warn(`[Extensions] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
30353
31179
  }
30354
- lastKey = snapshotKey(scanExtensions());
31180
+ const snap = scanExtensions();
31181
+ lastKey = snapshotKey(snap);
31182
+ reconcileSttModels(snap);
30355
31183
  openWatcher(root);
30356
31184
  const { extensions, errors } = getSnapshot();
30357
31185
  if (extensions.length > 0 || errors.length > 0) {
@@ -30372,6 +31200,7 @@ var init_watcher = __esm({
30372
31200
  "../server/dist/extensions/watcher.js"() {
30373
31201
  "use strict";
30374
31202
  init_broadcast();
31203
+ init_models();
30375
31204
  init_registry2();
30376
31205
  watcher = null;
30377
31206
  flushTimer = null;
@@ -30382,8 +31211,8 @@ var init_watcher = __esm({
30382
31211
 
30383
31212
  // ../server/dist/routes/extensions.js
30384
31213
  import { Hono as Hono19 } from "hono";
30385
- import fs70 from "node:fs";
30386
- import path78 from "node:path";
31214
+ import fs71 from "node:fs";
31215
+ import path79 from "node:path";
30387
31216
  import { Readable as Readable2 } from "node:stream";
30388
31217
  function assetMime(fileName) {
30389
31218
  const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
@@ -30401,15 +31230,15 @@ function resolveAsset(id, version, rest) {
30401
31230
  let dir;
30402
31231
  let real;
30403
31232
  try {
30404
- dir = fs70.realpathSync(path78.join(extensionsRoot(), id));
30405
- real = fs70.realpathSync(path78.join(dir, ...segments));
31233
+ dir = fs71.realpathSync(path79.join(extensionsRoot(), id));
31234
+ real = fs71.realpathSync(path79.join(dir, ...segments));
30406
31235
  } catch {
30407
31236
  return null;
30408
31237
  }
30409
- if (real !== dir && !real.startsWith(dir + path78.sep))
31238
+ if (real !== dir && !real.startsWith(dir + path79.sep))
30410
31239
  return null;
30411
31240
  try {
30412
- if (!fs70.statSync(real).isFile())
31241
+ if (!fs71.statSync(real).isFile())
30413
31242
  return null;
30414
31243
  } catch {
30415
31244
  return null;
@@ -30471,8 +31300,8 @@ function createExtensionRoutes() {
30471
31300
  const abs = resolveAsset(c.req.param("id"), c.req.param("version"), c.req.param("rest"));
30472
31301
  if (!abs)
30473
31302
  return c.json({ error: "Not found" }, 404);
30474
- const stat = fs70.statSync(abs);
30475
- const nodeStream = fs70.createReadStream(abs);
31303
+ const stat = fs71.statSync(abs);
31304
+ const nodeStream = fs71.createReadStream(abs);
30476
31305
  c.req.raw.signal?.addEventListener("abort", () => nodeStream.destroy(), { once: true });
30477
31306
  return new Response(Readable2.toWeb(nodeStream), {
30478
31307
  headers: {
@@ -30526,7 +31355,7 @@ var init_extensions = __esm({
30526
31355
  });
30527
31356
 
30528
31357
  // ../server/dist/models/watcher.js
30529
- import fs71 from "node:fs";
31358
+ import fs72 from "node:fs";
30530
31359
  function snapshot2() {
30531
31360
  const { effective } = loadProviders();
30532
31361
  return {
@@ -30554,7 +31383,7 @@ function reloadAndBroadcast() {
30554
31383
  }
30555
31384
  function openWatcher2(root) {
30556
31385
  try {
30557
- watcher2 = fs71.watch(root, { persistent: false, recursive: false }, () => scheduleFlush2());
31386
+ watcher2 = fs72.watch(root, { persistent: false, recursive: false }, () => scheduleFlush2());
30558
31387
  watcher2.on("error", (err) => {
30559
31388
  console.warn(`[Models] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
30560
31389
  closeWatcher2();
@@ -30580,7 +31409,7 @@ function start2() {
30580
31409
  return;
30581
31410
  const root = globalHubModelsDir();
30582
31411
  try {
30583
- fs71.mkdirSync(root, { recursive: true });
31412
+ fs72.mkdirSync(root, { recursive: true });
30584
31413
  } catch (err) {
30585
31414
  console.warn(`[Models] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
30586
31415
  }
@@ -30857,7 +31686,7 @@ var init_ticker = __esm({
30857
31686
  });
30858
31687
 
30859
31688
  // ../server/dist/evolution/spawn.js
30860
- import path79 from "node:path";
31689
+ import path80 from "node:path";
30861
31690
  import { spawn as spawn4 } from "node:child_process";
30862
31691
  import { fileURLToPath as fileURLToPath2 } from "node:url";
30863
31692
  var __dirname2, WRAPPER_PATH, realEvoSpawner;
@@ -30865,8 +31694,8 @@ var init_spawn = __esm({
30865
31694
  "../server/dist/evolution/spawn.js"() {
30866
31695
  "use strict";
30867
31696
  init_child_env();
30868
- __dirname2 = path79.dirname(fileURLToPath2(import.meta.url));
30869
- WRAPPER_PATH = path79.resolve(__dirname2, "evo-wrapper.js");
31697
+ __dirname2 = path80.dirname(fileURLToPath2(import.meta.url));
31698
+ WRAPPER_PATH = path80.resolve(__dirname2, "evo-wrapper.js");
30870
31699
  realEvoSpawner = (mode, id) => {
30871
31700
  const child = spawn4(process.execPath, [WRAPPER_PATH, `--mode=${mode}`, `--id=${id}`], {
30872
31701
  detached: true,
@@ -31697,12 +32526,12 @@ var init_handler3 = __esm({
31697
32526
  });
31698
32527
 
31699
32528
  // ../server/dist/routes/channel-accounts.js
31700
- import fs72 from "node:fs";
32529
+ import fs73 from "node:fs";
31701
32530
  function accountListFields(a) {
31702
32531
  return {
31703
32532
  accountId: a.accountId,
31704
32533
  workspacePath: a.workspacePath,
31705
- workspaceMissing: !fs72.existsSync(a.workspacePath),
32534
+ workspaceMissing: !fs73.existsSync(a.workspacePath),
31706
32535
  label: a.label,
31707
32536
  enabled: a.enabled,
31708
32537
  accessLevel: a.accessLevel,
@@ -31921,7 +32750,7 @@ var init_descriptor = __esm({
31921
32750
  });
31922
32751
 
31923
32752
  // ../server/dist/channels/wechat/api.js
31924
- import crypto9 from "node:crypto";
32753
+ import crypto10 from "node:crypto";
31925
32754
  function buildClientVersion(version) {
31926
32755
  const [ma, mi, pa] = version.split(".").map((p) => parseInt(p, 10));
31927
32756
  return (ma & 255) << 16 | (mi & 255) << 8 | pa & 255;
@@ -31930,7 +32759,7 @@ function buildBaseInfo() {
31930
32759
  return { channel_version: CHANNEL_VERSION };
31931
32760
  }
31932
32761
  function randomWechatUin() {
31933
- const n = crypto9.randomBytes(4).readUInt32BE(0);
32762
+ const n = crypto10.randomBytes(4).readUInt32BE(0);
31934
32763
  return Buffer.from(String(n), "utf-8").toString("base64");
31935
32764
  }
31936
32765
  function commonHeaders() {
@@ -32238,7 +33067,7 @@ var init_event_adapter2 = __esm({
32238
33067
  });
32239
33068
 
32240
33069
  // ../server/dist/channels/wechat/cdn.js
32241
- import { createDecipheriv, createCipheriv, randomBytes as randomBytes4, createHash as createHash2 } from "node:crypto";
33070
+ import { createDecipheriv, createCipheriv, randomBytes as randomBytes4, createHash as createHash3 } from "node:crypto";
32242
33071
  function decryptAesEcb(ciphertext, key) {
32243
33072
  const decipher = createDecipheriv("aes-128-ecb", key, null);
32244
33073
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
@@ -32292,7 +33121,7 @@ function prepareUpload(plaintext) {
32292
33121
  aeskey,
32293
33122
  aeskeyHex: aeskey.toString("hex"),
32294
33123
  rawsize: plaintext.length,
32295
- rawfilemd5: createHash2("md5").update(plaintext).digest("hex"),
33124
+ rawfilemd5: createHash3("md5").update(plaintext).digest("hex"),
32296
33125
  filesize: aesEcbPaddedSize(plaintext.length)
32297
33126
  };
32298
33127
  }
@@ -32344,8 +33173,8 @@ var init_cdn = __esm({
32344
33173
  });
32345
33174
 
32346
33175
  // ../server/dist/channels/wechat/send-media.js
32347
- import fs73 from "node:fs/promises";
32348
- import path80 from "node:path";
33176
+ import fs74 from "node:fs/promises";
33177
+ import path81 from "node:path";
32349
33178
  function inferKind(filePath) {
32350
33179
  const cls = classifyMedia(filePath);
32351
33180
  if (cls === "image")
@@ -32357,7 +33186,7 @@ function inferKind(filePath) {
32357
33186
  async function sendMediaFile(params) {
32358
33187
  const { baseUrl, token, toUserId, contextToken, contextTokenAt, filePath } = params;
32359
33188
  const kind = params.kind ?? inferKind(filePath);
32360
- const plaintext = await fs73.readFile(filePath);
33189
+ const plaintext = await fs74.readFile(filePath);
32361
33190
  const prep = prepareUpload(plaintext);
32362
33191
  console.log(`[WeChat] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
32363
33192
  const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
@@ -32395,7 +33224,7 @@ async function sendMediaFile(params) {
32395
33224
  aesKeyBase64,
32396
33225
  fileSizeCiphertext: prep.filesize,
32397
33226
  fileSize: prep.rawsize,
32398
- fileName: path80.basename(filePath)
33227
+ fileName: path81.basename(filePath)
32399
33228
  });
32400
33229
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
32401
33230
  const body = {
@@ -32558,8 +33387,8 @@ var init_login = __esm({
32558
33387
  });
32559
33388
 
32560
33389
  // ../server/dist/channels/wechat/handler.js
32561
- import fs74 from "node:fs";
32562
- import path81 from "node:path";
33390
+ import fs75 from "node:fs";
33391
+ import path82 from "node:path";
32563
33392
  import QRCode from "qrcode";
32564
33393
  function wxRoute(fromUserId, contextToken, contextTokenAt) {
32565
33394
  return (prev) => {
@@ -32980,7 +33809,7 @@ async function handleSlashCommand(args) {
32980
33809
  await reply(login.message);
32981
33810
  return true;
32982
33811
  }
32983
- const tmpPath = path81.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
33812
+ const tmpPath = path82.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
32984
33813
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
32985
33814
  await sendMediaFile({
32986
33815
  baseUrl: account.baseUrl,
@@ -32989,7 +33818,7 @@ async function handleSlashCommand(args) {
32989
33818
  contextToken,
32990
33819
  filePath: tmpPath
32991
33820
  });
32992
- await fs74.promises.unlink(tmpPath).catch(() => {
33821
+ await fs75.promises.unlink(tmpPath).catch(() => {
32993
33822
  });
32994
33823
  await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
32995
33824
  waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
@@ -33069,8 +33898,8 @@ var init_handler4 = __esm({
33069
33898
 
33070
33899
  // ../server/dist/routes/wechat.js
33071
33900
  import { Hono as Hono21 } from "hono";
33072
- import path82 from "node:path";
33073
- import fs75 from "node:fs";
33901
+ import path83 from "node:path";
33902
+ import fs76 from "node:fs";
33074
33903
  function createWechatRoutes(deps) {
33075
33904
  const { db: db3, channel } = deps;
33076
33905
  const app2 = new Hono21();
@@ -33079,7 +33908,7 @@ function createWechatRoutes(deps) {
33079
33908
  return {
33080
33909
  ...stripSecrets(acc),
33081
33910
  workspacePath: acc.workspacePath,
33082
- workspaceMissing: !fs75.existsSync(acc.workspacePath)
33911
+ workspaceMissing: !fs76.existsSync(acc.workspacePath)
33083
33912
  };
33084
33913
  });
33085
33914
  return c.json({ accounts });
@@ -33095,7 +33924,7 @@ function createWechatRoutes(deps) {
33095
33924
  return c.json({ error: "sessionKey required" }, 400);
33096
33925
  if (!body.workspacePath)
33097
33926
  return c.json({ error: "workspacePath required" }, 400);
33098
- if (!path82.isAbsolute(body.workspacePath))
33927
+ if (!path83.isAbsolute(body.workspacePath))
33099
33928
  return c.json({ error: "workspacePath must be absolute" }, 400);
33100
33929
  const levelError = accessLevelError(body.accessLevel, CHAT_ACCESS_LEVELS);
33101
33930
  if (levelError)
@@ -33307,8 +34136,8 @@ var init_descriptor2 = __esm({
33307
34136
  // ../server/dist/routes/web.js
33308
34137
  import { Hono as Hono22 } from "hono";
33309
34138
  import { streamSSE as streamSSE2 } from "hono/streaming";
33310
- import crypto10 from "node:crypto";
33311
- import fs76 from "node:fs";
34139
+ import crypto11 from "node:crypto";
34140
+ import fs77 from "node:fs";
33312
34141
  function streamWithKeepalive(c, chunks) {
33313
34142
  return streamSSE2(c, async (stream) => {
33314
34143
  const timer2 = setInterval(() => {
@@ -33345,8 +34174,8 @@ function createWebRoutes(deps) {
33345
34174
  const bodyError = accountBodyError(body, ACCOUNT_ACCESS_LEVELS);
33346
34175
  if (bodyError)
33347
34176
  return c.json({ error: bodyError }, 400);
33348
- const accountId = crypto10.randomUUID().slice(0, 8);
33349
- const token = crypto10.randomBytes(24).toString("base64url");
34177
+ const accountId = crypto11.randomUUID().slice(0, 8);
34178
+ const token = crypto11.randomBytes(24).toString("base64url");
33350
34179
  insertAccount2(db3, {
33351
34180
  accountId,
33352
34181
  token,
@@ -33499,30 +34328,30 @@ function createWebRoutes(deps) {
33499
34328
  const filePath = c.req.query("path");
33500
34329
  if (!filePath)
33501
34330
  return c.json({ error: "path required" }, 400);
33502
- const path89 = await import("node:path");
33503
- const resolved = path89.default.resolve(account.workspacePath, filePath);
33504
- const root = path89.default.resolve(account.workspacePath);
33505
- if (resolved !== root && !resolved.startsWith(root + path89.default.sep)) {
34331
+ const path90 = await import("node:path");
34332
+ const resolved = path90.default.resolve(account.workspacePath, filePath);
34333
+ const root = path90.default.resolve(account.workspacePath);
34334
+ if (resolved !== root && !resolved.startsWith(root + path90.default.sep)) {
33506
34335
  return c.json({ error: "path traversal not allowed" }, 403);
33507
34336
  }
33508
34337
  let real;
33509
34338
  let realRoot;
33510
34339
  try {
33511
- real = fs76.realpathSync(resolved);
33512
- realRoot = fs76.realpathSync(root);
34340
+ real = fs77.realpathSync(resolved);
34341
+ realRoot = fs77.realpathSync(root);
33513
34342
  } catch {
33514
34343
  return c.json({ error: "file not found" }, 404);
33515
34344
  }
33516
- if (real !== realRoot && !real.startsWith(realRoot + path89.default.sep)) {
34345
+ if (real !== realRoot && !real.startsWith(realRoot + path90.default.sep)) {
33517
34346
  return c.json({ error: "path traversal not allowed" }, 403);
33518
34347
  }
33519
34348
  if (isHiddenWorkspacePath(real, realRoot)) {
33520
34349
  return c.json({ error: "path is not accessible" }, 403);
33521
34350
  }
33522
- const stat = fs76.statSync(real);
34351
+ const stat = fs77.statSync(real);
33523
34352
  if (stat.isDirectory())
33524
34353
  return c.json({ error: "cannot serve directory" }, 400);
33525
- const ext = path89.default.extname(real).toLowerCase();
34354
+ const ext = path90.default.extname(real).toLowerCase();
33526
34355
  const nonImageMimes = {
33527
34356
  ".mp4": "video/mp4",
33528
34357
  ".webm": "video/webm",
@@ -33530,8 +34359,8 @@ function createWebRoutes(deps) {
33530
34359
  ".txt": "text/plain"
33531
34360
  };
33532
34361
  const contentType = imageMimeFromExt(ext) || nonImageMimes[ext] || "application/octet-stream";
33533
- const fileName = path89.default.basename(resolved);
33534
- const data = fs76.readFileSync(real);
34362
+ const fileName = path90.default.basename(resolved);
34363
+ const data = fs77.readFileSync(real);
33535
34364
  c.header("content-type", contentType);
33536
34365
  c.header("content-disposition", `inline; filename="${fileName}"`);
33537
34366
  return c.body(data);
@@ -33684,7 +34513,7 @@ function formatForFeishu(input) {
33684
34513
  inFence = !inFence;
33685
34514
  continue;
33686
34515
  }
33687
- if (inFence) {
34516
+ if (inFence || /^MEDIA:/.test(raw)) {
33688
34517
  out.push(raw);
33689
34518
  continue;
33690
34519
  }
@@ -33709,16 +34538,16 @@ var init_markdown = __esm({
33709
34538
  });
33710
34539
 
33711
34540
  // ../server/dist/channels/slack/event-adapter.js
33712
- var HARD_CHARS, SlackResponder;
34541
+ var SLACK_TEXT_LIMIT, SlackResponder;
33713
34542
  var init_event_adapter3 = __esm({
33714
34543
  "../server/dist/channels/slack/event-adapter.js"() {
33715
34544
  "use strict";
33716
34545
  init_markdown();
33717
34546
  init_responder();
33718
- HARD_CHARS = 35e3;
34547
+ SLACK_TEXT_LIMIT = 35e3;
33719
34548
  SlackResponder = class extends ChunkedResponder {
33720
34549
  constructor(deps) {
33721
- super(deps, { limit: HARD_CHARS, logTag: "Slack", format: formatForSlack });
34550
+ super(deps, { limit: SLACK_TEXT_LIMIT, logTag: "Slack", format: formatForSlack });
33722
34551
  }
33723
34552
  };
33724
34553
  }
@@ -33753,10 +34582,10 @@ async function authTest(botToken) {
33753
34582
  return slackPost("auth.test", botToken, {});
33754
34583
  }
33755
34584
  async function uploadFile(args) {
33756
- const fs80 = await import("node:fs");
33757
- const path89 = await import("node:path");
33758
- const filename = args.filename ?? path89.basename(args.filePath);
33759
- const stat = fs80.statSync(args.filePath);
34585
+ const fs81 = await import("node:fs");
34586
+ const path90 = await import("node:path");
34587
+ const filename = args.filename ?? path90.basename(args.filePath);
34588
+ const stat = fs81.statSync(args.filePath);
33760
34589
  if (!stat.isFile())
33761
34590
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
33762
34591
  const length = stat.size;
@@ -33774,7 +34603,7 @@ async function uploadFile(args) {
33774
34603
  if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
33775
34604
  throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
33776
34605
  }
33777
- const buf = fs80.readFileSync(args.filePath);
34606
+ const buf = fs81.readFileSync(args.filePath);
33778
34607
  const step2 = await fetch(step1Parsed.upload_url, {
33779
34608
  method: "POST",
33780
34609
  body: buf
@@ -33919,7 +34748,7 @@ var init_api2 = __esm({
33919
34748
  });
33920
34749
 
33921
34750
  // ../server/dist/channels/slack/handler.js
33922
- import path83 from "node:path";
34751
+ import path84 from "node:path";
33923
34752
  import { WebSocket as WebSocket2 } from "ws";
33924
34753
  function slashToBang(text6, commandNames) {
33925
34754
  if (commandNames.length === 0)
@@ -34050,7 +34879,7 @@ function startSlackChannel(deps) {
34050
34879
  const route = bridge.getRoute(sessionId);
34051
34880
  if (!account || !route)
34052
34881
  return;
34053
- const resolved = path83.resolve(filePath);
34882
+ const resolved = path84.resolve(filePath);
34054
34883
  await sendMediaOrReport({
34055
34884
  filePath,
34056
34885
  account,
@@ -34454,18 +35283,17 @@ async function dispatch3(accountId, text6, explicitChatId, media) {
34454
35283
  const { channel, threadTs } = parseChatId(explicitChatId);
34455
35284
  const out = [];
34456
35285
  if (text6) {
34457
- try {
34458
- await postMessage({ botToken: acct.botToken, channel, threadTs, text: text6 });
34459
- out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
34460
- } catch (err) {
34461
- out.push({
34462
- channelType: "slack",
34463
- accountId,
34464
- chatId: explicitChatId,
34465
- ok: false,
34466
- error: err instanceof Error ? err.message : String(err)
34467
- });
35286
+ const chunks = splitText(text6, SLACK_TEXT_LIMIT);
35287
+ let failed;
35288
+ for (const [i, chunk] of chunks.entries()) {
35289
+ try {
35290
+ await postMessage({ botToken: acct.botToken, channel, threadTs, text: formatForSlack(chunk) });
35291
+ } catch (err) {
35292
+ failed = `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`;
35293
+ break;
35294
+ }
34468
35295
  }
35296
+ out.push(failed ? { channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: failed } : { channelType: "slack", accountId, chatId: explicitChatId, ok: true });
34469
35297
  }
34470
35298
  for (const filePath of media?.paths ?? []) {
34471
35299
  if (!isMediaPathAllowed(filePath, media.workspacePath)) {
@@ -34504,6 +35332,9 @@ var init_cron_dispatcher3 = __esm({
34504
35332
  init_channel_db();
34505
35333
  init_accounts5();
34506
35334
  init_api2();
35335
+ init_event_adapter3();
35336
+ init_chunk();
35337
+ init_markdown();
34507
35338
  init_media();
34508
35339
  init_dispatcher();
34509
35340
  }
@@ -34581,16 +35412,16 @@ var init_accounts6 = __esm({
34581
35412
  });
34582
35413
 
34583
35414
  // ../server/dist/channels/feishu/event-adapter.js
34584
- var HARD_CHARS2, FeishuResponder;
35415
+ var FEISHU_TEXT_LIMIT, FeishuResponder;
34585
35416
  var init_event_adapter4 = __esm({
34586
35417
  "../server/dist/channels/feishu/event-adapter.js"() {
34587
35418
  "use strict";
34588
35419
  init_markdown();
34589
35420
  init_responder();
34590
- HARD_CHARS2 = 4500;
35421
+ FEISHU_TEXT_LIMIT = 4500;
34591
35422
  FeishuResponder = class extends ChunkedResponder {
34592
35423
  constructor(deps) {
34593
- super(deps, { limit: HARD_CHARS2, logTag: "Feishu", format: formatForFeishu });
35424
+ super(deps, { limit: FEISHU_TEXT_LIMIT, logTag: "Feishu", format: formatForFeishu });
34594
35425
  }
34595
35426
  };
34596
35427
  }
@@ -34692,10 +35523,10 @@ async function replyMessage(args) {
34692
35523
  return { message_id: parsed.data?.message_id ?? "" };
34693
35524
  }
34694
35525
  async function uploadImage(args) {
34695
- const fs80 = await import("node:fs");
34696
- const path89 = await import("node:path");
34697
- const buf = fs80.readFileSync(args.filePath);
34698
- const filename = path89.basename(args.filePath);
35526
+ const fs81 = await import("node:fs");
35527
+ const path90 = await import("node:path");
35528
+ const buf = fs81.readFileSync(args.filePath);
35529
+ const filename = path90.basename(args.filePath);
34699
35530
  const send2 = async (token) => {
34700
35531
  const form = new FormData();
34701
35532
  form.append("image_type", "message");
@@ -34711,10 +35542,10 @@ async function uploadImage(args) {
34711
35542
  return { imageKey: parsed.data.image_key };
34712
35543
  }
34713
35544
  async function uploadFile2(args) {
34714
- const fs80 = await import("node:fs");
34715
- const path89 = await import("node:path");
34716
- const buf = fs80.readFileSync(args.filePath);
34717
- const filename = args.filename ?? path89.basename(args.filePath);
35545
+ const fs81 = await import("node:fs");
35546
+ const path90 = await import("node:path");
35547
+ const buf = fs81.readFileSync(args.filePath);
35548
+ const filename = args.filename ?? path90.basename(args.filePath);
34718
35549
  const send2 = async (token) => {
34719
35550
  const form = new FormData();
34720
35551
  form.append("file_type", args.fileType);
@@ -34781,7 +35612,7 @@ var init_api3 = __esm({
34781
35612
  });
34782
35613
 
34783
35614
  // ../server/dist/channels/feishu/handler.js
34784
- import path84 from "node:path";
35615
+ import path85 from "node:path";
34785
35616
  import * as Lark from "@larksuiteoapi/node-sdk";
34786
35617
  function buildSessionPrefixForThread2(chatId, rootId) {
34787
35618
  return sessionPrefix("feishu", `${chatId}:${rootId}`);
@@ -34984,7 +35815,7 @@ function startFeishuChannel(deps) {
34984
35815
  const route = bridge.getRoute(sessionId);
34985
35816
  if (!account || !route)
34986
35817
  return;
34987
- const resolved = path84.resolve(filePath);
35818
+ const resolved = path85.resolve(filePath);
34988
35819
  await sendMediaOrReport({
34989
35820
  filePath,
34990
35821
  account,
@@ -35151,7 +35982,7 @@ async function replyToInbound(args) {
35151
35982
  async function sendFeishuMedia(args) {
35152
35983
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
35153
35984
  const cls = classifyMedia(filePath);
35154
- const ext = path84.extname(filePath).toLowerCase();
35985
+ const ext = path85.extname(filePath).toLowerCase();
35155
35986
  if (cls === "image") {
35156
35987
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
35157
35988
  await replyToInboundRaw({
@@ -35412,25 +36243,28 @@ async function dispatch4(accountId, text6, explicitChatId) {
35412
36243
  throw new Error('feishu cron target requires an explicit chatId (e.g. "oc_abcd1234"). Create the cron from inside a feishu chat to auto-pin, or pass --targets feishu:<accountId>:<chatId>.');
35413
36244
  }
35414
36245
  const { chatId } = parseChatId2(explicitChatId);
35415
- try {
35416
- await sendMessage3({
35417
- appId: acct.appId,
35418
- appSecret: acct.appSecret,
35419
- receiveIdType: "chat_id",
35420
- receiveId: chatId,
35421
- msgType: "text",
35422
- content: { text: text6 }
35423
- });
35424
- return [{ channelType: "feishu", accountId, chatId: explicitChatId, ok: true }];
35425
- } catch (err) {
35426
- return [{
35427
- channelType: "feishu",
35428
- accountId,
35429
- chatId: explicitChatId,
35430
- ok: false,
35431
- error: err instanceof Error ? err.message : String(err)
35432
- }];
36246
+ const chunks = splitText(text6, FEISHU_TEXT_LIMIT);
36247
+ for (const [i, chunk] of chunks.entries()) {
36248
+ try {
36249
+ await sendMessage3({
36250
+ appId: acct.appId,
36251
+ appSecret: acct.appSecret,
36252
+ receiveIdType: "chat_id",
36253
+ receiveId: chatId,
36254
+ msgType: "text",
36255
+ content: { text: formatForFeishu(chunk) }
36256
+ });
36257
+ } catch (err) {
36258
+ return [{
36259
+ channelType: "feishu",
36260
+ accountId,
36261
+ chatId: explicitChatId,
36262
+ ok: false,
36263
+ error: `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`
36264
+ }];
36265
+ }
35433
36266
  }
36267
+ return [{ channelType: "feishu", accountId, chatId: explicitChatId, ok: true }];
35434
36268
  }
35435
36269
  function listTargets4() {
35436
36270
  const cdb = getChannelDb();
@@ -35452,6 +36286,9 @@ var init_cron_dispatcher4 = __esm({
35452
36286
  init_channel_db();
35453
36287
  init_accounts6();
35454
36288
  init_api3();
36289
+ init_event_adapter4();
36290
+ init_chunk();
36291
+ init_markdown();
35455
36292
  init_dispatcher();
35456
36293
  }
35457
36294
  });
@@ -35525,23 +36362,23 @@ var init_accounts7 = __esm({
35525
36362
  });
35526
36363
 
35527
36364
  // ../server/dist/channels/wecom/event-adapter.js
35528
- var HARD_CHARS3, WecomResponder;
36365
+ var WECOM_TEXT_LIMIT, WecomResponder;
35529
36366
  var init_event_adapter5 = __esm({
35530
36367
  "../server/dist/channels/wecom/event-adapter.js"() {
35531
36368
  "use strict";
35532
36369
  init_responder();
35533
- HARD_CHARS3 = 5e3;
36370
+ WECOM_TEXT_LIMIT = 5e3;
35534
36371
  WecomResponder = class extends ChunkedResponder {
35535
36372
  constructor(deps) {
35536
- super(deps, { limit: HARD_CHARS3, logTag: "WeCom" });
36373
+ super(deps, { limit: WECOM_TEXT_LIMIT, logTag: "WeCom" });
35537
36374
  }
35538
36375
  };
35539
36376
  }
35540
36377
  });
35541
36378
 
35542
36379
  // ../server/dist/channels/wecom/handler.js
35543
- import path85 from "node:path";
35544
- import fs77 from "node:fs/promises";
36380
+ import path86 from "node:path";
36381
+ import fs78 from "node:fs/promises";
35545
36382
  import { WSClient as WSClient2, generateReqId } from "@wecom/aibot-node-sdk";
35546
36383
  function pickConversation(body) {
35547
36384
  const userid = body.from?.userid ?? "unknown";
@@ -35664,13 +36501,13 @@ async function replyText(wsClient, route, text6) {
35664
36501
  }
35665
36502
  async function sendWecomMedia(args) {
35666
36503
  const { wsClient, route, filePath } = args;
35667
- const ext = path85.extname(filePath).toLowerCase();
36504
+ const ext = path86.extname(filePath).toLowerCase();
35668
36505
  const cls = classifyMedia(filePath);
35669
36506
  const type = cls === "image" && [".png", ".jpg", ".jpeg", ".gif"].includes(ext) ? "image" : cls === "video" && ext === ".mp4" ? "video" : "file";
35670
- const buf = await fs77.readFile(filePath);
36507
+ const buf = await fs78.readFile(filePath);
35671
36508
  if (buf.length > MAX_WECOM_DOWNLOAD_BYTES)
35672
36509
  throw new Error("file exceeds 20MB");
35673
- const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path85.basename(filePath) });
36510
+ const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path86.basename(filePath) });
35674
36511
  if (route.reqId)
35675
36512
  await wsClient.replyMedia({ headers: { req_id: route.reqId } }, type, mediaId);
35676
36513
  else
@@ -35715,7 +36552,7 @@ function startWecomChannel(deps) {
35715
36552
  const wsClient = states.get(accountId)?.wsClient;
35716
36553
  if (!account || !route || !wsClient)
35717
36554
  return;
35718
- const resolved = path85.resolve(filePath);
36555
+ const resolved = path86.resolve(filePath);
35719
36556
  await sendMediaOrReport({
35720
36557
  filePath,
35721
36558
  account,
@@ -36039,18 +36876,21 @@ async function dispatch5(accountId, text6, explicitChatId) {
36039
36876
  if (!client) {
36040
36877
  return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: false, error: "wecom long-connect not active" }];
36041
36878
  }
36042
- try {
36043
- await client.sendMessage(explicitChatId, { msgtype: "markdown", markdown: { content: text6 } });
36044
- return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: true }];
36045
- } catch (err) {
36046
- return [{
36047
- channelType: "wecom",
36048
- accountId,
36049
- chatId: explicitChatId,
36050
- ok: false,
36051
- error: err instanceof Error ? err.message : String(err)
36052
- }];
36879
+ const chunks = splitText(text6, WECOM_TEXT_LIMIT);
36880
+ for (const [i, chunk] of chunks.entries()) {
36881
+ try {
36882
+ await client.sendMessage(explicitChatId, { msgtype: "markdown", markdown: { content: chunk } });
36883
+ } catch (err) {
36884
+ return [{
36885
+ channelType: "wecom",
36886
+ accountId,
36887
+ chatId: explicitChatId,
36888
+ ok: false,
36889
+ error: `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`
36890
+ }];
36891
+ }
36053
36892
  }
36893
+ return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: true }];
36054
36894
  }
36055
36895
  function listTargets5() {
36056
36896
  const cdb = getChannelDb();
@@ -36072,6 +36912,8 @@ var init_cron_dispatcher5 = __esm({
36072
36912
  init_channel_db();
36073
36913
  init_accounts7();
36074
36914
  init_handler7();
36915
+ init_event_adapter5();
36916
+ init_chunk();
36075
36917
  init_dispatcher();
36076
36918
  }
36077
36919
  });
@@ -36116,6 +36958,64 @@ var init_descriptors = __esm({
36116
36958
  }
36117
36959
  });
36118
36960
 
36961
+ // ../server/dist/middleware/compress.js
36962
+ import { compress, COMPRESSIBLE_CONTENT_TYPE_REGEX } from "hono/compress";
36963
+ function compressResponses(threshold) {
36964
+ const inner = compress({ threshold });
36965
+ return (c, next) => inner(c, async () => {
36966
+ await next();
36967
+ if (c.res.status === 304)
36968
+ varyAcceptEncoding(c);
36969
+ else
36970
+ await setProbedLength(c, threshold);
36971
+ });
36972
+ }
36973
+ function varyAcceptEncoding(c) {
36974
+ const vary = c.res.headers.get("Vary");
36975
+ if (vary === "*" || vary && VARY_ACCEPT_ENCODING_RE.test(vary))
36976
+ return;
36977
+ c.header("Vary", vary ? `${vary}, Accept-Encoding` : "Accept-Encoding");
36978
+ }
36979
+ async function setProbedLength(c, threshold) {
36980
+ const res = c.res;
36981
+ const type = res.headers.get("Content-Type");
36982
+ if (res.headers.has("Content-Length") || res.headers.has("Content-Encoding") || res.headers.has("Transfer-Encoding") || c.req.method === "HEAD" || res.status === 204 || res.status === 206 || !res.body || !type || !COMPRESSIBLE_CONTENT_TYPE_REGEX.test(type))
36983
+ return;
36984
+ const reader = res.body.getReader();
36985
+ const head = [];
36986
+ let size = 0;
36987
+ while (size < threshold) {
36988
+ const { done, value } = await reader.read();
36989
+ if (done) {
36990
+ c.res = new Response(Buffer.concat(head, size), res);
36991
+ c.res.headers.set("Content-Length", String(size));
36992
+ return;
36993
+ }
36994
+ head.push(value);
36995
+ size += value.byteLength;
36996
+ }
36997
+ c.res = new Response(new ReadableStream({
36998
+ async pull(ctrl) {
36999
+ const chunk = head.shift();
37000
+ if (chunk)
37001
+ return ctrl.enqueue(chunk);
37002
+ const { done, value } = await reader.read();
37003
+ if (done)
37004
+ ctrl.close();
37005
+ else
37006
+ ctrl.enqueue(value);
37007
+ },
37008
+ cancel: (reason) => reader.cancel(reason)
37009
+ }), res);
37010
+ }
37011
+ var VARY_ACCEPT_ENCODING_RE;
37012
+ var init_compress = __esm({
37013
+ "../server/dist/middleware/compress.js"() {
37014
+ "use strict";
37015
+ VARY_ACCEPT_ENCODING_RE = /(?:^|,)\s*accept-encoding\s*(?:,|$)/i;
37016
+ }
37017
+ });
37018
+
36119
37019
  // ../server/dist/listen-banner.js
36120
37020
  function formatListenBanner(address, port, version) {
36121
37021
  if (address === "::" || address === "0.0.0.0") {
@@ -36137,21 +37037,20 @@ var init_listen_banner = __esm({
36137
37037
  var dist_exports = {};
36138
37038
  import { Hono as Hono26 } from "hono";
36139
37039
  import { cors } from "hono/cors";
36140
- import { compress } from "hono/compress";
36141
37040
  import { serve } from "@hono/node-server";
36142
37041
  import { serveStatic } from "@hono/node-server/serve-static";
36143
- import { WebSocketServer as WebSocketServer3 } from "ws";
36144
- import path86 from "node:path";
36145
- import fs78 from "node:fs";
37042
+ import { WebSocketServer as WebSocketServer4 } from "ws";
37043
+ import path87 from "node:path";
37044
+ import fs79 from "node:fs";
36146
37045
  import { spawnSync as spawnSync4 } from "node:child_process";
36147
37046
  import { homedir as homedir25 } from "node:os";
36148
37047
  import { fileURLToPath as fileURLToPath3 } from "node:url";
36149
37048
  function findProjectRoot() {
36150
37049
  let dir = process.cwd();
36151
37050
  for (let i = 0; i < 32; i++) {
36152
- if (fs78.existsSync(path86.join(dir, "pnpm-workspace.yaml")))
37051
+ if (fs79.existsSync(path87.join(dir, "pnpm-workspace.yaml")))
36153
37052
  return dir;
36154
- const parent = path86.dirname(dir);
37053
+ const parent = path87.dirname(dir);
36155
37054
  if (parent === dir)
36156
37055
  break;
36157
37056
  dir = parent;
@@ -36161,15 +37060,15 @@ function findProjectRoot() {
36161
37060
  return process.cwd();
36162
37061
  }
36163
37062
  function acquireSingleInstanceLock(lockFile) {
36164
- fs78.mkdirSync(path86.dirname(lockFile), { recursive: true });
36165
- const fd = fs78.openSync(lockFile, fs78.constants.O_RDWR | fs78.constants.O_CREAT, 420);
37063
+ fs79.mkdirSync(path87.dirname(lockFile), { recursive: true });
37064
+ const fd = fs79.openSync(lockFile, fs79.constants.O_RDWR | fs79.constants.O_CREAT, 420);
36166
37065
  const result = spawnSync4("flock", ["-n", "3"], {
36167
37066
  stdio: ["ignore", "ignore", "ignore", fd]
36168
37067
  });
36169
37068
  if (result.error) {
36170
37069
  const content = readLockPid(fd);
36171
37070
  if (content > 0 && isProcessAlive(content)) {
36172
- fs78.closeSync(fd);
37071
+ fs79.closeSync(fd);
36173
37072
  console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
36174
37073
  process.exit(1);
36175
37074
  }
@@ -36178,19 +37077,19 @@ function acquireSingleInstanceLock(lockFile) {
36178
37077
  if (content > 0 && !isProcessAlive(content)) {
36179
37078
  console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
36180
37079
  } else {
36181
- fs78.closeSync(fd);
37080
+ fs79.closeSync(fd);
36182
37081
  console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
36183
37082
  process.exit(1);
36184
37083
  }
36185
37084
  }
36186
- fs78.ftruncateSync(fd);
36187
- fs78.writeSync(fd, String(process.pid));
36188
- fs78.fsyncSync(fd);
37085
+ fs79.ftruncateSync(fd);
37086
+ fs79.writeSync(fd, String(process.pid));
37087
+ fs79.fsyncSync(fd);
36189
37088
  }
36190
37089
  function readLockPid(fd) {
36191
37090
  try {
36192
37091
  const buf = Buffer.alloc(32);
36193
- const n = fs78.readSync(fd, buf, 0, 32, 0);
37092
+ const n = fs79.readSync(fd, buf, 0, 32, 0);
36194
37093
  return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
36195
37094
  } catch {
36196
37095
  return 0;
@@ -36218,7 +37117,7 @@ function isProcessAlive(pid) {
36218
37117
  }
36219
37118
  }
36220
37119
  function readGitSha() {
36221
- if (!fs78.existsSync(path86.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs78.existsSync(path86.join(PROJECT_ROOT, ".git")))
37120
+ if (!fs79.existsSync(path87.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs79.existsSync(path87.join(PROJECT_ROOT, ".git")))
36222
37121
  return null;
36223
37122
  try {
36224
37123
  const rev = spawnSync4("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
@@ -36239,14 +37138,14 @@ function applySandboxPaths() {
36239
37138
  }
36240
37139
  function resolveFrontendDir() {
36241
37140
  if (process.env.HALO_FRONTEND_DIR)
36242
- return path86.resolve(process.env.HALO_FRONTEND_DIR);
36243
- const bundled = path86.resolve(__dirname3, "..", "admin-out");
36244
- if (fs78.existsSync(path86.join(bundled, "index.html")))
37141
+ return path87.resolve(process.env.HALO_FRONTEND_DIR);
37142
+ const bundled = path87.resolve(__dirname3, "..", "admin-out");
37143
+ if (fs79.existsSync(path87.join(bundled, "index.html")))
36245
37144
  return bundled;
36246
- const sibling = path86.resolve(__dirname3, "..", "..", "admin", "out");
36247
- if (fs78.existsSync(path86.join(sibling, "index.html")))
37145
+ const sibling = path87.resolve(__dirname3, "..", "..", "admin", "out");
37146
+ if (fs79.existsSync(path87.join(sibling, "index.html")))
36248
37147
  return sibling;
36249
- return path86.resolve(PROJECT_ROOT, "packages", "admin", "out");
37148
+ return path87.resolve(PROJECT_ROOT, "packages", "admin", "out");
36250
37149
  }
36251
37150
  async function gracefulShutdown(signal) {
36252
37151
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
@@ -36262,6 +37161,7 @@ async function gracefulShutdown(signal) {
36262
37161
  });
36263
37162
  wss.close();
36264
37163
  transcribe?.close();
37164
+ stt?.close();
36265
37165
  jupyterChannels?.close();
36266
37166
  await jupyter?.stop();
36267
37167
  if (server && typeof server.close === "function") {
@@ -36280,7 +37180,7 @@ async function gracefulShutdown(signal) {
36280
37180
  await shutdownObservability();
36281
37181
  process.exit(0);
36282
37182
  }
36283
- var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, sandboxWarning, app, allowlist, jupyter, channelDb, OWNS_RUNTIMES, registry, agentcoreLease, FRONTEND_DIR, server, wss, transcribe, jupyterChannels, upgradeRoutes;
37183
+ var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, sandboxWarning, app, allowlist, jupyter, channelDb, OWNS_RUNTIMES, registry, agentcoreLease, FRONTEND_DIR, server, wss, transcribe, stt, jupyterChannels, upgradeRoutes;
36284
37184
  var init_dist2 = __esm({
36285
37185
  async "../server/dist/index.js"() {
36286
37186
  "use strict";
@@ -36297,6 +37197,7 @@ var init_dist2 = __esm({
36297
37197
  init_metrics();
36298
37198
  init_commands3();
36299
37199
  init_transcribe_ws();
37200
+ init_stt_ws();
36300
37201
  init_jupyter();
36301
37202
  init_manager();
36302
37203
  init_upgrade_router();
@@ -36327,6 +37228,7 @@ var init_dist2 = __esm({
36327
37228
  init_registry4();
36328
37229
  init_descriptors();
36329
37230
  init_auth();
37231
+ init_compress();
36330
37232
  init_logger();
36331
37233
  init_listen_banner();
36332
37234
  init_otel();
@@ -36334,13 +37236,13 @@ var init_dist2 = __esm({
36334
37236
  init_sandbox();
36335
37237
  init_init();
36336
37238
  init_git_ssh();
36337
- __dirname3 = path86.dirname(fileURLToPath3(import.meta.url));
37239
+ __dirname3 = path87.dirname(fileURLToPath3(import.meta.url));
36338
37240
  PORT = config.server.port;
36339
- HALO_VERSION = "1.6.4";
37241
+ HALO_VERSION = "1.6.6";
36340
37242
  PROJECT_ROOT = findProjectRoot();
36341
- HALO_HOME4 = path86.join(homedir25(), ".halo");
37243
+ HALO_HOME4 = path87.join(homedir25(), ".halo");
36342
37244
  GIT_SHA = readGitSha();
36343
- if (!fs78.existsSync(path86.join(HALO_HOME4, "global", ".template-version"))) {
37245
+ if (!fs79.existsSync(path87.join(HALO_HOME4, "global", ".template-version"))) {
36344
37246
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
36345
37247
  process.exit(1);
36346
37248
  }
@@ -36351,7 +37253,7 @@ var init_dist2 = __esm({
36351
37253
  process.exit(1);
36352
37254
  }
36353
37255
  if (!AGENTCORE)
36354
- acquireSingleInstanceLock(path86.join(HALO_HOME4, "global", "server.lock"));
37256
+ acquireSingleInstanceLock(path87.join(HALO_HOME4, "global", "server.lock"));
36355
37257
  commandRegistry.assertDispatchCommands(DISPATCH_COMMANDS);
36356
37258
  await initObservability();
36357
37259
  initLogger();
@@ -36380,7 +37282,7 @@ var init_dist2 = __esm({
36380
37282
  maxAge: 86400,
36381
37283
  credentials: allowlist.length > 0
36382
37284
  }));
36383
- app.use("/*", compress({ threshold: 1024 }));
37285
+ app.use("/*", compressResponses(1024));
36384
37286
  jupyter = AGENTCORE ? null : createJupyterManager();
36385
37287
  if (!AGENTCORE) {
36386
37288
  app.use("/api/*", authMiddleware());
@@ -36421,12 +37323,12 @@ var init_dist2 = __esm({
36421
37323
  app.route("/api", createJupyterRoutes(jupyter));
36422
37324
  }
36423
37325
  start2();
36424
- channelDb = createChannelDb(path86.join(HALO_HOME4, "secrets"));
37326
+ channelDb = createChannelDb(path87.join(HALO_HOME4, "secrets"));
36425
37327
  setChannelDb(channelDb);
36426
- setEvoDb(createEvoDb(path86.join(HALO_HOME4, "global")));
36427
- setCronDb(createCronDb(path86.join(HALO_HOME4, "global")));
37328
+ setEvoDb(createEvoDb(path87.join(HALO_HOME4, "global")));
37329
+ setCronDb(createCronDb(path87.join(HALO_HOME4, "global")));
36428
37330
  if (!AGENTCORE)
36429
- setRunsDb(createRunsDb(path86.join(HALO_HOME4, "global")));
37331
+ setRunsDb(createRunsDb(path87.join(HALO_HOME4, "global")));
36430
37332
  setEvoSpawner(realEvoSpawner);
36431
37333
  if (!AGENTCORE) {
36432
37334
  startEvoTicker();
@@ -36441,7 +37343,7 @@ var init_dist2 = __esm({
36441
37343
  setCronSessionRegistry(registry);
36442
37344
  if (OWNS_RUNTIMES && !AGENTCORE) {
36443
37345
  for (const ws of listRunningWorkspaces()) {
36444
- if (!fs78.existsSync(path86.join(ws, ".halo")))
37346
+ if (!fs79.existsSync(path87.join(ws, ".halo")))
36445
37347
  continue;
36446
37348
  if (!claimWorkspaceRuntime(ws))
36447
37349
  continue;
@@ -36467,7 +37369,7 @@ var init_dist2 = __esm({
36467
37369
  startCronDaemon();
36468
37370
  }
36469
37371
  if (!AGENTCORE) {
36470
- setA2ADb(createA2ADb(path86.join(HALO_HOME4, "global")));
37372
+ setA2ADb(createA2ADb(path87.join(HALO_HOME4, "global")));
36471
37373
  setA2AOutboundRegistry(registry);
36472
37374
  app.route("/a2a", createA2ARoutes({ registry, ownsRuntimes: OWNS_RUNTIMES }));
36473
37375
  app.route("/", createA2APushRoutes());
@@ -36483,11 +37385,11 @@ var init_dist2 = __esm({
36483
37385
  }
36484
37386
  FRONTEND_DIR = resolveFrontendDir();
36485
37387
  if (!AGENTCORE) {
36486
- app.use("/*", serveStatic({ root: path86.relative(process.cwd(), FRONTEND_DIR) }));
37388
+ app.use("/*", serveStatic({ root: path87.relative(process.cwd(), FRONTEND_DIR) }));
36487
37389
  app.get("/*", (c) => {
36488
- const indexPath = path86.join(FRONTEND_DIR, "index.html");
37390
+ const indexPath = path87.join(FRONTEND_DIR, "index.html");
36489
37391
  try {
36490
- const html = fs78.readFileSync(indexPath, "utf-8");
37392
+ const html = fs79.readFileSync(indexPath, "utf-8");
36491
37393
  return c.html(html);
36492
37394
  } catch {
36493
37395
  return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
@@ -36506,7 +37408,7 @@ var init_dist2 = __esm({
36506
37408
  for (const line of AGENTCORE ? lines.slice(0, 1) : lines)
36507
37409
  logAlways(line);
36508
37410
  });
36509
- wss = new WebSocketServer3({
37411
+ wss = new WebSocketServer4({
36510
37412
  noServer: true,
36511
37413
  verifyClient: (info, callback) => {
36512
37414
  const token = getTokenFromCookieHeader(info.req.headers.cookie);
@@ -36522,12 +37424,15 @@ var init_dist2 = __esm({
36522
37424
  setBroadcastWss(wss);
36523
37425
  }
36524
37426
  transcribe = AGENTCORE ? null : createTranscribeProxy();
37427
+ stt = AGENTCORE ? null : createSttProxy();
36525
37428
  jupyterChannels = jupyter ? createJupyterChannels(jupyter) : null;
36526
37429
  upgradeRoutes = new Map(AGENTCORE ? [] : [
36527
37430
  ["/ws", (req, socket, head) => wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req))]
36528
37431
  ]);
36529
37432
  if (transcribe)
36530
37433
  upgradeRoutes.set(TRANSCRIBE_PATH, transcribe.handleUpgrade);
37434
+ if (stt)
37435
+ upgradeRoutes.set(STT_PATH, stt.handleUpgrade);
36531
37436
  if (jupyterChannels)
36532
37437
  upgradeRoutes.set(JUPYTER_CHANNELS_PATH, jupyterChannels.handleUpgrade);
36533
37438
  server.on("upgrade", createUpgradeRouter(upgradeRoutes));
@@ -36695,8 +37600,8 @@ var init_halo_client = __esm({
36695
37600
  }
36696
37601
  return await res.json();
36697
37602
  }
36698
- sessionUrl(path89, workspace, sessionId) {
36699
- const url = new URL(`${this.opts.baseUrl}${path89}`);
37603
+ sessionUrl(path90, workspace, sessionId) {
37604
+ const url = new URL(`${this.opts.baseUrl}${path90}`);
36700
37605
  url.searchParams.set("workspace", workspace);
36701
37606
  url.searchParams.set("sessionId", sessionId);
36702
37607
  return url;
@@ -36844,7 +37749,7 @@ var init_version = __esm({
36844
37749
  });
36845
37750
 
36846
37751
  // ../acp-adapter/dist/adapter.js
36847
- import path87 from "node:path";
37752
+ import path88 from "node:path";
36848
37753
  function composePrompt(blocks) {
36849
37754
  const segments = [];
36850
37755
  let run = "";
@@ -37037,7 +37942,7 @@ var init_adapter = __esm({
37037
37942
  if (cursor !== void 0 && !/^\d+$/.test(cursor))
37038
37943
  throw newError(`invalid cursor: ${cursor}`, -32602);
37039
37944
  const page = await this.client.listSessions(this.config.workspace, cursor === void 0 ? void 0 : Number(cursor));
37040
- if (p.cwd !== void 0 && path87.resolve(p.cwd) !== path87.resolve(page.workspace))
37945
+ if (p.cwd !== void 0 && path88.resolve(p.cwd) !== path88.resolve(page.workspace))
37041
37946
  return { sessions: [] };
37042
37947
  return {
37043
37948
  sessions: page.sessions.map((s) => ({
@@ -37651,8 +38556,8 @@ process.emitWarning = (warning, ...args) => {
37651
38556
 
37652
38557
  // src/index.ts
37653
38558
  init_server_supervisor();
37654
- import path88 from "node:path";
37655
- import fs79 from "node:fs";
38559
+ import path89 from "node:path";
38560
+ import fs80 from "node:fs";
37656
38561
  import { homedir as homedir26, userInfo } from "node:os";
37657
38562
  import { parseArgs as parseArgs2 } from "node:util";
37658
38563
 
@@ -37943,7 +38848,7 @@ function installUserUnit(opts, run = runCommand) {
37943
38848
  }
37944
38849
 
37945
38850
  // src/index.ts
37946
- var VERSION = "1.6.4";
38851
+ var VERSION = "1.6.6";
37947
38852
  var HELP_TOP = `Usage: halo <command> [options]
37948
38853
 
37949
38854
  Commands:
@@ -38285,8 +39190,8 @@ async function cmdSetup(argv = []) {
38285
39190
  const { buildBindProviderPlan: buildBindProviderPlan2, envKeyPlaceholders: envKeyPlaceholders2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
38286
39191
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
38287
39192
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
38288
- const haloHome2 = path88.join(homedir26(), ".halo");
38289
- const globalDir2 = path88.join(haloHome2, "global");
39193
+ const haloHome2 = path89.join(homedir26(), ".halo");
39194
+ const globalDir2 = path89.join(haloHome2, "global");
38290
39195
  ensureHaloHome2(haloHome2);
38291
39196
  process.stderr.write(`[setup] ${haloHome2}/ ready
38292
39197
  `);
@@ -38639,7 +39544,7 @@ async function cmdUpgrade() {
38639
39544
  `);
38640
39545
  process.exit(1);
38641
39546
  }
38642
- const broken = verifyNativeModules2(path88.join(rootDir, PKG));
39547
+ const broken = verifyNativeModules2(path89.join(rootDir, PKG));
38643
39548
  if (broken.length > 0) {
38644
39549
  process.stderr.write(
38645
39550
  `
@@ -38670,7 +39575,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
38670
39575
  async function cmdAgents(flags) {
38671
39576
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
38672
39577
  await initRuntime2();
38673
- const workspace = path88.resolve(flags.workspace ?? process.cwd());
39578
+ const workspace = path89.resolve(flags.workspace ?? process.cwd());
38674
39579
  const agents = await listAgents2(workspace);
38675
39580
  if (agents.length === 0) {
38676
39581
  process.stderr.write("No agents found.\n");
@@ -38686,7 +39591,7 @@ async function cmdAgents(flags) {
38686
39591
  async function cmdSessions(flags) {
38687
39592
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
38688
39593
  await initRuntime2();
38689
- const workspace = path88.resolve(flags.workspace ?? process.cwd());
39594
+ const workspace = path89.resolve(flags.workspace ?? process.cwd());
38690
39595
  const sessions = listSessions2(workspace);
38691
39596
  if (sessions.length === 0) {
38692
39597
  process.stderr.write("No sessions found.\n");
@@ -38717,7 +39622,7 @@ async function cmdModels(argv) {
38717
39622
  process.exitCode = 1;
38718
39623
  return;
38719
39624
  }
38720
- const plan = mod.planModelsInstall(path88.resolve(dir));
39625
+ const plan = mod.planModelsInstall(path89.resolve(dir));
38721
39626
  for (const line of mod.formatPlan(plan)) process.stdout.write(`${line}
38722
39627
  `);
38723
39628
  if (plan.needsConfirm && !values.yes) {
@@ -38826,11 +39731,11 @@ ${stdinContent}` : stdinContent;
38826
39731
  harness.destroy();
38827
39732
  }
38828
39733
  }
38829
- var SERVER_LOCK_PATH = path88.join(homedir26(), ".halo", "global", "server.lock");
38830
- var SERVER_LOG_PATH = path88.join(homedir26(), ".halo", "logs", "server.log");
39734
+ var SERVER_LOCK_PATH = path89.join(homedir26(), ".halo", "global", "server.lock");
39735
+ var SERVER_LOG_PATH = path89.join(homedir26(), ".halo", "logs", "server.log");
38831
39736
  function readServerPid() {
38832
39737
  try {
38833
- const txt = fs79.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
39738
+ const txt = fs80.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
38834
39739
  const n = parseInt(txt, 10);
38835
39740
  return Number.isFinite(n) && n > 0 ? n : null;
38836
39741
  } catch {
@@ -38937,8 +39842,8 @@ async function cmdServerStart(argv, restart) {
38937
39842
  }
38938
39843
  if (plan.note) writeDim(plan.note);
38939
39844
  const { spawn: spawn5 } = await import("node:child_process");
38940
- fs79.mkdirSync(path88.dirname(SERVER_LOG_PATH), { recursive: true });
38941
- const out = fs79.openSync(SERVER_LOG_PATH, "a");
39845
+ fs80.mkdirSync(path89.dirname(SERVER_LOG_PATH), { recursive: true });
39846
+ const out = fs80.openSync(SERVER_LOG_PATH, "a");
38942
39847
  const child = spawn5(process.argv[0], [process.argv[1], "server", "start"], {
38943
39848
  detached: true,
38944
39849
  stdio: ["ignore", out, out],
@@ -39018,8 +39923,8 @@ ${HELP_AGENTCORE}`);
39018
39923
  process.exitCode = 1;
39019
39924
  return;
39020
39925
  }
39021
- const workspace = path88.resolve(raw);
39022
- if (!fs79.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) {
39926
+ const workspace = path89.resolve(raw);
39927
+ if (!fs80.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) {
39023
39928
  process.stderr.write(`Workspace is not a directory: ${workspace}
39024
39929
  `);
39025
39930
  process.exitCode = 1;
@@ -39125,7 +40030,7 @@ async function cmdServerLogs(argv) {
39125
40030
  lines: { type: "string", short: "n" }
39126
40031
  }
39127
40032
  });
39128
- if (!fs79.existsSync(SERVER_LOG_PATH)) {
40033
+ if (!fs80.existsSync(SERVER_LOG_PATH)) {
39129
40034
  process.stderr.write(`No log file at ${SERVER_LOG_PATH}
39130
40035
  `);
39131
40036
  return;
@@ -39143,7 +40048,7 @@ function exitMissingSetup() {
39143
40048
  process.exit(1);
39144
40049
  }
39145
40050
  function isHaloHomeReady() {
39146
- return fs79.existsSync(path88.join(homedir26(), ".halo", "global", ".template-version"));
40051
+ return fs80.existsSync(path89.join(homedir26(), ".halo", "global", ".template-version"));
39147
40052
  }
39148
40053
  async function main2() {
39149
40054
  const argv = process.argv.slice(2);