khala-cli 0.4.2 → 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-T6RZE5IQ.mjs → chunk-6LC66VHK.mjs} +10 -7
- package/dist/chunk-7G2G4E4U.mjs +236 -0
- package/dist/chunk-AUJEUKVM.mjs +121 -0
- package/dist/{chunk-NUS6M27N.mjs → chunk-EW22K3JO.mjs} +445 -199
- package/dist/chunk-EXHCVFVH.mjs +26 -0
- package/dist/{chunk-CDHHFC6C.mjs → chunk-H46OG22B.mjs} +22 -6
- package/dist/{chunk-YP45XTOH.mjs → chunk-HB3UGIZG.mjs} +5 -5
- package/dist/chunk-IR3VN2ID.mjs +52 -0
- package/dist/{chunk-B5BFL7OD.mjs → chunk-KCJFSVDN.mjs} +55 -4
- package/dist/{chunk-K4MDSFHU.mjs → chunk-MBLP7LQJ.mjs} +103 -38
- package/dist/{chunk-RW3DXWNC.mjs → chunk-OLZ62X7S.mjs} +1 -1
- package/dist/chunk-P5CP32X2.mjs +170 -0
- package/dist/{chunk-A3LJU4JA.mjs → chunk-PXKGKVGW.mjs} +1 -1
- package/dist/{chunk-7MQAMABY.mjs → chunk-RSDOTTZQ.mjs} +4 -4
- package/dist/chunk-SN3STAO2.mjs +190 -0
- package/dist/{chunk-MHZWR4A3.mjs → chunk-SQXDATEC.mjs} +1 -1
- package/dist/chunk-T7YRVE43.mjs +238 -0
- package/dist/{chunk-BZ53MZEE.mjs → chunk-TZKEQIHP.mjs} +8 -4
- package/dist/{chunk-46PTKRZC.mjs → chunk-V4Y26O4R.mjs} +171 -113
- package/dist/chunk-V7M27QLT.mjs +21 -0
- package/dist/{chunk-2OO725IZ.mjs → chunk-Y2DRTRBJ.mjs} +3 -3
- package/dist/{chunk-JRZZVTQ4.mjs → chunk-Y4WVILEJ.mjs} +94 -10
- package/dist/khala.mjs +12 -10
- package/dist/web/assets/apple-touch-icon-Du9V6qit.png +0 -0
- package/dist/web/assets/favicon-16x16-D5waNSZD.png +0 -0
- package/dist/web/assets/favicon-32x32-B9Ubw1Dx.png +0 -0
- package/dist/web/assets/favicon-zCD0pKtT.ico +0 -0
- package/dist/web/assets/local-C1vz22ps.js +9 -0
- package/dist/web/assets/local-DdjwMERJ.css +1 -0
- package/dist/web/index.html +7 -2
- package/package.json +1 -1
- package/dist/chunk-N6LMHR7P.mjs +0 -455
- package/dist/chunk-U5ZMOPUK.mjs +0 -149
- package/dist/web/assets/local-CVA9799w.js +0 -9
- package/dist/web/assets/local-DHYvcQG0.css +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
readStatus,
|
|
4
|
+
sessionFiles
|
|
5
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
6
|
+
import "./chunk-SN3STAO2.mjs";
|
|
7
|
+
import "./chunk-PXKGKVGW.mjs";
|
|
8
|
+
|
|
9
|
+
// hooks/session-start.ts
|
|
10
|
+
async function run(stdin, argv) {
|
|
11
|
+
if (argv.length) return 0;
|
|
12
|
+
try {
|
|
13
|
+
const input = JSON.parse(stdin);
|
|
14
|
+
if (input?.hook_event_name !== "SessionStart" || typeof input.session_id !== "string") return 0;
|
|
15
|
+
const files = sessionFiles("claude", input.session_id);
|
|
16
|
+
const status = await readStatus(files);
|
|
17
|
+
if (!status?.channelName || status.state === "disconnected" && status.detail !== "closed") return 0;
|
|
18
|
+
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
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } }) + "\n");
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
run as default
|
|
26
|
+
};
|
|
@@ -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";
|
|
@@ -8,7 +8,7 @@ import os from "node:os";
|
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
9
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
10
10
|
var NOTICE = "Khala: new channel messages. They arrive in the next hook context.\n";
|
|
11
|
-
var DEADLINE_MS =
|
|
11
|
+
var DEADLINE_MS = 24 * 60 * 60 * 1e3;
|
|
12
12
|
var POLL_MS = 500;
|
|
13
13
|
async function readJson(file) {
|
|
14
14
|
try {
|
|
@@ -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
|
}
|
|
@@ -104,6 +119,7 @@ async function run(stdin, argv) {
|
|
|
104
119
|
return watch(stdin, argv);
|
|
105
120
|
}
|
|
106
121
|
export {
|
|
122
|
+
DEADLINE_MS,
|
|
107
123
|
run as default,
|
|
108
124
|
unreadMessages,
|
|
109
125
|
watch
|
|
@@ -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
|
+
import {
|
|
8
|
+
bundle
|
|
9
|
+
} from "./chunk-OLZ62X7S.mjs";
|
|
7
10
|
import {
|
|
8
11
|
StateError,
|
|
9
12
|
ensureStateDir,
|
|
10
13
|
readJson,
|
|
11
14
|
stateRoot
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import {
|
|
14
|
-
bundle
|
|
15
|
-
} from "./chunk-RW3DXWNC.mjs";
|
|
15
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
16
16
|
|
|
17
17
|
// src/local/lifecycle.ts
|
|
18
18
|
import * as fs from "node:fs/promises";
|
|
@@ -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-KCJFSVDN.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
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
HARNESSES
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SN3STAO2.mjs";
|
|
5
5
|
|
|
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,
|
|
@@ -1,22 +1,27 @@
|
|
|
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-
|
|
5
|
+
} from "./chunk-IR3VN2ID.mjs";
|
|
6
|
+
import "./chunk-P5CP32X2.mjs";
|
|
9
7
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
advanceCursor,
|
|
9
|
+
listChannels,
|
|
10
|
+
readListeningMode,
|
|
11
|
+
unread
|
|
12
|
+
} from "./chunk-T7YRVE43.mjs";
|
|
15
13
|
import {
|
|
16
14
|
CURSOR_DEFAULT_SESSION,
|
|
17
15
|
cursorSessionId
|
|
18
16
|
} from "./chunk-VUE2DDYY.mjs";
|
|
19
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
StateError,
|
|
19
|
+
readStatus,
|
|
20
|
+
sessionFiles
|
|
21
|
+
} from "./chunk-KCJFSVDN.mjs";
|
|
22
|
+
import "./chunk-AUJEUKVM.mjs";
|
|
23
|
+
import "./chunk-SN3STAO2.mjs";
|
|
24
|
+
import "./chunk-PXKGKVGW.mjs";
|
|
20
25
|
|
|
21
26
|
// hooks/deliver.ts
|
|
22
27
|
import * as fs from "node:fs/promises";
|
|
@@ -42,7 +47,7 @@ ${INTRO}${youLine}
|
|
|
42
47
|
${entries.map(renderLine).join("\n")}
|
|
43
48
|
</khala-channel-messages>`;
|
|
44
49
|
}
|
|
45
|
-
function selectFrame(channelName, entries, you) {
|
|
50
|
+
function selectFrame(channelName, entries, you, budget = MAX_FRAME_BYTES, truncate = true) {
|
|
46
51
|
const rendered = [];
|
|
47
52
|
let consumedCount = 0;
|
|
48
53
|
let channel = channelName;
|
|
@@ -50,18 +55,18 @@ function selectFrame(channelName, entries, you) {
|
|
|
50
55
|
const entry = entries[i];
|
|
51
56
|
if (rendered.length === 50) break;
|
|
52
57
|
channel ??= entry.roomId;
|
|
53
|
-
if (Buffer.byteLength(renderFrame(channel, [...rendered, entry], you)) >
|
|
54
|
-
if (rendered.length) break;
|
|
58
|
+
if (Buffer.byteLength(renderFrame(channel, [...rendered, entry], you)) > budget) {
|
|
59
|
+
if (rendered.length || !truncate) break;
|
|
55
60
|
const body = Array.from(entry.body);
|
|
56
61
|
let low = 0, high = body.length;
|
|
57
62
|
while (low < high) {
|
|
58
63
|
const middle = Math.ceil((low + high) / 2);
|
|
59
64
|
const candidate = { ...entry, body: body.slice(0, middle).join("") + TRUNCATED };
|
|
60
|
-
if (Buffer.byteLength(renderFrame(channel, [candidate], you)) <=
|
|
65
|
+
if (Buffer.byteLength(renderFrame(channel, [candidate], you)) <= budget) low = middle;
|
|
61
66
|
else high = middle - 1;
|
|
62
67
|
}
|
|
63
68
|
const truncated = { ...entry, body: body.slice(0, low).join("") + TRUNCATED };
|
|
64
|
-
if (Buffer.byteLength(renderFrame(channel, [truncated], you)) >
|
|
69
|
+
if (Buffer.byteLength(renderFrame(channel, [truncated], you)) > budget) throw new Error("frame_metadata_too_large");
|
|
65
70
|
rendered.push(truncated);
|
|
66
71
|
consumedCount = i + 1;
|
|
67
72
|
break;
|
|
@@ -74,6 +79,82 @@ function selectFrame(channelName, entries, you) {
|
|
|
74
79
|
consumed: entries.slice(0, consumedCount)
|
|
75
80
|
};
|
|
76
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
|
+
}
|
|
77
158
|
function diagnostic(io, code) {
|
|
78
159
|
try {
|
|
79
160
|
io.stderr.write(JSON.stringify({ ok: false, warning: "khala_hook_suppressed", code }) + "\n");
|
|
@@ -110,24 +191,13 @@ async function deliver(stdin, argv, io = {
|
|
|
110
191
|
return 0;
|
|
111
192
|
}
|
|
112
193
|
try {
|
|
113
|
-
const mode = await readListeningMode(files);
|
|
114
|
-
if (input.hook_event_name === "PostToolUse" && mode !== "steer") return 0;
|
|
115
194
|
if (input.hook_event_name === "UserPromptSubmit") await writeActivity(files, "busy", io.now);
|
|
116
195
|
if (input.hook_event_name === "Stop" && input.stop_hook_active === true) {
|
|
117
196
|
await writeActivity(files, "idle", io.now);
|
|
118
197
|
return 0;
|
|
119
198
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return 0;
|
|
123
|
-
}
|
|
124
|
-
const status = await readStatus(files);
|
|
125
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
126
|
-
const { entries, cursor } = await unread(files);
|
|
127
|
-
const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
|
|
128
|
-
if (!frame) break;
|
|
129
|
-
if (input.hook_event_name !== "UserPromptSubmit" && !consumed.some(isWakeEntry)) break;
|
|
130
|
-
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) {
|
|
131
201
|
await writeActivity(files, "busy", io.now);
|
|
132
202
|
const envelope = input.hook_event_name === "Stop" ? { decision: "block", reason: frame } : { hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: frame } };
|
|
133
203
|
io.stdout.write(JSON.stringify(envelope) + "\n");
|
|
@@ -176,22 +246,16 @@ async function deliverCursor(stdin, io) {
|
|
|
176
246
|
return 0;
|
|
177
247
|
}
|
|
178
248
|
async function cursorOutput(files, event, loopCount, io) {
|
|
179
|
-
const mode = await readListeningMode(files);
|
|
180
249
|
if (event === "beforeSubmitPrompt") {
|
|
181
250
|
await writeActivity(files, "busy", io.now);
|
|
182
251
|
return null;
|
|
183
252
|
}
|
|
184
|
-
if (event === "
|
|
185
|
-
if (event === "stop" && (mode === "async" || typeof loopCount === "number" && loopCount > 0)) {
|
|
253
|
+
if (event === "stop" && typeof loopCount === "number" && loopCount > 0) {
|
|
186
254
|
await writeActivity(files, "idle", io.now);
|
|
187
255
|
return null;
|
|
188
256
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const { entries, cursor } = await unread(files);
|
|
192
|
-
const { frame, consumed } = selectFrame(status?.channelName, entries, typeof status?.displayName === "string" ? status.displayName : void 0);
|
|
193
|
-
if (!frame || !consumed.some(isWakeEntry)) break;
|
|
194
|
-
if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
|
|
257
|
+
const frame = await channelFrames(files, event === "postToolUse", true, io);
|
|
258
|
+
if (frame) {
|
|
195
259
|
await writeActivity(files, "busy", io.now);
|
|
196
260
|
return event === "stop" ? { followup_message: frame } : { additional_context: frame };
|
|
197
261
|
}
|
|
@@ -203,5 +267,6 @@ export {
|
|
|
203
267
|
deliver,
|
|
204
268
|
deliverCursor,
|
|
205
269
|
renderFrame,
|
|
206
|
-
renderLine
|
|
270
|
+
renderLine,
|
|
271
|
+
selectFrames
|
|
207
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;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
decodeWith,
|
|
4
|
+
displayText,
|
|
5
|
+
fail,
|
|
6
|
+
label,
|
|
7
|
+
literal,
|
|
8
|
+
safeInteger,
|
|
9
|
+
utcTimestamp,
|
|
10
|
+
utf8Length,
|
|
11
|
+
version
|
|
12
|
+
} from "./chunk-SN3STAO2.mjs";
|
|
13
|
+
|
|
14
|
+
// ../contracts/src/m1/channel-event.ts
|
|
15
|
+
var CHANNEL_EVENT_TYPE = "com.khala.event.v1";
|
|
16
|
+
var CHANNEL_EVENT_STATUSES = ["info", "pending", "success", "failure", "attention"];
|
|
17
|
+
var MAX_CHANNEL_EVENT_BYTES = 4096;
|
|
18
|
+
function record(input, path) {
|
|
19
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path, "not_object");
|
|
20
|
+
const prototype = Object.getPrototypeOf(input);
|
|
21
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path, "not_object");
|
|
22
|
+
const value = input;
|
|
23
|
+
return {
|
|
24
|
+
has: (key) => Object.hasOwn(value, key) && value[key] !== void 0,
|
|
25
|
+
field: (key) => Object.hasOwn(value, key) ? value[key] : fail(path ? `${path}.${key}` : key, "missing_field"),
|
|
26
|
+
at: (key) => path ? `${path}.${key}` : key
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function nonempty(value, path) {
|
|
30
|
+
if (!value.length) fail(path, "empty");
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function display(input, path, maxBytes, maxPoints) {
|
|
34
|
+
const value = nonempty(displayText(input, path, maxBytes), path);
|
|
35
|
+
if ([...value].length > maxPoints) fail(path, "too_long");
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function matching(value, path, pattern) {
|
|
39
|
+
if (!pattern.test(value)) fail(path, "invalid_value");
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function safely(read) {
|
|
43
|
+
try {
|
|
44
|
+
return read();
|
|
45
|
+
} catch {
|
|
46
|
+
return { ok: false, error: { path: "", code: "invalid_value" } };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function decodeChannelEvent(raw) {
|
|
50
|
+
return safely(() => decodeWith(() => {
|
|
51
|
+
let serialized;
|
|
52
|
+
try {
|
|
53
|
+
serialized = JSON.stringify(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
fail("", "invalid_value");
|
|
56
|
+
}
|
|
57
|
+
if (serialized !== void 0 && utf8Length(serialized) > MAX_CHANNEL_EVENT_BYTES) fail("", "too_long");
|
|
58
|
+
const r = record(raw, "");
|
|
59
|
+
const v = version(r.field("v"), "v");
|
|
60
|
+
const kind = matching(nonempty(label(r.field("kind"), "kind", 128), "kind"), "kind", /^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/);
|
|
61
|
+
const summary = display(r.field("summary"), "summary", 800, 200);
|
|
62
|
+
if (!summary.trim()) fail("summary", "empty");
|
|
63
|
+
const body = label(r.field("body"), "body", 1024);
|
|
64
|
+
return {
|
|
65
|
+
v,
|
|
66
|
+
kind,
|
|
67
|
+
summary,
|
|
68
|
+
body,
|
|
69
|
+
...r.has("status") ? { status: literal(r.field("status"), "status", CHANNEL_EVENT_STATUSES) } : {},
|
|
70
|
+
...r.has("subject") ? { subject: readSubject(r.field("subject")) } : {},
|
|
71
|
+
...r.has("actor") ? { actor: display(r.field("actor"), "actor", 256, 64) } : {},
|
|
72
|
+
...r.has("url") ? { url: readUrl(r.field("url")) } : {},
|
|
73
|
+
...r.has("occurred_at") ? { occurred_at: utcTimestamp(r.field("occurred_at"), "occurred_at") } : {},
|
|
74
|
+
...r.has("source") ? { source: readSource(r.field("source")) } : {},
|
|
75
|
+
...r.has("key") ? { key: nonempty(label(r.field("key"), "key", 200), "key") } : {}
|
|
76
|
+
};
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
function readSubject(input) {
|
|
80
|
+
const r = record(input, "subject");
|
|
81
|
+
let pr;
|
|
82
|
+
if (r.has("pr")) {
|
|
83
|
+
pr = safeInteger(r.field("pr"), r.at("pr"));
|
|
84
|
+
if (pr === 0) fail(r.at("pr"), "invalid_value");
|
|
85
|
+
}
|
|
86
|
+
let branch;
|
|
87
|
+
if (r.has("branch")) {
|
|
88
|
+
branch = nonempty(label(r.field("branch"), r.at("branch"), 255), r.at("branch"));
|
|
89
|
+
if (branch.startsWith("refs/")) fail(r.at("branch"), "invalid_value");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...r.has("ticket") ? { ticket: display(r.field("ticket"), r.at("ticket"), 256, 64) } : {},
|
|
93
|
+
...pr !== void 0 ? { pr } : {},
|
|
94
|
+
...branch !== void 0 ? { branch } : {},
|
|
95
|
+
...r.has("repo") ? { repo: matching(label(r.field("repo"), r.at("repo"), 140), r.at("repo"), /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/) } : {},
|
|
96
|
+
...r.has("sha") ? { sha: matching(label(r.field("sha"), r.at("sha"), 64), r.at("sha"), /^[0-9a-f]{7,64}$/) } : {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function readSource(input) {
|
|
100
|
+
const r = record(input, "source");
|
|
101
|
+
return {
|
|
102
|
+
system: nonempty(label(r.field("system"), r.at("system"), 32), r.at("system")),
|
|
103
|
+
...r.has("topic") ? { topic: label(r.field("topic"), r.at("topic"), 256) } : {},
|
|
104
|
+
...r.has("event_id") ? { event_id: label(r.field("event_id"), r.at("event_id"), 64) } : {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function readUrl(input) {
|
|
108
|
+
if (typeof input !== "string") fail("url", "wrong_type");
|
|
109
|
+
if (utf8Length(input) > 2048) fail("url", "too_long");
|
|
110
|
+
try {
|
|
111
|
+
if (new URL(input).protocol !== "https:") fail("url", "invalid_value");
|
|
112
|
+
} catch {
|
|
113
|
+
fail("url", "invalid_value");
|
|
114
|
+
}
|
|
115
|
+
return input;
|
|
116
|
+
}
|
|
117
|
+
function formatChannelEventLine(content) {
|
|
118
|
+
const { summary, subject } = content;
|
|
119
|
+
const head = subject?.ticket ? `${subject.ticket} ${summary}` : summary;
|
|
120
|
+
const tail = subject?.branch ?? (subject?.pr !== void 0 ? `PR #${subject.pr}` : void 0);
|
|
121
|
+
return tail ? `${head} \xB7 ${tail}` : head;
|
|
122
|
+
}
|
|
123
|
+
function encodeChannelEvent(input) {
|
|
124
|
+
return safely(() => {
|
|
125
|
+
const checked = decodeWith(() => {
|
|
126
|
+
record(input, "");
|
|
127
|
+
return { ...input, v: 1, body: "" };
|
|
128
|
+
});
|
|
129
|
+
if (!checked.ok) return checked;
|
|
130
|
+
const decoded = decodeChannelEvent(checked.value);
|
|
131
|
+
if (!decoded.ok) return decoded;
|
|
132
|
+
return decodeChannelEvent({ ...decoded.value, body: formatChannelEventLine(decoded.value) });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function memberRenameContent(content, previous) {
|
|
136
|
+
const oldName = previous?.["displayname"];
|
|
137
|
+
const newName = content["displayname"];
|
|
138
|
+
if (previous?.["membership"] !== "join" || content["membership"] !== "join" || typeof oldName !== "string" || !oldName.trim() || typeof newName !== "string" || !newName.trim() || oldName === newName) return null;
|
|
139
|
+
const encoded = encodeChannelEvent({ kind: "member", summary: `${oldName} is now ${newName}`, status: "info", source: { system: "khala" } });
|
|
140
|
+
return encoded.ok ? encoded.value : null;
|
|
141
|
+
}
|
|
142
|
+
function statusFor(content) {
|
|
143
|
+
if (content.status !== void 0) return content.status;
|
|
144
|
+
switch (content.kind) {
|
|
145
|
+
case "ci.passed":
|
|
146
|
+
case "pr.merged":
|
|
147
|
+
case "agent.unblocked":
|
|
148
|
+
return "success";
|
|
149
|
+
case "ci.failed":
|
|
150
|
+
return "failure";
|
|
151
|
+
case "pr.ready_for_review":
|
|
152
|
+
case "pr.parked_ready":
|
|
153
|
+
return "pending";
|
|
154
|
+
case "agent.paused":
|
|
155
|
+
case "agent.blocked":
|
|
156
|
+
case "agent.pause.request":
|
|
157
|
+
return "attention";
|
|
158
|
+
default:
|
|
159
|
+
return content.kind.startsWith("agent.attention.") && !content.kind.endsWith(".resolved") ? "attention" : "info";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
CHANNEL_EVENT_TYPE,
|
|
165
|
+
decodeChannelEvent,
|
|
166
|
+
formatChannelEventLine,
|
|
167
|
+
encodeChannelEvent,
|
|
168
|
+
memberRenameContent,
|
|
169
|
+
statusFor
|
|
170
|
+
};
|
|
@@ -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
|