khala-cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +18 -1
  2. package/dist/{chunk-2U4E7AM7.mjs → chunk-2DHIAFEV.mjs} +294 -101
  3. package/dist/{chunk-CWHGNVZV.mjs → chunk-2VCAW4GH.mjs} +1 -1
  4. package/dist/{chunk-P5CP32X2.mjs → chunk-54XFVSEC.mjs} +1 -1
  5. package/dist/chunk-5BQTYU66.mjs +886 -0
  6. package/dist/{chunk-RRBIVI77.mjs → chunk-5LIE5GUU.mjs} +1 -1
  7. package/dist/{chunk-6E3UCO22.mjs → chunk-5PUQT3YS.mjs} +200 -20
  8. package/dist/{chunk-55KBLEX5.mjs → chunk-7HBRFY3Z.mjs} +1 -1
  9. package/dist/{chunk-PFDUXY3N.mjs → chunk-APMWDF6B.mjs} +1 -1
  10. package/dist/{chunk-PY2ZNVNY.mjs → chunk-BKLW2SPG.mjs} +55 -8
  11. package/dist/{chunk-OW3DSTEZ.mjs → chunk-CBPLCCRZ.mjs} +5 -5
  12. package/dist/{chunk-JG5J3SHW.mjs → chunk-CJTCU755.mjs} +4 -4
  13. package/dist/{chunk-AWBKVTLV.mjs → chunk-FH34B3I3.mjs} +14 -17
  14. package/dist/chunk-FV32AQTC.mjs +40 -0
  15. package/dist/{chunk-FH2CZX5T.mjs → chunk-H3K4GA2P.mjs} +83 -36
  16. package/dist/{chunk-AUJEUKVM.mjs → chunk-IQJQ4RF5.mjs} +1 -1
  17. package/dist/{chunk-SN3STAO2.mjs → chunk-NKX3SLS7.mjs} +40 -3
  18. package/dist/{chunk-Z4EDYB6Y.mjs → chunk-RNDUFTBF.mjs} +97 -57
  19. package/dist/{chunk-OECJ45NM.mjs → chunk-VAI7IUI4.mjs} +24 -8
  20. package/dist/{chunk-XUJIJ4KB.mjs → chunk-VKQ3KUYO.mjs} +16 -2
  21. package/dist/{chunk-4X35OUAN.mjs → chunk-VUSV2ADL.mjs} +92 -97
  22. package/dist/{chunk-2XTYRBYX.mjs → chunk-W6R4UJUQ.mjs} +12 -5
  23. package/dist/{chunk-B2MOLCJM.mjs → chunk-Z4JF2M5C.mjs} +7 -9
  24. package/dist/{chunk-SZIEG3RS.mjs → chunk-ZJ4W7P33.mjs} +12 -14
  25. package/dist/khala.mjs +11 -11
  26. package/dist/web/assets/local-BQl2KrE1.js +9 -0
  27. package/dist/web/assets/local-OwDq47O4.css +1 -0
  28. package/dist/web/index.html +2 -2
  29. package/package.json +1 -1
  30. package/dist/chunk-DPW43YNC.mjs +0 -36
  31. package/dist/chunk-IULQYQJZ.mjs +0 -144
  32. package/dist/chunk-WHONG7UF.mjs +0 -267
  33. package/dist/chunk-ZHMGB3PP.mjs +0 -82
  34. package/dist/web/assets/local-K8EcTVxW.js +0 -9
  35. package/dist/web/assets/local-R2y0Gl4T.css +0 -1
@@ -5,21 +5,20 @@ import {
5
5
  reportReady,
6
6
  requestJoin,
7
7
  saveHostedUsername
8
- } from "./chunk-OECJ45NM.mjs";
9
- import {
10
- KHALA_AGENT_VERSION
11
- } from "./chunk-RRBIVI77.mjs";
8
+ } from "./chunk-VAI7IUI4.mjs";
12
9
  import {
13
10
  createEventKeyFilter,
14
11
  isWakeEntry,
15
12
  toEventInboxEntry
16
- } from "./chunk-CWHGNVZV.mjs";
13
+ } from "./chunk-2VCAW4GH.mjs";
17
14
  import {
18
15
  KhalaClientError,
19
- LOCAL_OWNER_USER_ID,
20
- MODEL_NAMES
21
- } from "./chunk-2U4E7AM7.mjs";
22
- import "./chunk-PFDUXY3N.mjs";
16
+ LOCAL_OWNER_USER_ID
17
+ } from "./chunk-2DHIAFEV.mjs";
18
+ import {
19
+ KHALA_AGENT_VERSION
20
+ } from "./chunk-5LIE5GUU.mjs";
21
+ import "./chunk-APMWDF6B.mjs";
23
22
  import "./chunk-5GLTKWJ5.mjs";
