khala-cli 0.4.7 → 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 (27) hide show
  1. package/README.md +18 -1
  2. package/dist/{chunk-25AX7UMF.mjs → chunk-2DHIAFEV.mjs} +5 -5
  3. package/dist/{chunk-OAFYPBCP.mjs → chunk-2VCAW4GH.mjs} +1 -1
  4. package/dist/{chunk-LNG3ZZBK.mjs → chunk-54XFVSEC.mjs} +1 -1
  5. package/dist/{chunk-JEI32DEA.mjs → chunk-5BQTYU66.mjs} +285 -33
  6. package/dist/{chunk-YIHRHD7W.mjs → chunk-5LIE5GUU.mjs} +1 -1
  7. package/dist/{chunk-S3FX7VCY.mjs → chunk-5PUQT3YS.mjs} +200 -17
  8. package/dist/{chunk-APIWKRKG.mjs → chunk-7HBRFY3Z.mjs} +1 -1
  9. package/dist/{chunk-SU6362YU.mjs → chunk-APMWDF6B.mjs} +1 -1
  10. package/dist/{chunk-C5SMOUTO.mjs → chunk-BKLW2SPG.mjs} +27 -9
  11. package/dist/{chunk-WNOIHCCC.mjs → chunk-CBPLCCRZ.mjs} +1 -1
  12. package/dist/{chunk-IG2NU5J6.mjs → chunk-CJTCU755.mjs} +4 -4
  13. package/dist/{chunk-4LSCOWWN.mjs → chunk-FH34B3I3.mjs} +6 -8
  14. package/dist/chunk-FV32AQTC.mjs +40 -0
  15. package/dist/{chunk-L7L6UNXN.mjs → chunk-H3K4GA2P.mjs} +9 -9
  16. package/dist/{chunk-3OFKVYOX.mjs → chunk-IQJQ4RF5.mjs} +1 -1
  17. package/dist/{chunk-TOBQUAPD.mjs → chunk-RNDUFTBF.mjs} +56 -32
  18. package/dist/{chunk-ODTSGEID.mjs → chunk-VAI7IUI4.mjs} +3 -3
  19. package/dist/{chunk-OP2HZ7EM.mjs → chunk-VKQ3KUYO.mjs} +16 -2
  20. package/dist/{chunk-K6EJSZYO.mjs → chunk-VUSV2ADL.mjs} +13 -15
  21. package/dist/{chunk-7V2YQIRI.mjs → chunk-W6R4UJUQ.mjs} +12 -5
  22. package/dist/{chunk-6YEYXZZB.mjs → chunk-Z4JF2M5C.mjs} +7 -8
  23. package/dist/{chunk-W5HS2PQO.mjs → chunk-ZJ4W7P33.mjs} +8 -8
  24. package/dist/khala.mjs +11 -11
  25. package/package.json +1 -1
  26. package/dist/chunk-5RGEO5HN.mjs +0 -142
  27. package/dist/{chunk-U3MKSPRO.mjs → chunk-NKX3SLS7.mjs} +37 -37
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # khala-cli
2
2
 
3
- The `khala` command connects your existing Claude Code, Codex or Cursor session to a
3
+ The `khala` command connects your existing Claude Code, Codex, Cursor or OpenCode session to a
4
4
  [Khala](https://khala.aiur.team) channel: an MCP server (`khala_join`, `khala_status`,
5
5
  `khala_read`, `khala_send`, `khala_event`), delivery hooks that bring channel messages into
6
6
  the session, and local channels on this computer. Requires Node 22 or newer.
@@ -40,6 +40,23 @@ under Settings → MCP). Undo with `npx -y khala-cli install cursor --uninstall`
40
40
  cannot wake an idle chat; new messages arrive when a chat finishes a turn (Sync) or after a
41
41
  tool call (Steer).
42
42
 
43
+ ## OpenCode
44
+
45
+ ```sh
46
+ npx -y khala-cli install opencode
47
+ ```
48
+
49
+ This installs a stable CLI and, when published on npm, pins `khala-opencode` at the same version in the global
50
+ `~/.config/opencode/opencode.json` (or under `XDG_CONFIG_HOME`). The plugin registers
51
+ Khala's MCP server. If the version is unpublished or the registry check fails, it configures
52
+ MCP-only mode (Async, no wake); re-run `khala install opencode` after updating. Existing
53
+ `opencode.jsonc` files require a manual merge or conversion to JSON before retrying. Restart OpenCode after installation. Existing settings and sibling
54
+ plugins are preserved. Undo with `npx -y khala-cli install opencode --uninstall`.
55
+
56
+ For unpublished-build testing only, `KHALA_INSTALL_SPEC` selects a CLI tarball and
57
+ `KHALA_OPENCODE_PLUGIN_SPEC` forces plugin mode with a spec such as
58
+ `file:/tmp/khala-opencode-0.1.0.tgz`.
59
+
43
60
  ## Then
