khala-cli 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LN3G2LJV.mjs → chunk-6LC66VHK.mjs} +5 -5
- package/dist/{chunk-ENPBK5W2.mjs → chunk-7G2G4E4U.mjs} +84 -35
- package/dist/{chunk-LQ6KNUNN.mjs → chunk-EW22K3JO.mjs} +393 -160
- package/dist/{chunk-GE3RXO2X.mjs → chunk-EXHCVFVH.mjs} +2 -2
- package/dist/{chunk-MU4FUENP.mjs → chunk-H46OG22B.mjs} +20 -5
- package/dist/{chunk-LHAMBQK7.mjs → chunk-HB3UGIZG.mjs} +3 -3
- package/dist/{chunk-DC3OX6F4.mjs → chunk-IR3VN2ID.mjs} +1 -1
- package/dist/{chunk-XEYKJMZL.mjs → chunk-KCJFSVDN.mjs} +54 -3
- package/dist/{chunk-6OADQWV7.mjs → chunk-MBLP7LQJ.mjs} +93 -32
- package/dist/{chunk-ZRFXZK72.mjs → chunk-OLZ62X7S.mjs} +1 -1
- package/dist/{chunk-ZKDDKUUA.mjs → chunk-PXKGKVGW.mjs} +1 -1
- package/dist/{chunk-AK74S7EN.mjs → chunk-RSDOTTZQ.mjs} +2 -2
- package/dist/{chunk-7CVU5OZ7.mjs → chunk-SQXDATEC.mjs} +1 -1
- package/dist/chunk-T7YRVE43.mjs +238 -0
- package/dist/{chunk-OS2JLDL7.mjs → chunk-TZKEQIHP.mjs} +4 -2
- package/dist/{chunk-PJ3E3IVH.mjs → chunk-V4Y26O4R.mjs} +7 -7
- package/dist/{chunk-FA6Z65DW.mjs → chunk-V7M27QLT.mjs} +6 -4
- package/dist/{chunk-MZS7XFMC.mjs → chunk-Y2DRTRBJ.mjs} +2 -2
- package/dist/{chunk-FUU775QM.mjs → chunk-Y4WVILEJ.mjs} +14 -1
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-C1vz22ps.js +9 -0
- package/dist/web/assets/local-DdjwMERJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-N5VYKQMR.mjs +0 -108
- package/dist/web/assets/local-Bk8hMIT-.js +0 -9
- package/dist/web/assets/local-vdvavyq0.css +0 -1
|
@@ -2,9 +2,9 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
|
|
|
2
2
|
import {
|
|
3
3
|
readStatus,
|
|
4
4
|
sessionFiles
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
6
6
|
import "./chunk-SN3STAO2.mjs";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-PXKGKVGW.mjs";
|
|
8
8
|
|
|
9
9
|
// hooks/session-start.ts
|
|
10
10
|
async function run(stdin, argv) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-PXKGKVGW.mjs";
|
|
3
3
|
|
|
4
4
|
// hooks/claude-wake.ts
|
|
5
5
|
import fs from "node:fs/promises";
|
|
@@ -18,7 +18,7 @@ async function readJson(file) {
|
|
|
18
18
|
throw error;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
21
|
+
async function unreadChannelMessages(dir) {
|
|
22
22
|
const cursor = await readJson(path.join(dir, "cursor.json"));
|
|
23
23
|
const delivered = cursor && (cursor.lastDeliveredEventId === null || typeof cursor.lastDeliveredEventId === "string") && Number.isSafeInteger(cursor.deliveredCount) && typeof cursor.deliveredCount === "number" && cursor.deliveredCount >= 0 ? cursor.deliveredCount : 0;
|
|
24
24
|
let content;
|
|
@@ -41,6 +41,22 @@ async function unreadMessages(dir) {
|
|
|
41
41
|
}
|
|
42
42
|
return messages;
|
|
43
43
|
}
|
|
44
|
+
async function unreadMessages(dir) {
|
|
45
|
+
const directories = [dir];
|
|
46
|
+
try {
|
|
47
|
+
for (const entry of await fs.readdir(path.join(dir, "channels"), { withFileTypes: true })) {
|
|
48
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) directories.push(path.join(dir, "channels", entry.name));
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT") throw error;
|
|
52
|
+
}
|
|
53
|
+
let messages = 0;
|
|
54
|
+
for (const directory of directories) {
|
|
55
|
+
if ((await readJson(path.join(directory, "mode.json")))?.mode === "async") continue;
|
|
56
|
+
messages += await unreadChannelMessages(directory);
|
|
57
|
+
}
|
|
58
|
+
return messages;
|
|
59
|
+
}
|
|
44
60
|
function testDuration(value, fallback) {
|
|
45
61
|
const number = Number(value);
|
|
46
62
|
return Number.isSafeInteger(number) && number > 0 ? number : fallback;
|
|
@@ -78,11 +94,10 @@ async function watch(stdin, _argv, io = { stderr: process.stderr, env: process.e
|
|
|
78
94
|
const activity = await readJson(path.join(dir, "activity.json"));
|
|
79
95
|
return activity?.state === "idle" && typeof activity.updatedAt === "string" && Number.isFinite(Date.parse(activity.updatedAt));
|
|
80
96
|
};
|
|
81
|
-
const listening = async () => (await readJson(path.join(dir, "mode.json")))?.mode !== "async";
|
|
82
97
|
while (io.now().getTime() < deadline) {
|
|
83
98
|
if (!await owns() || !parentAlive(parent)) return 0;
|
|
84
|
-
if (await
|
|
85
|
-
if (await unreadMessages(dir) > 0 && await
|
|
99
|
+
if (await idle() && await unreadMessages(dir) > 0) {
|
|
100
|
+
if (await unreadMessages(dir) > 0 && await idle() && await owns() && parentAlive(parent) && io.now().getTime() < deadline) {
|
|
86
101
|
io.stderr.write(NOTICE);
|
|
87
102
|
return 2;
|
|
88
103
|
}
|
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
KhalaClientError,
|
|
4
4
|
LOCAL_DEFAULT_PORT,
|
|
5
5
|
decodeHelperFile
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TZKEQIHP.mjs";
|
|
7
7
|
import {
|
|
8
8
|
bundle
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OLZ62X7S.mjs";
|
|
10
10
|
import {
|
|
11
11
|
StateError,
|
|
12
12
|
ensureStateDir,
|
|
13
13
|
readJson,
|
|
14
14
|
stateRoot
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
16
16
|
|
|
17
17
|
// src/local/lifecycle.ts
|
|
18
18
|
import * as fs from "node:fs/promises";
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
// src/state.ts
|
|
7
7
|
import { constants } from "node:fs";
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
@@ -38,6 +38,38 @@ function stateRoot(env = process.env) {
|
|
|
38
38
|
function filesForDir(dir) {
|
|
39
39
|
return { dir, mode: path.join(dir, "mode.json"), join: path.join(dir, "join.json"), session: path.join(dir, "session.json"), inbox: path.join(dir, "inbox.jsonl"), cursor: path.join(dir, "cursor.json"), status: path.join(dir, "status.json") };
|
|
40
40
|
}
|
|
41
|
+
function stateKey(value) {
|
|
42
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
43
|
+
}
|
|
44
|
+
function channelsDir(files) {
|
|
45
|
+
return path.join(files.dir, "channels");
|
|
46
|
+
}
|
|
47
|
+
function channelFiles(files, roomId) {
|
|
48
|
+
return filesForDir(path.join(channelsDir(files), stateKey(roomId)));
|
|
49
|
+
}
|
|
50
|
+
function joinFilePath(files, key) {
|
|
51
|
+
if (!/^[a-f0-9]{24}$/.test(key)) throw new StateError("storage_failed");
|
|
52
|
+
return path.join(files.dir, "joins", `${key}.json`);
|
|
53
|
+
}
|
|
54
|
+
async function writeJoinFile(files, link, join) {
|
|
55
|
+
const file = joinFilePath(files, stateKey(link));
|
|
56
|
+
await ensureStateDir(path.dirname(file));
|
|
57
|
+
await writeJsonAtomic(file, join);
|
|
58
|
+
}
|
|
59
|
+
async function readJoinFile(files, link) {
|
|
60
|
+
const file = joinFilePath(files, stateKey(link));
|
|
61
|
+
await ensureStateDir(path.dirname(file));
|
|
62
|
+
return readJson(file);
|
|
63
|
+
}
|
|
64
|
+
async function removeJoinFile(files, link) {
|
|
65
|
+
const file = joinFilePath(files, stateKey(link));
|
|
66
|
+
await ensureStateDir(path.dirname(file));
|
|
67
|
+
try {
|
|
68
|
+
await fs.unlink(file);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code !== "ENOENT") throw new StateError("storage_failed");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
41
73
|
function sessionFiles(harness, sessionId, env) {
|
|
42
74
|
if (!HARNESSES.includes(harness) || !SESSION_ID_PATTERN.test(sessionId)) throw new StateError("invalid_session_id");
|
|
43
75
|
return filesForDir(path.join(stateRoot(env), harness, sessionId));
|
|
@@ -47,7 +79,20 @@ function resolveStateDir(harness, sessionId, env) {
|
|
|
47
79
|
}
|
|
48
80
|
async function ensureStateDir(dir) {
|
|
49
81
|
try {
|
|
50
|
-
|
|
82
|
+
let sessionDir = dir;
|
|
83
|
+
if (path.basename(path.dirname(dir)) === "channels" && /^[a-f0-9]{24}$/.test(path.basename(dir)) && HARNESSES.includes(path.basename(path.dirname(path.dirname(path.dirname(dir)))))) {
|
|
84
|
+
sessionDir = path.dirname(path.dirname(dir));
|
|
85
|
+
} else if (["channels", "joins"].includes(path.basename(dir)) && HARNESSES.includes(path.basename(path.dirname(path.dirname(dir))))) {
|
|
86
|
+
sessionDir = path.dirname(dir);
|
|
87
|
+
}
|
|
88
|
+
const directories = [path.dirname(path.dirname(sessionDir)), path.dirname(sessionDir), sessionDir];
|
|
89
|
+
const relative = path.relative(sessionDir, dir);
|
|
90
|
+
let parent = sessionDir;
|
|
91
|
+
for (const component of relative ? relative.split(path.sep) : []) {
|
|
92
|
+
parent = path.join(parent, component);
|
|
93
|
+
directories.push(parent);
|
|
94
|
+
}
|
|
95
|
+
for (const directory of directories) {
|
|
51
96
|
try {
|
|
52
97
|
await fs.mkdir(directory, { recursive: true, mode: 448 });
|
|
53
98
|
} catch (error) {
|
|
@@ -105,7 +150,7 @@ function readStatus(files) {
|
|
|
105
150
|
return readJson(files.status);
|
|
106
151
|
}
|
|
107
152
|
async function writeStateFile(dir, name, value) {
|
|
108
|
-
if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json", "rejoin.json"].includes(name)) throw new StateError("storage_failed");
|
|
153
|
+
if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json", "rejoin.json", "channel.json"].includes(name)) throw new StateError("storage_failed");
|
|
109
154
|
await writeJsonAtomic(path.join(dir, name), value);
|
|
110
155
|
}
|
|
111
156
|
function readStateFile(dir, name) {
|
|
@@ -126,6 +171,12 @@ export {
|
|
|
126
171
|
StateError,
|
|
127
172
|
stateRoot,
|
|
128
173
|
filesForDir,
|
|
174
|
+
stateKey,
|
|
175
|
+
channelsDir,
|
|
176
|
+
channelFiles,
|
|
177
|
+
writeJoinFile,
|
|
178
|
+
readJoinFile,
|
|
179
|
+
removeJoinFile,
|
|
129
180
|
sessionFiles,
|
|
130
181
|
resolveStateDir,
|
|
131
182
|
ensureStateDir,
|
|
@@ -2,13 +2,14 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
|
|
|
2
2
|
import {
|
|
3
3
|
isWakeEntry,
|
|
4
4
|
writeActivity
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IR3VN2ID.mjs";
|
|
6
6
|
import "./chunk-P5CP32X2.mjs";
|
|
7
7
|
import {
|
|
8
8
|
advanceCursor,
|
|
9
|
+
listChannels,
|
|
9
10
|
readListeningMode,
|
|
10
11
|
unread
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-T7YRVE43.mjs";
|
|
12
13
|
import {
|
|
13
14
|
CURSOR_DEFAULT_SESSION,
|
|
14
15
|
cursorSessionId
|
|
@@ -17,10 +18,10 @@ import {
|
|
|
17
18
|
StateError,
|
|
18
19
|
readStatus,
|
|
19
20
|
sessionFiles
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
21
22
|
import "./chunk-AUJEUKVM.mjs";
|
|
22
23
|
import "./chunk-SN3STAO2.mjs";
|
|
23
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-PXKGKVGW.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
|
import {
|
|
3
3
|
define_KHALA_BUNDLE_default
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PXKGKVGW.mjs";
|
|
5
5
|
|
|
6
6
|
// src/bundle.ts
|
|
7
7
|
var bundle = typeof define_KHALA_BUNDLE_default === "undefined" ? void 0 : define_KHALA_BUNDLE_default;
|
|
@@ -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.5" };
|
|
5
5
|
|
|
6
6
|
export {
|
|
7
7
|
define_KHALA_BUNDLE_default
|
|
@@ -1,11 +1,11 @@
|
|
|
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-OLZ62X7S.mjs";
|
|
5
5
|
import {
|
|
6
6
|
CURSOR_WORKSPACE_ENV
|
|
7
7
|
} from "./chunk-VUE2DDYY.mjs";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-PXKGKVGW.mjs";
|
|
9
9
|
|
|
10
10
|
// src/install/main.ts
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
@@ -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-
|
|
4
|
+
} from "./chunk-OLZ62X7S.mjs";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
var KHALA_AGENT_VERSION = bundle?.version ?? "0.0.0";
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
StateError,
|
|
4
|
+
channelFiles,
|
|
5
|
+
channelsDir,
|
|
6
|
+
ensureStateDir,
|
|
7
|
+
filesForDir,
|
|
8
|
+
readJson,
|
|
9
|
+
readStatus,
|
|
10
|
+
removeStateFile,
|
|
11
|
+
stateKey,
|
|
12
|
+
writeJsonAtomic,
|
|
13
|
+
writeStateFile
|
|
14
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_LISTENING_MODE,
|
|
17
|
+
decodeListeningMode
|
|
18
|
+
} from "./chunk-AUJEUKVM.mjs";
|
|
19
|
+
|
|
20
|
+
// src/inbox.ts
|
|
21
|
+
import * as fs from "node:fs/promises";
|
|
22
|
+
async function readEntries(files) {
|
|
23
|
+
let content;
|
|
24
|
+
try {
|
|
25
|
+
content = await fs.readFile(files.inbox, "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === "ENOENT") return [];
|
|
28
|
+
throw new StateError("storage_failed");
|
|
29
|
+
}
|
|
30
|
+
const entries = [];
|
|
31
|
+
for (const line of content.split("\n").slice(0, -1)) {
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
if (entry && ["eventId", "roomId", "ts", "sender", "senderLabel", "body"].every((key) => typeof entry[key] === "string") && ["human", "agent", "unknown"].includes(entry.senderKind) && ["message", "event"].includes(entry.kind)) entries.push(entry);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
async function appendEntries(files, entries) {
|
|
41
|
+
const seen = new Set((await readEntries(files)).map((entry) => entry.eventId));
|
|
42
|
+
const fresh = entries.filter((entry) => {
|
|
43
|
+
if (seen.has(entry.eventId)) return false;
|
|
44
|
+
seen.add(entry.eventId);
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
if (fresh.length) {
|
|
48
|
+
try {
|
|
49
|
+
await fs.appendFile(files.inbox, fresh.map((entry) => JSON.stringify(entry) + "\n").join(""), { mode: 384, flag: "a" });
|
|
50
|
+
} catch {
|
|
51
|
+
throw new StateError("storage_failed");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fresh;
|
|
55
|
+
}
|
|
56
|
+
async function readCursor(files) {
|
|
57
|
+
const cursor = await readJson(files.cursor);
|
|
58
|
+
return cursor && (cursor.lastDeliveredEventId === null || typeof cursor.lastDeliveredEventId === "string") && Number.isSafeInteger(cursor.deliveredCount) && cursor.deliveredCount >= 0 ? cursor : { lastDeliveredEventId: null, deliveredCount: 0 };
|
|
59
|
+
}
|
|
60
|
+
async function unread(files) {
|
|
61
|
+
const cursor = await readCursor(files);
|
|
62
|
+
const entries = await readEntries(files);
|
|
63
|
+
return { entries: entries.slice(cursor.deliveredCount), cursor };
|
|
64
|
+
}
|
|
65
|
+
async function unreadCounts(files) {
|
|
66
|
+
const { entries } = await unread(files);
|
|
67
|
+
return { total: entries.length, messages: entries.filter((entry) => entry.kind === "message").length };
|
|
68
|
+
}
|
|
69
|
+
async function advanceCursor(files, expected, delivered) {
|
|
70
|
+
const current = await readCursor(files);
|
|
71
|
+
if (current.deliveredCount !== expected.deliveredCount || current.lastDeliveredEventId !== expected.lastDeliveredEventId) return "conflict";
|
|
72
|
+
if (!delivered.length) return expected;
|
|
73
|
+
const entries = await readEntries(files);
|
|
74
|
+
const slice = entries.slice(expected.deliveredCount, expected.deliveredCount + delivered.length);
|
|
75
|
+
if (slice.length !== delivered.length || slice.some((entry, index) => entry.eventId !== delivered[index]?.eventId)) return "conflict";
|
|
76
|
+
const next = { lastDeliveredEventId: delivered[delivered.length - 1].eventId, deliveredCount: expected.deliveredCount + delivered.length };
|
|
77
|
+
await writeJsonAtomic(files.cursor, next);
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
async function appendInbox(dir, entry) {
|
|
81
|
+
return (await appendEntries(filesForDir(dir), [entry])).length === 1;
|
|
82
|
+
}
|
|
83
|
+
function unreadCount(dir) {
|
|
84
|
+
return unreadCounts(filesForDir(dir));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/channels.ts
|
|
88
|
+
import * as fs2 from "node:fs/promises";
|
|
89
|
+
import path from "node:path";
|
|
90
|
+
function channelKey(roomId2) {
|
|
91
|
+
return stateKey(roomId2);
|
|
92
|
+
}
|
|
93
|
+
async function exists(file) {
|
|
94
|
+
try {
|
|
95
|
+
await fs2.lstat(file);
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === "ENOENT") return false;
|
|
99
|
+
throw new StateError("storage_failed");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function roomId(value) {
|
|
103
|
+
if (value && typeof value === "object" && "roomId" in value && typeof value.roomId === "string" && value.roomId.length) return value.roomId;
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
async function legacyRoomId(files) {
|
|
107
|
+
const saved = roomId(await readJson(files.session));
|
|
108
|
+
if (saved) return saved;
|
|
109
|
+
let content;
|
|
110
|
+
try {
|
|
111
|
+
content = await fs2.readFile(files.inbox, "utf8");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === "ENOENT") return void 0;
|
|
114
|
+
throw new StateError("storage_failed");
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return roomId(JSON.parse(content.split("\n")[0]));
|
|
118
|
+
} catch {
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function listChannels(files) {
|
|
123
|
+
await ensureStateDir(files.dir);
|
|
124
|
+
const channels = [];
|
|
125
|
+
const dir = channelsDir(files);
|
|
126
|
+
if (await exists(dir)) {
|
|
127
|
+
await ensureStateDir(dir);
|
|
128
|
+
for (const key of await fs2.readdir(dir)) {
|
|
129
|
+
if (!/^[a-f0-9]{24}$/.test(key)) continue;
|
|
130
|
+
const nested = filesForDir(path.join(dir, key));
|
|
131
|
+
await ensureStateDir(nested.dir);
|
|
132
|
+
const metadata = await readJson(path.join(nested.dir, "channel.json"));
|
|
133
|
+
const id = roomId(metadata);
|
|
134
|
+
if (!id || channelKey(id) !== key) continue;
|
|
135
|
+
channels.push({ key, roomId: id, ...typeof metadata?.channelName === "string" ? { channelName: metadata.channelName } : {}, files: nested, legacy: false });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (await exists(files.inbox)) {
|
|
139
|
+
const id = await legacyRoomId(files);
|
|
140
|
+
if (id) {
|
|
141
|
+
const status = await readStatus(files);
|
|
142
|
+
channels.push({ key: channelKey(id), roomId: id, ...typeof status?.channelName === "string" ? { channelName: status.channelName } : {}, files, legacy: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return channels.sort((a, b) => (a.channelName ?? "").localeCompare(b.channelName ?? "") || a.roomId.localeCompare(b.roomId));
|
|
146
|
+
}
|
|
147
|
+
async function migrateLegacy(files) {
|
|
148
|
+
await ensureStateDir(files.dir);
|
|
149
|
+
const names = ["cursor.json", "mode.json", "inbox.jsonl"];
|
|
150
|
+
const present = await Promise.all(names.map((name) => exists(path.join(files.dir, name))));
|
|
151
|
+
if (!present.some(Boolean)) return "none";
|
|
152
|
+
const id = await legacyRoomId(files);
|
|
153
|
+
if (!id) {
|
|
154
|
+
for (const name of names) await removeStateFile(files.dir, name);
|
|
155
|
+
return "discarded";
|
|
156
|
+
}
|
|
157
|
+
const nested = channelFiles(files, id);
|
|
158
|
+
await ensureStateDir(nested.dir);
|
|
159
|
+
const destinationInbox = await exists(nested.inbox);
|
|
160
|
+
if (present[2] && !destinationInbox && present[0] && await exists(nested.cursor)) {
|
|
161
|
+
if (await fs2.readFile(files.cursor, "utf8") !== await fs2.readFile(nested.cursor, "utf8")) {
|
|
162
|
+
throw new StateError("storage_failed");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const status = await readStatus(files);
|
|
166
|
+
const metadata = await readJson(path.join(nested.dir, "channel.json"));
|
|
167
|
+
if (!metadata || roomId(metadata) !== id) {
|
|
168
|
+
await writeStateFile(nested.dir, "channel.json", {
|
|
169
|
+
roomId: id,
|
|
170
|
+
...typeof status?.channelName === "string" ? { channelName: status.channelName } : {},
|
|
171
|
+
joinedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (destinationInbox) {
|
|
175
|
+
if (present[2]) await appendEntries(nested, (await unread(files)).entries);
|
|
176
|
+
for (const name of ["inbox.jsonl", "cursor.json", "mode.json"]) await removeStateFile(files.dir, name);
|
|
177
|
+
return "moved";
|
|
178
|
+
}
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
if (await exists(path.join(nested.dir, name))) {
|
|
181
|
+
await removeStateFile(files.dir, name);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await fs2.rename(path.join(files.dir, name), path.join(nested.dir, name));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.code !== "ENOENT") throw new StateError("storage_failed");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return "moved";
|
|
191
|
+
}
|
|
192
|
+
function resolveChannel(list, ref) {
|
|
193
|
+
const channels = list.map((item) => ({ channel: item.channelName ?? item.roomId, roomId: item.roomId }));
|
|
194
|
+
if (ref === void 0) {
|
|
195
|
+
if (list.length === 1) return { ok: true, channel: list[0] };
|
|
196
|
+
return { ok: false, code: list.length ? "channel_required" : "channel_unknown", channels };
|
|
197
|
+
}
|
|
198
|
+
const exact = list.find((item) => item.roomId === ref);
|
|
199
|
+
if (exact) return { ok: true, channel: exact };
|
|
200
|
+
const normalized = ref.replace(/^#/, "").toLowerCase();
|
|
201
|
+
const matches = list.filter((item) => item.channelName?.replace(/^#/, "").toLowerCase() === normalized);
|
|
202
|
+
if (matches.length === 1) return { ok: true, channel: matches[0] };
|
|
203
|
+
return { ok: false, code: matches.length ? "channel_ambiguous" : "channel_unknown", channels };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/mode.ts
|
|
207
|
+
async function readListeningMode(files) {
|
|
208
|
+
const content = await readJson(files.mode);
|
|
209
|
+
const decoded = decodeListeningMode(typeof content === "object" && content !== null ? content.mode : void 0);
|
|
210
|
+
return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
|
|
211
|
+
}
|
|
212
|
+
async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */ new Date()) {
|
|
213
|
+
const previous = await readListeningMode(files);
|
|
214
|
+
if (previous === "async" && next !== "async") {
|
|
215
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
216
|
+
const { cursor, entries } = await unread(files);
|
|
217
|
+
if (await advanceCursor(files, cursor, entries) !== "conflict") break;
|
|
218
|
+
if (attempt === 1) throw new StateError("storage_failed");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await writeJsonAtomic(files.mode, { mode: next, ...meta, updatedAt: now().toISOString() });
|
|
222
|
+
return { previous, next };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
readEntries,
|
|
227
|
+
readCursor,
|
|
228
|
+
unread,
|
|
229
|
+
advanceCursor,
|
|
230
|
+
appendInbox,
|
|
231
|
+
unreadCount,
|
|
232
|
+
channelKey,
|
|
233
|
+
listChannels,
|
|
234
|
+
migrateLegacy,
|
|
235
|
+
resolveChannel,
|
|
236
|
+
readListeningMode,
|
|
237
|
+
applyListeningMode
|
|
238
|
+
};
|