24
23
  import {
25
24
  CHANNEL_EVENT_TYPE,
@@ -27,19 +26,20 @@ import {
27
26
  formatChannelEventLine,
28
27
  memberRenameContent,
29
28
  statusFor
30
- } from "./chunk-P5CP32X2.mjs";
29
+ } from "./chunk-54XFVSEC.mjs";
31
30
  import {
32
31
  monitorArmed,
33
32
  resolveHarness,
34
- resolveSessionId
35
- } from "./chunk-AWBKVTLV.mjs";
36
- import {
37
- adapterFor
38
- } from "./chunk-WHONG7UF.mjs";
33
+ resolveSession
34
+ } from "./chunk-FH34B3I3.mjs";
39
35
  import {
36
+ SESSION_UNKNOWN_HINT,
37
+ adapterFor,
40
38
  applyListeningMode,
41
- readListeningMode
42
- } from "./chunk-ZHMGB3PP.mjs";
39
+ createWakeLadder,
40
+ readListeningMode,
41
+ readWakeState
42
+ } from "./chunk-5BQTYU66.mjs";
43
43
  import {
44
44
  appendInbox,
45
45
  channelKey,
@@ -47,8 +47,7 @@ import {
47
47
  migrateLegacy,
48
48
  resolveChannel,
49
49
  unreadCount
50
- } from "./chunk-XUJIJ4KB.mjs";
51
- import "./chunk-DPW43YNC.mjs";
50
+ } from "./chunk-VKQ3KUYO.mjs";
52
51
  import {
53
52
  StateError,
54
53
  TERMINAL_SESSION_DETAILS,
@@ -64,18 +63,19 @@ import {
64
63
  sessionFiles,
65
64
  writeJoinFile,
66
65
  writeStateFile
67
- } from "./chunk-OW3DSTEZ.mjs";
66
+ } from "./chunk-CBPLCCRZ.mjs";
68
67
  import {
69
68
  decodeListeningModeCommand
70
- } from "./chunk-AUJEUKVM.mjs";
69
+ } from "./chunk-IQJQ4RF5.mjs";
71
70
  import {
72
71
  displayText,
72
+ harnessInfo,
73
73
  isWellFormed,
74
74
  label,
75
75
  utf8Length,
76
76
  validAgentRejoinSecret
77
- } from "./chunk-SN3STAO2.mjs";
78
- import "./chunk-55KBLEX5.mjs";
77
+ } from "./chunk-NKX3SLS7.mjs";
78
+ import "./chunk-7HBRFY3Z.mjs";
79
79
 
80
80
  // src/mcp/main.ts
81
81
  import { Console } from "node:console";
