khala-cli 0.4.1 → 0.4.4

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 (34) hide show
  1. package/dist/{chunk-S72K3GSP.mjs → chunk-6OADQWV7.mjs} +31 -20
  2. package/dist/{chunk-YMDAY234.mjs → chunk-7CVU5OZ7.mjs} +1 -1
  3. package/dist/{chunk-PSN7P4AV.mjs → chunk-AK74S7EN.mjs} +4 -4
  4. package/dist/chunk-AUJEUKVM.mjs +121 -0
  5. package/dist/chunk-DC3OX6F4.mjs +52 -0
  6. package/dist/chunk-ENPBK5W2.mjs +187 -0
  7. package/dist/chunk-FA6Z65DW.mjs +19 -0
  8. package/dist/{chunk-KEGR6VEQ.mjs → chunk-FUU775QM.mjs} +81 -10
  9. package/dist/chunk-GE3RXO2X.mjs +26 -0
  10. package/dist/chunk-LHAMBQK7.mjs +144 -0
  11. package/dist/{chunk-EVLXC2WS.mjs → chunk-LN3G2LJV.mjs} +13 -10
  12. package/dist/{chunk-35KKPOKI.mjs → chunk-LQ6KNUNN.mjs} +87 -53
  13. package/dist/{chunk-CSIFRJFF.mjs → chunk-MU4FUENP.mjs} +3 -2
  14. package/dist/{chunk-L2HRDTA5.mjs → chunk-MZS7XFMC.mjs} +3 -3
  15. package/dist/{chunk-U5ZMOPUK.mjs → chunk-N5VYKQMR.mjs} +5 -46
  16. package/dist/{chunk-3SWCRLWX.mjs → chunk-OS2JLDL7.mjs} +172 -227
  17. package/dist/chunk-P5CP32X2.mjs +170 -0
  18. package/dist/{chunk-RX5HGJQT.mjs → chunk-PJ3E3IVH.mjs} +184 -126
  19. package/dist/chunk-SN3STAO2.mjs +190 -0
  20. package/dist/{chunk-B5BFL7OD.mjs → chunk-XEYKJMZL.mjs} +1 -1
  21. package/dist/{chunk-SR5BG7FL.mjs → chunk-ZKDDKUUA.mjs} +1 -1
  22. package/dist/{chunk-HAD5ZILW.mjs → chunk-ZRFXZK72.mjs} +1 -1
  23. package/dist/khala.mjs +12 -10
  24. package/dist/web/assets/apple-touch-icon-Du9V6qit.png +0 -0
  25. package/dist/web/assets/favicon-16x16-D5waNSZD.png +0 -0
  26. package/dist/web/assets/favicon-32x32-B9Ubw1Dx.png +0 -0
  27. package/dist/web/assets/favicon-zCD0pKtT.ico +0 -0
  28. package/dist/web/assets/local-Bk8hMIT-.js +9 -0
  29. package/dist/web/assets/{local-DHYvcQG0.css → local-vdvavyq0.css} +1 -1
  30. package/dist/web/index.html +7 -2
  31. package/package.json +1 -1
  32. package/dist/chunk-AVJKIJ34.mjs +0 -91
  33. package/dist/chunk-N6LMHR7P.mjs +0 -455
  34. package/dist/web/assets/local-DGdQ2afg.js +0 -9
@@ -1,22 +1,26 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
- advanceCursor,
4
3
  isWakeEntry,
5
- readListeningMode,
6
- unread,
7
4
  writeActivity
8
- } from "./chunk-U5ZMOPUK.mjs";
5
+ } from "./chunk-DC3OX6F4.mjs";
6
+ import "./chunk-P5CP32X2.mjs";
9
7
  import {
10
- StateError,
11
- readStatus,
12
- sessionFiles
13
- } from "./chunk-B5BFL7OD.mjs";
14
- import "./chunk-N6LMHR7P.mjs";
8
+ advanceCursor,
9
+ readListeningMode,
10
+ unread
11
+ } from "./chunk-N5VYKQMR.mjs";
15
12
  import {
16
13
  CURSOR_DEFAULT_SESSION,
17
14
  cursorSessionId
18
15
  } from "./chunk-VUE2DDYY.mjs";
19
- import "./chunk-SR5BG7FL.mjs";
16
+ import {
17
+ StateError,
18
+ readStatus,
19
+ sessionFiles
20
+ } from "./chunk-XEYKJMZL.mjs";
21
+ import "./chunk-AUJEUKVM.mjs";
22
+ import "./chunk-SN3STAO2.mjs";
23
+ import "./chunk-ZKDDKUUA.mjs";
20
24
 
