@turmind/halo 1.6.4 → 1.6.5

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 (29) hide show
  1. package/README.md +17 -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/canvas-extensions.md +38 -7
  10. package/bundled-docs/design/cron.md +12 -4
  11. package/bundled-docs/design/halo-city.md +14 -149
  12. package/bundled-docs/design/storage.md +6 -2
  13. package/bundled-docs/design/stt-local.md +106 -0
  14. package/bundled-docs/design/web.md +1 -1
  15. package/bundled-docs/dev/api.md +11 -8
  16. package/bundled-docs/dev/desktop-packaging.md +37 -9
  17. package/bundled-docs/dev/env.md +1 -1
  18. package/bundled-docs/requirements/chat.md +8 -0
  19. package/bundled-docs/requirements/workspace.md +4 -0
  20. package/dist/index.js +1342 -538
  21. package/package.json +2 -1
  22. package/templates/skills/cron/SKILL.md +2 -1
  23. package/templates/skills/cron/manage-cron.py +2 -1
  24. package/admin-out/_next/static/chunks/147-d4200c70445aabb6.js +0 -1
  25. package/admin-out/_next/static/chunks/989-5d7743ff56d00dd7.js +0 -1
  26. package/admin-out/_next/static/chunks/app/page-8881b4f87a3d5ae8.js +0 -1
  27. package/admin-out/_next/static/css/3b4dcd0ac6b8eae8.css +0 -1
  28. /package/admin-out/_next/static/{kfp1cMc36J6hjZ06L9mm- → 8v1hCXbUqJhyMuK7LVeet}/_buildManifest.js +0 -0
  29. /package/admin-out/_next/static/{kfp1cMc36J6hjZ06L9mm- → 8v1hCXbUqJhyMuK7LVeet}/_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 = 102;
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.5", 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 };
@@ -25250,7 +25557,7 @@ var init_session_types = __esm({
25250
25557
  });
25251
25558
 
25252
25559
  // ../server/dist/routes/halo-city.js
25253
- import { Hono as Hono11 } from "hono";
25560
+ import { Hono as Hono12 } from "hono";
25254
25561
  import fs59 from "node:fs";
25255
25562
  import path67 from "node:path";
25256
25563
  import Database7 from "better-sqlite3";
@@ -25537,20 +25844,25 @@ function discoverWorkspaces(registry2) {
25537
25844
  return out;
25538
25845
  }
25539
25846
  function createShowRoutes(registry2) {
25540
- const app2 = new Hono11();
25847
+ const app2 = new Hono12();
25541
25848
  function auth(c) {
25849
+ const cookie = getTokenFromCookieHeader(c.req.header("cookie"));
25850
+ if (!requestToken(c) && cookie && isAuthenticated(cookie)) {
25851
+ return { ok: true, accessLevel: "full", scoped: null };
25852
+ }
25542
25853
  const a = resolveTokenAuth(c, getChannelDb());
25543
25854
  if (!a.ok)
25544
25855
  return { ok: false, response: tokenAuthJsonError(c, a.reason) };
25545
- return { ok: true, account: a.account };
25856
+ const { account } = a;
25857
+ const globalView = account.accessLevel === "full" || account.accessLevel === "observer";
25858
+ return { ok: true, accessLevel: account.accessLevel, scoped: globalView ? null : account };
25546
25859
  }
25547
25860
  app2.get("/show/state", (c) => {
25548
25861
  const a = auth(c);
25549
25862
  if (!a.ok)
25550
25863
  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)]]);
25864
+ const { scoped } = a;
25865
+ const targets = scoped ? /* @__PURE__ */ new Map([[scoped.workspacePath, path67.basename(scoped.workspacePath)]]) : discoverWorkspaces(registry2);
25554
25866
  const workspaces = [];