@@ -504,6 +504,7 @@ function resolveEventInput(args) {
504
504
 
505
505
  // src/mcp/tools.ts
506
506
  var ERROR_CODES = [
507
+ "update_required",
507
508
  "channel_required",
508
509
  "channel_unknown",
509
510
  "channel_ambiguous",
@@ -522,19 +523,24 @@ function errorCode(error) {
522
523
  return typeof code === "string" && ERROR_CODES.includes(code) ? code : "internal_error";
523
524
  }
524
525
  function createKhalaTools(input) {
526
+ const sessionProperties = input.harness === "opencode" ? { khala_session: { type: "string", minLength: 1, description: "Session ID stamped by the OpenCode plugin." } } : {};
527
+ const validSession = (args) => !Object.hasOwn(args, "khala_session") || input.harness === "opencode" && typeof args.khala_session === "string" && args.khala_session.length > 0;
528
+ const sessionMeta = (args, meta) => input.harness === "opencode" && Object.hasOwn(args, "khala_session") ? { ...meta, khala_session: args.khala_session } : meta;
525
529
  function tool(name, description, properties, required, validate, invoke, render = JSON.stringify) {
526
530
  return {
527
531
  name,
528
- definition: () => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } }),
532
+ definition: () => ({ name, description, inputSchema: { type: "object", properties: { ...properties, ...sessionProperties }, required, additionalProperties: false } }),
529
533
  async call(args, context) {
530
- if (!hasOnly(args, Object.keys(properties)) || !validate(args)) return failure(context.id, -32602, "Invalid params");
534
+ if (!hasOnly(args, Object.keys({ ...properties, ...sessionProperties })) || !validSession(args) || !validate(args)) return failure(context.id, -32602, "Invalid params");
531
535
  try {
532
- const client = input.clientFor(context.meta);
533
- if (client === null) return success(context.id, toolError("session_unknown"));
536
+ const client = await input.clientFor(sessionMeta(args, context.meta));
537
+ if (client === null) return success(context.id, toolError("session_unknown", { hint: SESSION_UNKNOWN_HINT }));
534
538
  const structuredContent = await invoke(client, args);
535
539
  return success(context.id, { content: [{ type: "text", text: render(structuredContent) }], structuredContent });
536
540
  } catch (error) {
537
- return success(context.id, toolError(errorCode(error), error instanceof KhalaClientError ? error.extra : void 0));
541
+ const code = errorCode(error);
542
+ const extra = error instanceof KhalaClientError ? { ...error.extra, ...code === "update_required" ? { message: error.message } : {} } : void 0;
543
+ return success(context.id, toolError(code, extra));
538
544
  }
539
545
  }
540
546
  };
@@ -549,11 +555,11 @@ function createKhalaTools(input) {
549
555
  type: "string",
550
556
  minLength: 1,
551
557
  maxLength: 40,
552
- description: "Optional and ignored: Khala names you <OwnerUsername>-<Claude|Codex>, and your owner can rename you."
558
+ description: "Optional and ignored: Khala names you <OwnerUsername>-<Model>, and your owner can rename you."
553
559
  } },
554
560
  ["link"],
555
561
  (args) => typeof args.link === "string" && (!Object.hasOwn(args, "label") || typeof args.label === "string" && args.label.trim().length > 0 && [...args.label].length <= 40),
556
- (client, args) => client.join(args.link, args.label ?? MODEL_NAMES[input.harness]),
562
+ (client, args) => client.join(args.link, args.label ?? harnessInfo(input.harness).modelName),
557
563
  (result) => {
558
564
  const joined = result;
559
565
  if (joined.state === "connected") return `Connected to ${joined.channelName}.`;
@@ -597,15 +603,16 @@ function createKhalaTools(input) {
597
603
  description: 'Select channel by name or channel ID; required when joined to more than one channel. Post a compact progress event (PR, CI, ticket status) into the Khala channel. Events are progress signals, not channel messages: they never wake other agents. Pass Khala JSON as "event", or a raw Aiur bus event, wake record or alert as "aiur".',
598
604
  inputSchema: {
599
605
  type: "object",
600
- properties: { channel: channelProperty, event: { type: "object" }, aiur: { type: "object" }, ticketPrefix: { type: "string", minLength: 0, maxLength: 16 } },
606
+ properties: { ...sessionProperties, channel: channelProperty, event: { type: "object" }, aiur: { type: "object" }, ticketPrefix: { type: "string", minLength: 0, maxLength: 16 } },
601
607
  required: [],
602
608
  additionalProperties: false,
603
609
  oneOf: [{ required: ["event"], not: { required: ["aiur"] } }, { required: ["aiur"], not: { required: ["event"] } }]
604
610
  }
605
611
  }),
606
612
  async call(args, context) {
607
- if (!validChannel(args)) return failure(context.id, -32602, "Invalid params");
613
+ if (!validSession(args) || !validChannel(args)) return failure(context.id, -32602, "Invalid params");
608
614
  const { channel, ...eventArgs } = args;
615
+ if (input.harness === "opencode") delete eventArgs.khala_session;
609
616
  const resolved = resolveEventInput(eventArgs);
610
617
  if (resolved.kind === "invalid") {
611
618
  const error = { error: "invalid_event", path: resolved.path, code: resolved.code };
@@ -615,12 +622,14 @@ function createKhalaTools(input) {
615
622
  return success(context.id, { content: [{ type: "text", text: "Skipped channel event." }], structuredContent: { skipped: true } });
616
623
  }
617
624
  try {
618
- const client = input.clientFor(context.meta);
619
- if (client === null) return success(context.id, toolError("session_unknown"));
625
+ const client = await input.clientFor(sessionMeta(args, context.meta));
626
+ if (client === null) return success(context.id, toolError("session_unknown", { hint: SESSION_UNKNOWN_HINT }));
620
627
  const sent = await (channel === void 0 ? client.sendChannelEvent(resolved.content) : client.sendChannelEvent(resolved.content, channel));
621
628
  return success(context.id, { content: [{ type: "text", text: `Posted channel event: ${resolved.content.body}` }], structuredContent: sent });
622
629
  } catch (error) {
623
- return success(context.id, toolError(errorCode(error), error instanceof KhalaClientError ? error.extra : void 0));
630
+ const code = errorCode(error);
631
+ const extra = error instanceof KhalaClientError ? { ...error.extra, ...code === "update_required" ? { message: error.message } : {} } : void 0;
632
+ return success(context.id, toolError(code, extra));
624
633
  }
625
634
  }
626
635
  }