44
61
 
45
62
  Tell your agent "Join this Khala channel: <link>" and open the confirmation link it returns.
@@ -1,19 +1,19 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  KHALA_AGENT_VERSION
4
- } from "./chunk-YIHRHD7W.mjs";
4
+ } from "./chunk-5LIE5GUU.mjs";
5
5
  import {
6
6
  bundle
7
- } from "./chunk-SU6362YU.mjs";
7
+ } from "./chunk-APMWDF6B.mjs";
8
8
  import {
9
9
  StateError,
10
10
  ensureStateDir,
11
11
  readJson,
12
12
  stateRoot
13
- } from "./chunk-WNOIHCCC.mjs";
13
+ } from "./chunk-CBPLCCRZ.mjs";
14
14
  import {
15
15
  LISTENING_MODES
16
- } from "./chunk-3OFKVYOX.mjs";
16
+ } from "./chunk-IQJQ4RF5.mjs";
17
17
  import {
18
18
  HARNESS_REGISTRY,
19
19
  M1_LABEL_MAX_BYTES,
@@ -37,7 +37,7 @@ import {
37
37
  utcTimestamp,
38
38
  utf8Length,
39
39
  version
40
- } from "./chunk-U3MKSPRO.mjs";
40
+ } from "./chunk-NKX3SLS7.mjs";
41
41
 
42
42
  // src/client.ts
43
43
  var KhalaClientError = class extends Error {
@@ -2,7 +2,7 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
2
2
  import {
3
3
  decodeChannelEvent,
4
4
  formatChannelEventLine
5
- } from "./chunk-LNG3ZZBK.mjs";
5
+ } from "./chunk-54XFVSEC.mjs";
6
6
 
7
7
  // src/events/receive.ts
8
8
  function toEventInboxEntry(base, rawContent) {
@@ -9,7 +9,7 @@ import {
9
9
  utcTimestamp,
10
10
  utf8Length,
11
11
  version
12
- } from "./chunk-U3MKSPRO.mjs";
12
+ } from "./chunk-NKX3SLS7.mjs";
13
13
 
14
14
  // ../contracts/src/m1/channel-event.ts
15
15
  var CHANNEL_EVENT_TYPE = "com.khala.event.v1";
@@ -2,23 +2,26 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
2
2
  import {
3
3
  advanceCursor,
4
4
  listChannels,
5
+ readActivity,
5
6
  readCursor,
6
7
  unread
7
- } from "./chunk-OP2HZ7EM.mjs";
8
+ } from "./chunk-VKQ3KUYO.mjs";
8
9
  import {
9
10
  SESSION_ID_PATTERN,
10
11
  StateError,
12
+ ensureStateDir,
11
13
  readJson,
12
14
  stateRoot,
13
15
  writeJsonAtomic
14
- } from "./chunk-WNOIHCCC.mjs";
16
+ } from "./chunk-CBPLCCRZ.mjs";
15
17
  import {
16
18
  DEFAULT_LISTENING_MODE,
17
19
  decodeListeningMode
18
- } from "./chunk-3OFKVYOX.mjs";
20
+ } from "./chunk-IQJQ4RF5.mjs";
19
21
  import {
20
- HARNESS_REGISTRY
21
- } from "./chunk-U3MKSPRO.mjs";
22
+ HARNESS_REGISTRY,
23
+ isHarnessId
24
+ } from "./chunk-NKX3SLS7.mjs";
22
25
 
23
26
  // src/mode.ts