25555
25867
  for (const [wsPath, label] of targets) {
25556
25868
  try {
@@ -25566,7 +25878,7 @@ function createShowRoutes(registry2) {
25566
25878
  return c.json({
25567
25879
  serverTime: Date.now(),
25568
25880
  uptime: process.uptime(),
25569
- accessLevel: account.accessLevel,
25881
+ accessLevel: a.accessLevel,
25570
25882
  skills: scanSkills2(GLOBAL_SKILLS_DIR),
25571
25883
  workspaces
25572
25884
  });
@@ -25575,19 +25887,18 @@ function createShowRoutes(registry2) {
25575
25887
  const a = auth(c);
25576
25888
  if (!a.ok)
25577
25889
  return a.response;
25578
- const { account } = a;
25890
+ const { scoped } = a;
25579
25891
  const wsPath = c.req.query("ws") ?? "";
25580
25892
  const id = c.req.query("id") ?? "";
25581
25893
  if (!wsPath || !id)
25582
25894
  return c.json({ error: "ws and id required" }, 400);
25583
- const globalView = account.accessLevel === "full" || account.accessLevel === "observer";
25584
- if (!globalView) {
25895
+ if (scoped) {
25585
25896
  let allowed = false;
25586
25897
  try {
25587
- allowed = fs59.realpathSync(wsPath) === fs59.realpathSync(account.workspacePath);
25898
+ allowed = fs59.realpathSync(wsPath) === fs59.realpathSync(scoped.workspacePath);
25588
25899
  } catch {
25589
25900
  }
25590
- if (!allowed || !canAddressSession(account, id))
25901
+ if (!allowed || !canAddressSession(scoped, id))
25591
25902
  return c.json({ error: "forbidden" }, 403);
25592
25903
  }
25593
25904
  try {
@@ -25640,6 +25951,7 @@ var init_halo_city = __esm({
25640
25951
  "use strict";
25641
25952
  init_channel_db();
25642
25953
  init_web_token();
25954
+ init_auth();
25643
25955
  init_handler();
25644
25956
  init_agent_loader();
25645
25957
  init_session_store();
@@ -25655,7 +25967,7 @@ var init_halo_city = __esm({
25655
25967
  });
25656
25968
 
25657
25969
  // ../server/dist/routes/metrics.js
25658
- import { Hono as Hono12 } from "hono";
25970
+ import { Hono as Hono13 } from "hono";
25659
25971
  function family(name, type, help, samples) {
25660
25972
  const lines = [`# HELP ${name} ${help}`, `# TYPE ${name} ${type}`];
25661
25973
  for (const s of samples)
@@ -25663,7 +25975,7 @@ function family(name, type, help, samples) {
25663
25975
  return lines.join("\n");
25664
25976
  }
25665
25977
  function createMetricsRoutes(registry2) {
25666
- const app2 = new Hono12();
25978
+ const app2 = new Hono13();
25667
25979
  function auth(c) {
25668
25980
  const a = resolveTokenAuth(c, getChannelDb());
25669
25981
  if (!a.ok) {
@@ -25748,9 +26060,9 @@ var init_metrics = __esm({
25748
26060
  });
25749
26061
 
25750
26062
  // ../server/dist/routes/commands.js
25751
- import { Hono as Hono13 } from "hono";
26063
+ import { Hono as Hono14 } from "hono";
25752
26064
  function createCommandRoutes(registry2, smRegistry) {
25753
- const app2 = new Hono13();
26065
+ const app2 = new Hono14();
25754
26066
  app2.get("/commands", async (c) => {
25755
26067
  const projectId = c.req.query("projectId");
25756
26068
  const sessionId = c.req.query("sessionId");
@@ -25777,233 +26089,6 @@ var init_commands3 = __esm({
25777
26089
  }
25778
26090
  });
25779
26091
 
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
26092
  // ../server/dist/routes/transcribe-ws.js
26008
26093
  import { WebSocketServer } from "ws";
26009
26094
  import { StartStreamTranscriptionCommand, TranscribeStreamingClient } from "@aws-sdk/client-transcribe-streaming";
@@ -26217,19 +26302,688 @@ var init_transcribe_ws = __esm({
26217
26302
  }
26218
26303
  });
26219
26304
 
26305
+ // ../server/dist/stt/models.js
26306
+ import { createHash as createHash2 } from "node:crypto";
26307
+ import fs60 from "node:fs";
26308
+ import fsp from "node:fs/promises";
26309
+ import http2 from "node:http";
26310
+ import https2 from "node:https";
26311
+ import path68 from "node:path";
26312
+ function httpGet(url, headers, signal, hops = 5) {
26313
+ return new Promise((resolve2, reject) => {
26314
+ const u = new URL(url);
26315
+ const req = (u.protocol === "https:" ? https2 : http2).get(u, { headers: { "User-Agent": "halo-stt", ...headers }, signal }, (res) => {
26316
+ res.on("error", () => {
26317
+ });
26318
+ const location = res.headers.location;
26319
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && location) {
26320
+ res.resume();
26321
+ if (hops === 0)
26322
+ reject(new Error("too many redirects"));
26323
+ else
26324
+ resolve2(httpGet(new URL(location, u).toString(), headers, signal, hops - 1));
26325
+ return;
26326
+ }
26327
+ resolve2(res);
26328
+ });
26329
+ req.on("error", reject);
26330
+ });
26331
+ }
26332
+ function createSttModelManager(opts = {}) {
26333
+ const files = opts.files ?? STT_MODEL_FILES;
26334
+ const connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
26335
+ const stallTimeoutMs = opts.stallTimeoutMs ?? 3e4;
26336
+ const progressIntervalMs = opts.progressIntervalMs ?? 500;
26337
+ const filePath = (rel) => path68.join(opts.dir ?? globalSttModelsDir(), rel);
26338
+ const total = files.reduce((n, f) => n + f.size, 0);
26339
+ const listeners2 = /* @__PURE__ */ new Set();
26340
+ let inflight = null;
26341
+ let received = 0;
26342
+ let lastError = "";
26343
+ let lastEmit = 0;
26344
+ let current = null;
26345
+ let closed = false;
26346
+ const fileReady = (f) => {
26347
+ try {
26348
+ return fs60.statSync(filePath(f.path)).size === f.size;
26349
+ } catch {
26350
+ return false;
26351
+ }
26352
+ };
26353
+ function status() {
26354
+ if (inflight)
26355
+ return { state: "downloading", received, total };
26356
+ if (files.every(fileReady))
26357
+ return { state: "ready" };
26358
+ if (lastError)
26359
+ return { state: "error", message: lastError };
26360
+ return { state: "missing" };
26361
+ }
26362
+ function emit2() {
26363
+ lastEmit = Date.now();
26364
+ const s = status();
26365
+ for (const l of listeners2)
26366
+ l(s);
26367
+ }
26368
+ async function fetchFile(f, url, base) {
26369
+ const final = filePath(f.path);
26370
+ const part = `${final}.part`;
26371
+ fs60.mkdirSync(path68.dirname(final), { recursive: true });
26372
+ let have = 0;
26373
+ try {
26374
+ have = fs60.statSync(part).size;
26375
+ } catch {
26376
+ }
26377
+ if (have > f.size) {
26378
+ fs60.rmSync(part, { force: true });
26379
+ have = 0;
26380
+ }
26381
+ received = base + have;
26382
+ if (have === f.size) {
26383
+ const hash = createHash2("sha256");
26384
+ for await (const chunk of fs60.createReadStream(part))
26385
+ hash.update(chunk);
26386
+ if (hash.digest("hex") === f.sha256) {
26387
+ fs60.renameSync(part, final);
26388
+ return;
26389
+ }
26390
+ fs60.rmSync(part, { force: true });
26391
+ have = 0;
26392
+ }
26393
+ const ac = new AbortController();
26394
+ current = ac;
26395
+ let reason = "";
26396
+ let timer2 = null;
26397
+ const arm = (ms, why) => {
26398
+ if (timer2)
26399
+ clearTimeout(timer2);
26400
+ timer2 = setTimeout(() => {
26401
+ reason = why;
26402
+ ac.abort();
26403
+ }, ms);
26404
+ };
26405
+ arm(connectTimeoutMs, `no data within ${connectTimeoutMs / 1e3}s`);
26406
+ try {
26407
+ const res = await httpGet(url, have > 0 ? { Range: `bytes=${have}-` } : {}, ac.signal);
26408
+ if (res.statusCode === 416) {
26409
+ fs60.rmSync(part, { force: true });
26410
+ throw new Error("HTTP 416");
26411
+ }
26412
+ if (res.statusCode !== 200 && res.statusCode !== 206)
26413
+ throw new Error(`HTTP ${res.statusCode}`);
26414
+ const resume = res.statusCode === 206;
26415
+ if (resume && !(res.headers["content-range"] ?? "").startsWith(`bytes ${have}-`)) {
26416
+ throw new Error(`unexpected Content-Range ${res.headers["content-range"] ?? "(none)"}`);
26417
+ }
26418
+ if (have > 0)
26419
+ console.log(`[STT] ${f.path}: ${resume ? `resuming at ${have} bytes` : `source ignored Range, restarting from 0 (had ${have} bytes)`}`);
26420
+ const hash = createHash2("sha256");
26421
+ let got = 0;
26422
+ if (resume) {
26423
+ for await (const chunk of fs60.createReadStream(part))
26424
+ hash.update(chunk);
26425
+ got = have;
26426
+ }
26427
+ const fh = await fsp.open(part, resume ? "a" : "w");
26428
+ try {
26429
+ for await (const value of res) {
26430
+ arm(stallTimeoutMs, `stalled for ${stallTimeoutMs / 1e3}s`);
26431
+ got += value.length;
26432
+ if (got > f.size)
26433
+ break;
26434
+ hash.update(value);
26435
+ await fh.write(value);
26436
+ received = base + got;
26437
+ if (Date.now() - lastEmit >= progressIntervalMs)
26438
+ emit2();
26439
+ }
26440
+ } finally {
26441
+ await fh.close();
26442
+ }
26443
+ if (got < f.size)
26444
+ throw new Error(`truncated (${got} of ${f.size} bytes)`);
26445
+ if (got > f.size || hash.digest("hex") !== f.sha256) {
26446
+ fs60.rmSync(part, { force: true });
26447
+ throw new Error(got > f.size ? `size mismatch (more than ${f.size} bytes)` : "sha256 mismatch");
26448
+ }
26449
+ fs60.renameSync(part, final);
26450
+ } catch (err) {
26451
+ if (reason)
26452
+ throw new Error(reason);
26453
+ throw err;
26454
+ } finally {
26455
+ if (timer2)
26456
+ clearTimeout(timer2);
26457
+ ac.abort();
26458
+ current = null;
26459
+ }
26460
+ }
26461
+ async function run() {
26462
+ let base = files.filter(fileReady).reduce((n, f) => n + f.size, 0);
26463
+ received = base;
26464
+ for (const f of files) {
26465
+ if (fileReady(f))
26466
+ continue;
26467
+ const failures = [];
26468
+ for (const src of f.sources) {
26469
+ if (closed)
26470
+ return;
26471
+ const t0 = Date.now();
26472
+ try {
26473
+ await fetchFile(f, src.url, base);
26474
+ console.log(`[STT] downloaded ${f.path} from ${src.name} (${(f.size / 1048576).toFixed(1)} MB in ${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
26475
+ failures.length = 0;
26476
+ break;
26477
+ } catch (err) {
26478
+ if (closed)
26479
+ return;
26480
+ const msg = err instanceof Error ? err.message : String(err);
26481
+ failures.push(`${src.name}: ${msg}`);
26482
+ console.log(`[STT] download ${f.path} from ${src.name} failed: ${msg}`);
26483
+ }
26484
+ }
26485
+ if (failures.length > 0) {
26486
+ lastError = `${path68.basename(f.path)}: all sources failed (${failures.join("; ")})`;
26487
+ return;
26488
+ }
26489
+ base += f.size;
26490
+ received = base;
26491
+ }
26492
+ }
26493
+ function ensure() {
26494
+ if (inflight)
26495
+ return inflight;
26496
+ if (closed || files.every(fileReady))
26497
+ return Promise.resolve();
26498
+ lastError = "";
26499
+ console.log("[STT] model download start");
26500
+ inflight = run().finally(() => {
26501
+ inflight = null;
26502
+ emit2();
26503
+ });
26504
+ emit2();
26505
+ return inflight;
26506
+ }
26507
+ return {
26508
+ status,
26509
+ ensure,
26510
+ subscribe(listener) {
26511
+ listeners2.add(listener);
26512
+ return () => {
26513
+ listeners2.delete(listener);
26514
+ };
26515
+ },
26516
+ filePath,
26517
+ close() {
26518
+ closed = true;
26519
+ current?.abort();
26520
+ }
26521
+ };
26522
+ }
26523
+ var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
26524
+ var init_models = __esm({
26525
+ "../server/dist/stt/models.js"() {
26526
+ "use strict";
26527
+ init_paths();
26528
+ SENSE_VOICE_MODEL = "sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx";
26529
+ SENSE_VOICE_TOKENS = "sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt";
26530
+ SILERO_VAD_MODEL = "silero-vad-v4/silero_vad.onnx";
26531
+ HF_SENSE_VOICE = "https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07";
26532
+ MS_SENSE_VOICE = "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/repo?Revision=master&FilePath=";
26533
+ STT_MODEL_FILES = [
26534
+ {
26535
+ path: SENSE_VOICE_MODEL,
26536
+ size: 239233841,
26537
+ sha256: "c71f0ce00bec95b07744e116345e33d8cbbe08cef896382cf907bf4b51a2cd51",
26538
+ sources: [
26539
+ { name: "huggingface", url: `${HF_SENSE_VOICE}/model.int8.onnx` },
26540
+ { name: "modelscope", url: `${MS_SENSE_VOICE}model.int8.onnx` }
26541
+ ]
26542
+ },
26543
+ {
26544
+ path: SENSE_VOICE_TOKENS,
26545
+ size: 315894,
26546
+ sha256: "f449eb28dc567533d7fa59be34e2abca8784f771850c78a47fb731a31429a1dc",
26547
+ sources: [
26548
+ { name: "huggingface", url: `${HF_SENSE_VOICE}/tokens.txt` },
26549
+ { name: "modelscope", url: `${MS_SENSE_VOICE}tokens.txt` }
26550
+ ]
26551
+ },
26552
+ {
26553
+ path: SILERO_VAD_MODEL,
26554
+ size: 1807522,
26555
+ sha256: "a35ebf52fd3ce5f1469b2a36158dba761bc47b973ea3382b3186ca15b1f5af28",
26556
+ sources: [
26557
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/vad/resolve/fba88cd2e921609e7675c3aaf51e0b9b295da4bc/silero_vad.onnx" },
26558
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/silero-vad/repo?Revision=master&FilePath=v4/silero_vad.onnx" }
26559
+ ]
26560
+ }
26561
+ ];
26562
+ sttModels = createSttModelManager();
26563
+ }
26564
+ });
26565
+
26566
+ // ../server/dist/stt/engine.js
26567
+ import os8 from "node:os";
26568
+ function createSttEngine(deps = {}) {
26569
+ const loadSherpa = deps.loadSherpa ?? defaultLoadSherpa;
26570
+ const models = deps.models ?? sttModels;
26571
+ let sherpa = null;
26572
+ let recognizer = null;
26573
+ let loading = null;
26574
+ const queue = [];
26575
+ let decoding = false;
26576
+ function load() {
26577
+ if (recognizer)
26578
+ return Promise.resolve();
26579
+ loading ??= doLoad().finally(() => {
26580
+ loading = null;
26581
+ });
26582
+ return loading;
26583
+ }
26584
+ async function doLoad() {
26585
+ const t0 = Date.now();
26586
+ try {
26587
+ const m = await loadSherpa();
26588
+ recognizer = await m.OfflineRecognizer.createAsync({
26589
+ featConfig: { sampleRate: SAMPLE_RATE, featureDim: 80 },
26590
+ modelConfig: {
26591
+ // language '' = auto: setting one barely changed results in testing.
26592
+ senseVoice: { model: models.filePath(SENSE_VOICE_MODEL), language: "", useInverseTextNormalization: 1 },
26593
+ tokens: models.filePath(SENSE_VOICE_TOKENS),
26594
+ numThreads: NUM_THREADS,
26595
+ provider: "cpu",
26596
+ debug: 0
26597
+ }
26598
+ });
26599
+ sherpa = m;
26600
+ console.log(`[STT] engine loaded in ${Date.now() - t0}ms (threads=${NUM_THREADS})`);
26601
+ } catch (err) {
26602
+ console.log(`[STT] engine load failed: ${errMsg2(err)}`);
26603
+ throw new SttError("engine", `speech engine unavailable: ${errMsg2(err)}`);
26604
+ }
26605
+ }
26606
+ async function pump() {
26607
+ if (decoding || !recognizer)
26608
+ return;
26609
+ decoding = true;
26610
+ while (queue.length > 0) {
26611
+ const job = queue.shift();
26612
+ try {
26613
+ const stream = recognizer.createStream();
26614
+ stream.acceptWaveform({ samples: job.samples, sampleRate: SAMPLE_RATE });
26615
+ await recognizer.decodeAsync(stream);
26616
+ job.owner.done(job, recognizer.getResult(stream));
26617
+ } catch (err) {
26618
+ job.owner.fail(new SttError("engine", `decode failed: ${errMsg2(err)}`));
26619
+ }
26620
+ }
26621
+ decoding = false;
26622
+ }
26623
+ function open(handlers) {
26624
+ if (!sherpa)
26625
+ throw new SttError("engine", "speech engine not loaded");
26626
+ let vad;
26627
+ try {
26628
+ vad = new sherpa.Vad({
26629
+ sileroVad: {
26630
+ model: models.filePath(SILERO_VAD_MODEL),
26631
+ threshold: VAD_THRESHOLD,
26632
+ minSilenceDuration: VAD_MIN_SILENCE_S,
26633
+ minSpeechDuration: VAD_MIN_SPEECH_S,
26634
+ maxSpeechDuration: VAD_MAX_SPEECH_S,
26635
+ windowSize: VAD_WINDOW
26636
+ },
26637
+ sampleRate: SAMPLE_RATE,
26638
+ numThreads: 1,
26639
+ debug: false
26640
+ }, RING_S);
26641
+ } catch (err) {
26642
+ throw new SttError("engine", `VAD unavailable: ${errMsg2(err)}`);
26643
+ }
26644
+ const ringLen = RING_S * SAMPLE_RATE;
26645
+ const ring = new Float32Array(ringLen);
26646
+ const win = new Float32Array(VAD_WINDOW);
26647
+ let winFill = 0;
26648
+ let total = 0;
26649
+ let carry = null;
26650
+ let prevEnd = 0;
26651
+ let pendingSamples = 0;
26652
+ let inFlight2 = 0;
26653
+ let segments = 0;
26654
+ let decodedSamples = 0;
26655
+ let stopped = false;
26656
+ let drained = null;
26657
+ const checkDrain = () => {
26658
+ if (drained && (stopped || inFlight2 === 0)) {
26659
+ const d = drained;
26660
+ drained = null;
26661
+ d();
26662
+ }
26663
+ };
26664
+ const dropQueued = () => {
26665
+ for (let i = queue.length - 1; i >= 0; i--) {
26666
+ if (queue[i].owner !== owner)
26667
+ continue;
26668
+ pendingSamples -= queue[i].samples.length;
26669
+ inFlight2--;
26670
+ queue.splice(i, 1);
26671
+ }
26672
+ };
26673
+ function failStream(err) {
26674
+ if (stopped)
26675
+ return;
26676
+ stopped = true;
26677
+ dropQueued();
26678
+ handlers.onError(err);
26679
+ checkDrain();
26680
+ }
26681
+ const owner = {
26682
+ done(job, result) {
26683
+ pendingSamples -= job.samples.length;
26684
+ inFlight2--;
26685
+ if (!stopped) {
26686
+ segments++;
26687
+ decodedSamples += job.samples.length;
26688
+ const text6 = (result.text ?? "").trim();
26689
+ if (text6)
26690
+ handlers.onFinal({ start: round3(job.start / SAMPLE_RATE), end: round3(job.end / SAMPLE_RATE), text: text6, lang: (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1") });
26691
+ }
26692
+ checkDrain();
26693
+ },
26694
+ fail(err) {
26695
+ inFlight2--;
26696
+ failStream(err);
26697
+ checkDrain();
26698
+ }
26699
+ };
26700
+ function enqueue(segStart, segEnd) {
26701
+ const start3 = Math.max(segStart - PAD_BEFORE_S * SAMPLE_RATE, prevEnd, total - ringLen, 0);
26702
+ const end = Math.min(segEnd + PAD_AFTER_S * SAMPLE_RATE, total);
26703
+ if (end <= start3)
26704
+ return;
26705
+ prevEnd = end;
26706
+ if (queue.length >= MAX_QUEUED_SEGMENTS) {
26707
+ failStream(new SttError("busy", `decode queue full (${MAX_QUEUED_SEGMENTS} segments waiting)`));
26708
+ return;
26709
+ }
26710
+ if ((pendingSamples + end - start3) / SAMPLE_RATE > MAX_PENDING_AUDIO_S) {
26711
+ failStream(new SttError("busy", `more than ${MAX_PENDING_AUDIO_S}s of audio waiting to be decoded`));
26712
+ return;
26713
+ }
26714
+ const samples = new Float32Array(end - start3);
26715
+ for (let i = 0; i < samples.length; i++)
26716
+ samples[i] = ring[(start3 + i) % ringLen];
26717
+ pendingSamples += samples.length;
26718
+ inFlight2++;
26719
+ queue.push({ owner, samples, start: start3, end });
26720
+ void pump();
26721
+ }
26722
+ function drainVad() {
26723
+ while (!stopped && !vad.isEmpty()) {
26724
+ const seg = vad.front(false);
26725
+ vad.pop();
26726
+ enqueue(seg.start, seg.start + seg.samples.length);
26727
+ }
26728
+ }
26729
+ function feed(samples) {
26730
+ vad.acceptWaveform(samples);
26731
+ drainVad();
26732
+ }
26733
+ return {
26734
+ write(pcm) {
26735
+ if (stopped)
26736
+ return;
26737
+ try {
26738
+ const bytes = carry ? Buffer.concat([carry, pcm]) : pcm;
26739
+ const n = bytes.length >> 1;
26740
+ carry = bytes.length & 1 ? Buffer.from(bytes.subarray(bytes.length - 1)) : null;
26741
+ for (let i = 0; i < n && !stopped; i++) {
26742
+ const f = bytes.readInt16LE(i * 2) / 32768;
26743
+ ring[total % ringLen] = f;
26744
+ total++;
26745
+ win[winFill++] = f;
26746
+ if (winFill === VAD_WINDOW) {
26747
+ winFill = 0;
26748
+ feed(win);
26749
+ }
26750
+ }
26751
+ } catch (err) {
26752
+ failStream(new SttError("engine", `VAD failed: ${errMsg2(err)}`));
26753
+ }
26754
+ },
26755
+ end() {
26756
+ if (!stopped) {
26757
+ try {
26758
+ if (winFill > 0) {
26759
+ win.fill(0, winFill);
26760
+ winFill = 0;
26761
+ feed(win);
26762
+ }
26763
+ vad.flush();
26764
+ drainVad();
26765
+ } catch (err) {
26766
+ failStream(new SttError("engine", `VAD failed: ${errMsg2(err)}`));
26767
+ }
26768
+ }
26769
+ return new Promise((resolve2) => {
26770
+ drained = resolve2;
26771
+ checkDrain();
26772
+ });
26773
+ },
26774
+ abort() {
26775
+ stopped = true;
26776
+ dropQueued();
26777
+ checkDrain();
26778
+ },
26779
+ stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE) })
26780
+ };
26781
+ }
26782
+ return { load, open };
26783
+ }
26784
+ 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;
26785
+ var init_engine = __esm({
26786
+ "../server/dist/stt/engine.js"() {
26787
+ "use strict";
26788
+ init_models();
26789
+ SAMPLE_RATE = 16e3;
26790
+ VAD_WINDOW = 512;
26791
+ VAD_THRESHOLD = 0.5;
26792
+ VAD_MIN_SILENCE_S = 0.5;
26793
+ VAD_MIN_SPEECH_S = 0.25;
26794
+ VAD_MAX_SPEECH_S = 20;
26795
+ PAD_BEFORE_S = 0.3;
26796
+ PAD_AFTER_S = 0.2;
26797
+ RING_S = 30;
26798
+ MAX_QUEUED_SEGMENTS = 32;
26799
+ MAX_PENDING_AUDIO_S = 60;
26800
+ NUM_THREADS = Math.min(4, Math.max(1, Math.floor(os8.availableParallelism() / 2)));
26801
+ SttError = class extends Error {
26802
+ code;
26803
+ constructor(code, message) {
26804
+ super(message);
26805
+ this.code = code;
26806
+ this.name = "SttError";
26807
+ }
26808
+ };
26809
+ defaultLoadSherpa = async () => (await import("sherpa-onnx-node")).default;
26810
+ errMsg2 = (err) => err instanceof Error ? err.message : String(err);
26811
+ round3 = (s) => Math.round(s * 1e3) / 1e3;
26812
+ sttEngine = createSttEngine();
26813
+ }
26814
+ });
26815
+
26816
+ // ../server/dist/routes/stt-ws.js
26817
+ import { WebSocketServer as WebSocketServer2 } from "ws";
26818
+ function parseQuery2(req) {
26819
+ const url = req.url ?? "";
26820
+ const i = url.indexOf("?");
26821
+ return new URLSearchParams(i < 0 ? "" : url.slice(i + 1));
26822
+ }
26823
+ function createSttProxy(deps = {}) {
26824
+ const models = deps.models ?? sttModels;
26825
+ const engine = deps.engine ?? sttEngine;
26826
+ const wss2 = new WebSocketServer2({
26827
+ noServer: true,
26828
+ maxPayload: MAX_FRAME_BYTES2,
26829
+ // Same cookie gate as /ws; the extension iframe is same-origin with the
26830
+ // admin, so its WS handshake carries the login cookie.
26831
+ verifyClient: (info, callback) => {
26832
+ if (!isAuthenticated(getTokenFromCookieHeader(info.req.headers.cookie)))
26833
+ return callback(false, 401, "Unauthorized");
26834
+ const ext = getExtension(parseQuery2(info.req).get("ext") ?? "");
26835
+ if (!ext || !ext.capabilities.includes("stt-local"))
26836
+ return callback(false, 403, "Forbidden");
26837
+ callback(true);
26838
+ }
26839
+ });
26840
+ wss2.on("connection", (ws, req) => {
26841
+ const extId = parseQuery2(req).get("ext");
26842
+ const openedAt = Date.now();
26843
+ let closed = false;
26844
+ let failed = false;
26845
+ let ended = false;
26846
+ let stream = null;
26847
+ let unsubscribe = null;
26848
+ ws.on("error", (err) => {
26849
+ console.log(`[STT] client error ext=${extId}: ${err.message}`);
26850
+ });
26851
+ const send2 = (frame) => {
26852
+ if (ws.readyState === ws.OPEN)
26853
+ ws.send(JSON.stringify(frame));
26854
+ };
26855
+ const fail2 = (code, message) => {
26856
+ if (failed || closed)
26857
+ return;
26858
+ failed = true;
26859
+ console.log(`[STT] stream error ext=${extId} code=${code}`);
26860
+ send2({ type: "error", code, message });
26861
+ ws.close(1011, code);
26862
+ };
26863
+ async function startEngine() {
26864
+ try {
26865
+ await engine.load();
26866
+ if (closed || failed || ended)
26867
+ return;
26868
+ stream = engine.open({
26869
+ // Recognized text goes to the client only — never to the log.
26870
+ onFinal: (f) => send2({ type: "final", ...f }),
26871
+ onError: (err) => fail2(err.code, err.message)
26872
+ });
26873
+ } catch (err) {
26874
+ if (err instanceof SttError)
26875
+ fail2(err.code, err.message);
26876
+ else
26877
+ fail2("io", err instanceof Error ? err.message : String(err));
26878
+ return;
26879
+ }
26880
+ if (closed || failed)
26881
+ return;
26882
+ send2({ type: "ready" });
26883
+ }
26884
+ const onModel = (s) => {
26885
+ if (closed || failed)
26886
+ return;
26887
+ send2({ type: "model", ...s });
26888
+ if (s.state === "ready" || s.state === "error") {
26889
+ unsubscribe?.();
26890
+ unsubscribe = null;
26891
+ }
26892
+ if (s.state === "ready")
26893
+ void startEngine();
26894
+ else if (s.state === "error")
26895
+ fail2("model", s.message ?? "model download failed");
26896
+ };
26897
+ console.log(`[STT] stream open ext=${extId}`);
26898
+ ws.on("message", (data, isBinary) => {
26899
+ if (ended || failed)
26900
+ return;
26901
+ if (isBinary) {
26902
+ if (!stream)
26903
+ return;
26904
+ stream.write(Buffer.isBuffer(data) ? data : Buffer.concat(Array.isArray(data) ? data : [Buffer.from(data)]));
26905
+ return;
26906
+ }
26907
+ let msg;
26908
+ try {
26909
+ msg = JSON.parse(data.toString());
26910
+ } catch {
26911
+ msg = null;
26912
+ }
26913
+ if (msg?.type !== "end") {
26914
+ fail2("bad-request", 'expected binary PCM or {"type":"end"}');
26915
+ return;
26916
+ }
26917
+ ended = true;
26918
+ if (!stream) {
26919
+ ws.close(1e3);
26920
+ return;
26921
+ }
26922
+ void stream.end().then(() => {
26923
+ if (!closed && !failed)
26924
+ ws.close(1e3);
26925
+ });
26926
+ });
26927
+ ws.on("close", () => {
26928
+ closed = true;
26929
+ unsubscribe?.();
26930
+ unsubscribe = null;
26931
+ stream?.abort();
26932
+ const st = stream?.stats();
26933
+ 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` : ""}`);
26934
+ });
26935
+ const initial = models.status();
26936
+ if (initial.state === "ready") {
26937
+ onModel(initial);
26938
+ return;
26939
+ }
26940
+ let pushed = false;
26941
+ unsubscribe = models.subscribe((s) => {
26942
+ pushed = true;
26943
+ onModel(s);
26944
+ });
26945
+ void models.ensure();
26946
+ if (!pushed)
26947
+ onModel(models.status());
26948
+ });
26949
+ return {
26950
+ handleUpgrade(req, socket, head) {
26951
+ wss2.handleUpgrade(req, socket, head, (ws) => wss2.emit("connection", ws, req));
26952
+ },
26953
+ close() {
26954
+ models.close();
26955
+ for (const client of wss2.clients)
26956
+ client.close(1001, "Server shutting down");
26957
+ wss2.close();
26958
+ }
26959
+ };
26960
+ }
26961
+ var STT_PATH, MAX_FRAME_BYTES2;
26962
+ var init_stt_ws = __esm({
26963
+ "../server/dist/routes/stt-ws.js"() {
26964
+ "use strict";
26965
+ init_auth();
26966
+ init_registry2();
26967
+ init_models();
26968
+ init_engine();
26969
+ STT_PATH = "/api/stt/stream";
26970
+ MAX_FRAME_BYTES2 = 1024 * 1024;
26971
+ }
26972
+ });
26973
+
26220
26974
  // ../server/dist/jupyter/manager.js
26221
26975
  import { spawn as nodeSpawn } from "node:child_process";
26222
26976
  import { randomBytes as randomBytes2 } from "node:crypto";
26223
- import fs60 from "node:fs";
26977
+ import fs61 from "node:fs";
26224
26978
  import net from "node:net";
26225
- import os8 from "node:os";
26226
- import path68 from "node:path";
26979
+ import os9 from "node:os";
26980
+ import path69 from "node:path";
26227
26981
  import { WebSocket } from "ws";
26228
26982
  function resolveExecutable(configured) {
26229
26983
  const isExe = (p) => {
26230
26984
  try {
26231
- fs60.accessSync(p, fs60.constants.X_OK);
26232
- return fs60.statSync(p).isFile();
26985
+ fs61.accessSync(p, fs61.constants.X_OK);
26986
+ return fs61.statSync(p).isFile();
26233
26987
  } catch {
26234
26988
  return false;
26235
26989
  }
@@ -26237,9 +26991,9 @@ function resolveExecutable(configured) {
26237
26991
  if (configured)
26238
26992
  return isExe(configured) ? configured : null;
26239
26993
  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);
26994
+ for (const dir of (process.env.PATH ?? "").split(path69.delimiter)) {
26995
+ if (dir && isExe(path69.join(dir, name)))
26996
+ return path69.join(dir, name);
26243
26997
  }
26244
26998
  return null;
26245
26999
  }
@@ -26267,9 +27021,9 @@ function createJupyterManager(deps = {}) {
26267
27021
  let err = "";
26268
27022
  let child;
26269
27023
  try {
26270
- child = spawnFn(exe, ["server", "--version"], { cwd: os8.homedir(), env: cleanChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
27024
+ child = spawnFn(exe, ["server", "--version"], { cwd: os9.homedir(), env: cleanChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
26271
27025
  } catch (e) {
26272
- resolve2({ ok: false, error: errMsg2(e) });
27026
+ resolve2({ ok: false, error: errMsg3(e) });
26273
27027
  return;
26274
27028
  }
26275
27029
  const timer2 = setTimeout(() => child.kill(), PROBE_TIMEOUT_MS);
@@ -26350,7 +27104,7 @@ function createJupyterManager(deps = {}) {
26350
27104
  "--ServerApp.ip=127.0.0.1",
26351
27105
  `--ServerApp.port=${port}`,
26352
27106
  "--ServerApp.port_retries=0",
26353
- `--ServerApp.root_dir=${path68.parse(process.cwd()).root}`,
27107
+ `--ServerApp.root_dir=${path69.parse(process.cwd()).root}`,
26354
27108
  "--ServerApp.terminals_enabled=False",
26355
27109
  "--ServerApp.shutdown_no_activity_timeout=600",
26356
27110
  "--MappingKernelManager.cull_idle_timeout=3600",
@@ -26360,7 +27114,7 @@ function createJupyterManager(deps = {}) {
26360
27114
  // its terminal does too, and kernels match the terminal.
26361
27115
  ...process.getuid?.() === 0 ? ["--ServerApp.allow_root=True"] : []
26362
27116
  ];
26363
- const child = spawnFn(exe, args, { cwd: os8.homedir(), env: { ...cleanChildEnv(), JUPYTER_TOKEN: token }, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
27117
+ const child = spawnFn(exe, args, { cwd: os9.homedir(), env: { ...cleanChildEnv(), JUPYTER_TOKEN: token }, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
26364
27118
  proc = child;
26365
27119
  let tail = "";
26366
27120
  child.stderr?.on("data", (d) => {
@@ -26428,7 +27182,7 @@ function createJupyterManager(deps = {}) {
26428
27182
  if (kernels.get(id) !== k || k.sockets.size > 0)
26429
27183
  return;
26430
27184
  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)}`));
27185
+ kernelOp(id, "delete").catch((e) => console.log(`[Jupyter] kernel ${id} shutdown failed: ${errMsg3(e)}`));
26432
27186
  }, KERNEL_GRACE_MS);
26433
27187
  k.grace.unref?.();
26434
27188
  }
@@ -26538,7 +27292,7 @@ function createJupyterManager(deps = {}) {
26538
27292
  }
26539
27293
  };
26540
27294
  }
26541
- var JUPYTER_HINT, KERNEL_GRACE_MS, START_TIMEOUT_MS, PROBE_TIMEOUT_MS, STOP_TIMEOUT_MS, JupyterError, errMsg2, lastLine;
27295
+ var JUPYTER_HINT, KERNEL_GRACE_MS, START_TIMEOUT_MS, PROBE_TIMEOUT_MS, STOP_TIMEOUT_MS, JupyterError, errMsg3, lastLine;
26542
27296
  var init_manager = __esm({
26543
27297
  "../server/dist/jupyter/manager.js"() {
26544
27298
  "use strict";
@@ -26558,16 +27312,16 @@ var init_manager = __esm({
26558
27312
  this.hint = hint;
26559
27313
  }
26560
27314
  };
26561
- errMsg2 = (err) => err instanceof Error ? err.message : String(err);
27315
+ errMsg3 = (err) => err instanceof Error ? err.message : String(err);
26562
27316
  lastLine = (s) => s.trim().split("\n").pop()?.trim() ?? "";
26563
27317
  }
26564
27318
  });
26565
27319
 
26566
27320
  // ../server/dist/routes/jupyter.js
26567
- import fs61 from "node:fs/promises";
26568
- import path69 from "node:path";
27321
+ import fs62 from "node:fs/promises";
27322
+ import path70 from "node:path";
26569
27323
  import { Hono as Hono15 } from "hono";
26570
- import { WebSocketServer as WebSocketServer2 } from "ws";
27324
+ import { WebSocketServer as WebSocketServer3 } from "ws";
26571
27325
  function fail(c, err) {
26572
27326
  if (err instanceof JupyterError)
26573
27327
  return c.json({ error: err.message, ...err.hint ? { hint: err.hint } : {} }, err.status);
@@ -26596,8 +27350,8 @@ function createJupyterRoutes(jm) {
26596
27350
  return c.json({ error: "Project not found" }, 400);
26597
27351
  if (!validatePath(nbPath, projectPath))
26598
27352
  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())
27353
+ const cwd = await fs62.realpath(path70.dirname(path70.resolve(projectPath, nbPath))).catch(() => null);
27354
+ if (!cwd || !(await fs62.stat(cwd)).isDirectory())
26601
27355
  return c.json({ error: "Notebook directory not found" }, 400);
26602
27356
  try {
26603
27357
  return c.json(await jm.startKernel(name, cwd));
@@ -26641,9 +27395,9 @@ function queryOf(req) {
26641
27395
  return new URLSearchParams(i < 0 ? "" : url.slice(i + 1));
26642
27396
  }
26643
27397
  function createJupyterChannels(jm) {
26644
- const wss2 = new WebSocketServer2({
27398
+ const wss2 = new WebSocketServer3({
26645
27399
  noServer: true,
26646
- maxPayload: MAX_FRAME_BYTES2,
27400
+ maxPayload: MAX_FRAME_BYTES3,
26647
27401
  // The client speaks the legacy JSON protocol; never echo a subprotocol.
26648
27402
  handleProtocols: () => false,
26649
27403
  // Same cookie gate as /ws and the transcribe proxy.
@@ -26737,7 +27491,7 @@ function createJupyterChannels(jm) {
26737
27491
  }
26738
27492
  };
26739
27493
  }
26740
- var JUPYTER_CHANNELS_PATH, KERNEL_NAME_RE, SESSION_ID_RE, MAX_FRAME_BYTES2;
27494
+ var JUPYTER_CHANNELS_PATH, KERNEL_NAME_RE, SESSION_ID_RE, MAX_FRAME_BYTES3;
26741
27495
  var init_jupyter = __esm({
26742
27496
  "../server/dist/routes/jupyter.js"() {
26743
27497
  "use strict";
@@ -26747,7 +27501,7 @@ var init_jupyter = __esm({
26747
27501
  JUPYTER_CHANNELS_PATH = "/api/jupyter/channels";
26748
27502
  KERNEL_NAME_RE = /^[\w.-]{1,128}$/;
26749
27503
  SESSION_ID_RE = /^[\w-]{1,128}$/;
26750
- MAX_FRAME_BYTES2 = 64 * 1024 * 1024;
27504
+ MAX_FRAME_BYTES3 = 64 * 1024 * 1024;
26751
27505
  }
26752
27506
  });
26753
27507
 
@@ -26768,8 +27522,8 @@ var init_upgrade_router = __esm({
26768
27522
  });
26769
27523
 
26770
27524
  // ../server/dist/ws/git-dir-watcher.js
26771
- import fs62 from "node:fs";
26772
- import path70 from "node:path";
27525
+ import fs63 from "node:fs";
27526
+ import path71 from "node:path";
26773
27527
  var GitDirWatcher;
26774
27528
  var init_git_dir_watcher = __esm({
26775
27529
  "../server/dist/ws/git-dir-watcher.js"() {
@@ -26793,8 +27547,8 @@ var init_git_dir_watcher = __esm({
26793
27547
  return;
26794
27548
  this.stop();
26795
27549
  this.workspaceRoot = workspaceRoot;
26796
- const gitDir = path70.join(workspaceRoot, ".git");
26797
- if (fs62.existsSync(gitDir)) {
27550
+ const gitDir = path71.join(workspaceRoot, ".git");
27551
+ if (fs63.existsSync(gitDir)) {
26798
27552
  this.watchGitDir(gitDir);
26799
27553
  } else {
26800
27554
  this.watchParentForGit(workspaceRoot);
@@ -26803,7 +27557,7 @@ var init_git_dir_watcher = __esm({
26803
27557
  /** Watch the `.git` dir for index/HEAD changes (steady state). */
26804
27558
  watchGitDir(gitDir) {
26805
27559
  try {
26806
- this.watcher = fs62.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
27560
+ this.watcher = fs63.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
26807
27561
  this.handleGitEvent(filename);
26808
27562
  });
26809
27563
  this.watcher.on("error", (err) => {
@@ -26822,7 +27576,7 @@ var init_git_dir_watcher = __esm({
26822
27576
  * Exists solely during the non-repo window; upgraded away on first `.git`. */
26823
27577
  watchParentForGit(workspaceRoot) {
26824
27578
  try {
26825
- this.watcher = fs62.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
27579
+ this.watcher = fs63.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
26826
27580
  this.handleParentEvent(filename);
26827
27581
  });
26828
27582
  this.watcher.on("error", (err) => {
@@ -26858,8 +27612,8 @@ var init_git_dir_watcher = __esm({
26858
27612
  const root = this.workspaceRoot;
26859
27613
  if (!root)
26860
27614
  return;
26861
- const gitDir = path70.join(root, ".git");
26862
- if (!fs62.existsSync(gitDir))
27615
+ const gitDir = path71.join(root, ".git");
27616
+ if (!fs63.existsSync(gitDir))
26863
27617
  return;
26864
27618
  this.closeWatcher();
26865
27619
  this.watchGitDir(gitDir);
@@ -27277,7 +28031,7 @@ var init_terminal_manager = __esm({
27277
28031
  });
27278
28032
 
27279
28033
  // ../server/dist/ws/handler.js
27280
- import path71 from "node:path";
28034
+ import path72 from "node:path";
27281
28035
  function setupWebSocketHandler(deps) {
27282
28036
  const { wss: wss2, registry: registry2 } = deps;
27283
28037
  const clients = /* @__PURE__ */ new Set();
@@ -27300,7 +28054,7 @@ function setupWebSocketHandler(deps) {
27300
28054
  ackedChatIds.add(id);
27301
28055
  }
27302
28056
  function resolveProjectPath2(projectId) {
27303
- if (path71.isAbsolute(projectId))
28057
+ if (path72.isAbsolute(projectId))
27304
28058
  return projectId;
27305
28059
  return null;
27306
28060
  }
@@ -27901,11 +28655,11 @@ var init_handler2 = __esm({
27901
28655
  import Database8 from "better-sqlite3";
27902
28656
  import { drizzle as drizzle5 } from "drizzle-orm/better-sqlite3";
27903
28657
  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";
28658
+ import path73 from "node:path";
28659
+ import fs64 from "node:fs";
27906
28660
  function createCronDb(globalDir2) {
27907
- fs63.mkdirSync(globalDir2, { recursive: true });
27908
- const dbPath = path72.join(globalDir2, "cron.db");
28661
+ fs64.mkdirSync(globalDir2, { recursive: true });
28662
+ const dbPath = path73.join(globalDir2, "cron.db");
27909
28663
  const sqlite = new Database8(dbPath);
27910
28664
  sqlite.pragma("journal_mode = WAL");
27911
28665
  sqlite.exec(CREATE_SQL5);
@@ -28015,7 +28769,12 @@ var init_cron_db = __esm({
28015
28769
  /** Session this run's cli was spawned on (`-s`). The orphan sweep's
28016
28770
  * fingerprint — a stateless job's per-run id can't be re-derived from
28017
28771
  * the job row. NULL on skipped rows and rows from before the column. */
28018
- sessionId: text5("session_id")
28772
+ sessionId: text5("session_id"),
28773
+ /** Workspace the run fired against (the job's at fire time) — where a
28774
+ * stateless run's session lives, even after the job moves workspace or is
28775
+ * deleted. NULL on rows from before the column whose job was already gone
28776
+ * (deleteRuns falls back to the job row). */
28777
+ workspacePath: text5("workspace_path")
28019
28778
  });
28020
28779
  CREATE_SQL5 = `
28021
28780
  CREATE TABLE IF NOT EXISTS cron_jobs (
@@ -28057,7 +28816,8 @@ CREATE TABLE IF NOT EXISTS cron_runs (
28057
28816
  log_path TEXT,
28058
28817
  dispatch_results TEXT,
28059
28818
  pid INTEGER,
28060
- session_id TEXT
28819
+ session_id TEXT,
28820
+ workspace_path TEXT
28061
28821
  );
28062
28822
  CREATE INDEX IF NOT EXISTS idx_cron_runs_job ON cron_runs(job_id, started_at);
28063
28823
  CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
@@ -28098,6 +28858,15 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
28098
28858
  addColumnIfMissing(s, "cron_jobs", "working_dir", "TEXT");
28099
28859
  addColumnIfMissing(s, "cron_jobs", "stateless", "INTEGER NOT NULL DEFAULT 0");
28100
28860
  addColumnIfMissing(s, "cron_runs", "session_id", "TEXT");
28861
+ },
28862
+ // v6: `cron_runs.workspace_path` — the workspace a run's session is in.
28863
+ // Backfilled from the job row; runs of an already-deleted job stay NULL.
28864
+ (s) => {
28865
+ addColumnIfMissing(s, "cron_runs", "workspace_path", "TEXT");
28866
+ s.exec(`
28867
+ UPDATE cron_runs SET workspace_path = (SELECT workspace_path FROM cron_jobs WHERE cron_jobs.id = cron_runs.job_id)
28868
+ WHERE workspace_path IS NULL
28869
+ `);
28101
28870
  }
28102
28871
  ];
28103
28872
  _cronDb = null;
@@ -28105,8 +28874,8 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
28105
28874
  });
28106
28875
 
28107
28876
  // ../server/dist/a2a/exposure.js
28108
- import fs64 from "node:fs";
28109
- import path73 from "node:path";
28877
+ import fs65 from "node:fs";
28878
+ import path74 from "node:path";
28110
28879
  import crypto6 from "node:crypto";
28111
28880
  function samePath(a, b, platform = process.platform) {
28112
28881
  if (platform !== "win32")
@@ -28119,7 +28888,7 @@ function realBase() {
28119
28888
  if (baseCache?.raw !== raw) {
28120
28889
  let real = null;
28121
28890
  try {
28122
- real = fs64.realpathSync(raw);
28891
+ real = fs65.realpathSync(raw);
28123
28892
  } catch {
28124
28893
  console.warn(`[A2A] base dir ${raw} not found \u2014 nothing is exposed`);
28125
28894
  }
@@ -28144,14 +28913,14 @@ function resolveExposedWorkspace(base, rel) {
28144
28913
  }
28145
28914
  let real;
28146
28915
  try {
28147
- real = fs64.realpathSync(path73.join(base, ...segs));
28916
+ real = fs65.realpathSync(path74.join(base, ...segs));
28148
28917
  } catch {
28149
28918
  return null;
28150
28919
  }
28151
- if (!real.startsWith(base.endsWith(path73.sep) ? base : base + path73.sep))
28920
+ if (!real.startsWith(base.endsWith(path74.sep) ? base : base + path74.sep))
28152
28921
  return null;
28153
28922
  try {
28154
- if (!fs64.statSync(path73.join(real, CARD_FILE)).isFile())
28923
+ if (!fs65.statSync(path74.join(real, CARD_FILE)).isFile())
28155
28924
  return null;
28156
28925
  } catch {
28157
28926
  return null;
@@ -28166,14 +28935,14 @@ function publicOrigin(c) {
28166
28935
  return `${proto}://${c.req.header("host") ?? url.host}`;
28167
28936
  }
28168
28937
  function buildCard(workspace, interfaceUrl, caps) {
28169
- const raw = JSON.parse(fs64.readFileSync(path73.join(workspace, CARD_FILE), "utf8"));
28938
+ const raw = JSON.parse(fs65.readFileSync(path74.join(workspace, CARD_FILE), "utf8"));
28170
28939
  if (typeof raw.name !== "string" || typeof raw.description !== "string" || !Array.isArray(raw.skills)) {
28171
28940
  throw new Error('agent-card.json needs string "name", string "description" and array "skills"');
28172
28941
  }
28173
28942
  return {
28174
28943
  name: raw.name,
28175
28944
  description: raw.description,
28176
- version: typeof raw.version === "string" ? raw.version : "1.6.4",
28945
+ version: typeof raw.version === "string" ? raw.version : "1.6.5",
28177
28946
  supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
28178
28947
  capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
28179
28948
  ...caps.tokenAuth === false ? {} : {
@@ -28200,7 +28969,7 @@ var init_exposure = __esm({
28200
28969
  init_channel_db();
28201
28970
  init_media_store();
28202
28971
  init_wire();
28203
- CARD_FILE = path73.join(".halo", "agent-card.json");
28972
+ CARD_FILE = path74.join(".halo", "agent-card.json");
28204
28973
  IO_MODES = ["text/plain", ...VISION_IMAGE_MIME_TYPES];
28205
28974
  baseCache = null;
28206
28975
  homeStrategies = {
@@ -28214,7 +28983,7 @@ var init_exposure = __esm({
28214
28983
  return { ok: false, status: 404 };
28215
28984
  let bound;
28216
28985
  try {
28217
- bound = fs64.realpathSync(auth.account.workspacePath);
28986
+ bound = fs65.realpathSync(auth.account.workspacePath);
28218
28987
  } catch {
28219
28988
  return { ok: false, status: 404 };
28220
28989
  }
@@ -28656,17 +29425,17 @@ var init_routes = __esm({
28656
29425
  });
28657
29426
 
28658
29427
  // ../server/dist/a2a/agentcore.js
28659
- import fs65 from "node:fs";
28660
- import os9 from "node:os";
28661
- import path74 from "node:path";
29428
+ import fs66 from "node:fs";
29429
+ import os10 from "node:os";
29430
+ import path75 from "node:path";
28662
29431
  import crypto8 from "node:crypto";
28663
29432
  import { Hono as Hono17 } from "hono";
28664
29433
  function readLease(file) {
28665
29434
  let raw;
28666
29435
  let mtimeMs;
28667
29436
  try {
28668
- raw = fs65.readFileSync(file, "utf8");
28669
- mtimeMs = fs65.statSync(file).mtimeMs;
29437
+ raw = fs66.readFileSync(file, "utf8");
29438
+ mtimeMs = fs66.statSync(file).mtimeMs;
28670
29439
  } catch (err) {
28671
29440
  if (err.code === "ENOENT")
28672
29441
  return null;
@@ -28680,16 +29449,16 @@ function readLease(file) {
28680
29449
  }
28681
29450
  }
28682
29451
  function leaseJson(owner, now) {
28683
- return JSON.stringify({ owner, host: os9.hostname(), pid: process.pid, heartbeatAt: now });
29452
+ return JSON.stringify({ owner, host: os10.hostname(), pid: process.pid, heartbeatAt: now });
28684
29453
  }
28685
29454
  function writeLease(file, owner, now) {
28686
29455
  const tmp = `${file}.${owner}.tmp`;
28687
- fs65.writeFileSync(tmp, leaseJson(owner, now));
28688
- fs65.renameSync(tmp, file);
29456
+ fs66.writeFileSync(tmp, leaseJson(owner, now));
29457
+ fs66.renameSync(tmp, file);
28689
29458
  }
28690
29459
  function tryAcquireLease(file, owner, now = Date.now()) {
28691
29460
  try {
28692
- fs65.writeFileSync(file, leaseJson(owner, now), { flag: "wx" });
29461
+ fs66.writeFileSync(file, leaseJson(owner, now), { flag: "wx" });
28693
29462
  return "held";
28694
29463
  } catch (err) {
28695
29464
  if (err.code !== "EEXIST")
@@ -28717,7 +29486,7 @@ function heartbeatLease(file, owner, now = Date.now()) {
28717
29486
  function releaseLease(file, owner) {
28718
29487
  try {
28719
29488
  if (readLease(file)?.body?.owner === owner)
28720
- fs65.unlinkSync(file);
29489
+ fs66.unlinkSync(file);
28721
29490
  } catch {
28722
29491
  }
28723
29492
  }
@@ -28739,7 +29508,7 @@ function upstreamStrategies(workspace, accessLevel) {
28739
29508
  function seedAgentCard(workspace) {
28740
29509
  const card = { name: "Halo", description: "Halo multi-agent workspace on Amazon Bedrock AgentCore. Send a task as plain text.", skills: [] };
28741
29510
  try {
28742
- fs65.writeFileSync(path74.join(workspace, CARD_FILE), `${JSON.stringify(card, null, 2)}
29511
+ fs66.writeFileSync(path75.join(workspace, CARD_FILE), `${JSON.stringify(card, null, 2)}
28743
29512
  `, { flag: "wx" });
28744
29513
  return true;
28745
29514
  } catch (err) {
@@ -28787,16 +29556,16 @@ function createAgentCoreA2ARoutes(deps) {
28787
29556
  return app2;
28788
29557
  }
28789
29558
  function createAgentCoreA2A(registry2, rawWorkspace, onLost) {
28790
- const workspace = fs65.realpathSync(rawWorkspace);
28791
- const haloDir = path74.join(workspace, ".halo");
29559
+ const workspace = fs66.realpathSync(rawWorkspace);
29560
+ const haloDir = path75.join(workspace, ".halo");
28792
29561
  if (!config.a2a.publicUrl)
28793
29562
  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
29563
  const accessLevel = upstreamAccessLevel();
28795
- const lease = new WorkspaceLease(path74.join(haloDir, LEASE_FILE), {
29564
+ const lease = new WorkspaceLease(path75.join(haloDir, LEASE_FILE), {
28796
29565
  onAcquired() {
28797
29566
  setA2ADb(createA2ADb(haloDir));
28798
29567
  setRunsDb(createRunsDb(haloDir));
28799
- fs65.rmSync(path74.join(haloDir, RUNTIME_LOCK_FILE), { force: true });
29568
+ fs66.rmSync(path75.join(haloDir, RUNTIME_LOCK_FILE), { force: true });
28800
29569
  registry2.getOrCreate(workspace);
28801
29570
  startPushSender({ maxAgeMs: PUSH_MAX_AGE_MS });
28802
29571
  reconcileOpenDispatches();
@@ -28810,9 +29579,9 @@ function createAgentCoreA2A(registry2, rawWorkspace, onLost) {
28810
29579
  const activate = () => {
28811
29580
  if (active)
28812
29581
  return;
28813
- fs65.mkdirSync(haloDir, { recursive: true });
29582
+ fs66.mkdirSync(haloDir, { recursive: true });
28814
29583
  if (seedAgentCard(workspace))
28815
- console.log(`[AgentCoreA2A] seeded ${path74.join(workspace, CARD_FILE)}`);
29584
+ console.log(`[AgentCoreA2A] seeded ${path75.join(workspace, CARD_FILE)}`);
28816
29585
  active = true;
28817
29586
  };
28818
29587
  return { app: createAgentCoreA2ARoutes({ registry: registry2, workspace, accessLevel, lease, activate }), lease, workspace };
@@ -28977,12 +29746,12 @@ var init_dispatcher = __esm({
28977
29746
 
28978
29747
  // ../server/dist/cron/runner.js
28979
29748
  import { spawn as spawn3, execFileSync } from "node:child_process";
28980
- import fs66 from "node:fs";
28981
- import path75 from "node:path";
29749
+ import fs67 from "node:fs";
29750
+ import path76 from "node:path";
28982
29751
  import { homedir as homedir24 } from "node:os";
28983
29752
  import { Cron } from "croner";
28984
29753
  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";
29754
+ import { and as and11, eq as eq14, gte as gte3, inArray as inArray2, lt as lt5, notInArray, or as or2 } from "drizzle-orm";
28986
29755
  function resolveHaloCli() {
28987
29756
  if (process.env.HALO_CLI)
28988
29757
  return process.env.HALO_CLI;
@@ -29083,9 +29852,9 @@ function jobChangedFields(job) {
29083
29852
  function sessionKeyFor(workspacePath, sessionId) {
29084
29853
  let ws;
29085
29854
  try {
29086
- ws = fs66.realpathSync(workspacePath);
29855
+ ws = fs67.realpathSync(workspacePath);
29087
29856
  } catch {
29088
- ws = path75.resolve(workspacePath);
29857
+ ws = path76.resolve(workspacePath);
29089
29858
  }
29090
29859
  return `${ws}
29091
29860
  ${sessionId}`;
@@ -29094,10 +29863,10 @@ function setCronSessionRegistry(r) {
29094
29863
  _sessionRegistry = r;
29095
29864
  }
29096
29865
  function logsDir() {
29097
- return path75.join(homedir24(), ".halo", "global", "logs", "cron");
29866
+ return path76.join(homedir24(), ".halo", "global", "logs", "cron");
29098
29867
  }
29099
29868
  function logPathFor(runId) {
29100
- return path75.join(logsDir(), `${runId}.log`);
29869
+ return path76.join(logsDir(), `${runId}.log`);
29101
29870
  }
29102
29871
  function newRunId() {
29103
29872
  return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`;
@@ -29158,7 +29927,7 @@ function sweepOrphanRuns(opts) {
29158
29927
  }
29159
29928
  }
29160
29929
  function startCronDaemon() {
29161
- fs66.mkdirSync(logsDir(), { recursive: true });
29930
+ fs67.mkdirSync(logsDir(), { recursive: true });
29162
29931
  sweepOrphanRuns();
29163
29932
  reloadAll();
29164
29933
  _daemonTimers.push(setInterval(() => {
@@ -29288,6 +30057,7 @@ function reconcileFromDb() {
29288
30057
  if (dv !== null && _lastCronDataVersion === dv)
29289
30058
  return;
29290
30059
  _lastCronDataVersion = dv;
30060
+ deleteOrphanRuns().catch((err) => console.warn(`[Cron] deleting runs of deleted jobs failed: ${err instanceof Error ? err.message : String(err)}`));
29291
30061
  const db3 = getCronDb();
29292
30062
  const rows = db3.select().from(cronJobs).all();
29293
30063
  const liveIds = /* @__PURE__ */ new Set();
@@ -29342,7 +30112,7 @@ async function runJob(jobId, triggerKind) {
29342
30112
  const runId = newRunId();
29343
30113
  const startedAt = Date.now();
29344
30114
  const logPath = logPathFor(runId);
29345
- fs66.mkdirSync(logsDir(), { recursive: true });
30115
+ fs67.mkdirSync(logsDir(), { recursive: true });
29346
30116
  const sessionId = job.stateless ? perRunSessionId(job, startedAt) : effectiveSessionId(job);
29347
30117
  const sessionKey = sessionKeyFor(job.workspacePath, sessionId);
29348
30118
  const host = _sessionRegistry?.peek(job.workspacePath);
@@ -29359,7 +30129,8 @@ async function runJob(jobId, triggerKind) {
29359
30129
  exitCode: null,
29360
30130
  failureReason: skipReason,
29361
30131
  logPath: null,
29362
- dispatchResults: null
30132
+ dispatchResults: null,
30133
+ workspacePath: job.workspacePath
29363
30134
  }).run();
29364
30135
  broadcast({ type: "cron:run_changed", jobId, runId, status: "skipped" });
29365
30136
  return runId;
@@ -29379,11 +30150,12 @@ async function runJob(jobId, triggerKind) {
29379
30150
  failureReason: null,
29380
30151
  logPath,
29381
30152
  dispatchResults: null,
29382
- sessionId
30153
+ sessionId,
30154
+ workspacePath: job.workspacePath
29383
30155
  }).run();
29384
30156
  broadcast({ type: "cron:run_changed", jobId, runId, status: "running" });
29385
30157
  const targets = parseTargets(job.targets);
29386
- if (!fs66.existsSync(job.workspacePath)) {
30158
+ if (!fs67.existsSync(job.workspacePath)) {
29387
30159
  finalize(runId, jobId, {
29388
30160
  status: "failed",
29389
30161
  output: null,
@@ -29426,7 +30198,7 @@ async function runJob(jobId, triggerKind) {
29426
30198
  args.push("--access", accessLevel);
29427
30199
  const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
29428
30200
  const timeoutSec = job.timeoutSec ?? CLI_TIMEOUT_SEC;
29429
- const teeStream = fs66.createWriteStream(logPath, { flags: "a" });
30201
+ const teeStream = fs67.createWriteStream(logPath, { flags: "a" });
29430
30202
  teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
29431
30203
  `);
29432
30204
  teeStream.write(`agent=${job.agentId} session=${sessionId}${job.stateless ? " (stateless)" : ""}${accessLevel ? ` access=${accessLevel}` : ""} workspace=${job.workspacePath} dir=${job.workingDir ?? "."} schedule=${job.schedule}
@@ -29552,8 +30324,8 @@ function readSessionRow(workspacePath, sessionId) {
29552
30324
  try {
29553
30325
  let entry = _sessionRows.get(workspacePath);
29554
30326
  if (!entry) {
29555
- const file = path75.join(workspacePath, ".halo", "halo.db");
29556
- if (!fs66.existsSync(file))
30327
+ const file = path76.join(workspacePath, ".halo", "halo.db");
30328
+ if (!fs67.existsSync(file))
29557
30329
  return null;
29558
30330
  entry = { db: new Database9(file, { readonly: true, fileMustExist: true }), version: -1, rows: /* @__PURE__ */ new Map() };
29559
30331
  _sessionRows.set(workspacePath, entry);
@@ -29624,10 +30396,14 @@ function parseTargets(raw) {
29624
30396
  async function pruneOldRuns(jobId) {
29625
30397
  const db3 = getCronDb();
29626
30398
  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;
30399
+ if (!job) {
30400
+ await deleteJobRuns(jobId);
30401
+ return;
30402
+ }
30403
+ const keep = job.stateless ? STATELESS_RUNS_PER_JOB_KEEP : RUNS_PER_JOB_KEEP;
29628
30404
  const doomed = rawSqlite(db3).prepare(`
29629
30405
  SELECT id, job_id AS jobId, session_id AS sessionId, started_at AS startedAt,
29630
- completed_at AS completedAt, status
30406
+ completed_at AS completedAt, status, workspace_path AS workspacePath
29631
30407
  FROM cron_runs
29632
30408
  WHERE job_id = ?
29633
30409
  AND id NOT IN (
@@ -29642,31 +30418,44 @@ async function pruneOldRuns(jobId) {
29642
30418
  async function deleteJobRuns(jobId) {
29643
30419
  await deleteRuns(getCronDb().select(DOOMED_RUN_COLUMNS).from(cronRuns).where(eq14(cronRuns.jobId, jobId)).all());
29644
30420
  }
30421
+ async function deleteOrphanRuns() {
30422
+ const db3 = getCronDb();
30423
+ await deleteRuns(db3.select(DOOMED_RUN_COLUMNS).from(cronRuns).where(notInArray(cronRuns.jobId, db3.select({ id: cronJobs.id }).from(cronJobs))).all());
30424
+ }
29645
30425
  async function deleteRuns(runs) {
29646
30426
  if (runs.length === 0)
29647
30427
  return;
29648
30428
  const db3 = getCronDb();
29649
30429
  const workspaceOf = /* @__PURE__ */ new Map();
29650
- const failed = /* @__PURE__ */ new Set();
30430
+ const openWorkspace = (workspacePath) => {
30431
+ let ws = workspaceOf.get(workspacePath);
30432
+ if (ws === void 0) {
30433
+ ws = fs67.existsSync(path76.join(workspacePath, ".halo", "halo.db")) ? { workspacePath, db: getWorkspaceDb(workspacePath).db } : null;
30434
+ workspaceOf.set(workspacePath, ws);
30435
+ }
30436
+ return ws;
30437
+ };
30438
+ const keptRows = /* @__PURE__ */ new Set();
29651
30439
  let deleted = 0;
29652
30440
  for (const run of runs) {
29653
30441
  if (run.sessionId !== perRunSessionId({ id: run.jobId }, run.startedAt))
29654
30442
  continue;
30443
+ if (run.status === "running" || run.completedAt === null) {
30444
+ keptRows.add(run.id);
30445
+ console.log(`[Cron] run ${run.id}: still in progress, kept with its session ${run.sessionId}`);
30446
+ continue;
30447
+ }
29655
30448
  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
- }
30449
+ const workspacePath = run.workspacePath ?? db3.select({ ws: cronJobs.workspacePath }).from(cronJobs).where(eq14(cronJobs.id, run.jobId)).get()?.ws;
30450
+ const ws = workspacePath ? openWorkspace(workspacePath) : null;
29662
30451
  if (ws && await deleteRunSession(ws, run))
29663
30452
  deleted++;
29664
30453
  } catch (err) {
29665
- failed.add(run.id);
30454
+ keptRows.add(run.id);
29666
30455
  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
30456
  }
29668
30457
  }
29669
- const ids = runs.map((r) => r.id).filter((id) => !failed.has(id));
30458
+ const ids = runs.map((r) => r.id).filter((id) => !keptRows.has(id));
29670
30459
  if (ids.length > 0)
29671
30460
  db3.delete(cronRuns).where(inArray2(cronRuns.id, ids)).run();
29672
30461
  if (deleted > 0) {
@@ -29680,8 +30469,6 @@ async function deleteRunSession({ workspacePath, db: db3 }, run) {
29680
30469
  console.log(`[Cron] run ${run.id}: kept session ${sid} (${why}); pruning the run row only`);
29681
30470
  return false;
29682
30471
  };
29683
- if (run.status === "running" || run.completedAt === null)
29684
- return keep("its run is still in progress");
29685
30472
  const host = _sessionRegistry?.peek(workspacePath);
29686
30473
  if (host?.hasActiveWorkInTree(sid))
29687
30474
  return keep("a turn is running in it");
@@ -29700,15 +30487,15 @@ async function deleteRunSession({ workspacePath, db: db3 }, run) {
29700
30487
  }
29701
30488
  async function pruneOldLogs() {
29702
30489
  const dir = logsDir();
29703
- if (!fs66.existsSync(dir))
30490
+ if (!fs67.existsSync(dir))
29704
30491
  return;
29705
30492
  const cutoff = Date.now() - LOG_RETENTION_DAYS * DAY_MS2;
29706
- for (const name of fs66.readdirSync(dir)) {
29707
- const p = path75.join(dir, name);
30493
+ for (const name of fs67.readdirSync(dir)) {
30494
+ const p = path76.join(dir, name);
29708
30495
  try {
29709
- const stat = fs66.statSync(p);
30496
+ const stat = fs67.statSync(p);
29710
30497
  if (stat.mtimeMs < cutoff)
29711
- fs66.rmSync(p, { force: true });
30498
+ fs67.rmSync(p, { force: true });
29712
30499
  } catch {
29713
30500
  }
29714
30501
  }
@@ -29751,7 +30538,8 @@ var init_runner = __esm({
29751
30538
  sessionId: cronRuns.sessionId,
29752
30539
  startedAt: cronRuns.startedAt,
29753
30540
  completedAt: cronRuns.completedAt,
29754
- status: cronRuns.status
30541
+ status: cronRuns.status,
30542
+ workspacePath: cronRuns.workspacePath
29755
30543
  };
29756
30544
  }
29757
30545
  });
@@ -29759,8 +30547,8 @@ var init_runner = __esm({
29759
30547
  // ../server/dist/routes/cron.js
29760
30548
  import { Hono as Hono18 } from "hono";
29761
30549
  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";
30550
+ import fs68 from "node:fs/promises";
30551
+ import path77 from "node:path";
29764
30552
  import { Cron as Cron2 } from "croner";
29765
30553
  function newJobId() {
29766
30554
  return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -29805,7 +30593,7 @@ async function parseWorkingDir(workspacePath, v) {
29805
30593
  return { workingDir: null };
29806
30594
  try {
29807
30595
  const abs = await resolveWorkingDir(workspacePath, s);
29808
- return { workingDir: path76.relative(path76.resolve(workspacePath), abs) || null };
30596
+ return { workingDir: path77.relative(path77.resolve(workspacePath), abs) || null };
29809
30597
  } catch (err) {
29810
30598
  return { error: `workingDir: ${err instanceof Error ? err.message : String(err)}` };
29811
30599
  }
@@ -30110,7 +30898,7 @@ function createCronRoutes() {
30110
30898
  return c.json({ log: null });
30111
30899
  let log2 = null;
30112
30900
  try {
30113
- log2 = await fs67.readFile(row.logPath, "utf-8");
30901
+ log2 = await fs68.readFile(row.logPath, "utf-8");
30114
30902
  } catch {
30115
30903
  }
30116
30904
  return c.json({ log: log2 });
@@ -30176,8 +30964,8 @@ var init_cron = __esm({
30176
30964
  });
30177
30965
 
30178
30966
  // ../server/dist/extensions/install.js
30179
- import fs68 from "node:fs";
30180
- import path77 from "node:path";
30967
+ import fs69 from "node:fs";
30968
+ import path78 from "node:path";
30181
30969
  import { randomBytes as randomBytes3 } from "node:crypto";
30182
30970
  import JSZip2 from "jszip";
30183
30971
  function safeEntryPath(name) {
@@ -30208,43 +30996,43 @@ async function unpackZip(zipBuf, dest) {
30208
30996
  const perm = entry.unixPermissions;
30209
30997
  if (typeof perm === "number" && (perm & S_IFMT) === S_IFLNK)
30210
30998
  throw new ExtensionInstallError(`zip entry is a symlink: ${entry.name}`);
30211
- const abs = path77.join(dest, rel);
30999
+ const abs = path78.join(dest, rel);
30212
31000
  if (entry.dir) {
30213
- fs68.mkdirSync(abs, { recursive: true });
31001
+ fs69.mkdirSync(abs, { recursive: true });
30214
31002
  continue;
30215
31003
  }
30216
- fs68.mkdirSync(path77.dirname(abs), { recursive: true });
30217
- fs68.writeFileSync(abs, await entry.async("nodebuffer"));
31004
+ fs69.mkdirSync(path78.dirname(abs), { recursive: true });
31005
+ fs69.writeFileSync(abs, await entry.async("nodebuffer"));
30218
31006
  }
30219
31007
  }
30220
31008
  function stripWrapperDir(dir) {
30221
- if (fs68.existsSync(path77.join(dir, MANIFEST_FILE)))
31009
+ if (fs69.existsSync(path78.join(dir, MANIFEST_FILE)))
30222
31010
  return;
30223
- const children = fs68.readdirSync(dir, { withFileTypes: true });
31011
+ const children = fs69.readdirSync(dir, { withFileTypes: true });
30224
31012
  if (children.length !== 1 || !children[0].isDirectory())
30225
31013
  return;
30226
- const inner = path77.join(dir, children[0].name);
30227
- if (!fs68.existsSync(path77.join(inner, MANIFEST_FILE)))
31014
+ const inner = path78.join(dir, children[0].name);
31015
+ if (!fs69.existsSync(path78.join(inner, MANIFEST_FILE)))
30228
31016
  return;
30229
- for (const name of fs68.readdirSync(inner)) {
30230
- fs68.renameSync(path77.join(inner, name), path77.join(dir, name));
31017
+ for (const name of fs69.readdirSync(inner)) {
31018
+ fs69.renameSync(path78.join(inner, name), path78.join(dir, name));
30231
31019
  }
30232
- fs68.rmdirSync(inner);
31020
+ fs69.rmdirSync(inner);
30233
31021
  }
30234
31022
  function rand() {
30235
31023
  return randomBytes3(3).toString("hex");
30236
31024
  }
30237
31025
  async function installExtensionZip(zipBuf, expectId) {
30238
31026
  const root = extensionsRoot();
30239
- fs68.mkdirSync(root, { recursive: true });
30240
- const stage = path77.join(root, `.tmp-unpack-${rand()}`);
30241
- fs68.mkdirSync(stage);
31027
+ fs69.mkdirSync(root, { recursive: true });
31028
+ const stage = path78.join(root, `.tmp-unpack-${rand()}`);
31029
+ fs69.mkdirSync(stage);
30242
31030
  try {
30243
31031
  await unpackZip(zipBuf, stage);
30244
31032
  stripWrapperDir(stage);
30245
31033
  let manifestId;
30246
31034
  try {
30247
- const raw = JSON.parse(fs68.readFileSync(path77.join(stage, MANIFEST_FILE), "utf-8"));
31035
+ const raw = JSON.parse(fs69.readFileSync(path78.join(stage, MANIFEST_FILE), "utf-8"));
30248
31036
  manifestId = typeof raw.id === "string" ? raw.id : "";
30249
31037
  } catch (err) {
30250
31038
  throw new ExtensionInstallError(`${MANIFEST_FILE} missing or unreadable at the zip root: ${err instanceof Error ? err.message : String(err)}`);
@@ -30256,36 +31044,36 @@ async function installExtensionZip(zipBuf, expectId) {
30256
31044
  const verdict2 = readExtensionDir(stage, manifestId);
30257
31045
  if ("error" in verdict2)
30258
31046
  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);
31047
+ const target = path78.join(root, manifestId);
31048
+ const old = path78.join(root, `.old-${manifestId}-${rand()}`);
31049
+ const hadOld = fs69.existsSync(target);
30262
31050
  if (hadOld)
30263
- fs68.renameSync(target, old);
31051
+ fs69.renameSync(target, old);
30264
31052
  try {
30265
- fs68.renameSync(stage, target);
31053
+ fs69.renameSync(stage, target);
30266
31054
  } catch (err) {
30267
31055
  if (hadOld)
30268
- fs68.renameSync(old, target);
31056
+ fs69.renameSync(old, target);
30269
31057
  throw err;
30270
31058
  }
30271
31059
  if (hadOld)
30272
- fs68.rmSync(old, { recursive: true, force: true });
31060
+ fs69.rmSync(old, { recursive: true, force: true });
30273
31061
  const info = readExtensionDir(target);
30274
31062
  if ("error" in info)
30275
31063
  throw new ExtensionInstallError(`installed but failed re-validation: ${info.error}`);
30276
31064
  console.log(`[Extensions] installed ${info.id}@${info.version}${hadOld ? " (replaced previous)" : ""}`);
30277
31065
  return info;
30278
31066
  } finally {
30279
- fs68.rmSync(stage, { recursive: true, force: true });
31067
+ fs69.rmSync(stage, { recursive: true, force: true });
30280
31068
  }
30281
31069
  }
30282
31070
  function uninstallExtension(id) {
30283
31071
  if (!isExtensionId(id))
30284
31072
  throw new ExtensionInstallError(`"${id}" is not a valid extension id`);
30285
- const dir = path77.join(extensionsRoot(), id);
30286
- if (!fs68.existsSync(dir))
31073
+ const dir = path78.join(extensionsRoot(), id);
31074
+ if (!fs69.existsSync(dir))
30287
31075
  return false;
30288
- fs68.rmSync(dir, { recursive: true, force: true });
31076
+ fs69.rmSync(dir, { recursive: true, force: true });
30289
31077
  console.log(`[Extensions] removed ${id}`);
30290
31078
  return true;
30291
31079
  }
@@ -30302,7 +31090,7 @@ var init_install2 = __esm({
30302
31090
  });
30303
31091
 
30304
31092
  // ../server/dist/extensions/watcher.js
30305
- import fs69 from "node:fs";
31093
+ import fs70 from "node:fs";
30306
31094
  function scheduleFlush() {
30307
31095
  if (flushTimer)
30308
31096
  return;
@@ -30313,15 +31101,23 @@ function scheduleFlush() {
30313
31101
  }
30314
31102
  function rescanAndBroadcast() {
30315
31103
  const snap = scanExtensions();
31104
+ reconcileSttModels(snap);
30316
31105
  const key = snapshotKey(snap);
30317
31106
  if (key === lastKey)
30318
31107
  return;
30319
31108
  lastKey = key;
30320
31109
  broadcast({ type: "extension:changed", ...snap });
30321
31110
  }
31111
+ function reconcileSttModels(snap) {
31112
+ if (!snap.extensions.some((e) => e.capabilities.includes("stt-local")))
31113
+ return;
31114
+ const { state } = sttModels.status();
31115
+ if (state !== "ready" && state !== "downloading")
31116
+ void sttModels.ensure();
31117
+ }
30322
31118
  function openWatcher(root) {
30323
31119
  try {
30324
- watcher = fs69.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
31120
+ watcher = fs70.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
30325
31121
  watcher.on("error", (err) => {
30326
31122
  console.warn(`[Extensions] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
30327
31123
  closeWatcher();
@@ -30347,11 +31143,13 @@ function start() {
30347
31143
  return;
30348
31144
  const root = extensionsRoot();
30349
31145
  try {
30350
- fs69.mkdirSync(root, { recursive: true });
31146
+ fs70.mkdirSync(root, { recursive: true });
30351
31147
  } catch (err) {
30352
31148
  console.warn(`[Extensions] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
30353
31149
  }
30354
- lastKey = snapshotKey(scanExtensions());
31150
+ const snap = scanExtensions();
31151
+ lastKey = snapshotKey(snap);
31152
+ reconcileSttModels(snap);
30355
31153
  openWatcher(root);
30356
31154
  const { extensions, errors } = getSnapshot();
30357
31155
  if (extensions.length > 0 || errors.length > 0) {
@@ -30372,6 +31170,7 @@ var init_watcher = __esm({
30372
31170
  "../server/dist/extensions/watcher.js"() {
30373
31171
  "use strict";
30374
31172
  init_broadcast();
31173
+ init_models();
30375
31174
  init_registry2();
30376
31175
  watcher = null;
30377
31176
  flushTimer = null;
@@ -30382,8 +31181,8 @@ var init_watcher = __esm({
30382
31181
 
30383
31182
  // ../server/dist/routes/extensions.js
30384
31183
  import { Hono as Hono19 } from "hono";
30385
- import fs70 from "node:fs";
30386
- import path78 from "node:path";
31184
+ import fs71 from "node:fs";
31185
+ import path79 from "node:path";
30387
31186
  import { Readable as Readable2 } from "node:stream";
30388
31187
  function assetMime(fileName) {
30389
31188
  const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
@@ -30401,15 +31200,15 @@ function resolveAsset(id, version, rest) {
30401
31200
  let dir;
30402
31201
  let real;
30403
31202
  try {
30404
- dir = fs70.realpathSync(path78.join(extensionsRoot(), id));
30405
- real = fs70.realpathSync(path78.join(dir, ...segments));
31203
+ dir = fs71.realpathSync(path79.join(extensionsRoot(), id));
31204
+ real = fs71.realpathSync(path79.join(dir, ...segments));
30406
31205
  } catch {
30407
31206
  return null;
30408
31207
  }
30409
- if (real !== dir && !real.startsWith(dir + path78.sep))
31208
+ if (real !== dir && !real.startsWith(dir + path79.sep))
30410
31209
  return null;
30411
31210
  try {
30412
- if (!fs70.statSync(real).isFile())
31211
+ if (!fs71.statSync(real).isFile())
30413
31212
  return null;
30414
31213
  } catch {
30415
31214
  return null;
@@ -30471,8 +31270,8 @@ function createExtensionRoutes() {
30471
31270
  const abs = resolveAsset(c.req.param("id"), c.req.param("version"), c.req.param("rest"));
30472
31271
  if (!abs)
30473
31272
  return c.json({ error: "Not found" }, 404);
30474
- const stat = fs70.statSync(abs);
30475
- const nodeStream = fs70.createReadStream(abs);
31273
+ const stat = fs71.statSync(abs);
31274
+ const nodeStream = fs71.createReadStream(abs);
30476
31275
  c.req.raw.signal?.addEventListener("abort", () => nodeStream.destroy(), { once: true });
30477
31276
  return new Response(Readable2.toWeb(nodeStream), {
30478
31277
  headers: {
@@ -30526,7 +31325,7 @@ var init_extensions = __esm({
30526
31325
  });
30527
31326
 
30528
31327
  // ../server/dist/models/watcher.js
30529
- import fs71 from "node:fs";
31328
+ import fs72 from "node:fs";
30530
31329
  function snapshot2() {
30531
31330
  const { effective } = loadProviders();
30532
31331
  return {
@@ -30554,7 +31353,7 @@ function reloadAndBroadcast() {
30554
31353
  }
30555
31354
  function openWatcher2(root) {
30556
31355
  try {
30557
- watcher2 = fs71.watch(root, { persistent: false, recursive: false }, () => scheduleFlush2());
31356
+ watcher2 = fs72.watch(root, { persistent: false, recursive: false }, () => scheduleFlush2());
30558
31357
  watcher2.on("error", (err) => {
30559
31358
  console.warn(`[Models] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
30560
31359
  closeWatcher2();
@@ -30580,7 +31379,7 @@ function start2() {
30580
31379
  return;
30581
31380
  const root = globalHubModelsDir();
30582
31381
  try {
30583
- fs71.mkdirSync(root, { recursive: true });
31382
+ fs72.mkdirSync(root, { recursive: true });
30584
31383
  } catch (err) {
30585
31384
  console.warn(`[Models] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
30586
31385
  }
@@ -30857,7 +31656,7 @@ var init_ticker = __esm({
30857
31656
  });
30858
31657
 
30859
31658
  // ../server/dist/evolution/spawn.js
30860
- import path79 from "node:path";
31659
+ import path80 from "node:path";
30861
31660
  import { spawn as spawn4 } from "node:child_process";
30862
31661
  import { fileURLToPath as fileURLToPath2 } from "node:url";
30863
31662
  var __dirname2, WRAPPER_PATH, realEvoSpawner;
@@ -30865,8 +31664,8 @@ var init_spawn = __esm({
30865
31664
  "../server/dist/evolution/spawn.js"() {
30866
31665
  "use strict";
30867
31666
  init_child_env();
30868
- __dirname2 = path79.dirname(fileURLToPath2(import.meta.url));
30869
- WRAPPER_PATH = path79.resolve(__dirname2, "evo-wrapper.js");
31667
+ __dirname2 = path80.dirname(fileURLToPath2(import.meta.url));
31668
+ WRAPPER_PATH = path80.resolve(__dirname2, "evo-wrapper.js");
30870
31669
  realEvoSpawner = (mode, id) => {
30871
31670
  const child = spawn4(process.execPath, [WRAPPER_PATH, `--mode=${mode}`, `--id=${id}`], {
30872
31671
  detached: true,
@@ -31697,12 +32496,12 @@ var init_handler3 = __esm({
31697
32496
  });
31698
32497
 
31699
32498
  // ../server/dist/routes/channel-accounts.js
31700
- import fs72 from "node:fs";
32499
+ import fs73 from "node:fs";
31701
32500
  function accountListFields(a) {
31702
32501
  return {
31703
32502
  accountId: a.accountId,
31704
32503
  workspacePath: a.workspacePath,
31705
- workspaceMissing: !fs72.existsSync(a.workspacePath),
32504
+ workspaceMissing: !fs73.existsSync(a.workspacePath),
31706
32505
  label: a.label,
31707
32506
  enabled: a.enabled,
31708
32507
  accessLevel: a.accessLevel,
@@ -32238,7 +33037,7 @@ var init_event_adapter2 = __esm({
32238
33037
  });
32239
33038
 
32240
33039
  // ../server/dist/channels/wechat/cdn.js
32241
- import { createDecipheriv, createCipheriv, randomBytes as randomBytes4, createHash as createHash2 } from "node:crypto";
33040
+ import { createDecipheriv, createCipheriv, randomBytes as randomBytes4, createHash as createHash3 } from "node:crypto";
32242
33041
  function decryptAesEcb(ciphertext, key) {
32243
33042
  const decipher = createDecipheriv("aes-128-ecb", key, null);
32244
33043
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
@@ -32292,7 +33091,7 @@ function prepareUpload(plaintext) {
32292
33091
  aeskey,
32293
33092
  aeskeyHex: aeskey.toString("hex"),
32294
33093
  rawsize: plaintext.length,
32295
- rawfilemd5: createHash2("md5").update(plaintext).digest("hex"),
33094
+ rawfilemd5: createHash3("md5").update(plaintext).digest("hex"),
32296
33095
  filesize: aesEcbPaddedSize(plaintext.length)
32297
33096
  };
32298
33097
  }
@@ -32344,8 +33143,8 @@ var init_cdn = __esm({
32344
33143
  });
32345
33144
 
32346
33145
  // ../server/dist/channels/wechat/send-media.js
32347
- import fs73 from "node:fs/promises";
32348
- import path80 from "node:path";
33146
+ import fs74 from "node:fs/promises";
33147
+ import path81 from "node:path";
32349
33148
  function inferKind(filePath) {
32350
33149
  const cls = classifyMedia(filePath);
32351
33150
  if (cls === "image")
@@ -32357,7 +33156,7 @@ function inferKind(filePath) {
32357
33156
  async function sendMediaFile(params) {
32358
33157
  const { baseUrl, token, toUserId, contextToken, contextTokenAt, filePath } = params;
32359
33158
  const kind = params.kind ?? inferKind(filePath);
32360
- const plaintext = await fs73.readFile(filePath);
33159
+ const plaintext = await fs74.readFile(filePath);
32361
33160
  const prep = prepareUpload(plaintext);
32362
33161
  console.log(`[WeChat] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
32363
33162
  const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
@@ -32395,7 +33194,7 @@ async function sendMediaFile(params) {
32395
33194
  aesKeyBase64,
32396
33195
  fileSizeCiphertext: prep.filesize,
32397
33196
  fileSize: prep.rawsize,
32398
- fileName: path80.basename(filePath)
33197
+ fileName: path81.basename(filePath)
32399
33198
  });
32400
33199
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
32401
33200
  const body = {
@@ -32558,8 +33357,8 @@ var init_login = __esm({
32558
33357
  });
32559
33358
 
32560
33359
  // ../server/dist/channels/wechat/handler.js
32561
- import fs74 from "node:fs";
32562
- import path81 from "node:path";
33360
+ import fs75 from "node:fs";
33361
+ import path82 from "node:path";
32563
33362
  import QRCode from "qrcode";
32564
33363
  function wxRoute(fromUserId, contextToken, contextTokenAt) {
32565
33364
  return (prev) => {
@@ -32980,7 +33779,7 @@ async function handleSlashCommand(args) {
32980
33779
  await reply(login.message);
32981
33780
  return true;
32982
33781
  }
32983
- const tmpPath = path81.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
33782
+ const tmpPath = path82.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
32984
33783
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
32985
33784
  await sendMediaFile({
32986
33785
  baseUrl: account.baseUrl,
@@ -32989,7 +33788,7 @@ async function handleSlashCommand(args) {
32989
33788
  contextToken,
32990
33789
  filePath: tmpPath
32991
33790
  });
32992
- await fs74.promises.unlink(tmpPath).catch(() => {
33791
+ await fs75.promises.unlink(tmpPath).catch(() => {
32993
33792
  });
32994
33793
  await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
32995
33794
  waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
@@ -33069,8 +33868,8 @@ var init_handler4 = __esm({
33069
33868
 
33070
33869
  // ../server/dist/routes/wechat.js
33071
33870
  import { Hono as Hono21 } from "hono";
33072
- import path82 from "node:path";
33073
- import fs75 from "node:fs";
33871
+ import path83 from "node:path";
33872
+ import fs76 from "node:fs";
33074
33873
  function createWechatRoutes(deps) {
33075
33874
  const { db: db3, channel } = deps;
33076
33875
  const app2 = new Hono21();
@@ -33079,7 +33878,7 @@ function createWechatRoutes(deps) {
33079
33878
  return {
33080
33879
  ...stripSecrets(acc),
33081
33880
  workspacePath: acc.workspacePath,
33082
- workspaceMissing: !fs75.existsSync(acc.workspacePath)
33881
+ workspaceMissing: !fs76.existsSync(acc.workspacePath)
33083
33882
  };
33084
33883
  });
33085
33884
  return c.json({ accounts });
@@ -33095,7 +33894,7 @@ function createWechatRoutes(deps) {
33095
33894
  return c.json({ error: "sessionKey required" }, 400);
33096
33895
  if (!body.workspacePath)
33097
33896
  return c.json({ error: "workspacePath required" }, 400);
33098
- if (!path82.isAbsolute(body.workspacePath))
33897
+ if (!path83.isAbsolute(body.workspacePath))
33099
33898
  return c.json({ error: "workspacePath must be absolute" }, 400);
33100
33899
  const levelError = accessLevelError(body.accessLevel, CHAT_ACCESS_LEVELS);
33101
33900
  if (levelError)
@@ -33308,7 +34107,7 @@ var init_descriptor2 = __esm({
33308
34107
  import { Hono as Hono22 } from "hono";
33309
34108
  import { streamSSE as streamSSE2 } from "hono/streaming";
33310
34109
  import crypto10 from "node:crypto";
33311
- import fs76 from "node:fs";
34110
+ import fs77 from "node:fs";
33312
34111
  function streamWithKeepalive(c, chunks) {
33313
34112
  return streamSSE2(c, async (stream) => {
33314
34113
  const timer2 = setInterval(() => {
@@ -33499,30 +34298,30 @@ function createWebRoutes(deps) {
33499
34298
  const filePath = c.req.query("path");
33500
34299
  if (!filePath)
33501
34300
  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)) {
34301
+ const path90 = await import("node:path");
34302
+ const resolved = path90.default.resolve(account.workspacePath, filePath);
34303
+ const root = path90.default.resolve(account.workspacePath);
34304
+ if (resolved !== root && !resolved.startsWith(root + path90.default.sep)) {
33506
34305
  return c.json({ error: "path traversal not allowed" }, 403);
33507
34306
  }
33508
34307
  let real;
33509
34308
  let realRoot;
33510
34309
  try {
33511
- real = fs76.realpathSync(resolved);
33512
- realRoot = fs76.realpathSync(root);
34310
+ real = fs77.realpathSync(resolved);
34311
+ realRoot = fs77.realpathSync(root);
33513
34312
  } catch {
33514
34313
  return c.json({ error: "file not found" }, 404);
33515
34314
  }
33516
- if (real !== realRoot && !real.startsWith(realRoot + path89.default.sep)) {
34315
+ if (real !== realRoot && !real.startsWith(realRoot + path90.default.sep)) {
33517
34316
  return c.json({ error: "path traversal not allowed" }, 403);
33518
34317
  }
33519
34318
  if (isHiddenWorkspacePath(real, realRoot)) {
33520
34319
  return c.json({ error: "path is not accessible" }, 403);
33521
34320
  }
33522
- const stat = fs76.statSync(real);
34321
+ const stat = fs77.statSync(real);
33523
34322
  if (stat.isDirectory())
33524
34323
  return c.json({ error: "cannot serve directory" }, 400);
33525
- const ext = path89.default.extname(real).toLowerCase();
34324
+ const ext = path90.default.extname(real).toLowerCase();
33526
34325
  const nonImageMimes = {
33527
34326
  ".mp4": "video/mp4",
33528
34327
  ".webm": "video/webm",
@@ -33530,8 +34329,8 @@ function createWebRoutes(deps) {
33530
34329
  ".txt": "text/plain"
33531
34330
  };
33532
34331
  const contentType = imageMimeFromExt(ext) || nonImageMimes[ext] || "application/octet-stream";
33533
- const fileName = path89.default.basename(resolved);
33534
- const data = fs76.readFileSync(real);
34332
+ const fileName = path90.default.basename(resolved);
34333
+ const data = fs77.readFileSync(real);
33535
34334
  c.header("content-type", contentType);
33536
34335
  c.header("content-disposition", `inline; filename="${fileName}"`);
33537
34336
  return c.body(data);
@@ -33753,10 +34552,10 @@ async function authTest(botToken) {
33753
34552
  return slackPost("auth.test", botToken, {});
33754
34553
  }
33755
34554
  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);
34555
+ const fs81 = await import("node:fs");
34556
+ const path90 = await import("node:path");
34557
+ const filename = args.filename ?? path90.basename(args.filePath);
34558
+ const stat = fs81.statSync(args.filePath);
33760
34559
  if (!stat.isFile())
33761
34560
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
33762
34561
  const length = stat.size;
@@ -33774,7 +34573,7 @@ async function uploadFile(args) {
33774
34573
  if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
33775
34574
  throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
33776
34575
  }
33777
- const buf = fs80.readFileSync(args.filePath);
34576
+ const buf = fs81.readFileSync(args.filePath);
33778
34577
  const step2 = await fetch(step1Parsed.upload_url, {
33779
34578
  method: "POST",
33780
34579
  body: buf
@@ -33919,7 +34718,7 @@ var init_api2 = __esm({
33919
34718
  });
33920
34719
 
33921
34720
  // ../server/dist/channels/slack/handler.js
33922
- import path83 from "node:path";
34721
+ import path84 from "node:path";
33923
34722
  import { WebSocket as WebSocket2 } from "ws";
33924
34723
  function slashToBang(text6, commandNames) {
33925
34724
  if (commandNames.length === 0)
@@ -34050,7 +34849,7 @@ function startSlackChannel(deps) {
34050
34849
  const route = bridge.getRoute(sessionId);
34051
34850
  if (!account || !route)
34052
34851
  return;
34053
- const resolved = path83.resolve(filePath);
34852
+ const resolved = path84.resolve(filePath);
34054
34853
  await sendMediaOrReport({
34055
34854
  filePath,
34056
34855
  account,
@@ -34692,10 +35491,10 @@ async function replyMessage(args) {
34692
35491
  return { message_id: parsed.data?.message_id ?? "" };
34693
35492
  }
34694
35493
  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);
35494
+ const fs81 = await import("node:fs");
35495
+ const path90 = await import("node:path");
35496
+ const buf = fs81.readFileSync(args.filePath);
35497
+ const filename = path90.basename(args.filePath);
34699
35498
  const send2 = async (token) => {
34700
35499
  const form = new FormData();
34701
35500
  form.append("image_type", "message");
@@ -34711,10 +35510,10 @@ async function uploadImage(args) {
34711
35510
  return { imageKey: parsed.data.image_key };
34712
35511
  }
34713
35512
  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);
35513
+ const fs81 = await import("node:fs");
35514
+ const path90 = await import("node:path");
35515
+ const buf = fs81.readFileSync(args.filePath);
35516
+ const filename = args.filename ?? path90.basename(args.filePath);
34718
35517
  const send2 = async (token) => {
34719
35518
  const form = new FormData();
34720
35519
  form.append("file_type", args.fileType);
@@ -34781,7 +35580,7 @@ var init_api3 = __esm({
34781
35580
  });
34782
35581
 
34783
35582
  // ../server/dist/channels/feishu/handler.js
34784
- import path84 from "node:path";
35583
+ import path85 from "node:path";
34785
35584
  import * as Lark from "@larksuiteoapi/node-sdk";
34786
35585
  function buildSessionPrefixForThread2(chatId, rootId) {
34787
35586
  return sessionPrefix("feishu", `${chatId}:${rootId}`);
@@ -34984,7 +35783,7 @@ function startFeishuChannel(deps) {
34984
35783
  const route = bridge.getRoute(sessionId);
34985
35784
  if (!account || !route)
34986
35785
  return;
34987
- const resolved = path84.resolve(filePath);
35786
+ const resolved = path85.resolve(filePath);
34988
35787
  await sendMediaOrReport({
34989
35788
  filePath,
34990
35789
  account,
@@ -35151,7 +35950,7 @@ async function replyToInbound(args) {
35151
35950
  async function sendFeishuMedia(args) {
35152
35951
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
35153
35952
  const cls = classifyMedia(filePath);
35154
- const ext = path84.extname(filePath).toLowerCase();
35953
+ const ext = path85.extname(filePath).toLowerCase();
35155
35954
  if (cls === "image") {
35156
35955
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
35157
35956
  await replyToInboundRaw({
@@ -35540,8 +36339,8 @@ var init_event_adapter5 = __esm({
35540
36339
  });
35541
36340
 
35542
36341
  // ../server/dist/channels/wecom/handler.js
35543
- import path85 from "node:path";
35544
- import fs77 from "node:fs/promises";
36342
+ import path86 from "node:path";
36343
+ import fs78 from "node:fs/promises";
35545
36344
  import { WSClient as WSClient2, generateReqId } from "@wecom/aibot-node-sdk";
35546
36345
  function pickConversation(body) {
35547
36346
  const userid = body.from?.userid ?? "unknown";
@@ -35664,13 +36463,13 @@ async function replyText(wsClient, route, text6) {
35664
36463
  }
35665
36464
  async function sendWecomMedia(args) {
35666
36465
  const { wsClient, route, filePath } = args;
35667
- const ext = path85.extname(filePath).toLowerCase();
36466
+ const ext = path86.extname(filePath).toLowerCase();
35668
36467
  const cls = classifyMedia(filePath);
35669
36468
  const type = cls === "image" && [".png", ".jpg", ".jpeg", ".gif"].includes(ext) ? "image" : cls === "video" && ext === ".mp4" ? "video" : "file";
35670
- const buf = await fs77.readFile(filePath);
36469
+ const buf = await fs78.readFile(filePath);
35671
36470
  if (buf.length > MAX_WECOM_DOWNLOAD_BYTES)
35672
36471
  throw new Error("file exceeds 20MB");
35673
- const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path85.basename(filePath) });
36472
+ const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path86.basename(filePath) });
35674
36473
  if (route.reqId)
35675
36474
  await wsClient.replyMedia({ headers: { req_id: route.reqId } }, type, mediaId);
35676
36475
  else
@@ -35715,7 +36514,7 @@ function startWecomChannel(deps) {
35715
36514
  const wsClient = states.get(accountId)?.wsClient;
35716
36515
  if (!account || !route || !wsClient)
35717
36516
  return;
35718
- const resolved = path85.resolve(filePath);
36517
+ const resolved = path86.resolve(filePath);
35719
36518
  await sendMediaOrReport({
35720
36519
  filePath,
35721
36520
  account,
@@ -36140,18 +36939,18 @@ import { cors } from "hono/cors";
36140
36939
  import { compress } from "hono/compress";
36141
36940
  import { serve } from "@hono/node-server";
36142
36941
  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";
36942
+ import { WebSocketServer as WebSocketServer4 } from "ws";
36943
+ import path87 from "node:path";
36944
+ import fs79 from "node:fs";
36146
36945
  import { spawnSync as spawnSync4 } from "node:child_process";
36147
36946
  import { homedir as homedir25 } from "node:os";
36148
36947
  import { fileURLToPath as fileURLToPath3 } from "node:url";
36149
36948
  function findProjectRoot() {
36150
36949
  let dir = process.cwd();
36151
36950
  for (let i = 0; i < 32; i++) {
36152
- if (fs78.existsSync(path86.join(dir, "pnpm-workspace.yaml")))
36951
+ if (fs79.existsSync(path87.join(dir, "pnpm-workspace.yaml")))
36153
36952
  return dir;
36154
- const parent = path86.dirname(dir);
36953
+ const parent = path87.dirname(dir);
36155
36954
  if (parent === dir)
36156
36955
  break;
36157
36956
  dir = parent;
@@ -36161,15 +36960,15 @@ function findProjectRoot() {
36161
36960
  return process.cwd();
36162
36961
  }
36163
36962
  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);
36963
+ fs79.mkdirSync(path87.dirname(lockFile), { recursive: true });
36964
+ const fd = fs79.openSync(lockFile, fs79.constants.O_RDWR | fs79.constants.O_CREAT, 420);
36166
36965
  const result = spawnSync4("flock", ["-n", "3"], {
36167
36966
  stdio: ["ignore", "ignore", "ignore", fd]
36168
36967
  });
36169
36968
  if (result.error) {
36170
36969
  const content = readLockPid(fd);
36171
36970
  if (content > 0 && isProcessAlive(content)) {
36172
- fs78.closeSync(fd);
36971
+ fs79.closeSync(fd);
36173
36972
  console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
36174
36973
  process.exit(1);
36175
36974
  }
@@ -36178,19 +36977,19 @@ function acquireSingleInstanceLock(lockFile) {
36178
36977
  if (content > 0 && !isProcessAlive(content)) {
36179
36978
  console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
36180
36979
  } else {
36181
- fs78.closeSync(fd);
36980
+ fs79.closeSync(fd);
36182
36981
  console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
36183
36982
  process.exit(1);
36184
36983
  }
36185
36984
  }
36186
- fs78.ftruncateSync(fd);
36187
- fs78.writeSync(fd, String(process.pid));
36188
- fs78.fsyncSync(fd);
36985
+ fs79.ftruncateSync(fd);
36986
+ fs79.writeSync(fd, String(process.pid));
36987
+ fs79.fsyncSync(fd);
36189
36988
  }
36190
36989
  function readLockPid(fd) {
36191
36990
  try {
36192
36991
  const buf = Buffer.alloc(32);
36193
- const n = fs78.readSync(fd, buf, 0, 32, 0);
36992
+ const n = fs79.readSync(fd, buf, 0, 32, 0);
36194
36993
  return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
36195
36994
  } catch {
36196
36995
  return 0;
@@ -36218,7 +37017,7 @@ function isProcessAlive(pid) {
36218
37017
  }
36219
37018
  }
36220
37019
  function readGitSha() {
36221
- if (!fs78.existsSync(path86.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs78.existsSync(path86.join(PROJECT_ROOT, ".git")))
37020
+ if (!fs79.existsSync(path87.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs79.existsSync(path87.join(PROJECT_ROOT, ".git")))
36222
37021
  return null;
36223
37022
  try {
36224
37023
  const rev = spawnSync4("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
@@ -36239,14 +37038,14 @@ function applySandboxPaths() {
36239
37038
  }
36240
37039
  function resolveFrontendDir() {
36241
37040
  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")))
37041
+ return path87.resolve(process.env.HALO_FRONTEND_DIR);
37042
+ const bundled = path87.resolve(__dirname3, "..", "admin-out");
37043
+ if (fs79.existsSync(path87.join(bundled, "index.html")))
36245
37044
  return bundled;
36246
- const sibling = path86.resolve(__dirname3, "..", "..", "admin", "out");
36247
- if (fs78.existsSync(path86.join(sibling, "index.html")))
37045
+ const sibling = path87.resolve(__dirname3, "..", "..", "admin", "out");
37046
+ if (fs79.existsSync(path87.join(sibling, "index.html")))
36248
37047
  return sibling;
36249
- return path86.resolve(PROJECT_ROOT, "packages", "admin", "out");
37048
+ return path87.resolve(PROJECT_ROOT, "packages", "admin", "out");
36250
37049
  }
36251
37050
  async function gracefulShutdown(signal) {
36252
37051
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
@@ -36262,6 +37061,7 @@ async function gracefulShutdown(signal) {
36262
37061
  });
36263
37062
  wss.close();
36264
37063
  transcribe?.close();
37064
+ stt?.close();
36265
37065
  jupyterChannels?.close();
36266
37066
  await jupyter?.stop();
36267
37067
  if (server && typeof server.close === "function") {
@@ -36280,7 +37080,7 @@ async function gracefulShutdown(signal) {
36280
37080
  await shutdownObservability();
36281
37081
  process.exit(0);
36282
37082
  }
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;
37083
+ 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
37084
  var init_dist2 = __esm({
36285
37085
  async "../server/dist/index.js"() {
36286
37086
  "use strict";
@@ -36297,6 +37097,7 @@ var init_dist2 = __esm({
36297
37097
  init_metrics();
36298
37098
  init_commands3();
36299
37099
  init_transcribe_ws();
37100
+ init_stt_ws();
36300
37101
  init_jupyter();
36301
37102
  init_manager();
36302
37103
  init_upgrade_router();
@@ -36334,13 +37135,13 @@ var init_dist2 = __esm({
36334
37135
  init_sandbox();
36335
37136
  init_init();
36336
37137
  init_git_ssh();
36337
- __dirname3 = path86.dirname(fileURLToPath3(import.meta.url));
37138
+ __dirname3 = path87.dirname(fileURLToPath3(import.meta.url));
36338
37139
  PORT = config.server.port;
36339
- HALO_VERSION = "1.6.4";
37140
+ HALO_VERSION = "1.6.5";
36340
37141
  PROJECT_ROOT = findProjectRoot();
36341
- HALO_HOME4 = path86.join(homedir25(), ".halo");
37142
+ HALO_HOME4 = path87.join(homedir25(), ".halo");
36342
37143
  GIT_SHA = readGitSha();
36343
- if (!fs78.existsSync(path86.join(HALO_HOME4, "global", ".template-version"))) {
37144
+ if (!fs79.existsSync(path87.join(HALO_HOME4, "global", ".template-version"))) {
36344
37145
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
36345
37146
  process.exit(1);
36346
37147
  }
@@ -36351,7 +37152,7 @@ var init_dist2 = __esm({
36351
37152
  process.exit(1);
36352
37153
  }
36353
37154
  if (!AGENTCORE)
36354
- acquireSingleInstanceLock(path86.join(HALO_HOME4, "global", "server.lock"));
37155
+ acquireSingleInstanceLock(path87.join(HALO_HOME4, "global", "server.lock"));
36355
37156
  commandRegistry.assertDispatchCommands(DISPATCH_COMMANDS);
36356
37157
  await initObservability();
36357
37158
  initLogger();
@@ -36421,12 +37222,12 @@ var init_dist2 = __esm({
36421
37222
  app.route("/api", createJupyterRoutes(jupyter));
36422
37223
  }
36423
37224
  start2();
36424
- channelDb = createChannelDb(path86.join(HALO_HOME4, "secrets"));
37225
+ channelDb = createChannelDb(path87.join(HALO_HOME4, "secrets"));
36425
37226
  setChannelDb(channelDb);
36426
- setEvoDb(createEvoDb(path86.join(HALO_HOME4, "global")));
36427
- setCronDb(createCronDb(path86.join(HALO_HOME4, "global")));
37227
+ setEvoDb(createEvoDb(path87.join(HALO_HOME4, "global")));
37228
+ setCronDb(createCronDb(path87.join(HALO_HOME4, "global")));
36428
37229
  if (!AGENTCORE)
36429
- setRunsDb(createRunsDb(path86.join(HALO_HOME4, "global")));
37230
+ setRunsDb(createRunsDb(path87.join(HALO_HOME4, "global")));
36430
37231
  setEvoSpawner(realEvoSpawner);
36431
37232
  if (!AGENTCORE) {
36432
37233
  startEvoTicker();
@@ -36441,7 +37242,7 @@ var init_dist2 = __esm({
36441
37242
  setCronSessionRegistry(registry);
36442
37243
  if (OWNS_RUNTIMES && !AGENTCORE) {
36443
37244
  for (const ws of listRunningWorkspaces()) {
36444
- if (!fs78.existsSync(path86.join(ws, ".halo")))
37245
+ if (!fs79.existsSync(path87.join(ws, ".halo")))
36445
37246
  continue;
36446
37247
  if (!claimWorkspaceRuntime(ws))
36447
37248
  continue;
@@ -36467,7 +37268,7 @@ var init_dist2 = __esm({
36467
37268
  startCronDaemon();
36468
37269
  }
36469
37270
  if (!AGENTCORE) {
36470
- setA2ADb(createA2ADb(path86.join(HALO_HOME4, "global")));
37271
+ setA2ADb(createA2ADb(path87.join(HALO_HOME4, "global")));
36471
37272
  setA2AOutboundRegistry(registry);
36472
37273
  app.route("/a2a", createA2ARoutes({ registry, ownsRuntimes: OWNS_RUNTIMES }));
36473
37274
  app.route("/", createA2APushRoutes());
@@ -36483,11 +37284,11 @@ var init_dist2 = __esm({
36483
37284
  }
36484
37285
  FRONTEND_DIR = resolveFrontendDir();
36485
37286
  if (!AGENTCORE) {
36486
- app.use("/*", serveStatic({ root: path86.relative(process.cwd(), FRONTEND_DIR) }));
37287
+ app.use("/*", serveStatic({ root: path87.relative(process.cwd(), FRONTEND_DIR) }));
36487
37288
  app.get("/*", (c) => {
36488
- const indexPath = path86.join(FRONTEND_DIR, "index.html");
37289
+ const indexPath = path87.join(FRONTEND_DIR, "index.html");
36489
37290
  try {
36490
- const html = fs78.readFileSync(indexPath, "utf-8");
37291
+ const html = fs79.readFileSync(indexPath, "utf-8");
36491
37292
  return c.html(html);
36492
37293
  } catch {
36493
37294
  return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
@@ -36506,7 +37307,7 @@ var init_dist2 = __esm({
36506
37307
  for (const line of AGENTCORE ? lines.slice(0, 1) : lines)
36507
37308
  logAlways(line);
36508
37309
  });
36509
- wss = new WebSocketServer3({
37310
+ wss = new WebSocketServer4({
36510
37311
  noServer: true,
36511
37312
  verifyClient: (info, callback) => {
36512
37313
  const token = getTokenFromCookieHeader(info.req.headers.cookie);
@@ -36522,12 +37323,15 @@ var init_dist2 = __esm({
36522
37323
  setBroadcastWss(wss);
36523
37324
  }
36524
37325
  transcribe = AGENTCORE ? null : createTranscribeProxy();
37326
+ stt = AGENTCORE ? null : createSttProxy();
36525
37327
  jupyterChannels = jupyter ? createJupyterChannels(jupyter) : null;
36526
37328
  upgradeRoutes = new Map(AGENTCORE ? [] : [
36527
37329
  ["/ws", (req, socket, head) => wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req))]
36528
37330
  ]);
36529
37331
  if (transcribe)
36530
37332
  upgradeRoutes.set(TRANSCRIBE_PATH, transcribe.handleUpgrade);
37333
+ if (stt)
37334
+ upgradeRoutes.set(STT_PATH, stt.handleUpgrade);
36531
37335
  if (jupyterChannels)
36532
37336
  upgradeRoutes.set(JUPYTER_CHANNELS_PATH, jupyterChannels.handleUpgrade);
36533
37337
  server.on("upgrade", createUpgradeRouter(upgradeRoutes));
@@ -36695,8 +37499,8 @@ var init_halo_client = __esm({
36695
37499
  }
36696
37500
  return await res.json();
36697
37501
  }
36698
- sessionUrl(path89, workspace, sessionId) {
36699
- const url = new URL(`${this.opts.baseUrl}${path89}`);
37502
+ sessionUrl(path90, workspace, sessionId) {
37503
+ const url = new URL(`${this.opts.baseUrl}${path90}`);
36700
37504
  url.searchParams.set("workspace", workspace);
36701
37505
  url.searchParams.set("sessionId", sessionId);
36702
37506
  return url;
@@ -36844,7 +37648,7 @@ var init_version = __esm({
36844
37648
  });
36845
37649
 
36846
37650
  // ../acp-adapter/dist/adapter.js
36847
- import path87 from "node:path";
37651
+ import path88 from "node:path";
36848
37652
  function composePrompt(blocks) {
36849
37653
  const segments = [];
36850
37654
  let run = "";
@@ -37037,7 +37841,7 @@ var init_adapter = __esm({
37037
37841
  if (cursor !== void 0 && !/^\d+$/.test(cursor))
37038
37842
  throw newError(`invalid cursor: ${cursor}`, -32602);
37039
37843
  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))
37844
+ if (p.cwd !== void 0 && path88.resolve(p.cwd) !== path88.resolve(page.workspace))
37041
37845
  return { sessions: [] };
37042
37846
  return {
37043
37847
  sessions: page.sessions.map((s) => ({
@@ -37651,8 +38455,8 @@ process.emitWarning = (warning, ...args) => {
37651
38455
 
37652
38456
  // src/index.ts
37653
38457
  init_server_supervisor();
37654
- import path88 from "node:path";
37655
- import fs79 from "node:fs";
38458
+ import path89 from "node:path";
38459
+ import fs80 from "node:fs";
37656
38460
  import { homedir as homedir26, userInfo } from "node:os";
37657
38461
  import { parseArgs as parseArgs2 } from "node:util";
37658
38462
 
@@ -37943,7 +38747,7 @@ function installUserUnit(opts, run = runCommand) {
37943
38747
  }
37944
38748
 
37945
38749
  // src/index.ts
37946
- var VERSION = "1.6.4";
38750
+ var VERSION = "1.6.5";
37947
38751
  var HELP_TOP = `Usage: halo <command> [options]
37948
38752
 
37949
38753
  Commands:
@@ -38285,8 +39089,8 @@ async function cmdSetup(argv = []) {
38285
39089
  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
39090
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
38287
39091
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
38288
- const haloHome2 = path88.join(homedir26(), ".halo");
38289
- const globalDir2 = path88.join(haloHome2, "global");
39092
+ const haloHome2 = path89.join(homedir26(), ".halo");
39093
+ const globalDir2 = path89.join(haloHome2, "global");
38290
39094
  ensureHaloHome2(haloHome2);
38291
39095
  process.stderr.write(`[setup] ${haloHome2}/ ready
38292
39096
  `);
@@ -38639,7 +39443,7 @@ async function cmdUpgrade() {
38639
39443
  `);
38640
39444
  process.exit(1);
38641
39445
  }
38642
- const broken = verifyNativeModules2(path88.join(rootDir, PKG));
39446
+ const broken = verifyNativeModules2(path89.join(rootDir, PKG));
38643
39447
  if (broken.length > 0) {
38644
39448
  process.stderr.write(
38645
39449
  `
@@ -38670,7 +39474,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
38670
39474
  async function cmdAgents(flags) {
38671
39475
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
38672
39476
  await initRuntime2();
38673
- const workspace = path88.resolve(flags.workspace ?? process.cwd());
39477
+ const workspace = path89.resolve(flags.workspace ?? process.cwd());
38674
39478
  const agents = await listAgents2(workspace);
38675
39479
  if (agents.length === 0) {
38676
39480
  process.stderr.write("No agents found.\n");
@@ -38686,7 +39490,7 @@ async function cmdAgents(flags) {
38686
39490
  async function cmdSessions(flags) {
38687
39491
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
38688
39492
  await initRuntime2();
38689
- const workspace = path88.resolve(flags.workspace ?? process.cwd());
39493
+ const workspace = path89.resolve(flags.workspace ?? process.cwd());
38690
39494
  const sessions = listSessions2(workspace);
38691
39495
  if (sessions.length === 0) {
38692
39496
  process.stderr.write("No sessions found.\n");
@@ -38717,7 +39521,7 @@ async function cmdModels(argv) {
38717
39521
  process.exitCode = 1;
38718
39522
  return;
38719
39523
  }
38720
- const plan = mod.planModelsInstall(path88.resolve(dir));
39524
+ const plan = mod.planModelsInstall(path89.resolve(dir));
38721
39525
  for (const line of mod.formatPlan(plan)) process.stdout.write(`${line}
38722
39526
  `);
38723
39527
  if (plan.needsConfirm && !values.yes) {
@@ -38826,11 +39630,11 @@ ${stdinContent}` : stdinContent;
38826
39630
  harness.destroy();
38827
39631
  }
38828
39632
  }
38829
- var SERVER_LOCK_PATH = path88.join(homedir26(), ".halo", "global", "server.lock");
38830
- var SERVER_LOG_PATH = path88.join(homedir26(), ".halo", "logs", "server.log");
39633
+ var SERVER_LOCK_PATH = path89.join(homedir26(), ".halo", "global", "server.lock");
39634
+ var SERVER_LOG_PATH = path89.join(homedir26(), ".halo", "logs", "server.log");
38831
39635
  function readServerPid() {
38832
39636
  try {
38833
- const txt = fs79.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
39637
+ const txt = fs80.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
38834
39638
  const n = parseInt(txt, 10);
38835
39639
  return Number.isFinite(n) && n > 0 ? n : null;
38836
39640
  } catch {
@@ -38937,8 +39741,8 @@ async function cmdServerStart(argv, restart) {
38937
39741
  }
38938
39742
  if (plan.note) writeDim(plan.note);
38939
39743
  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");
39744
+ fs80.mkdirSync(path89.dirname(SERVER_LOG_PATH), { recursive: true });
39745
+ const out = fs80.openSync(SERVER_LOG_PATH, "a");
38942
39746
  const child = spawn5(process.argv[0], [process.argv[1], "server", "start"], {
38943
39747
  detached: true,
38944
39748
  stdio: ["ignore", out, out],
@@ -39018,8 +39822,8 @@ ${HELP_AGENTCORE}`);
39018
39822
  process.exitCode = 1;
39019
39823
  return;
39020
39824
  }
39021
- const workspace = path88.resolve(raw);
39022
- if (!fs79.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) {
39825
+ const workspace = path89.resolve(raw);
39826
+ if (!fs80.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) {
39023
39827
  process.stderr.write(`Workspace is not a directory: ${workspace}
39024
39828
  `);
39025
39829
  process.exitCode = 1;
@@ -39125,7 +39929,7 @@ async function cmdServerLogs(argv) {
39125
39929
  lines: { type: "string", short: "n" }
39126
39930
  }
39127
39931
  });
39128
- if (!fs79.existsSync(SERVER_LOG_PATH)) {
39932
+ if (!fs80.existsSync(SERVER_LOG_PATH)) {
39129
39933
  process.stderr.write(`No log file at ${SERVER_LOG_PATH}
39130
39934
  `);
39131
39935
  return;
@@ -39143,7 +39947,7 @@ function exitMissingSetup() {
39143
39947
  process.exit(1);
39144
39948
  }
39145
39949
  function isHaloHomeReady() {
39146
- return fs79.existsSync(path88.join(homedir26(), ".halo", "global", ".template-version"));
39950
+ return fs80.existsSync(path89.join(homedir26(), ".halo", "global", ".template-version"));
39147
39951
  }
39148
39952
  async function main2() {
39149
39953
  const argv = process.argv.slice(2);