@@ -637,7 +646,7 @@ import * as fs from "node:fs/promises";
637
646
  import { createHash, randomBytes } from "node:crypto";
638
647
 
639
648
  // src/transport.ts
640
- var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-SZIEG3RS.mjs")).createLocalSession(creds) : (await import("./chunk-JG5J3SHW.mjs")).createAgentMatrixSession(creds, options);
649
+ var startChannelSession = async (creds, options) => creds.transport === "local" ? (await import("./chunk-ZJ4W7P33.mjs")).createLocalSession(creds) : (await import("./chunk-CJTCU755.mjs")).createAgentMatrixSession(creds, options);
641
650
 
642
651
  // src/sender.ts
643
652
  function localpart(userId) {
@@ -674,13 +683,15 @@ function errorDetail(error, fallback) {
674
683
  return ["unsafe_state_dir", "storage_failed"].includes(failure2.message) ? failure2.message : fallback;
675
684
  }
676
685
  function createKhalaAgentClient(options) {
686
+ const asyncOnly = !adapterFor(options.harness)?.codec;
687
+ const defaultMode = asyncOnly ? "async" : "sync";
677
688
  const dir = resolveStateDir(options.harness, options.sessionId, options.env);
678
689
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
679
690
  const fetchDeps = options.fetch ? { fetch: options.fetch } : {};
680
691
  const api = options.joinApi ?? { requestJoin, pollJoin, reportReady };
681
692
  let status = { state: "idle", updatedAt: now().toISOString() };
682
693
  let initialization;
683
- const rejoinable = adapterFor(options.harness)?.rejoinable(options.sessionId) ?? false;
694
+ const rejoinable = options.rejoinable ?? adapterFor(options.harness)?.sessionSources[0]?.rejoinable(options.sessionId) ?? false;
684
695
  let rejoinSecret;
685
696
  let savedSecret;
686
697
  let resuming;
@@ -962,8 +973,9 @@ function createKhalaAgentClient(options) {
962
973
  if (!decoded.ok || decoded.value.agent !== session.userId) return;
963
974
  attempt.appends = attempt.appends.then(async () => {
964
975
  if (!current(attempt)) return;
965
- await applyListeningMode(attempt.files, decoded.value.mode, { changedBy: "owner", eventId: command.eventId }, now);
966
- await publishMode(attempt, session, credentials.roomId, decoded.value.mode);
976
+ const mode = asyncOnly ? "async" : decoded.value.mode;
977
+ await applyListeningMode(attempt.files, mode, { changedBy: "owner", eventId: command.eventId }, now);
978
+ await publishMode(attempt, session, credentials.roomId, mode);
967
979
  }).catch(async () => {
968
980
  if (current(attempt)) await setStatus(attempt, "disconnected", "internal_error").catch(() => {
969
981
  });
@@ -975,7 +987,10 @@ function createKhalaAgentClient(options) {
975
987
  await wait(session.waitForInvite(credentials.roomId, options.inviteTimeoutMs ?? 12e4));
976
988
  await wait(session.join(credentials.roomId));
977
989
  if (!current(attempt)) return;
978
- if (session.listeningMode) await writeStateFile(channelDir, "mode.json", { mode: session.listeningMode(credentials.roomId) });
990
+ if (asyncOnly) {
991
+ await writeStateFile(channelDir, "mode.json", { mode: "async" });
992
+ await publishMode(attempt, session, credentials.roomId, "async");
993
+ } else if (session.listeningMode) await writeStateFile(channelDir, "mode.json", { mode: session.listeningMode(credentials.roomId) });
979
994
  else await removeStateFile(channelDir, "mode.json");
980
995
  attempt.joined = true;
981
996
  attempt.status.channelName = session.roomName(credentials.roomId) ?? credentials.roomId;
@@ -1072,7 +1087,7 @@ function createKhalaAgentClient(options) {
1072
1087
  signal.addEventListener("abort", releaseAbort, { once: true });
1073
1088
  if (signal.aborted) releaseAbort();
1074
1089
  });
1075
- const request = restore?.localCredentials ? Promise.resolve({ ...attempt.created, origin: new URL(link).origin, autoConfirmed: true }) : api.requestJoin({ link, harness: options.harness, label: label2, ...rejoinSecret === void 0 ? {} : { sessionId: options.sessionId, rejoinSecret } }, restore ? { fetch: (input, init) => (options.fetch ?? fetch)(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal }) } : fetchDeps);
1090
+ const request = restore?.localCredentials ? Promise.resolve({ ...attempt.created, origin: new URL(link).origin, autoConfirmed: true }) : api.requestJoin({ link, harness: options.harness, label: label2, ...rejoinSecret === void 0 ? {} : { sessionId: options.sessionId, rejoinSecret } }, { ...options.env ? { env: options.env } : {}, ...restore ? { fetch: (input, init) => (options.fetch ?? fetch)(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal }) } : fetchDeps });
1076
1091
  const created = await Promise.race([request, aborted]).finally(() => {
1077
1092
  if (releaseAbort) signal.removeEventListener("abort", releaseAbort);
1078
1093
  });
@@ -1163,7 +1178,7 @@ function createKhalaAgentClient(options) {
1163
1178
  link: attempt.link,
1164
1179
  state: "joining",
1165
1180
  unread: 0,
1166
- listeningMode: "sync"
1181
+ listeningMode: defaultMode
1167
1182
  });
1168
1183
  }
1169
1184
  }
