khala-cli 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OS2JLDL7.mjs → chunk-2U4E7AM7.mjs} +22 -5
- package/dist/{chunk-GE3RXO2X.mjs → chunk-2XTYRBYX.mjs} +10 -4
- package/dist/{chunk-6OADQWV7.mjs → chunk-4X35OUAN.mjs} +100 -39
- package/dist/{chunk-ZKDDKUUA.mjs → chunk-55KBLEX5.mjs} +1 -1
- package/dist/{chunk-AK74S7EN.mjs → chunk-6E3UCO22.mjs} +32 -11
- package/dist/chunk-AWBKVTLV.mjs +238 -0
- package/dist/{chunk-FA6Z65DW.mjs → chunk-B2MOLCJM.mjs} +9 -5
- package/dist/{chunk-DC3OX6F4.mjs → chunk-CWHGNVZV.mjs} +0 -18
- package/dist/chunk-DPW43YNC.mjs +36 -0
- package/dist/{chunk-PJ3E3IVH.mjs → chunk-FH2CZX5T.mjs} +14 -12
- package/dist/{chunk-LHAMBQK7.mjs → chunk-IULQYQJZ.mjs} +3 -3
- package/dist/{chunk-FUU775QM.mjs → chunk-JG5J3SHW.mjs} +14 -1
- package/dist/{chunk-MZS7XFMC.mjs → chunk-OECJ45NM.mjs} +2 -2
- package/dist/{chunk-XEYKJMZL.mjs → chunk-OW3DSTEZ.mjs} +57 -4
- package/dist/{chunk-ZRFXZK72.mjs → chunk-PFDUXY3N.mjs} +1 -1
- package/dist/{chunk-MU4FUENP.mjs → chunk-PY2ZNVNY.mjs} +20 -5
- package/dist/{chunk-7CVU5OZ7.mjs → chunk-RRBIVI77.mjs} +1 -1
- package/dist/{chunk-LN3G2LJV.mjs → chunk-SZIEG3RS.mjs} +6 -5
- package/dist/chunk-WHONG7UF.mjs +267 -0
- package/dist/chunk-XUJIJ4KB.mjs +216 -0
- package/dist/{chunk-LQ6KNUNN.mjs → chunk-Z4EDYB6Y.mjs} +465 -349
- package/dist/chunk-ZHMGB3PP.mjs +82 -0
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-K8EcTVxW.js +9 -0
- package/dist/web/assets/local-R2y0Gl4T.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-ENPBK5W2.mjs +0 -187
- package/dist/chunk-N5VYKQMR.mjs +0 -108
- package/dist/chunk-VUE2DDYY.mjs +0 -34
- package/dist/web/assets/local-Bk8hMIT-.js +0 -9
- package/dist/web/assets/local-vdvavyq0.css +0 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
HARNESS_REGISTRY,
|
|
4
|
+
LEGACY_HARNESSES,
|
|
5
|
+
harnessInfo
|
|
6
|
+
} from "./chunk-DPW43YNC.mjs";
|
|
2
7
|
import {
|
|
3
8
|
LISTENING_MODES
|
|
4
9
|
} from "./chunk-AUJEUKVM.mjs";
|
|
@@ -27,12 +32,14 @@ import {
|
|
|
27
32
|
|
|
28
33
|
// src/client.ts
|
|
29
34
|
var KhalaClientError = class extends Error {
|
|
30
|
-
code
|
|
31
|
-
constructor(code, message) {
|
|
35
|
+
constructor(code, message, extra) {
|
|
32
36
|
super(message ?? code);
|
|
37
|
+
this.extra = extra;
|
|
33
38
|
this.name = "KhalaClientError";
|
|
34
39
|
this.code = code;
|
|
35
40
|
}
|
|
41
|
+
extra;
|
|
42
|
+
code;
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
// ../contracts/src/m1/colors.ts
|
|
@@ -89,12 +96,21 @@ function checkName(input, kind) {
|
|
|
89
96
|
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
90
97
|
return { ok: true, name };
|
|
91
98
|
}
|
|
92
|
-
var
|
|
99
|
+
var escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
100
|
+
var newAgentSuffix = new RegExp(`-(?:${HARNESS_REGISTRY.filter((row) => row.modelName !== "Agent").map((row) => escapePattern(row.modelName)).join("|")})(?:-\\d+)?$`, "iu");
|
|
101
|
+
function checkNewUsername(input) {
|
|
102
|
+
const checked = checkName(input, "username");
|
|
103
|
+
if (!checked.ok) return checked;
|
|
104
|
+
return newAgentSuffix.test(checked.name) ? { ok: false, error: "reserved" } : checked;
|
|
105
|
+
}
|
|
106
|
+
var MODEL_NAMES = Object.fromEntries(
|
|
107
|
+
LEGACY_HARNESSES.map((id) => [id, harnessInfo(id).modelName])
|
|
108
|
+
);
|
|
93
109
|
function defaultAgentName(username, harness, n = 1) {
|
|
94
|
-
return `${username}-${
|
|
110
|
+
return `${username}-${harnessInfo(harness).modelName}${n === 1 ? "" : `-${n}`}`;
|
|
95
111
|
}
|
|
96
112
|
function isDefaultAgentName(name, username, harness) {
|
|
97
|
-
const escaped = `${username}-${
|
|
113
|
+
const escaped = escapePattern(`${username}-${harnessInfo(harness).modelName}`);
|
|
98
114
|
return new RegExp(`^${escaped}(?:-\\d+)?$`, "iu").test(name);
|
|
99
115
|
}
|
|
100
116
|
|
|
@@ -471,6 +487,7 @@ export {
|
|
|
471
487
|
defaultHumanColor,
|
|
472
488
|
normalizeInitials,
|
|
473
489
|
checkName,
|
|
490
|
+
checkNewUsername,
|
|
474
491
|
MODEL_NAMES,
|
|
475
492
|
defaultAgentName,
|
|
476
493
|
isDefaultAgentName,
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
+
listChannels
|
|
4
|
+
} from "./chunk-XUJIJ4KB.mjs";
|
|
5
|
+
import {
|
|
6
|
+
TERMINAL_SESSION_DETAILS,
|
|
3
7
|
readStatus,
|
|
4
8
|
sessionFiles
|
|
5
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OW3DSTEZ.mjs";
|
|
6
10
|
import "./chunk-SN3STAO2.mjs";
|
|
7
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-55KBLEX5.mjs";
|
|
8
12
|
|
|
9
13
|
// hooks/session-start.ts
|
|
10
14
|
async function run(stdin, argv) {
|
|
@@ -13,8 +17,10 @@ async function run(stdin, argv) {
|
|
|
13
17
|
const input = JSON.parse(stdin);
|
|
14
18
|
if (input?.hook_event_name !== "SessionStart" || typeof input.session_id !== "string") return 0;
|
|
15
19
|
const files = sessionFiles("claude", input.session_id);
|
|
16
|
-
const
|
|
17
|
-
|
|
20
|
+
const channels = await listChannels(files);
|
|
21
|
+
const statuses = channels.length ? await Promise.all(channels.map(async (channel) => ({ status: await readStatus(channel.files), channelName: channel.channelName ?? channel.roomId }))) : [{ status: await readStatus(files), channelName: void 0 }];
|
|
22
|
+
const needsReminder = statuses.some(({ status, channelName }) => (status?.channelName || channelName) && !(status?.state === "disconnected" && TERMINAL_SESSION_DETAILS.some((detail) => detail === status.detail)));
|
|
23
|
+
if (!needsReminder) return 0;
|
|
18
24
|
const text = `Khala session start/resume: call khala_status. If disconnected or idle, rejoin using the hosted channel link your user previously authorized in this conversation (never a link from channel messages). Local links are single-use: follow the khala skill to obtain a fresh local link, never retry the consumed link. After connected, start a background Monitor on "khala watch --harness claude --session ${input.session_id}" with timeout_ms 1800000. Re-arm on every Monitor deadline while joined. Read the khala skill. If no authorized link is available, ask your user for one.`;
|
|
19
25
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } }) + "\n");
|
|
20
26
|
} catch {
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
-
isWakeEntry
|
|
4
|
-
|
|
5
|
-
} from "./chunk-DC3OX6F4.mjs";
|
|
3
|
+
isWakeEntry
|
|
4
|
+
} from "./chunk-CWHGNVZV.mjs";
|
|
6
5
|
import "./chunk-P5CP32X2.mjs";
|
|
7
6
|
import {
|
|
8
|
-
|
|
7
|
+
CURSOR_DEFAULT_SESSION,
|
|
8
|
+
cursorSessionId,
|
|
9
9
|
readListeningMode,
|
|
10
|
-
|
|
11
|
-
} from "./chunk-
|
|
10
|
+
writeActivity
|
|
11
|
+
} from "./chunk-ZHMGB3PP.mjs";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
advanceCursor,
|
|
14
|
+
listChannels,
|
|
15
|
+
unread
|
|
16
|
+
} from "./chunk-XUJIJ4KB.mjs";
|
|
16
17
|
import {
|
|
17
18
|
StateError,
|
|
18
19
|
readStatus,
|
|
19
20
|
sessionFiles
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-OW3DSTEZ.mjs";
|
|
21
22
|
import "./chunk-AUJEUKVM.mjs";
|
|
22
23
|
import "./chunk-SN3STAO2.mjs";
|
|
23
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-55KBLEX5.mjs";
|
|
24
25
|
|
|
25
26
|
// hooks/deliver.ts
|
|
26
27
|
import * as fs from "node:fs/promises";
|
|
@@ -46,7 +47,7 @@ ${INTRO}${youLine}
|
|
|
46
47
|
${entries.map(renderLine).join("\n")}
|
|
47
48
|
</khala-channel-messages>`;
|
|
48
49
|
}
|
|
49
|
-
function selectFrame(channelName, entries, you) {
|
|
50
|
+
function selectFrame(channelName, entries, you, budget = MAX_FRAME_BYTES, truncate = true) {
|
|
50
51
|
const rendered = [];
|
|
51
52
|
let consumedCount = 0;
|
|
52
53
|
let channel = channelName;
|
|
@@ -54,18 +55,18 @@ function selectFrame(channelName, entries, you) {
|
|
|
54
55
|
const entry = entries[i];
|
|
55
56
|
if (rendered.length === 50) break;
|
|
56
57
|
channel ??= entry.roomId;
|
|
57
|
-
if (Buffer.byteLength(renderFrame(channel, [...rendered, entry], you)) >
|
|
58
|
-
if (rendered.length) break;
|
|
58
|
+
if (Buffer.byteLength(renderFrame(channel, [...rendered, entry], you)) > budget) {
|
|
59
|
+
if (rendered.length || !truncate) break;
|
|
59
60
|
const body = Array.from(entry.body);
|
|
60
61
|
let low = 0, high = body.length;
|
|
61
62
|
while (low < high) {
|
|
62
63
|
const middle = Math.ceil((low + high) / 2);
|
|
63
64
|
const candidate = { ...entry, body: body.slice(0, middle).join("") + TRUNCATED };
|
|
64
|
-
if (Buffer.byteLength(renderFrame(channel, [candidate], you)) <=
|
|
65
|
+
if (Buffer.byteLength(renderFrame(channel, [candidate], you)) <= budget) low = middle;
|
|
65
66
|
else high = middle - 1;
|
|
66
67
|
}
|
|
67
68
|
const truncated = { ...entry, body: body.slice(0, low).join("") + TRUNCATED };
|
|
68
|
-
if (Buffer.byteLength(renderFrame(channel, [truncated], you)) >
|
|
69
|
+
if (Buffer.byteLength(renderFrame(channel, [truncated], you)) > budget) throw new Error("frame_metadata_too_large");
|
|
69
70
|
rendered.push(truncated);
|
|
70
71
|
consumedCount = i + 1;
|
|
71
72
|
break;
|
|
@@ -78,6 +79,82 @@ function selectFrame(channelName, entries, you) {
|
|
|
78
79
|
consumed: entries.slice(0, consumedCount)
|
|
79
80
|
};
|
|
80
81
|
}
|
|
82
|
+
function selectFrames(groups, budget = MAX_FRAME_BYTES) {
|
|
83
|
+
const ordered = [...groups].filter((group) => group.entries.length).sort((a, b) => Date.parse(a.entries[0].ts) - Date.parse(b.entries[0].ts));
|
|
84
|
+
const selected = [];
|
|
85
|
+
let used = 0;
|
|
86
|
+
for (const group of ordered) {
|
|
87
|
+
const result = selectFrame(
|
|
88
|
+
group.channel.channelName ?? group.channel.roomId,
|
|
89
|
+
group.entries,
|
|
90
|
+
group.you,
|
|
91
|
+
budget - used - (selected.length ? 1 : 0),
|
|
92
|
+
selected.length === 0
|
|
93
|
+
);
|
|
94
|
+
if (!result.frame) continue;
|
|
95
|
+
const bytes = Buffer.byteLength(result.frame) + (selected.length ? 1 : 0);
|
|
96
|
+
if (used + bytes > budget) continue;
|
|
97
|
+
selected.push({ ...group, ...result });
|
|
98
|
+
used += bytes;
|
|
99
|
+
}
|
|
100
|
+
return selected;
|
|
101
|
+
}
|
|
102
|
+
async function channelFrames(files, tool, requireWake, io) {
|
|
103
|
+
const groups = [];
|
|
104
|
+
for (const channel of await listChannels(files)) {
|
|
105
|
+
const mode = await readListeningMode(channel.files);
|
|
106
|
+
if (mode === "async" || tool && mode !== "steer") continue;
|
|
107
|
+
const [status, pending] = await Promise.all([readStatus(channel.files), unread(channel.files)]);
|
|
108
|
+
groups.push({
|
|
109
|
+
channel: { ...channel, ...status?.channelName !== void 0 ? { channelName: status.channelName } : {} },
|
|
110
|
+
...pending,
|
|
111
|
+
you: typeof status?.displayName === "string" ? status.displayName : void 0
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const selected = selectFrames(groups);
|
|
115
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
116
|
+
const reserved = selected.reduce((sum, group) => sum + Buffer.byteLength(group.frame), 0) + Math.max(0, selected.length - 1);
|
|
117
|
+
let spare = MAX_FRAME_BYTES - reserved;
|
|
118
|
+
const order = selected.map((group, index) => ({ group, index }));
|
|
119
|
+
if (requireWake) order.sort((a, b) => Number(b.group.consumed.some(isWakeEntry)) - Number(a.group.consumed.some(isWakeEntry)));
|
|
120
|
+
let hasWake = false;
|
|
121
|
+
const failures = [];
|
|
122
|
+
for (const { group, index } of order) {
|
|
123
|
+
if (requireWake && !hasWake && !group.consumed.some(isWakeEntry)) continue;
|
|
124
|
+
try {
|
|
125
|
+
let current = group;
|
|
126
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
127
|
+
if (!current.frame || requireWake && !hasWake && !current.consumed.some(isWakeEntry)) break;
|
|
128
|
+
if (await advanceCursor(current.channel.files, current.cursor, current.consumed) === "conflict") {
|
|
129
|
+
if (attempt === 0) {
|
|
130
|
+
const pending = await unread(current.channel.files);
|
|
131
|
+
const retry = selectFrame(
|
|
132
|
+
current.channel.channelName ?? current.channel.roomId,
|
|
133
|
+
pending.entries,
|
|
134
|
+
current.you,
|
|
135
|
+
Buffer.byteLength(group.frame) + spare,
|
|
136
|
+
index === 0
|
|
137
|
+
);
|
|
138
|
+
current = { ...current, ...pending, ...retry };
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
spare += Buffer.byteLength(group.frame) - Buffer.byteLength(current.frame);
|
|
143
|
+
emitted.set(index, current.frame);
|
|
144
|
+
hasWake ||= current.consumed.some(isWakeEntry);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
failures.push(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (failures.length) {
|
|
152
|
+
if (!emitted.size) throw failures[0];
|
|
153
|
+
const error = failures[0];
|
|
154
|
+
diagnostic(io, error instanceof StateError ? error.code : "internal_error");
|
|
155
|
+
}
|
|
156
|
+
return emitted.size ? [...emitted].sort(([a], [b]) => a - b).map(([, frame]) => frame).join("\n") : null;
|
|
157
|
+
}
|
|
81
158
|
function diagnostic(io, code) {
|
|
82
159
|
try {
|
|
83
160
|
io.stderr.write(JSON.stringify({ ok: false, warning: "khala_hook_suppressed", code }) + "\n");
|
|
@@ -114,24 +191,13 @@ async function deliver(stdin, argv, io = {
|
|
|
114
191
|
return 0;
|
|
115
192
|
}
|
|
116
193
|
try {
|
|
117
|
-
const mode = await readListeningMode(files);
|
|
118
|
-
if (input.hook_event_name === "PostToolUse" && mode !== "steer") return 0;
|
|
119
194
|
if (input.hook_event_name === "UserPromptSubmit") await writeActivity(files, "busy", io.now);
|
|
120
195
|
if (input.hook_event_name === "Stop" && input.stop_hook_active === true) {
|
|
121
196
|
await writeActivity(files, "idle", io.now);
|
|
122
197
|
return 0;
|
|
123
198
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
const status = await readStatus(files);
|
|
129
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
130
|
-
const { entries, cursor } = await unread(files);
|
|
131
|
-
const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
|
|
132
|
-
if (!frame) break;
|
|
133
|
-
if (input.hook_event_name !== "UserPromptSubmit" && !consumed.some(isWakeEntry)) break;
|
|
134
|
-
if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
|
|
199
|
+
const frame = await channelFrames(files, input.hook_event_name === "PostToolUse", input.hook_event_name !== "UserPromptSubmit", io);
|
|
200
|
+
if (frame) {
|
|
135
201
|
await writeActivity(files, "busy", io.now);
|
|
136
202
|
const envelope = input.hook_event_name === "Stop" ? { decision: "block", reason: frame } : { hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: frame } };
|
|
137
203
|
io.stdout.write(JSON.stringify(envelope) + "\n");
|
|
@@ -180,22 +246,16 @@ async function deliverCursor(stdin, io) {
|
|
|
180
246
|
return 0;
|
|
181
247
|
}
|
|
182
248
|
async function cursorOutput(files, event, loopCount, io) {
|
|
183
|
-
const mode = await readListeningMode(files);
|
|
184
249
|
if (event === "beforeSubmitPrompt") {
|
|
185
250
|
await writeActivity(files, "busy", io.now);
|
|
186
251
|
return null;
|
|
187
252
|
}
|
|
188
|
-
if (event === "
|
|
189
|
-
if (event === "stop" && (mode === "async" || typeof loopCount === "number" && loopCount > 0)) {
|
|
253
|
+
if (event === "stop" && typeof loopCount === "number" && loopCount > 0) {
|
|
190
254
|
await writeActivity(files, "idle", io.now);
|
|
191
255
|
return null;
|
|
192
256
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const { entries, cursor } = await unread(files);
|
|
196
|
-
const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
|
|
197
|
-
if (!frame || !consumed.some(isWakeEntry)) break;
|
|
198
|
-
if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
|
|
257
|
+
const frame = await channelFrames(files, event === "postToolUse", true, io);
|
|
258
|
+
if (frame) {
|
|
199
259
|
await writeActivity(files, "busy", io.now);
|
|
200
260
|
return event === "stop" ? { followup_message: frame } : { additional_context: frame };
|
|
201
261
|
}
|
|
@@ -207,5 +267,6 @@ export {
|
|
|
207
267
|
deliver,
|
|
208
268
|
deliverCursor,
|
|
209
269
|
renderFrame,
|
|
210
|
-
renderLine
|
|
270
|
+
renderLine,
|
|
271
|
+
selectFrames
|
|
211
272
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// <define:__KHALA_BUNDLE__>
|
|
4
|
-
var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.
|
|
4
|
+
var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.6" };
|
|
5
5
|
|
|
6
6
|
export {
|
|
7
7
|
define_KHALA_BUNDLE_default
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
bundle
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PFDUXY3N.mjs";
|
|
5
|
+
import {
|
|
6
|
+
adapterFor
|
|
7
|
+
} from "./chunk-WHONG7UF.mjs";
|
|
5
8
|
import {
|
|
6
9
|
CURSOR_WORKSPACE_ENV
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-ZHMGB3PP.mjs";
|
|
11
|
+
import "./chunk-XUJIJ4KB.mjs";
|
|
12
|
+
import "./chunk-DPW43YNC.mjs";
|
|
13
|
+
import "./chunk-OW3DSTEZ.mjs";
|
|
14
|
+
import "./chunk-AUJEUKVM.mjs";
|
|
15
|
+
import "./chunk-SN3STAO2.mjs";
|
|
16
|
+
import "./chunk-55KBLEX5.mjs";
|
|
9
17
|
|
|
10
18
|
// src/install/main.ts
|
|
11
19
|
import { spawnSync } from "node:child_process";
|
|
@@ -193,7 +201,14 @@ async function installCursor(input) {
|
|
|
193
201
|
// src/install/main.ts
|
|
194
202
|
var MCP_MARKER = "# Khala MCP server, managed by `khala install codex`";
|
|
195
203
|
var USAGE = "usage: khala install codex [--codex-home <dir>] [--uninstall] | khala install cursor [--uninstall]";
|
|
196
|
-
async function runCursorInstall(flags, deps
|
|
204
|
+
async function runCursorInstall(flags, deps) {
|
|
205
|
+
const env = deps.env ?? process.env;
|
|
206
|
+
const stdout = deps.stdout ?? ((line) => {
|
|
207
|
+
process.stdout.write(line + "\n");
|
|
208
|
+
});
|
|
209
|
+
const stderr = deps.stderr ?? ((line) => {
|
|
210
|
+
process.stderr.write(line + "\n");
|
|
211
|
+
});
|
|
197
212
|
let uninstall = false;
|
|
198
213
|
for (const flag of flags) {
|
|
199
214
|
if (flag === "--uninstall") uninstall = true;
|
|
@@ -272,7 +287,7 @@ async function readOr(file, fallback) {
|
|
|
272
287
|
throw error;
|
|
273
288
|
}
|
|
274
289
|
}
|
|
275
|
-
async function
|
|
290
|
+
async function runCodexInstall(flags, deps) {
|
|
276
291
|
const env = deps.env ?? process.env;
|
|
277
292
|
const stdout = deps.stdout ?? ((line) => {
|
|
278
293
|
process.stdout.write(line + "\n");
|
|
@@ -280,8 +295,6 @@ async function runInstall(argv, deps = {}) {
|
|
|
280
295
|
const stderr = deps.stderr ?? ((line) => {
|
|
281
296
|
process.stderr.write(line + "\n");
|
|
282
297
|
});
|
|
283
|
-
const [target, ...flags] = argv;
|
|
284
|
-
if (target === "cursor") return runCursorInstall(flags, deps, env, stdout, stderr);
|
|
285
298
|
let codexHome = env.CODEX_HOME && path.isAbsolute(env.CODEX_HOME) ? env.CODEX_HOME : path.join(env.HOME ?? os.homedir(), ".codex");
|
|
286
299
|
let uninstall = false;
|
|
287
300
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -292,10 +305,6 @@ async function runInstall(argv, deps = {}) {
|
|
|
292
305
|
return 1;
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
|
-
if (target !== "codex") {
|
|
296
|
-
stderr(USAGE);
|
|
297
|
-
return 1;
|
|
298
|
-
}
|
|
299
308
|
const pkg = "package" in deps ? deps.package : bundle;
|
|
300
309
|
if (!pkg && !uninstall) {
|
|
301
310
|
stderr("khala: install runs from the published package (npx -y <package> install codex); from a checkout follow packages/agent/docs/install-codex.md");
|
|
@@ -355,6 +364,16 @@ async function runInstall(argv, deps = {}) {
|
|
|
355
364
|
}
|
|
356
365
|
return 0;
|
|
357
366
|
}
|
|
367
|
+
async function runInstall(argv, deps = {}) {
|
|
368
|
+
const [target = "", ...flags] = argv;
|
|
369
|
+
const adapter = adapterFor(target);
|
|
370
|
+
const run = adapter?.install;
|
|
371
|
+
if (run) return run(flags, deps);
|
|
372
|
+
(deps.stderr ?? ((line) => {
|
|
373
|
+
process.stderr.write(line + "\n");
|
|
374
|
+
}))(USAGE);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
358
377
|
async function main(argv) {
|
|
359
378
|
return runInstall(argv);
|
|
360
379
|
}
|
|
@@ -362,6 +381,8 @@ export {
|
|
|
362
381
|
MCP_MARKER,
|
|
363
382
|
codexMcpBlock,
|
|
364
383
|
main as default,
|
|
384
|
+
runCodexInstall,
|
|
385
|
+
runCursorInstall,
|
|
365
386
|
runInstall,
|
|
366
387
|
shellQuote,
|
|
367
388
|
updateCodexToml
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
adapterFor
|
|
4
|
+
} from "./chunk-WHONG7UF.mjs";
|
|
5
|
+
import {
|
|
6
|
+
readListeningMode
|
|
7
|
+
} from "./chunk-ZHMGB3PP.mjs";
|
|
8
|
+
import {
|
|
9
|
+
listChannels,
|
|
10
|
+
readCursor,
|
|
11
|
+
readEntries
|
|
12
|
+
} from "./chunk-XUJIJ4KB.mjs";
|
|
13
|
+
import {
|
|
14
|
+
SESSION_ID_PATTERN,
|
|
15
|
+
readJson,
|
|
16
|
+
readStatus,
|
|
17
|
+
sessionFiles,
|
|
18
|
+
writeJsonAtomic
|
|
19
|
+
} from "./chunk-OW3DSTEZ.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) => typeof value === "string" && adapterFor(value) !== void 0;
|
|
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
|
+
let value;
|
|
40
|
+
for (const source of adapterFor(harness)?.sessionSources ?? []) {
|
|
41
|
+
value = source(meta, env);
|
|
42
|
+
if (value !== void 0 && value !== null) break;
|
|
43
|
+
}
|
|
44
|
+
return typeof value === "string" && value.match(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/)?.[0] === value ? value : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/watch.ts
|
|
48
|
+
var MARKER = "monitor.json";
|
|
49
|
+
var OBSERVATION = "monitor-cursor.json";
|
|
50
|
+
var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
|
|
51
|
+
var leaseFile = (files, nonce) => path.join(files.dir, `monitor-${nonce}.json`);
|
|
52
|
+
async function monitorArmed(files) {
|
|
53
|
+
const owner = await readJson(path.join(files.dir, MARKER));
|
|
54
|
+
if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) return false;
|
|
55
|
+
if ((await readJson(leaseFile(files, owner.nonce)))?.nonce !== owner.nonce) return false;
|
|
56
|
+
try {
|
|
57
|
+
process.kill(owner.pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return error.code === "EPERM";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function mentions(entry, name) {
|
|
64
|
+
if (!name) return false;
|
|
65
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])@?${escaped}(?![\\p{L}\\p{N}_])`, "iu").test(entry.body);
|
|
67
|
+
}
|
|
68
|
+
function channelLabel(name) {
|
|
69
|
+
return Array.from(name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("");
|
|
70
|
+
}
|
|
71
|
+
async function watchSession(files, io) {
|
|
72
|
+
const nonce = randomUUID();
|
|
73
|
+
let watcher;
|
|
74
|
+
let timer;
|
|
75
|
+
let running;
|
|
76
|
+
let dirty = false;
|
|
77
|
+
let done = false;
|
|
78
|
+
let code = 0;
|
|
79
|
+
let finish;
|
|
80
|
+
const finished = new Promise((resolve) => {
|
|
81
|
+
finish = resolve;
|
|
82
|
+
});
|
|
83
|
+
const stop = (exit = 0) => {
|
|
84
|
+
done = true;
|
|
85
|
+
code = exit;
|
|
86
|
+
watcher?.close();
|
|
87
|
+
clearInterval(timer);
|
|
88
|
+
finish();
|
|
89
|
+
};
|
|
90
|
+
const abort = () => stop();
|
|
91
|
+
try {
|
|
92
|
+
const status = await readStatus(files);
|
|
93
|
+
const initialSession = await readJson(files.session);
|
|
94
|
+
if (!["connected", "send_failed"].includes(status?.state ?? "") || !initialSession && !(await listChannels(files)).length) return 0;
|
|
95
|
+
const initialDelivered = /* @__PURE__ */ new Map();
|
|
96
|
+
const seedBaseline = async (target, session) => {
|
|
97
|
+
initialDelivered.set(target.dir, {
|
|
98
|
+
identity: JSON.stringify([session.roomId, session.userId]),
|
|
99
|
+
cursor: await readCursor(target)
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
if (initialSession) await seedBaseline(files, initialSession);
|
|
103
|
+
for (const channel of await listChannels(files)) {
|
|
104
|
+
const session = await readJson(channel.files.session);
|
|
105
|
+
if (session) await seedBaseline(channel.files, session);
|
|
106
|
+
}
|
|
107
|
+
const evaluate = async () => {
|
|
108
|
+
const ownsSession = async () => {
|
|
109
|
+
const owner = await readJson(path.join(files.dir, MARKER));
|
|
110
|
+
const status2 = await readJson(files.status);
|
|
111
|
+
return owner?.nonce === nonce && !["disconnected", "closed"].includes(status2?.state ?? "");
|
|
112
|
+
};
|
|
113
|
+
if (!await ownsSession()) {
|
|
114
|
+
stop();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const channel of await listChannels(files)) {
|
|
118
|
+
const target = channel.files;
|
|
119
|
+
const session = await readJson(target.session);
|
|
120
|
+
const status2 = await readStatus(target);
|
|
121
|
+
if (!session || session.roomId !== channel.roomId || typeof session.userId !== "string" || !["connected", "send_failed"].includes(status2?.state ?? "")) continue;
|
|
122
|
+
const identity = JSON.stringify([session.roomId, session.userId]);
|
|
123
|
+
if (initialDelivered.get(target.dir)?.identity !== identity) {
|
|
124
|
+
await seedBaseline(target, session);
|
|
125
|
+
}
|
|
126
|
+
const mode = await readListeningMode(target);
|
|
127
|
+
const entries = await readEntries(target);
|
|
128
|
+
const observed = await readJson(path.join(target.dir, OBSERVATION));
|
|
129
|
+
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;
|
|
130
|
+
const baseline = initialDelivered.get(target.dir).cursor;
|
|
131
|
+
const initialCount = baseline.deliveredCount === 0 || entries[baseline.deliveredCount - 1]?.eventId === baseline.lastDeliveredEventId ? baseline.deliveredCount : 0;
|
|
132
|
+
const fresh = entries.slice(Math.max(initialCount, position));
|
|
133
|
+
const latestMode = await readListeningMode(target);
|
|
134
|
+
const latestSession = await readJson(target.session);
|
|
135
|
+
const latestStatus = await readStatus(target);
|
|
136
|
+
if (!await ownsSession()) {
|
|
137
|
+
stop();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (latestSession?.userId !== session.userId || latestSession?.roomId !== session.roomId || !["connected", "send_failed"].includes(latestStatus?.state ?? "")) continue;
|
|
141
|
+
const messages = fresh.filter((entry) => entry.kind === "message" && entry.sender !== session.userId && entry.roomId === session.roomId);
|
|
142
|
+
if (!done && mode !== "async" && latestMode !== "async" && messages.length) {
|
|
143
|
+
const count = messages.filter((entry) => mentions(entry, latestStatus?.displayName)).length;
|
|
144
|
+
io.write(`khala: ${messages.length} new message${messages.length === 1 ? "" : "s"} in #${channelLabel(latestStatus?.channelName ?? channel.channelName ?? "channel")} (${count} mentions you)
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(target.session)) === JSON.stringify(latestSession)) {
|
|
148
|
+
try {
|
|
149
|
+
await writeJsonAtomic(path.join(target.dir, OBSERVATION), {
|
|
150
|
+
userId: session.userId,
|
|
151
|
+
roomId: session.roomId,
|
|
152
|
+
count: entries.length,
|
|
153
|
+
lastEventId: entries.at(-1)?.eventId ?? null
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (await readJson(target.session)) throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const notify = () => {
|
|
162
|
+
if (done) return;
|
|
163
|
+
dirty = true;
|
|
164
|
+
if (running) return;
|
|
165
|
+
running = (async () => {
|
|
166
|
+
do {
|
|
167
|
+
dirty = false;
|
|
168
|
+
await evaluate();
|
|
169
|
+
} while (dirty && !done);
|
|
170
|
+
})().catch(() => stop(1)).finally(() => {
|
|
171
|
+
running = void 0;
|
|
172
|
+
if (dirty && !done) notify();
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
await writeJsonAtomic(leaseFile(files, nonce), { nonce, pid: process.pid });
|
|
176
|
+
await writeJsonAtomic(path.join(files.dir, MARKER), { nonce, pid: process.pid });
|
|
177
|
+
watcher = watchDirectory(files.dir, (_event, filename) => {
|
|
178
|
+
if (filename === null || [MARKER, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json"].includes(String(filename))) notify();
|
|
179
|
+
});
|
|
180
|
+
watcher.on("error", () => stop(1));
|
|
181
|
+
timer = setInterval(notify, 100);
|
|
182
|
+
io.signal.addEventListener("abort", abort, { once: true });
|
|
183
|
+
if (io.signal.aborted) stop();
|
|
184
|
+
else notify();
|
|
185
|
+
await finished;
|
|
186
|
+
await running;
|
|
187
|
+
return code;
|
|
188
|
+
} finally {
|
|
189
|
+
clearInterval(timer);
|
|
190
|
+
watcher?.close();
|
|
191
|
+
io.signal.removeEventListener("abort", abort);
|
|
192
|
+
await fs.unlink(leaseFile(files, nonce)).catch((error) => {
|
|
193
|
+
if (error.code !== "ENOENT") throw error;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
var WATCH_USAGE = "usage: khala watch [--harness claude|codex|cursor --session <id>]";
|
|
198
|
+
async function run(argv) {
|
|
199
|
+
if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) {
|
|
200
|
+
console.log(WATCH_USAGE);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
const invalid = (reason) => {
|
|
204
|
+
console.error(`khala: ${reason}
|
|
205
|
+
${WATCH_USAGE}`);
|
|
206
|
+
return 1;
|
|
207
|
+
};
|
|
208
|
+
if (argv.length !== 0 && !(argv.length === 2 && argv[0] === "--session") && !(argv.length === 4 && argv[0] === "--harness" && argv[2] === "--session")) return invalid("invalid_arguments");
|
|
209
|
+
const harness = resolveHarness(argv, process.env);
|
|
210
|
+
if (harness === "invalid") return invalid("invalid_harness");
|
|
211
|
+
const index = argv.indexOf("--session");
|
|
212
|
+
const id = index === -1 ? resolveSessionId(harness, void 0, process.env) : argv[index + 1];
|
|
213
|
+
if (!id) return invalid("session_unknown");
|
|
214
|
+
if (!SESSION_ID_PATTERN.test(id)) return invalid("invalid_session_id");
|
|
215
|
+
const files = sessionFiles(harness, id, process.env);
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const stop = () => controller.abort();
|
|
218
|
+
process.once("SIGTERM", stop);
|
|
219
|
+
process.once("SIGINT", stop);
|
|
220
|
+
try {
|
|
221
|
+
return await watchSession(files, { write: (line) => {
|
|
222
|
+
process.stdout.write(line);
|
|
223
|
+
}, signal: controller.signal });
|
|
224
|
+
} finally {
|
|
225
|
+
process.removeListener("SIGTERM", stop);
|
|
226
|
+
process.removeListener("SIGINT", stop);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
resolveHarness,
|
|
232
|
+
resolveSessionId,
|
|
233
|
+
monitorArmed,
|
|
234
|
+
mentions,
|
|
235
|
+
watchSession,
|
|
236
|
+
WATCH_USAGE,
|
|
237
|
+
run
|
|
238
|
+
};
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
+
WATCH_USAGE,
|
|
3
4
|
mentions,
|
|
4
5
|
monitorArmed,
|
|
5
6
|
run,
|
|
6
7
|
watchSession
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-AWBKVTLV.mjs";
|
|
9
|
+
import "./chunk-WHONG7UF.mjs";
|
|
10
|
+
import "./chunk-ZHMGB3PP.mjs";
|
|
11
|
+
import "./chunk-XUJIJ4KB.mjs";
|
|
12
|
+
import "./chunk-DPW43YNC.mjs";
|
|
13
|
+
import "./chunk-OW3DSTEZ.mjs";
|
|
11
14
|
import "./chunk-AUJEUKVM.mjs";
|
|
12
15
|
import "./chunk-SN3STAO2.mjs";
|
|
13
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-55KBLEX5.mjs";
|
|
14
17
|
export {
|
|
18
|
+
WATCH_USAGE,
|
|
15
19
|
run as default,
|
|
16
20
|
mentions,
|
|
17
21
|
monitorArmed,
|