24
27
  async function readListeningMode(files) {
@@ -88,8 +91,8 @@ function wakeLine(nonce) {
88
91
  }
89
92
 
90
93
  // src/wake/idle-wake.ts
91
- function codexIdleWakeArgv(sessionId) {
92
- return ["queue", "--thread", sessionId, "--message", CODEX_IDLE_WAKE_NOTICE];
94
+ function codexIdleWakeArgv(sessionId, line) {
95
+ return ["queue", "--thread", sessionId, "--message", line];
93
96
  }
94
97
 
95
98
  // src/wake/idle-wake-process.ts
@@ -159,21 +162,57 @@ function createCodexQueueProcessPort(deps) {
159
162
  };
160
163
  }
161
164
 
162
- // src/wake/ladder.ts
163
- import path4 from "node:path";
164
-
165
- // src/activity.ts
165
+ // src/wake/codex-probe.ts
166
+ import { execFile } from "node:child_process";
167
+ import { access, realpath, stat } from "node:fs/promises";
168
+ import { constants } from "node:fs";
166
169
  import path from "node:path";
167
- async function readActivity(files) {
168
- const activity = await readJson(path.join(files.dir, "activity.json"));
169
- 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() };
170
- }
171
- async function writeActivity(files, state, now = () => /* @__PURE__ */ new Date()) {
172
- const activity = { state, updatedAt: now().toISOString() };
173
- await writeJsonAtomic(path.join(files.dir, "activity.json"), activity);
174
- return activity;
170
+ import { promisify } from "node:util";
171
+ var execute = promisify(execFile);
172
+ var probes = /* @__PURE__ */ new Map();
173
+ async function probeCodexQueue(env) {
174
+ let command;
175
+ for (const dir of (env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
176
+ try {
177
+ const candidate = path.join(dir, process.platform === "win32" ? "codex.exe" : "codex");
178
+ await access(candidate, constants.X_OK);
179
+ if (!(await stat(candidate)).isFile()) continue;
180
+ command = await realpath(candidate);
181
+ break;
182
+ } catch {
183
+ }
184
+ }
185
+ if (!command) return { available: false, reason: "codex_binary_missing" };
186
+ let mtime;
187
+ try {
188
+ mtime = (await stat(command)).mtimeMs;
189
+ } catch {
190
+ return { available: false, reason: "codex_binary_missing" };
191
+ }
192
+ const cached = probes.get(command);
193
+ if (cached?.mtime === mtime) return cached.result;
194
+ const binary = command;
195
+ const result = (async () => {
196
+ try {
197
+ const { stdout } = await execute(binary, ["queue", "--help"], {
198
+ env: scrubbedQueueEnv(env),
199
+ timeout: 5e3,
200
+ killSignal: "SIGKILL",
201
+ maxBuffer: 64 * 1024,
202
+ windowsHide: true
203
+ });
204
+ return /--thread\b/.test(stdout) && /--message\b/.test(stdout) ? { available: true, command: binary } : { available: false, reason: "codex_queue_unavailable" };
205
+ } catch {
206
+ return { available: false, reason: "codex_queue_unavailable" };
207
+ }
208
+ })();
209
+ probes.set(binary, { mtime, result });
210
+ return result;
175
211
  }
176
212
 
213
+ // src/wake/ladder.ts
214
+ import path4 from "node:path";
215
+
177
216
  // src/wake/shared/settings.ts
178
217
  import * as fs from "node:fs/promises";
179
218
  import path2 from "node:path";
@@ -499,18 +538,20 @@ function createCodexWakeDriver(deps = {}) {
499
538
  optIn: false,
500
539
  minIdleMs: 0,
501
540
  deadlineMs: 3e4,
502
- // U13 introduces the nonce-bearing argv. Preserve U2 queue goldens until then.
503
- verification: "none",
504
- available: () => true,
505
- async wake(ctx) {
506
- const port = deps.port ?? createCodexQueueProcessPort({ command: "codex", env: ctx.env });
507
- const outcome = await port.run(codexIdleWakeArgv(ctx.sessionId), ctx.signal);
541
+ verification: "nonce",
542
+ available: async (ctx) => (await (deps.probe ?? probeCodexQueue)(ctx.env)).available,
543
+ unavailableReason: async (ctx) => (await (deps.probe ?? probeCodexQueue)(ctx.env)).reason,
544
+ async wake(ctx, line) {
545
+ const probe = await (deps.probe ?? probeCodexQueue)(ctx.env);
546
+ if (!probe.available || ctx.signal.aborted) return;
547
+ const port = deps.port ?? createCodexQueueProcessPort({ command: probe.command, env: ctx.env });
548
+ const outcome = await port.run(codexIdleWakeArgv(ctx.sessionId, line), ctx.signal);
508
549
  if (outcome.status !== "queued") {
509
- const line = JSON.stringify({ ok: false, warning: "codex_queue_failed", status: outcome.status }) + "\n";
550
+ const line2 = JSON.stringify({ ok: false, warning: "codex_queue_failed", status: outcome.status }) + "\n";
510
551
  try {
511
552
  (deps.stderr ?? ((value) => {
512
553
  process.stderr.write(value);
513
- }))(line);
554
+ }))(line2);
514
555
  } catch {
515
556
  }
516
557
  }
@@ -520,7 +561,7 @@ function createCodexWakeDriver(deps = {}) {
520
561
  var codexWakeDriver = createCodexWakeDriver();
521
562
 
522
563
  // src/harness/codex.ts
523
- var install = async (flags, deps) => (await import("./chunk-S3FX7VCY.mjs")).runCodexInstall(flags, deps);
564
+ var install = async (flags, deps) => (await import("./chunk-5PUQT3YS.mjs")).runCodexInstall(flags, deps);
524
565
  var codex = {
525
566
  id: "codex",
526
567
  // A string metadata id wins even when empty or invalid; validation happens after selection.
@@ -594,7 +635,7 @@ var cursorCodec = {
594
635
  };
595
636
 
596
637
  // src/harness/cursor.ts
597
- var install2 = async (flags, deps) => (await import("./chunk-S3FX7VCY.mjs")).runCursorInstall(flags, deps);
638
+ var install2 = async (flags, deps) => (await import("./chunk-5PUQT3YS.mjs")).runCursorInstall(flags, deps);
598
639
  var cursor = {
599
640
  id: "cursor",
600
641
  sessionSources: [{
@@ -608,6 +649,213 @@ var cursor = {
608
649
  uninstall: (flags, deps) => install2([...flags, "--uninstall"], deps)
609
650
  };
610
651
 
652
+ // src/harness/codecs/opencode.ts
653
+ var events3 = { "session-start": "start", prompt: "prompt", "post-tool": "tool", "turn-end": "stop", idle: "stop" };
654
+ var opencodeCodec = {
655
+ promptAcceptsContext: true,
656
+ promptDeliversWithoutWake: true,
657
+ suppressOutputErrors: false,
658
+ parse(stdin) {
659
+ try {
660
+ const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
661
+ if (!input || typeof input.session_id !== "string" || typeof input.event !== "string" || !Object.hasOwn(events3, input.event)) return null;
662
+ return {
663
+ sessionId: input.session_id,
664
+ event: events3[input.event],
665
+ continuation: false,
666
+ ...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
667
+ };
668
+ } catch {
669
+ return null;
670
+ }
671
+ },
672
+ noop: () => "",
673
+ render: (_kind, frame) => frame
674
+ };
675
+
676
+ // src/harness/session-sources.ts
677
+ import path5 from "node:path";
678
+ import { lstat } from "node:fs/promises";
679
+
680
+ // src/harness/proc.ts
681
+ import { execFile as execFile2 } from "node:child_process";
682
+ import { readFile as readFile3 } from "node:fs/promises";
683
+ import { promisify as promisify2 } from "node:util";
684
+ var execFileAsync = promisify2(execFile2);
685
+ var validPid = (pid) => Number.isSafeInteger(pid) && pid > 0;
686
+ function startToken(value) {
687
+ return value.trim().replace(/[^a-zA-Z0-9_-]+/g, "-");
688
+ }
689
+ function parseLinuxStat(pid, text) {
690
+ const open = text.indexOf("(");
691
+ const close = text.lastIndexOf(")");
692
+ if (!validPid(pid) || open < 0 || close < open || Number(text.slice(0, open).trim()) !== pid) return null;
693
+ const fields = text.slice(close + 1).trim().split(/\s+/);
694
+ const ppid = Number(fields[1]);
695
+ const startTime = fields[19];
696
+ if (!Number.isSafeInteger(ppid) || ppid < 0 || !startTime || !/^\d+$/.test(startTime)) return null;
697
+ return { pid, ppid, startTime, command: text.slice(open + 1, close) };
698
+ }
699
+ function parseDarwinPs(pid, text) {
700
+ const match = text.trim().match(/^(\d+)\s+([A-Za-z]{3}\s+[A-Za-z]{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/);
701
+ if (!validPid(pid) || !match) return null;
702
+ return { pid, ppid: Number(match[1]), startTime: startToken(match[2]), command: match[3] };
703
+ }
704
+ function csvFields(line) {
705
+ const fields = [];
706
+ let value = "";
707
+ let quoted = false;
708
+ for (let i = 0; i < line.length; i++) {
709
+ const char = line[i];
710
+ if (char === '"') {
711
+ if (quoted && line[i + 1] === '"') {
712
+ value += '"';
713
+ i++;
714
+ } else quoted = !quoted;
715
+ } else if (char === "," && !quoted) {
716
+ fields.push(value);
717
+ value = "";
718
+ } else value += char;
719
+ }
720
+ if (quoted) return null;
721
+ fields.push(value);
722
+ return fields;
723
+ }
724
+ function parseWindowsCsv(text, pid) {
725
+ if (!validPid(pid)) return null;
726
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/).filter((line) => line.trim() && !line.startsWith("#"));
727
+ const headers = csvFields(lines[0] ?? "");
728
+ if (!headers) return null;
729
+ for (const line of lines.slice(1)) {
730
+ const values = csvFields(line);
731
+ if (!values) continue;
732
+ const field = (name) => values[headers.indexOf(name)] ?? "";
733
+ if (Number(field("ProcessId")) !== pid) continue;
734
+ const ppid = Number(field("ParentProcessId"));
735
+ const startTime = startToken(field("CreationDate"));
736
+ const command = field("Name");
737
+ if (!field("ParentProcessId") || !Number.isSafeInteger(ppid) || ppid < 0 || !startTime || !command) return null;
738
+ return { pid, ppid, startTime, command };
739
+ }
740
+ return null;
741
+ }
742
+ async function readProcess(pid) {
743
+ if (!validPid(pid)) return null;
744
+ try {
745
+ if (process.platform === "linux") return parseLinuxStat(pid, await readFile3(`/proc/${pid}/stat`, "utf8"));
746
+ if (process.platform === "darwin") {
747
+ const { stdout } = await execFileAsync("ps", ["-o", "ppid=,lstart=,comm=", "-p", String(pid)], { env: { ...process.env, LC_ALL: "C" }, timeout: 5e3 });
748
+ return parseDarwinPs(pid, stdout);
749
+ }
750
+ if (process.platform === "win32") {
751
+ const script = `Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}' | Select-Object ProcessId,ParentProcessId,@{Name='CreationDate';Expression={$_.CreationDate.ToUniversalTime().ToString('o')}},Name | ConvertTo-Csv -NoTypeInformation`;
752
+ const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 5e3 });
753
+ return parseWindowsCsv(stdout, pid);
754
+ }
755
+ } catch {
756
+ }
757
+ return null;
758
+ }
759
+ async function* walkAncestors(pid, read = readProcess) {
760
+ const seen = /* @__PURE__ */ new Set([pid]);
761
+ let current = await read(pid);
762
+ while (current && validPid(current.ppid) && !seen.has(current.ppid)) {
763
+ seen.add(current.ppid);
764
+ current = await read(current.ppid);
765
+ if (current) yield current;
766
+ }
767
+ }
768
+ async function nearestNonShellAncestor(pid, read = readProcess) {
769
+ for await (const ancestor of walkAncestors(pid, read)) {
770
+ const name = ancestor.command.split(/[\\/]/).pop()?.replace(/^-/, "").toLowerCase() ?? "";
771
+ if (!/^(?:sh|bash|dash|zsh|fish|ksh|csh|tcsh|ash|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh(?:\.exe)?)$/.test(name)) return ancestor;
772
+ }
773
+ return null;
774
+ }
775
+
776
+ // src/harness/session-sources.ts
777
+ var SESSION_UNKNOWN_HINT = "Send the agent one message first, then retry.";
778
+ var mappingDir = (harness, env) => path5.join(stateRoot(env), harness, ".by-pid");
779
+ async function recordHookSession(harness, sessionId, env, options = {}) {
780
+ if (!SESSION_ID_PATTERN.test(sessionId)) return;
781
+ const parent = await nearestNonShellAncestor(options.pid ?? process.pid, options.readProcess ?? readProcess);
782
+ if (!parent) return;
783
+ const dir = mappingDir(harness, env);
784
+ await ensureStateDir(dir);
785
+ const file = path5.join(dir, `${parent.pid}.json`);
786
+ const at = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
787
+ const previous = await readJson(file);
788
+ if (previous?.startTime === parent.startTime && typeof previous.at === "string" && previous.at > at) return;
789
+ await writeJsonAtomic(file, {
790
+ sessionId,
791
+ startTime: parent.startTime,
792
+ at,
793
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {}
794
+ });
795
+ }
796
+ var hookMapSource = {
797
+ kind: "hook-map",
798
+ rejoinable: () => true,
799
+ async resolve(_meta, env, context) {
800
+ const dir = mappingDir(context.harness, env);
801
+ try {
802
+ await lstat(dir);
803
+ } catch (error) {
804
+ if (error.code === "ENOENT") return null;
805
+ throw error;
806
+ }
807
+ await ensureStateDir(dir);
808
+ const parent = await nearestNonShellAncestor(context.pid ?? process.pid, context.readProcess ?? readProcess);
809
+ if (!parent) return null;
810
+ const entry = await readJson(path5.join(dir, `${parent.pid}.json`));
811
+ if (entry?.startTime === parent.startTime && typeof entry.sessionId === "string" && SESSION_ID_PATTERN.test(entry.sessionId) && typeof entry.at === "string" && Number.isFinite(Date.parse(entry.at))) return entry.sessionId;
812
+ return null;
813
+ }
814
+ };
815
+ var processSource = {
816
+ kind: "process",
817
+ rejoinable: () => false,
818
+ async resolve(_meta, _env, context) {
819
+ for await (const parent of walkAncestors(context.pid ?? process.pid, context.readProcess ?? readProcess)) {
820
+ return `proc-${parent.pid}-${parent.startTime}`;
821
+ }
822
+ return null;
823
+ }
824
+ };
825
+ async function resolveSources(sources, meta, env, context) {
826
+ for (const source of sources) {
827
+ const value = await source.resolve(meta, env, context);
828
+ if (value === void 0 || value === null) continue;
829
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value) ? { sessionId: value, rejoinable: source.rejoinable(value) } : null;
830
+ }
831
+ return null;
832
+ }
833
+
834
+ // src/harness/opencode.ts
835
+ var install3 = async (flags, deps) => (await import("./chunk-5PUQT3YS.mjs")).runOpenCodeInstall(flags, deps);
836
+ var opencode = {
837
+ id: "opencode",
838
+ sessionSources: [{ kind: "meta", resolve: (meta) => meta?.khala_session, rejoinable: () => true }, hookMapSource],
839
+ codec: opencodeCodec,
840
+ restoreAtStartup: false,
841
+ install: install3,
842
+ uninstall: (flags, deps) => install3([...flags, "--uninstall"], deps)
843
+ };
844
+
845
+ // src/harness/generic.ts
846
+ var generic = {
847
+ id: "generic",
848
+ sessionSources: [
849
+ { kind: "env", resolve: (_meta, env) => env.KHALA_SESSION_ID, rejoinable: () => true },
850
+ processSource
851
+ ],
852
+ codec: void 0,
853
+ restoreAtStartup: true
854
+ };
855
+ function genericAdapter(id) {
856
+ return { ...generic, id };
857
+ }
858
+
611
859
  // src/harness/index.ts
612
860
  function createAdapterRegistry(adapters) {
613
861
  const byId = /* @__PURE__ */ new Map();
@@ -618,17 +866,21 @@ function createAdapterRegistry(adapters) {
618
866
  }
619
867
  return Object.freeze({ resolve: (id) => byId.get(id) });
620
868
  }
621
- var ADAPTERS = Object.freeze([claude, codex, cursor]);
869
+ var ADAPTERS = Object.freeze([claude, codex, cursor, opencode, generic]);
622
870
  var registry = createAdapterRegistry(ADAPTERS);
623
- var adapterFor = registry.resolve;
871
+ function adapterFor(id) {
872
+ return registry.resolve(id) ?? (isHarnessId(id) ? genericAdapter(id) : void 0);
873
+ }
624
874
 
625
875
  export {
626
876
  readListeningMode,
627
877
  applyListeningMode,
628
- readActivity,
629
- writeActivity,
878
+ readWakeState,
630
879
  settleAttempts,
631
880
  createWakeLadder,
632
881
  CURSOR_WORKSPACE_ENV,
882
+ SESSION_UNKNOWN_HINT,
883
+ recordHookSession,
884
+ resolveSources,
633
885
  adapterFor
634
886
  };
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  bundle
4
- } from "./chunk-SU6362YU.mjs";
4
+ } from "./chunk-APMWDF6B.mjs";
5
5
 
6
6
  // src/version.ts
7
7
  var KHALA_AGENT_VERSION = bundle?.version ?? "0.0.0";