21
25
  // hooks/deliver.ts
22
26
  import * as fs from "node:fs/promises";
@@ -29,13 +33,20 @@ function renderLine(entry) {
29
33
  const body = entry.body.replace(/\r\n|\r|\n/g, "\n ").replace(/<\/?khala-channel-messages/gi, (tag) => "&lt;" + tag.slice(1));
30
34
  return entry.kind === "event" ? `[${ts}] [khala event from ${label}] ${body}` : `[${ts}] ${label} (${entry.senderKind}): ${body}`;
31
35
  }
32
- function renderFrame(channel, entries) {
33
- return `<khala-channel-messages channel="${channel.replace(/"/g, "&quot;")}" count="${entries.length}">
34
- ${INTRO}
36
+ function plainName(name) {
37
+ return Array.from(name.replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("").replace(/</g, "&lt;");
38
+ }
39
+ function renderFrame(channel, entries, you) {
40
+ const name = you === void 0 ? "" : plainName(you);
41
+ const youAttr = name ? ` you="${name.replace(/"/g, "&quot;")}"` : "";
42
+ const youLine = name ? `
43
+ You are ${name} in this channel; messages that name or @mention you are addressed to you.` : "";
44
+ return `<khala-channel-messages channel="${channel.replace(/"/g, "&quot;")}"${youAttr} count="${entries.length}">
45
+ ${INTRO}${youLine}
35
46
  ${entries.map(renderLine).join("\n")}
36
47
  </khala-channel-messages>`;
37
48
  }