@@ -1174,11 +1189,11 @@ function createKhalaAgentClient(options) {
1174
1189
  ...single?.detail !== void 0 ? { detail: single.detail } : channel === void 0 && status.detail ? { detail: status.detail } : {},
1175
1190
  ...single ? {
1176
1191
  channelName: single.channel,
1177
- ...single.you !== void 0 ? { displayName: single.you } : {},
1192
+ ...single.you !== void 0 ? { displayName: single.you, you: single.you } : {},
1178
1193
  ...single.agentUserId !== void 0 ? { agentUserId: single.agentUserId } : {}
1179
1194
  } : {},
1180
1195
  unread: items.reduce((sum, item) => sum + item.unread, 0),
1181
- ...single ? { listeningMode: single.listeningMode } : items.length === 0 ? { listeningMode: "sync" } : {},
1196
+ ...single ? { listeningMode: single.listeningMode } : items.length === 0 ? { listeningMode: defaultMode } : {},
1182
1197
  channels: items
1183
1198
  };
1184
1199
  },
@@ -1284,14 +1299,22 @@ function createKhalaAgentClient(options) {
1284
1299
 
1285
1300
  // src/mcp/wiring.ts
1286
1301
  function createRealClientFactory(env, deps = {}) {
1287
- return ({ harness, sessionId }) => {
1302
+ return ({ harness, sessionId, rejoinable }) => {
1288
1303
  const files = sessionFiles(harness, sessionId, env);
1289
1304
  const adapter = adapterFor(harness);
1290
- const createWaker = adapter?.waker && (deps.createWaker ?? adapter.waker);
1291
- const waker = createWaker?.({ files, threadId: sessionId });
1305
+ const drivers = adapter?.wakeLadder;
1306
+ const waker = drivers?.length ? deps.createWaker ? deps.createWaker({ files, threadId: sessionId }) : createWakeLadder({
1307
+ files,
1308
+ harness,
1309
+ sessionId,
1310
+ drivers,
1311
+ env,
1312
+ warningPrefix: adapter?.wakeWarningName ?? "wake"
1313
+ }) : void 0;
1292
1314
  const client = (deps.createClient ?? createKhalaAgentClient)({
1293
1315
  harness,
1294
1316
  sessionId,
1317
+ ...rejoinable !== void 0 ? { rejoinable } : {},
1295
1318
  env,
1296
1319
  ...waker ? { onInboxAppend: () => waker.notify() } : {}
1297
1320
  });
@@ -1314,7 +1337,22 @@ function createRealClientFactory(env, deps = {}) {
1314
1337
  async status(channel) {
1315
1338
  await initialize();
1316
1339
  const status = await (channel === void 0 ? client.status() : client.status(channel));
1317
- return adapter?.watcherStatus && ["connected", "send_failed"].includes(status.state) ? { ...status, watcherArmed: await monitorArmed(files) } : status;
1340
+ let wakeDrivers;
1341
+ if (drivers?.length) {
1342
+ try {
1343
+ const states = await readWakeState(files.dir);
1344
+ wakeDrivers = await Promise.all(drivers.map(async (driver) => {
1345
+ const ctx = { files, harness, sessionId, env, now: Date.now(), signal: new AbortController().signal };
1346
+ const available = !states[driver.id]?.disabled && await driver.available(ctx);
1347
+ const reason = states[driver.id]?.disabled ? states[driver.id]?.reason : available ? void 0 : await driver.unavailableReason?.(ctx);
1348
+ return { id: driver.id, available, ...reason ? { reason } : {} };
1349
+ }));
1350
+ } catch {
1351
+ wakeDrivers = drivers.map((driver) => ({ id: driver.id, available: false, reason: "wake_status_unavailable" }));
1352
+ }
1353
+ }
1354
+ const withWake = wakeDrivers ? { ...status, wakeDrivers } : status;
1355
+ return adapter?.watcherStatus && ["connected", "send_failed"].includes(status.state) ? { ...withWake, watcherArmed: await monitorArmed(files) } : withWake;
1318
1356
  },
1319
1357
  async read(limit, before, channel) {
1320
1358
  await initialize();
@@ -1386,21 +1424,23 @@ async function runMcpCommand(argv, deps) {
1386
1424
  return 2;
1387
1425
  }
1388
1426
  const clients = /* @__PURE__ */ new Map();
1389
- const clientForSession = (sessionId) => {
1390
- let client = clients.get(sessionId);
1427
+ const clientForSession = (session) => {
1428
+ const { sessionId, rejoinable } = session;
1429
+ const key = `${rejoinable}:${sessionId}`;
1430
+ let client = clients.get(key);
1391
1431
  if (client === void 0) {
1392
- client = deps.createClient({ harness, sessionId });
1393
- clients.set(sessionId, client);
1432
+ client = deps.createClient({ harness, sessionId, rejoinable });
1433
+ clients.set(key, client);
1394
1434
  }
1395
1435
  return client;
1396
1436
  };
1397
- const startupSession = resolveSessionId(harness, void 0, env);
1398
- if (startupSession !== null && harness !== "cursor") clientForSession(startupSession);
1437
+ const startupSession = adapterFor(harness)?.restoreAtStartup ? await resolveSession(harness, void 0, env) : null;
1438
+ if (startupSession !== null) clientForSession(startupSession);
1399
1439
  const tools = createKhalaTools({
1400
1440
  harness,
1401
- clientFor(meta) {
1402
- const sessionId = resolveSessionId(harness, meta, env);
1403
- return sessionId === null ? null : clientForSession(sessionId);
1441
+ async clientFor(meta) {
1442
+ const session = await resolveSession(harness, meta, env);
1443
+ return session === null ? null : clientForSession(session);
1404
1444
  }
1405
1445
  });
1406
1446
  try {
@@ -1,20 +1,21 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  KhalaClientError,
4
- MODEL_NAMES,
5
4
  checkName,
6
- isDefaultAgentName
7
- } from "./chunk-2U4E7AM7.mjs";
5
+ isDefaultAgentName,
6
+ restartHelper
7
+ } from "./chunk-2DHIAFEV.mjs";
8
8
  import {
9
9
  StateError,
10
10
  readJson,
11
11
  stateRoot,
12
12
  writeJsonAtomic
13
- } from "./chunk-OW3DSTEZ.mjs";
13
+ } from "./chunk-CBPLCCRZ.mjs";
14
14
  import {
15
- HARNESSES,
15
+ harnessInfo,
16
+ isHarnessId,
16
17
  validateAgentName
17
- } from "./chunk-SN3STAO2.mjs";
18
+ } from "./chunk-NKX3SLS7.mjs";
18
19
 
19
20
  // src/local/identity.ts
20
21
  import * as fs from "node:fs/promises";
@@ -23,7 +24,7 @@ import path from "node:path";
23
24
  var HOSTED_PROFILE_FILE = "hosted-profile.json";
24
25
  var LOCAL_OWNER_FALLBACK_NAME = "User";
25
26
  function hostedUsernameFromAgentName(displayName, harness) {
26
- const suffix = new RegExp(`-${MODEL_NAMES[harness]}(?:-\\d+)?$`, "iu");
27
+ const suffix = new RegExp(`-${harnessInfo(harness).modelName}(?:-\\d+)?$`, "iu");
27
28
  if (!suffix.test(displayName)) return null;
28
29
  const candidate = displayName.replace(suffix, "");
29
30
  const checked = checkName(candidate, "username");
@@ -139,7 +140,7 @@ async function post(url, origin, body, deps, pollSecret) {
139
140
  async function requestJoin(input, deps = {}) {
140
141
  const parsed = parseChannelLink(input.link);
141
142
  if (!parsed) throw new KhalaClientError("invalid_link", "invalid_link");
142
- if (!HARNESSES.includes(input.harness)) fail("invalid_harness");
143
+ if (!isHarnessId(input.harness)) fail("invalid_harness");
143
144
  const label = validateAgentName(input.label);
144
145
  if (!label.ok || [...label.name].length > 40) fail("invalid_label");
145
146
  const { origin } = parsed;
@@ -154,6 +155,21 @@ async function requestJoin(input, deps = {}) {
154
155
  body = await json(response);
155
156
  code = serverCode(body);
156
157
  }
158
+ if (response.status === 400 && code === "invalid_harness") {
159
+ if (new URL(origin).protocol === "https:") {
160
+ throw new KhalaClientError("update_required", `Khala's hosted service does not accept ${harnessInfo(input.harness).displayName} agents yet. Local channels work now.`);
161
+ }
162
+ if (await (deps.restartHelper ?? ((origin2) => restartHelper(origin2, deps.env, deps.fetch ? { fetch: deps.fetch } : {})))(origin)) {
163
+ response = await post(url, origin, rejoin ? { ...base, sessionId: input.sessionId, rejoinSecret: input.rejoinSecret } : base, deps);
164
+ body = await json(response);
165
+ code = serverCode(body);
166
+ if (rejoin && response.status === 400 && code === "invalid_link") {
167
+ response = await post(url, origin, base, deps);
168
+ body = await json(response);
169
+ code = serverCode(body);
170
+ }
171
+ }
172
+ }
157
173
  if (response.status !== 201) {
158
174
  if (response.status === 400 && code === "invalid_link") throw new KhalaClientError("invalid_link", "invalid_link");
159
175
  if (response.status === 400 && (code === "invalid_label" || code === "invalid_harness")) fail(code);
@@ -11,7 +11,7 @@ import {
11
11
  stateKey,
12
12
  writeJsonAtomic,
13
13
  writeStateFile
14
- } from "./chunk-OW3DSTEZ.mjs";
14
+ } from "./chunk-CBPLCCRZ.mjs";
15
15
 
16
16
  // src/inbox.ts
17
17
  import * as fs from "node:fs/promises";
@@ -202,6 +202,18 @@ function resolveChannel(list, ref) {
202
202
  return { ok: false, code: matches.length ? "channel_ambiguous" : "channel_unknown", channels };
203
203
  }
204
204
 
205
+ // src/activity.ts
206
+ import path2 from "node:path";
207
+ async function readActivity(files) {
208
+ const activity = await readJson(path2.join(files.dir, "activity.json"));
209
+ return activity && ["idle", "busy"].includes(activity.state) && typeof activity.updatedAt === "string" && Number.isFinite(Date.parse(activity.updatedAt)) ? activity : { state: "busy", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
210
+ }
211
+ async function writeActivity(files, state, now = () => /* @__PURE__ */ new Date()) {
212
+ const activity = { state, updatedAt: now().toISOString() };
213
+ await writeJsonAtomic(path2.join(files.dir, "activity.json"), activity);
214
+ return activity;
215
+ }
216
+
205
217
  export {
206
218
  readEntries,
207
219
  readCursor,
@@ -212,5 +224,7 @@ export {
212
224
  channelKey,
213
225
  listChannels,
214
226
  migrateLegacy,
215
- resolveChannel
227
+ resolveChannel,
228
+ readActivity,
229
+ writeActivity
216
230
  };