38
- function selectFrame(channelName, entries) {
49
+ function selectFrame(channelName, entries, you) {
39
50
  const rendered = [];
40
51
  let consumedCount = 0;
41
52
  let channel = channelName;
@@ -43,18 +54,18 @@ function selectFrame(channelName, entries) {
43
54
  const entry = entries[i];
44
55
  if (rendered.length === 50) break;
45
56
  channel ??= entry.roomId;
46
- if (Buffer.byteLength(renderFrame(channel, [...rendered, entry])) > MAX_FRAME_BYTES) {
57
+ if (Buffer.byteLength(renderFrame(channel, [...rendered, entry], you)) > MAX_FRAME_BYTES) {
47
58
  if (rendered.length) break;
48
59
  const body = Array.from(entry.body);
49
60
  let low = 0, high = body.length;
50
61
  while (low < high) {
51
62
  const middle = Math.ceil((low + high) / 2);
52
63
  const candidate = { ...entry, body: body.slice(0, middle).join("") + TRUNCATED };
53
- if (Buffer.byteLength(renderFrame(channel, [candidate])) <= MAX_FRAME_BYTES) low = middle;
64
+ if (Buffer.byteLength(renderFrame(channel, [candidate], you)) <= MAX_FRAME_BYTES) low = middle;
54
65
  else high = middle - 1;
55
66
  }
56
67
  const truncated = { ...entry, body: body.slice(0, low).join("") + TRUNCATED };
57
- if (Buffer.byteLength(renderFrame(channel, [truncated])) > MAX_FRAME_BYTES) throw new Error("frame_metadata_too_large");
68
+ if (Buffer.byteLength(renderFrame(channel, [truncated], you)) > MAX_FRAME_BYTES) throw new Error("frame_metadata_too_large");
58
69
  rendered.push(truncated);
59
70
  consumedCount = i + 1;
60
71
  break;
@@ -63,7 +74,7 @@ function selectFrame(channelName, entries) {
63
74
  consumedCount = i + 1;
64
75
  }
65
76
  return {
66
- frame: rendered.length ? renderFrame(channel, rendered) : null,
77
+ frame: rendered.length ? renderFrame(channel, rendered, you) : null,
67
78
  consumed: entries.slice(0, consumedCount)
68
79
  };
69
80
  }
@@ -117,7 +128,7 @@ async function deliver(stdin, argv, io = {
117
128
  const status = await readStatus(files);
118
129
  for (let attempt = 0; attempt < 2; attempt++) {
119
130
  const { entries, cursor } = await unread(files);
120
- const { frame, consumed } = selectFrame(status?.channelName, entries);
131
+ const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
121
132
  if (!frame) break;
122
133
  if (input.hook_event_name !== "UserPromptSubmit" && !consumed.some(isWakeEntry)) break;
123
134
  if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
@@ -182,7 +193,7 @@ async function cursorOutput(files, event, loopCount, io) {
182
193
  const status = await readStatus(files);
183
194
  for (let attempt = 0; attempt < 2; attempt++) {
184
195
  const { entries, cursor } = await unread(files);
185
- const { frame, consumed } = selectFrame(status?.channelName, entries);
196
+ const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
186
197
  if (!frame || !consumed.some(isWakeEntry)) break;
187
198
  if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
188
199
  await writeActivity(files, "busy", io.now);
@@ -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-HAD5ZILW.mjs";
4
+ } from "./chunk-ZRFXZK72.mjs";
5
5
 
6
6
  // src/version.ts
7
7
  var KHALA_AGENT_VERSION = bundle?.version ?? "0.0.0";
@@ -1,11 +1,11 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ bundle
4
+ } from "./chunk-ZRFXZK72.mjs";
2
5
  import {
3
6
  CURSOR_WORKSPACE_ENV
4
7
  } from "./chunk-VUE2DDYY.mjs";
5
- import {
6
- bundle
7
- } from "./chunk-HAD5ZILW.mjs";
8
- import "./chunk-SR5BG7FL.mjs";
8
+ import "./chunk-ZKDDKUUA.mjs";
9
9
 
10
10
  // src/install/main.ts
11
11
  import { spawnSync } from "node:child_process";
@@ -0,0 +1,121 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ decodeWith,
4
+ literal,
5
+ object,
6
+ readMatrixUserId,
7
+ version
8
+ } from "./chunk-SN3STAO2.mjs";
9
+
10
+ // ../contracts/src/delivery/decode.ts
11
+ var MAX_IDENTIFIER_BYTES = 512;
12
+ var DecodeFailure = class extends Error {
13
+ code;
14
+ field;
15
+ constructor(field, code) {
16
+ super(`${field || "<root>"}: ${code}`);
17
+ this.name = "DecodeFailure";
18
+ this.code = code;
19
+ this.field = field;
20
+ }
21
+ };
22
+ function fail(field, code) {
23
+ throw new DecodeFailure(field, code);
24
+ }
25
+ function decodeWith2(read) {
26
+ try {
27
+ return { ok: true, value: read() };
28
+ } catch (error) {
29
+ if (error instanceof DecodeFailure) return { ok: false, code: error.code, field: error.field };
30
+ return { ok: false, code: "invalid_field", field: "" };
31
+ }
32
+ }
33
+ function utf8Length(value) {
34
+ let bytes = 0;
35
+ for (const character of value) {
36
+ const codePoint = character.codePointAt(0) ?? 0;
37
+ if (codePoint < 128) bytes += 1;
38
+ else if (codePoint < 2048) bytes += 2;
39
+ else if (codePoint < 65536) bytes += 3;
40
+ else bytes += 4;
41
+ }
42
+ return bytes;
43
+ }
44
+ function isWellFormed(value) {
45
+ for (let index = 0; index < value.length; index += 1) {
46
+ const unit = value.charCodeAt(index);
47
+ if (unit >= 55296 && unit <= 56319) {
48
+ const next = value.charCodeAt(index + 1);
49
+ if (!(next >= 56320 && next <= 57343)) return false;
50
+ index += 1;
51
+ } else if (unit >= 56320 && unit <= 57343) {
52
+ return false;
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
58
+ function identifier(input, field) {
59
+ if (typeof input !== "string") fail(field, "invalid_field");
60
+ if (!isWellFormed(input) || input.length === 0 || CONTROL.test(input)) fail(field, "invalid_field");
61
+ if (utf8Length(input) > MAX_IDENTIFIER_BYTES) fail(field, "limit_exceeded");
62
+ return input;
63
+ }
64
+ function literal2(input, field, allowed) {
65
+ if (!allowed.includes(input)) fail(field, "invalid_field");
66
+ return input;
67
+ }
68
+
69
+ // ../contracts/src/delivery/ids.ts
70
+ function readId(input, field) {
71
+ return identifier(input, field);
72
+ }
73
+ var idDecoder = () => (input) => decodeWith2(() => readId(input, ""));
74
+ var decodeOwnerId = idDecoder();
75
+ var decodeParticipantId = idDecoder();
76
+ var decodeDeviceId = idDecoder();
77
+ var decodeBindingId = idDecoder();
78
+ var decodeRoomId = idDecoder();
79
+ var decodeEventId = idDecoder();
80
+ var decodeCommandId = idDecoder();
81
+ var decodeReleaseId = idDecoder();
82
+ var decodeReceiptId = idDecoder();
83
+ var decodeAuthorizationId = idDecoder();
84
+ var decodeOperationId = idDecoder();
85
+ var decodeCausalRootId = idDecoder();
86
+
87
+ // ../contracts/src/delivery/listening-mode.ts
88
+ var LISTENING_MODES = ["steer", "sync", "async"];
89
+ function decodeListeningMode(input) {
90
+ return decodeWith2(() => literal2(input, "", LISTENING_MODES));
91
+ }
92
+
93
+ // ../contracts/src/m1/listening-mode.ts
94
+ var LISTENING_MODE_COMMAND_TYPE = "com.khala.listening_mode.v1";
95
+ var LISTENING_MODE_MEMBER_KEY = "com.khala.listening_mode";
96
+ var DEFAULT_LISTENING_MODE = "sync";
97
+ function decodeListeningModeCommand(input) {
98
+ return decodeWith(() => {
99
+ const r = object(input, "", ["v", "agent", "mode"]);
100
+ return {
101
+ v: version(r.field("v"), r.at("v")),
102
+ agent: readMatrixUserId(r.field("agent"), r.at("agent")),
103
+ mode: literal(r.field("mode"), r.at("mode"), LISTENING_MODES)
104
+ };
105
+ });
106
+ }
107
+ function memberListeningMode(content) {
108
+ const value = typeof content === "object" && content !== null ? content[LISTENING_MODE_MEMBER_KEY] : void 0;
109
+ const decoded = decodeListeningMode(value);
110
+ return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
111
+ }
112
+
113
+ export {
114
+ LISTENING_MODES,
115
+ decodeListeningMode,
116
+ LISTENING_MODE_COMMAND_TYPE,
117
+ LISTENING_MODE_MEMBER_KEY,
118
+ DEFAULT_LISTENING_MODE,
119
+ decodeListeningModeCommand,
120
+ memberListeningMode
121
+ };
@@ -0,0 +1,52 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ decodeChannelEvent,
4
+ formatChannelEventLine
5
+ } from "./chunk-P5CP32X2.mjs";
6
+ import {
7
+ readJson,
8
+ writeJsonAtomic
9
+ } from "./chunk-XEYKJMZL.mjs";
10
+
11
+ // src/activity.ts
12
+ import path from "node:path";
13
+ async function readActivity(files) {
14
+ const activity = await readJson(path.join(files.dir, "activity.json"));
15
+ 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() };
16
+ }
17
+ async function writeActivity(files, state, now = () => /* @__PURE__ */ new Date()) {
18
+ const activity = { state, updatedAt: now().toISOString() };
19
+ await writeJsonAtomic(path.join(files.dir, "activity.json"), activity);
20
+ return activity;
21
+ }
22
+
23
+ // src/events/receive.ts
24
+ function toEventInboxEntry(base, rawContent) {
25
+ const decoded = decodeChannelEvent(rawContent);
26
+ if (!decoded.ok) return null;
27
+ const content = decoded.value;
28
+ return {
29
+ entry: { ...base, kind: "event", body: formatChannelEventLine(content) + (content.url ? " " + content.url : "") },
30
+ ...content.key !== void 0 ? { key: content.key } : {}
31
+ };
32
+ }
33
+ function createEventKeyFilter() {
34
+ const seen = /* @__PURE__ */ new Set();
35
+ return (key) => {
36
+ if (key === void 0) return true;
37
+ if (seen.has(key)) return false;
38
+ seen.add(key);
39
+ return true;
40
+ };
41
+ }
42
+ function isWakeEntry(entry) {
43
+ return entry.kind === "message";
44
+ }
45
+
46
+ export {
47
+ readActivity,
48
+ writeActivity,
49
+ toEventInboxEntry,
50
+ createEventKeyFilter,
51
+ isWakeEntry
52
+ };
@@ -0,0 +1,187 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ readCursor,
4
+ readEntries,
5
+ readListeningMode
6
+ } from "./chunk-N5VYKQMR.mjs";
7
+ import {
8
+ CURSOR_WORKSPACE_ENV,
9
+ cursorSessionId
10
+ } from "./chunk-VUE2DDYY.mjs";
11
+ import {
12
+ readJson,
13
+ readStatus,
14
+ sessionFiles,
15
+ writeJsonAtomic
16
+ } from "./chunk-XEYKJMZL.mjs";
17
+ import {
18
+ HARNESSES
19
+ } from "./chunk-SN3STAO2.mjs";
20
+
21
+ // src/watch.ts
22
+ import * as fs from "node:fs/promises";
23
+ import { watch as watchDirectory } from "node:fs";
24
+ import path from "node:path";
25
+ import { randomUUID } from "node:crypto";
26
+
27
+ // src/mcp/session-id.ts
28
+ var isHarness = (value) => HARNESSES.includes(value);
29
+ function resolveHarness(argv, env) {
30
+ const index = argv.indexOf("--harness");
31
+ if (index !== -1) {
32
+ const value = argv[index + 1];
33
+ return isHarness(value) ? value : "invalid";
34
+ }
35
+ if (isHarness(env.KHALA_MCP_HARNESS)) return env.KHALA_MCP_HARNESS;
36
+ return env.CLAUDE_CODE_SESSION_ID !== void 0 ? "claude" : "codex";
37
+ }
38
+ function resolveSessionId(harness, meta, env) {
39
+ const value = harness === "claude" ? env.CLAUDE_CODE_SESSION_ID : harness === "cursor" ? cursorSessionId(env[CURSOR_WORKSPACE_ENV]) : typeof meta?.threadId === "string" ? meta.threadId : env.CODEX_THREAD_ID;
40
+ return typeof value === "string" && value.match(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/)?.[0] === value ? value : null;
41
+ }
42
+
43
+ // src/watch.ts
44
+ var MARKER = "monitor.json";
45
+ var OBSERVATION = "monitor-cursor.json";
46
+ var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
47
+ var leaseFile = (files, nonce) => path.join(files.dir, `monitor-${nonce}.json`);
48
+ async function monitorArmed(files) {
49
+ const owner = await readJson(path.join(files.dir, MARKER));
50
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) return false;
51
+ if ((await readJson(leaseFile(files, owner.nonce)))?.nonce !== owner.nonce) return false;
52
+ try {
53
+ process.kill(owner.pid, 0);
54
+ return true;
55
+ } catch (error) {
56
+ return error.code === "EPERM";
57
+ }
58
+ }
59
+ function mentions(entry, name) {
60
+ if (!name) return false;
61
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
62
+ return new RegExp(`(?<![\\p{L}\\p{N}_])@?${escaped}(?![\\p{L}\\p{N}_])`, "iu").test(entry.body);
63
+ }
64
+ function channelLabel(name) {
65
+ return Array.from(name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("");
66
+ }
67
+ async function watchSession(files, io) {
68
+ const nonce = randomUUID();
69
+ let watcher;
70
+ let running;
71
+ let dirty = false;
72
+ let done = false;
73
+ let code = 0;
74
+ let finish;
75
+ const finished = new Promise((resolve) => {
76
+ finish = resolve;
77
+ });
78
+ const stop = (exit = 0) => {
79
+ done = true;
80
+ code = exit;
81
+ watcher?.close();
82
+ finish();
83
+ };
84
+ const abort = () => stop();
85
+ try {
86
+ const session = await readJson(files.session);
87
+ const status = await readStatus(files);
88
+ if (!session || typeof session.userId !== "string" || typeof session.roomId !== "string" || !["connected", "send_failed"].includes(status?.state ?? "")) return 0;
89
+ const initialDelivered = (await readCursor(files)).deliveredCount;
90
+ const evaluate = async () => {
91
+ const owner = await readJson(path.join(files.dir, MARKER));
92
+ const current = await readJson(files.session);
93
+ const status2 = await readStatus(files);
94
+ if (owner?.nonce !== nonce || current?.userId !== session.userId || current?.roomId !== session.roomId || !["connected", "send_failed"].includes(status2?.state ?? "")) {
95
+ stop();
96
+ return;
97
+ }
98
+ const mode = await readListeningMode(files);
99
+ const entries = await readEntries(files);
100
+ const observed = await readJson(path.join(files.dir, OBSERVATION));
101
+ const position = observed?.userId === session.userId && observed?.roomId === session.roomId && Number.isSafeInteger(observed.count) && observed.count >= 0 && observed.count <= entries.length && (observed.count === 0 || entries[observed.count - 1]?.eventId === observed.lastEventId) ? observed.count : 0;
102
+ const fresh = entries.slice(Math.max(initialDelivered, position));
103
+ const latestMode = await readListeningMode(files);
104
+ const latestSession = await readJson(files.session);
105
+ const latestStatus = await readStatus(files);
106
+ const latestOwner = await readJson(path.join(files.dir, MARKER));
107
+ if (latestOwner?.nonce !== nonce || latestSession?.userId !== session.userId || latestSession?.roomId !== session.roomId || !["connected", "send_failed"].includes(latestStatus?.state ?? "")) {
108
+ stop();
109
+ return;
110
+ }
111
+ for (const entry of fresh) {
112
+ if (done || mode === "async" || latestMode === "async" || entry.kind !== "message" || entry.sender === session.userId || entry.roomId !== session.roomId) continue;
113
+ const mention = mentions(entry, latestStatus?.displayName);
114
+ io.write(`khala: 1 new message in #${channelLabel(latestStatus?.channelName ?? "channel")} (${mention ? 1 : 0} mentions you)
115
+ `);
116
+ }
117
+ if (position !== entries.length && !done && (await readJson(path.join(files.dir, MARKER)))?.nonce === nonce) await writeJsonAtomic(path.join(files.dir, OBSERVATION), {
118
+ userId: session.userId,
119
+ roomId: session.roomId,
120
+ count: entries.length,
121
+ lastEventId: entries.at(-1)?.eventId ?? null
122
+ });
123
+ };
124
+ const notify = () => {
125
+ if (done) return;
126
+ dirty = true;
127
+ if (running) return;
128
+ running = (async () => {
129
+ do {
130
+ dirty = false;
131
+ await evaluate();
132
+ } while (dirty && !done);
133
+ })().catch(() => stop(1)).finally(() => {
134
+ running = void 0;
135
+ if (dirty && !done) notify();
136
+ });
137
+ };
138
+ await writeJsonAtomic(leaseFile(files, nonce), { nonce, pid: process.pid });
139
+ await writeJsonAtomic(path.join(files.dir, MARKER), { nonce, pid: process.pid });
140
+ watcher = watchDirectory(files.dir, (_event, filename) => {
141
+ if (filename === null || [MARKER, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json"].includes(String(filename))) notify();
142
+ });
143
+ watcher.on("error", () => stop(1));
144
+ io.signal.addEventListener("abort", abort, { once: true });
145
+ if (io.signal.aborted) stop();
146
+ else notify();
147
+ await finished;
148
+ await running;
149
+ return code;
150
+ } finally {
151
+ watcher?.close();
152
+ io.signal.removeEventListener("abort", abort);
153
+ await fs.unlink(leaseFile(files, nonce)).catch((error) => {
154
+ if (error.code !== "ENOENT") throw error;
155
+ });
156
+ }
157
+ }
158
+ async function run(argv) {
159
+ if (argv.length !== 0 && !(argv.length === 2 && argv[0] === "--session") && !(argv.length === 4 && argv[0] === "--harness" && argv[2] === "--session")) throw new Error("invalid_arguments");
160
+ const harness = resolveHarness(argv, process.env);
161
+ if (harness === "invalid") throw new Error("invalid_harness");
162
+ const index = argv.indexOf("--session");
163
+ const id = index === -1 ? resolveSessionId(harness, void 0, process.env) : argv[index + 1];
164
+ if (!id) throw new Error("session_unknown");
165
+ const files = sessionFiles(harness, id, process.env);
166
+ const controller = new AbortController();
167
+ const stop = () => controller.abort();
168
+ process.once("SIGTERM", stop);
169
+ process.once("SIGINT", stop);
170
+ try {
171
+ return await watchSession(files, { write: (line) => {
172
+ process.stdout.write(line);
173
+ }, signal: controller.signal });
174
+ } finally {
175
+ process.removeListener("SIGTERM", stop);
176
+ process.removeListener("SIGINT", stop);
177
+ }
178
+ }
179
+
180
+ export {
181
+ resolveHarness,
182
+ resolveSessionId,
183
+ monitorArmed,
184
+ mentions,
185
+ watchSession,
186
+ run
187
+ };
@@ -0,0 +1,19 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ mentions,
4
+ monitorArmed,
5
+ run,
6
+ watchSession
7
+ } from "./chunk-ENPBK5W2.mjs";
8
+ import "./chunk-N5VYKQMR.mjs";
9
+ import "./chunk-VUE2DDYY.mjs";
10
+ import "./chunk-XEYKJMZL.mjs";
11
+ import "./chunk-AUJEUKVM.mjs";
12
+ import "./chunk-SN3STAO2.mjs";
13
+ import "./chunk-ZKDDKUUA.mjs";
14
+ export {
15
+ run as default,
16
+ mentions,
17
+ monitorArmed,
18
+ watchSession
19
+ };
@@ -2,13 +2,16 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
2
2
  import "./chunk-5GLTKWJ5.mjs";
3
3
  import {
4
4
  CHANNEL_EVENT_TYPE,
5
- LISTENING_MODE_COMMAND_TYPE,
6
- LISTENING_MODE_MEMBER_KEY,
7
5
  encodeChannelEvent,
8
- memberListeningMode,
9
6
  memberRenameContent
10
- } from "./chunk-N6LMHR7P.mjs";
11
- import "./chunk-SR5BG7FL.mjs";
7
+ } from "./chunk-P5CP32X2.mjs";
8
+ import {
9
+ LISTENING_MODE_COMMAND_TYPE,
10
+ LISTENING_MODE_MEMBER_KEY,
11
+ memberListeningMode
12
+ } from "./chunk-AUJEUKVM.mjs";
13
+ import "./chunk-SN3STAO2.mjs";
14
+ import "./chunk-ZKDDKUUA.mjs";
12
15
 
13
16
  // src/matrix/session.ts
14
17
  import { createClient, ClientEvent, RoomEvent, MatrixEventEvent, SyncState, Direction, Method, EventType } from "matrix-js-sdk";
@@ -36,6 +39,7 @@ async function createAgentMatrixSession(creds, opts) {
36
39
  const log = (line) => opts?.log?.(line);
37
40
  const client = createClient({ baseUrl: creds.homeserver, userId: creds.userId, accessToken: creds.accessToken, deviceId: creds.deviceId });
38
41
  const handlers = /* @__PURE__ */ new Set();
42
+ const ended = /* @__PURE__ */ new Set();
39
43
  const modeHandlers = /* @__PURE__ */ new Set();
40
44
  const inviters = /* @__PURE__ */ new Map();
41
45
  const emitted = /* @__PURE__ */ new Set();
@@ -86,7 +90,45 @@ async function createAgentMatrixSession(creds, opts) {
86
90
  void client.decryptEventIfNeeded(event).then(() => deliver(event), () => log("live_decryption_failed"));
87
91
  };
88
92
  const decrypted = (event) => deliver(event);
89
- const syncLog = (state) => log(`sync_state=${state}`);
93
+ const syncLog = (state, _previous, data) => {
94
+ log(`sync_state=${state}`);
95
+ if (state === SyncState.Error && joinedRoom && matrixCode(data?.error) === "M_UNKNOWN_TOKEN") {
96
+ void checkEnded("unauthorized");
97
+ }
98
+ };
99
+ const matrixCode = (error) => typeof error === "object" && error !== null && "errcode" in error ? error.errcode : void 0;
100
+ let checkingEnded;
101
+ let pendingEndReason;
102
+ const checkEnded = (fallback) => {
103
+ if (stopped) return Promise.resolve();
104
+ if (fallback) pendingEndReason = fallback;
105
+ if (checkingEnded) return checkingEnded;
106
+ checkingEnded = (async () => {
107
+ let removed = false;
108
+ try {
109
+ removed = await opts?.checkRemoved?.() ?? false;
110
+ } catch {
111
+ }
112
+ if (removed) end("removed");
113
+ else if (pendingEndReason) end(pendingEndReason);
114
+ })().finally(() => {
115
+ checkingEnded = void 0;
116
+ pendingEndReason = void 0;
117
+ });
118
+ return checkingEnded;
119
+ };
120
+ const guarded = async (roomId, operation) => {
121
+ if (stopped) throw new Error("session_stopped");
122
+ try {
123
+ return await operation();
124
+ } catch (error) {
125
+ const code = matrixCode(error);
126
+ if (roomId === joinedRoom && (code === "M_FORBIDDEN" || code === "M_UNKNOWN_TOKEN")) {
127
+ await checkEnded(code === "M_UNKNOWN_TOKEN" ? "unauthorized" : void 0);
128
+ }
129
+ throw error;
130
+ }
131
+ };
90
132
  const wait = (subscribe, ready, timeoutMs, error) => {
91
133
  if (stopped) return Promise.reject(new Error("session_stopped"));
92
134
  if (ready()) return Promise.resolve();
@@ -117,11 +159,29 @@ async function createAgentMatrixSession(creds, opts) {
117
159
  client.removeListener(RoomEvent.Timeline, timeline);
118
160
  client.removeListener(MatrixEventEvent.Decrypted, decrypted);
119
161
  client.removeListener(ClientEvent.Sync, syncLog);
162
+ client.removeListener(RoomEvent.MyMembership, membershipEnded);
120
163
  for (const cancel of [...cancellations]) cancel();
121
164
  liveEvents.clear();
122
165
  emitted.clear();
123
166
  handlers.clear();
124
167
  modeHandlers.clear();
168
+ ended.clear();
169
+ };
170
+ const membershipEnded = (room, membership) => {
171
+ if (stopped || room.roomId !== joinedRoom || !["leave", "ban"].includes(membership)) return;
172
+ end("removed");
173
+ };
174
+ const end = (reason) => {
175
+ if (stopped) return;
176
+ const listeners = [...ended];
177
+ void stop();
178
+ for (const handler of listeners) {
179
+ try {
180
+ handler(reason);
181
+ } catch {
182
+ log("ended_handler_error");
183
+ }
184
+ }
125
185
  };
126
186
  try {
127
187
  await client.initRustCrypto({ useIndexedDB: false });
@@ -167,6 +227,12 @@ async function createAgentMatrixSession(creds, opts) {
167
227
  }, ready, timeoutMs, error);
168
228
  return {
169
229
  userId: creds.userId,
230
+ onEnded(handler) {
231
+ if (!stopped) ended.add(handler);
232
+ return () => {
233
+ ended.delete(handler);
234
+ };
235
+ },
170
236
  listeningMode: (roomId) => memberListeningMode(client.getRoom(roomId)?.currentState.getStateEvents("m.room.member", creds.userId)?.getContent()),
171
237
  inviter(roomId) {
172
238
  return inviters.get(roomId);
@@ -178,6 +244,7 @@ async function createAgentMatrixSession(creds, opts) {
178
244
  };
179
245
  },
180
246
  async publishListeningMode(roomId, mode, signal) {
247
+ if (stopped) throw new Error("session_stopped");
181
248
  const content = client.getRoom(roomId)?.currentState.getStateEvents("m.room.member", creds.userId)?.getContent();
182
249
  if (!content) throw new Error("join_state_unavailable");
183
250
  const next = { ...content, membership: "join", [LISTENING_MODE_MEMBER_KEY]: mode };
@@ -217,18 +284,20 @@ async function createAgentMatrixSession(creds, opts) {
217
284
  await client.sendStateEvent(roomId, EventType.RoomMember, content, creds.userId);
218
285
  }
219
286
  joinTimes.set(roomId, membership === "join" ? rejoinedAt : ownJoin.getTs());
287
+ if (!joinedRoom) client.on(RoomEvent.MyMembership, membershipEnded);
220
288
  joinedRoom = roomId;
221
289
  for (const event of liveEvents) {
222
290
  if (event.getRoomId() === roomId) deliver(event);
223
291
  }
224
292
  },
225
293
  async history(roomId, limit, before) {
294
+ if (stopped) throw new Error("session_stopped");
226
295
  let token = null;
227
296
  if (before !== void 0) {
228
- const context = await client.http.authedRequest(Method.Get, `/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(before)}`, { limit: "0" });
297
+ const context = await guarded(roomId, () => client.http.authedRequest(Method.Get, `/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(before)}`, { limit: "0" }));
229
298
  token = context.start;
230
299
  }
231
- const res = await client.createMessagesRequest(roomId, token, limit, Direction.Backward);
300
+ const res = await guarded(roomId, () => client.createMessagesRequest(roomId, token, limit, Direction.Backward));
232
301
  const messages = [];
233
302
  let undecryptable = 0;
234
303
  for (const raw of res.chunk) {
@@ -250,11 +319,13 @@ async function createAgentMatrixSession(creds, opts) {
250
319
  return typeof res.end === "string" && oldest ? { messages, nextBefore: oldest.eventId } : { messages };
251
320
  },
252
321
  async send(roomId, text) {
253
- const res = await client.sendTextMessage(roomId, text);
322
+ if (stopped) throw new Error("session_stopped");
323
+ const res = await guarded(roomId, () => client.sendTextMessage(roomId, text));
254
324
  return { eventId: res.event_id };
255
325
  },
256
326
  async sendChannelEvent(roomId, content, txnId) {
257
- const res = await client.sendEvent(roomId, CHANNEL_EVENT_TYPE, content, txnId);
327
+ if (stopped) throw new Error("session_stopped");
328
+ const res = await guarded(roomId, () => client.sendEvent(roomId, CHANNEL_EVENT_TYPE, content, txnId));
258
329
  return { eventId: res.event_id };
259
330
  },
260
331
  roomName(roomId) {