@kodizm/acp 0.6.9 → 0.8.0
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/index.js +2378 -11
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -6429,6 +6429,7 @@ class ClaudeDriver {
|
|
|
6429
6429
|
}
|
|
6430
6430
|
buildBaseSdkOptions(params) {
|
|
6431
6431
|
const additional = "additionalDirectories" in params && params.additionalDirectories !== undefined ? { additionalDirectories: [...params.additionalDirectories] } : {};
|
|
6432
|
+
const plugins = "plugins" in params && params.plugins !== undefined && params.plugins.length > 0 ? { plugins: params.plugins.map((path) => ({ type: "local", path })) } : {};
|
|
6432
6433
|
const skills = "skills" in params && params.skills !== undefined && params.skills.length > 0 ? { skills: [...params.skills] } : {};
|
|
6433
6434
|
const policyTranslation = "toolPolicy" in params && params.toolPolicy !== undefined ? translateToolPolicyToClaude(params.toolPolicy) : {};
|
|
6434
6435
|
const permissionMode = policyTranslation.permissionMode ?? "bypassPermissions";
|
|
@@ -6463,6 +6464,7 @@ class ClaudeDriver {
|
|
|
6463
6464
|
cwd: params.cwd,
|
|
6464
6465
|
mcpServers: translateMcpServers(params.mcpServers),
|
|
6465
6466
|
...additional,
|
|
6467
|
+
...plugins,
|
|
6466
6468
|
...skills,
|
|
6467
6469
|
...policyOptions,
|
|
6468
6470
|
...envOpt,
|
|
@@ -52131,11 +52133,2368 @@ var init_driver2 = __esm(() => {
|
|
|
52131
52133
|
};
|
|
52132
52134
|
logger = createLogger({ env: process.env });
|
|
52133
52135
|
});
|
|
52136
|
+
|
|
52137
|
+
// src/live/redact.ts
|
|
52138
|
+
function redact(text, knownValues) {
|
|
52139
|
+
const values = knownValues.filter((value) => value !== "").sort((left, right) => right.length - left.length);
|
|
52140
|
+
let result = text;
|
|
52141
|
+
for (const value of values) {
|
|
52142
|
+
result = result.replaceAll(value, REDACTED);
|
|
52143
|
+
}
|
|
52144
|
+
for (const pattern of CREDENTIAL_PATTERNS) {
|
|
52145
|
+
result = result.replace(pattern, REDACTED);
|
|
52146
|
+
}
|
|
52147
|
+
return result.replace(URL_CREDENTIAL, `$1${REDACTED}@`);
|
|
52148
|
+
}
|
|
52149
|
+
var REDACTED = "[REDACTED]", CREDENTIAL_PATTERNS, URL_CREDENTIAL;
|
|
52150
|
+
var init_redact = __esm(() => {
|
|
52151
|
+
CREDENTIAL_PATTERNS = [
|
|
52152
|
+
/kgw-[A-Za-z0-9]{8,}/g,
|
|
52153
|
+
/sk-ant-[\w-]+/g,
|
|
52154
|
+
/kdz-int-jwt\.[\w.-]+/g,
|
|
52155
|
+
/-----BEGIN[\s\S]*?(?:-----END[^\n]*?-----|$)/g
|
|
52156
|
+
];
|
|
52157
|
+
URL_CREDENTIAL = /(x-access-token:)[^@\s/]+@/g;
|
|
52158
|
+
});
|
|
52159
|
+
|
|
52160
|
+
// src/live/events.ts
|
|
52161
|
+
function redactDeep(value, knownValues) {
|
|
52162
|
+
if (typeof value === "string") {
|
|
52163
|
+
return redact(value, knownValues);
|
|
52164
|
+
}
|
|
52165
|
+
if (Array.isArray(value)) {
|
|
52166
|
+
return value.map((item) => redactDeep(item, knownValues));
|
|
52167
|
+
}
|
|
52168
|
+
if (isRecord(value)) {
|
|
52169
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactDeep(item, knownValues)]));
|
|
52170
|
+
}
|
|
52171
|
+
return value;
|
|
52172
|
+
}
|
|
52173
|
+
function sessionStarted(hook, source) {
|
|
52174
|
+
return draft("session_started", {
|
|
52175
|
+
claudeSessionId: text(hook.session_id),
|
|
52176
|
+
model: text(hook.model),
|
|
52177
|
+
source
|
|
52178
|
+
});
|
|
52179
|
+
}
|
|
52180
|
+
function turnStarted(promptId) {
|
|
52181
|
+
return draft("turn_started", { promptId });
|
|
52182
|
+
}
|
|
52183
|
+
function turnCompleted(stopReason, lastAssistantMessage) {
|
|
52184
|
+
return draft("turn_completed", {
|
|
52185
|
+
stopReason,
|
|
52186
|
+
lastAssistantMessage
|
|
52187
|
+
});
|
|
52188
|
+
}
|
|
52189
|
+
function apiStopReason(reason) {
|
|
52190
|
+
return reason !== null && API_STOP_REASONS.has(reason) ? reason : "end_turn";
|
|
52191
|
+
}
|
|
52192
|
+
function turnFailed(hook) {
|
|
52193
|
+
const error = text(hook.error) ?? "unknown";
|
|
52194
|
+
const details = text(hook.error_details);
|
|
52195
|
+
return draft("turn_failed", {
|
|
52196
|
+
reason: FAILURE_REASONS[error] ?? "api_error",
|
|
52197
|
+
detail: details === undefined ? `StopFailure ${error}` : `StopFailure ${error}: ${details}`.slice(0, TITLE_MAX * 2)
|
|
52198
|
+
});
|
|
52199
|
+
}
|
|
52200
|
+
function sessionEnded(reason) {
|
|
52201
|
+
return draft("session_ended", { reason });
|
|
52202
|
+
}
|
|
52203
|
+
function toolCallBegin(hook, toolCallId) {
|
|
52204
|
+
return draft("tool_call_begin", {
|
|
52205
|
+
toolCallId,
|
|
52206
|
+
name: text(hook.tool_name),
|
|
52207
|
+
input: hook.tool_input
|
|
52208
|
+
});
|
|
52209
|
+
}
|
|
52210
|
+
function toolCallEnd(toolCallId, status, output) {
|
|
52211
|
+
return draft("tool_call_end", {
|
|
52212
|
+
toolCallId,
|
|
52213
|
+
status,
|
|
52214
|
+
output
|
|
52215
|
+
});
|
|
52216
|
+
}
|
|
52217
|
+
function toolOutput(response) {
|
|
52218
|
+
if (response === undefined || response === null) {
|
|
52219
|
+
return;
|
|
52220
|
+
}
|
|
52221
|
+
if (typeof response === "string") {
|
|
52222
|
+
return response.slice(0, OUTPUT_MAX);
|
|
52223
|
+
}
|
|
52224
|
+
if (isRecord(response) && typeof response.stdout === "string") {
|
|
52225
|
+
const stderr = typeof response.stderr === "string" && response.stderr !== "" ? `
|
|
52226
|
+
${response.stderr}` : "";
|
|
52227
|
+
return `${response.stdout}${stderr}`.slice(0, OUTPUT_MAX);
|
|
52228
|
+
}
|
|
52229
|
+
return JSON.stringify(response).slice(0, OUTPUT_MAX);
|
|
52230
|
+
}
|
|
52231
|
+
function permissionRequest(hook, toolCallId) {
|
|
52232
|
+
const name = text(hook.tool_name) ?? "unknown";
|
|
52233
|
+
return {
|
|
52234
|
+
toolCallId,
|
|
52235
|
+
title: toolTitle(name, hook.tool_input),
|
|
52236
|
+
kind: TOOL_KINDS[name] ?? "other",
|
|
52237
|
+
rawInput: hook.tool_input ?? null,
|
|
52238
|
+
options: PERMISSION_OPTIONS
|
|
52239
|
+
};
|
|
52240
|
+
}
|
|
52241
|
+
function subagentSpawn(hook) {
|
|
52242
|
+
const type = text(hook.agent_type);
|
|
52243
|
+
return draft("subagent_spawn", {
|
|
52244
|
+
subagent_id: text(hook.agent_id),
|
|
52245
|
+
subagent_type: type === undefined || type === "" ? "general-purpose" : type
|
|
52246
|
+
});
|
|
52247
|
+
}
|
|
52248
|
+
function subagentComplete(hook) {
|
|
52249
|
+
return draft("subagent_complete", {
|
|
52250
|
+
subagent_id: text(hook.agent_id)
|
|
52251
|
+
});
|
|
52252
|
+
}
|
|
52253
|
+
function compactionStarted(hook) {
|
|
52254
|
+
return draft("compaction_started", {
|
|
52255
|
+
trigger: hook.trigger === "auto" ? "auto" : "manual"
|
|
52256
|
+
});
|
|
52257
|
+
}
|
|
52258
|
+
function compactionCompleted(hook) {
|
|
52259
|
+
return draft("compaction_completed", {
|
|
52260
|
+
succeeded: true,
|
|
52261
|
+
trigger: hook.trigger === "auto" ? "auto" : "manual"
|
|
52262
|
+
});
|
|
52263
|
+
}
|
|
52264
|
+
function usage(counts) {
|
|
52265
|
+
return draft("usage", { ...counts });
|
|
52266
|
+
}
|
|
52267
|
+
function outputChunk(chunk) {
|
|
52268
|
+
return draft("output_chunk", { text: chunk });
|
|
52269
|
+
}
|
|
52270
|
+
function thinkingChunk(chunk) {
|
|
52271
|
+
return draft("thinking_chunk", { text: chunk });
|
|
52272
|
+
}
|
|
52273
|
+
function heartbeat() {
|
|
52274
|
+
return draft("heartbeat", {});
|
|
52275
|
+
}
|
|
52276
|
+
function processDied() {
|
|
52277
|
+
return draft("process_died", { exitCode: null });
|
|
52278
|
+
}
|
|
52279
|
+
function sessionFailed(reason, detail) {
|
|
52280
|
+
return draft("session_failed", {
|
|
52281
|
+
reason,
|
|
52282
|
+
detail
|
|
52283
|
+
});
|
|
52284
|
+
}
|
|
52285
|
+
function text(value) {
|
|
52286
|
+
return typeof value === "string" ? value : undefined;
|
|
52287
|
+
}
|
|
52288
|
+
function isRecord(value) {
|
|
52289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52290
|
+
}
|
|
52291
|
+
function toolTitle(name, input) {
|
|
52292
|
+
if (!isRecord(input)) {
|
|
52293
|
+
return name;
|
|
52294
|
+
}
|
|
52295
|
+
const field = TITLE_FIELDS.find((key) => typeof input[key] === "string" && input[key] !== "");
|
|
52296
|
+
const subject = field === undefined ? JSON.stringify(input) : input[field];
|
|
52297
|
+
return `${name}: ${subject}`.slice(0, TITLE_MAX);
|
|
52298
|
+
}
|
|
52299
|
+
function draft(type, payload) {
|
|
52300
|
+
return {
|
|
52301
|
+
type,
|
|
52302
|
+
payload: Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined))
|
|
52303
|
+
};
|
|
52304
|
+
}
|
|
52305
|
+
var PERMISSION_OPTIONS, API_STOP_REASONS, TOOL_KINDS, TITLE_FIELDS, TITLE_MAX = 200, OUTPUT_MAX = 16000, FAILURE_REASONS;
|
|
52306
|
+
var init_events2 = __esm(() => {
|
|
52307
|
+
init_redact();
|
|
52308
|
+
PERMISSION_OPTIONS = [
|
|
52309
|
+
{
|
|
52310
|
+
optionId: "allow",
|
|
52311
|
+
kind: "allow_once"
|
|
52312
|
+
},
|
|
52313
|
+
{
|
|
52314
|
+
optionId: "deny",
|
|
52315
|
+
kind: "reject_once"
|
|
52316
|
+
}
|
|
52317
|
+
];
|
|
52318
|
+
API_STOP_REASONS = new Set(["end_turn", "max_tokens", "refusal"]);
|
|
52319
|
+
TOOL_KINDS = {
|
|
52320
|
+
Bash: "execute",
|
|
52321
|
+
BashOutput: "execute",
|
|
52322
|
+
KillShell: "execute",
|
|
52323
|
+
Edit: "edit",
|
|
52324
|
+
MultiEdit: "edit",
|
|
52325
|
+
Write: "edit",
|
|
52326
|
+
NotebookEdit: "edit",
|
|
52327
|
+
Read: "read",
|
|
52328
|
+
Glob: "search",
|
|
52329
|
+
Grep: "search",
|
|
52330
|
+
WebFetch: "fetch",
|
|
52331
|
+
WebSearch: "fetch"
|
|
52332
|
+
};
|
|
52333
|
+
TITLE_FIELDS = ["command", "file_path", "notebook_path", "path", "url", "pattern", "query", "description"];
|
|
52334
|
+
FAILURE_REASONS = {
|
|
52335
|
+
rate_limit: "rate_limit",
|
|
52336
|
+
authentication_failed: "auth_error",
|
|
52337
|
+
oauth_org_not_allowed: "auth_error",
|
|
52338
|
+
account_on_hold: "auth_error",
|
|
52339
|
+
billing_error: "auth_error",
|
|
52340
|
+
cloud_credential_error: "auth_error"
|
|
52341
|
+
};
|
|
52342
|
+
});
|
|
52343
|
+
|
|
52344
|
+
// src/live/hooks.ts
|
|
52345
|
+
import { readFile as readFile2, rename as rename3, writeFile } from "fs/promises";
|
|
52346
|
+
import { createConnection } from "net";
|
|
52347
|
+
async function buildPaneSettings(options) {
|
|
52348
|
+
const base = JSON.parse(await readFile2(options.basePath, "utf8"));
|
|
52349
|
+
if (!isRecord2(base)) {
|
|
52350
|
+
throw new Error(`${options.basePath} does not hold a JSON object`);
|
|
52351
|
+
}
|
|
52352
|
+
const hooks = Object.fromEntries(HOOK_EVENTS.map((event) => [
|
|
52353
|
+
event,
|
|
52354
|
+
[
|
|
52355
|
+
{
|
|
52356
|
+
hooks: [
|
|
52357
|
+
{
|
|
52358
|
+
type: "command",
|
|
52359
|
+
command: hookCommand(options, event),
|
|
52360
|
+
timeout: event === "PermissionRequest" ? PERMISSION_HOOK_TIMEOUT_S : HOOK_TIMEOUT_S
|
|
52361
|
+
}
|
|
52362
|
+
]
|
|
52363
|
+
}
|
|
52364
|
+
]
|
|
52365
|
+
]));
|
|
52366
|
+
return {
|
|
52367
|
+
...base,
|
|
52368
|
+
disableAllHooks: false,
|
|
52369
|
+
hooks
|
|
52370
|
+
};
|
|
52371
|
+
}
|
|
52372
|
+
async function writePaneSettings(path, options) {
|
|
52373
|
+
const settings = await buildPaneSettings(options);
|
|
52374
|
+
const staging = `${path}.${process.pid}.tmp`;
|
|
52375
|
+
await writeFile(staging, JSON.stringify(settings, null, 2), { mode: 384 });
|
|
52376
|
+
await rename3(staging, path);
|
|
52377
|
+
}
|
|
52378
|
+
async function runHookHelper(options) {
|
|
52379
|
+
const payload = parseObject(options.input);
|
|
52380
|
+
if (payload === null) {
|
|
52381
|
+
return "";
|
|
52382
|
+
}
|
|
52383
|
+
const blocking = options.event === "PermissionRequest";
|
|
52384
|
+
const timeoutMs = options.timeoutMs ?? (blocking ? PERMISSION_HELPER_TIMEOUT_MS : HELPER_TIMEOUT_MS);
|
|
52385
|
+
let response;
|
|
52386
|
+
try {
|
|
52387
|
+
response = await socketRequest(options.socketPath, {
|
|
52388
|
+
kind: "hook",
|
|
52389
|
+
pane: options.paneId,
|
|
52390
|
+
event: options.event,
|
|
52391
|
+
payload
|
|
52392
|
+
}, timeoutMs);
|
|
52393
|
+
} catch (error) {
|
|
52394
|
+
if (error instanceof SocketRequestError) {
|
|
52395
|
+
process.stderr.write(`${JSON.stringify({ level: "warn", message: error.message, event: options.event })}
|
|
52396
|
+
`);
|
|
52397
|
+
return "";
|
|
52398
|
+
}
|
|
52399
|
+
throw error;
|
|
52400
|
+
}
|
|
52401
|
+
return blocking && isRecord2(response) ? hookDecisionOutput(response.decision) : "";
|
|
52402
|
+
}
|
|
52403
|
+
function hookDecisionOutput(decision) {
|
|
52404
|
+
if (!isRecord2(decision) || decision.behavior !== "allow" && decision.behavior !== "deny") {
|
|
52405
|
+
return "";
|
|
52406
|
+
}
|
|
52407
|
+
const answer = {
|
|
52408
|
+
behavior: decision.behavior
|
|
52409
|
+
};
|
|
52410
|
+
if (decision.behavior === "deny" && typeof decision.message === "string") {
|
|
52411
|
+
answer.message = decision.message;
|
|
52412
|
+
}
|
|
52413
|
+
return JSON.stringify({
|
|
52414
|
+
hookSpecificOutput: {
|
|
52415
|
+
hookEventName: "PermissionRequest",
|
|
52416
|
+
decision: answer
|
|
52417
|
+
}
|
|
52418
|
+
});
|
|
52419
|
+
}
|
|
52420
|
+
function socketRequest(path, message, timeoutMs) {
|
|
52421
|
+
return new Promise((resolve, reject) => {
|
|
52422
|
+
const socket = createConnection(path);
|
|
52423
|
+
let buffer = "";
|
|
52424
|
+
let settled = false;
|
|
52425
|
+
const finish = (outcome) => {
|
|
52426
|
+
if (settled) {
|
|
52427
|
+
return;
|
|
52428
|
+
}
|
|
52429
|
+
settled = true;
|
|
52430
|
+
clearTimeout(timer);
|
|
52431
|
+
socket.destroy();
|
|
52432
|
+
outcome();
|
|
52433
|
+
};
|
|
52434
|
+
const timer = setTimeout(() => finish(() => reject(new SocketRequestError("timeout"))), timeoutMs);
|
|
52435
|
+
let connected = false;
|
|
52436
|
+
socket.on("connect", () => {
|
|
52437
|
+
connected = true;
|
|
52438
|
+
socket.write(`${JSON.stringify(message)}
|
|
52439
|
+
`);
|
|
52440
|
+
});
|
|
52441
|
+
socket.on("data", (chunk) => {
|
|
52442
|
+
buffer += chunk.toString("utf8");
|
|
52443
|
+
const newline = buffer.indexOf(`
|
|
52444
|
+
`);
|
|
52445
|
+
if (newline === -1) {
|
|
52446
|
+
return;
|
|
52447
|
+
}
|
|
52448
|
+
const answer = parseJson(buffer.slice(0, newline));
|
|
52449
|
+
finish(() => answer === undefined ? reject(new SocketRequestError("malformed")) : resolve(answer.value));
|
|
52450
|
+
});
|
|
52451
|
+
socket.on("error", (error) => finish(() => reject(new SocketRequestError(connected ? "closed" : "unreachable", error.message))));
|
|
52452
|
+
socket.on("close", () => finish(() => reject(new SocketRequestError(connected ? "closed" : "unreachable"))));
|
|
52453
|
+
});
|
|
52454
|
+
}
|
|
52455
|
+
function shellQuote(argument) {
|
|
52456
|
+
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
52457
|
+
}
|
|
52458
|
+
function hookCommand(options, event) {
|
|
52459
|
+
return [...options.helperArgv, "tmuxd", "hook", event, "--pane", options.paneId, "--socket", options.socketPath].map(shellQuote).join(" ");
|
|
52460
|
+
}
|
|
52461
|
+
function parseObject(text) {
|
|
52462
|
+
const parsed = parseJson(text);
|
|
52463
|
+
return parsed !== undefined && isRecord2(parsed.value) ? parsed.value : null;
|
|
52464
|
+
}
|
|
52465
|
+
function parseJson(text) {
|
|
52466
|
+
try {
|
|
52467
|
+
return { value: JSON.parse(text) };
|
|
52468
|
+
} catch (error) {
|
|
52469
|
+
if (error instanceof SyntaxError) {
|
|
52470
|
+
return;
|
|
52471
|
+
}
|
|
52472
|
+
throw error;
|
|
52473
|
+
}
|
|
52474
|
+
}
|
|
52475
|
+
function isRecord2(value) {
|
|
52476
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52477
|
+
}
|
|
52478
|
+
var HOOK_EVENTS, DEFAULT_BASE_SETTINGS_PATH = "/home/agent/.claude/kodizm-live-base.json", HOOK_TIMEOUT_S = 10, PERMISSION_HOOK_TIMEOUT_S = 3600, HELPER_TIMEOUT_MS = 3000, PERMISSION_HELPER_TIMEOUT_MS, SocketRequestError;
|
|
52479
|
+
var init_hooks = __esm(() => {
|
|
52480
|
+
HOOK_EVENTS = [
|
|
52481
|
+
"SessionStart",
|
|
52482
|
+
"UserPromptSubmit",
|
|
52483
|
+
"PreToolUse",
|
|
52484
|
+
"PermissionRequest",
|
|
52485
|
+
"PermissionDenied",
|
|
52486
|
+
"PostToolUse",
|
|
52487
|
+
"PostToolUseFailure",
|
|
52488
|
+
"Stop",
|
|
52489
|
+
"StopFailure",
|
|
52490
|
+
"SessionEnd",
|
|
52491
|
+
"SubagentStart",
|
|
52492
|
+
"SubagentStop",
|
|
52493
|
+
"PreCompact",
|
|
52494
|
+
"PostCompact"
|
|
52495
|
+
];
|
|
52496
|
+
PERMISSION_HELPER_TIMEOUT_MS = PERMISSION_HOOK_TIMEOUT_S * 1000 - 5000;
|
|
52497
|
+
SocketRequestError = class SocketRequestError extends Error {
|
|
52498
|
+
reason;
|
|
52499
|
+
constructor(reason, detail) {
|
|
52500
|
+
super(detail === undefined ? `daemon socket ${reason}` : `daemon socket ${reason}: ${detail}`);
|
|
52501
|
+
this.name = "SocketRequestError";
|
|
52502
|
+
this.reason = reason;
|
|
52503
|
+
}
|
|
52504
|
+
};
|
|
52505
|
+
});
|
|
52506
|
+
|
|
52507
|
+
// src/live/input.ts
|
|
52508
|
+
function sanitizePrompt(text) {
|
|
52509
|
+
let sanitized = "";
|
|
52510
|
+
for (const char of text.replaceAll("\t", TAB_REPLACEMENT)) {
|
|
52511
|
+
if (!isStrippedControl(char.charCodeAt(0))) {
|
|
52512
|
+
sanitized += char;
|
|
52513
|
+
}
|
|
52514
|
+
}
|
|
52515
|
+
const first = sanitized.trimStart().charAt(0);
|
|
52516
|
+
if (first === "") {
|
|
52517
|
+
throw new InputRejectedError("empty");
|
|
52518
|
+
}
|
|
52519
|
+
if (first === "!") {
|
|
52520
|
+
throw new InputRejectedError("bash_mode");
|
|
52521
|
+
}
|
|
52522
|
+
if (first === "/") {
|
|
52523
|
+
throw new InputRejectedError("slash_command");
|
|
52524
|
+
}
|
|
52525
|
+
return sanitized;
|
|
52526
|
+
}
|
|
52527
|
+
function isStrippedControl(code) {
|
|
52528
|
+
return code <= 31 && code !== 10 || code >= 127 && code <= 159;
|
|
52529
|
+
}
|
|
52530
|
+
function sanitizeCommand(command) {
|
|
52531
|
+
const trimmed = command.trim();
|
|
52532
|
+
if (BARE_COMMANDS.has(trimmed)) {
|
|
52533
|
+
return trimmed;
|
|
52534
|
+
}
|
|
52535
|
+
if (trimmed !== "/model" && !trimmed.startsWith("/model ")) {
|
|
52536
|
+
throw new InputRejectedError("unknown_command");
|
|
52537
|
+
}
|
|
52538
|
+
const alias = trimmed.slice("/model ".length);
|
|
52539
|
+
if (!MODEL_NAME.test(alias)) {
|
|
52540
|
+
throw new InputRejectedError("invalid_model_alias");
|
|
52541
|
+
}
|
|
52542
|
+
return `/model ${alias}`;
|
|
52543
|
+
}
|
|
52544
|
+
var InputRejectedError, TAB_REPLACEMENT = " ", MODEL_NAME, BARE_COMMANDS;
|
|
52545
|
+
var init_input = __esm(() => {
|
|
52546
|
+
InputRejectedError = class InputRejectedError extends Error {
|
|
52547
|
+
reason;
|
|
52548
|
+
constructor(reason) {
|
|
52549
|
+
super(`input rejected: ${reason}`);
|
|
52550
|
+
this.name = "InputRejectedError";
|
|
52551
|
+
this.reason = reason;
|
|
52552
|
+
}
|
|
52553
|
+
};
|
|
52554
|
+
MODEL_NAME = /^[A-Za-z0-9._:[\]-]{1,64}$/;
|
|
52555
|
+
BARE_COMMANDS = new Set(["/compact", "/clear"]);
|
|
52556
|
+
});
|
|
52557
|
+
|
|
52558
|
+
// src/live/pane.ts
|
|
52559
|
+
import { mkdir as mkdir3, readFile as readFile3, realpath as realpath3, rename as rename4, writeFile as writeFile2 } from "fs/promises";
|
|
52560
|
+
import { join as join6 } from "path";
|
|
52561
|
+
function supportsPasteBufferS(versionOutput) {
|
|
52562
|
+
const match = /^tmux (?:next-)?(\d+)\.(\d+)[a-z]?$/.exec(versionOutput.trim());
|
|
52563
|
+
if (match === null) {
|
|
52564
|
+
return false;
|
|
52565
|
+
}
|
|
52566
|
+
const major = Number(match[1]);
|
|
52567
|
+
const minor = Number(match[2]);
|
|
52568
|
+
return major > PASTE_S_SINCE[0] || major === PASTE_S_SINCE[0] && minor >= PASTE_S_SINCE[1];
|
|
52569
|
+
}
|
|
52570
|
+
async function createPane(options) {
|
|
52571
|
+
const pane = {
|
|
52572
|
+
id: options.id,
|
|
52573
|
+
socket: options.socket ?? LIVE_SOCKET
|
|
52574
|
+
};
|
|
52575
|
+
if (!PANE_ID.test(pane.id)) {
|
|
52576
|
+
throw new PaneError("invalid_pane_id");
|
|
52577
|
+
}
|
|
52578
|
+
if (options.resumeSessionId !== undefined && !RESUME_ID.test(options.resumeSessionId)) {
|
|
52579
|
+
throw new PaneError("invalid_resume_id");
|
|
52580
|
+
}
|
|
52581
|
+
if (options.model !== undefined && (!MODEL_NAME.test(options.model) || options.model.startsWith("-"))) {
|
|
52582
|
+
throw new PaneError("invalid_model");
|
|
52583
|
+
}
|
|
52584
|
+
const env = paneEnv(options);
|
|
52585
|
+
const cwd = await realpath3(options.cwd);
|
|
52586
|
+
await seedTrust(env.CLAUDE_CONFIG_DIR === undefined ? [env.HOME] : [env.HOME, env.CLAUDE_CONFIG_DIR], cwd);
|
|
52587
|
+
const { PATH: path, TERM: term, ...sessionEnv } = env;
|
|
52588
|
+
const created = await run(pane.socket, [
|
|
52589
|
+
"start-server",
|
|
52590
|
+
";",
|
|
52591
|
+
"set-option",
|
|
52592
|
+
"-g",
|
|
52593
|
+
"default-terminal",
|
|
52594
|
+
term,
|
|
52595
|
+
";",
|
|
52596
|
+
"new-session",
|
|
52597
|
+
"-d",
|
|
52598
|
+
"-s",
|
|
52599
|
+
pane.id,
|
|
52600
|
+
"-x",
|
|
52601
|
+
"200",
|
|
52602
|
+
"-y",
|
|
52603
|
+
"50",
|
|
52604
|
+
"-c",
|
|
52605
|
+
cwd,
|
|
52606
|
+
...Object.entries(sessionEnv).flatMap(([name, value]) => ["-e", `${name}=${value}`]),
|
|
52607
|
+
"--",
|
|
52608
|
+
...launchArgv(options)
|
|
52609
|
+
], { path });
|
|
52610
|
+
if (created.exitCode !== 0) {
|
|
52611
|
+
throw new PaneError("tmux_failed", created.stderr);
|
|
52612
|
+
}
|
|
52613
|
+
if (options.confirmStarted !== undefined && !await options.confirmStarted(SESSION_START_TIMEOUT_MS)) {
|
|
52614
|
+
await killPane(pane);
|
|
52615
|
+
throw new PaneError("start_timeout");
|
|
52616
|
+
}
|
|
52617
|
+
return pane;
|
|
52618
|
+
}
|
|
52619
|
+
async function deliverPrompt(pane, text, confirmSubmitted, abortIf) {
|
|
52620
|
+
const payload = sanitizePrompt(text);
|
|
52621
|
+
bufferSequence += 1;
|
|
52622
|
+
const buffer = `${pane.id}-${bufferSequence}`;
|
|
52623
|
+
await runOnPane(pane, ["load-buffer", "-b", buffer, "-"], new TextEncoder().encode(payload));
|
|
52624
|
+
const flags = await pasteKeepsControlBytes() ? ["-p", "-d", "-r", "-S"] : ["-p", "-d", "-r"];
|
|
52625
|
+
const pasted = await run(pane.socket, ["paste-buffer", ...flags, "-b", buffer, "-t", paneTarget(pane)]);
|
|
52626
|
+
if (pasted.exitCode !== 0) {
|
|
52627
|
+
await run(pane.socket, ["delete-buffer", "-b", buffer]);
|
|
52628
|
+
await failOnPane(pane, pasted.stderr);
|
|
52629
|
+
}
|
|
52630
|
+
await Bun.sleep(PASTE_ENTER_DELAY_MS);
|
|
52631
|
+
for (const windowMs of SUBMIT_WINDOWS_MS) {
|
|
52632
|
+
if (abortIf?.() === true) {
|
|
52633
|
+
return await confirmSubmitted(0) ? "submitted" : "aborted";
|
|
52634
|
+
}
|
|
52635
|
+
await runOnPane(pane, ["send-keys", "-t", paneTarget(pane), "Enter"]);
|
|
52636
|
+
if (await confirmSubmitted(windowMs)) {
|
|
52637
|
+
return "submitted";
|
|
52638
|
+
}
|
|
52639
|
+
}
|
|
52640
|
+
throw new PaneError("not_submitted");
|
|
52641
|
+
}
|
|
52642
|
+
async function deliverCommand(pane, command) {
|
|
52643
|
+
const typed = sanitizeCommand(command);
|
|
52644
|
+
await runOnPane(pane, ["send-keys", "-t", paneTarget(pane), "-l", "--", typed]);
|
|
52645
|
+
await Bun.sleep(PASTE_ENTER_DELAY_MS);
|
|
52646
|
+
await runOnPane(pane, ["send-keys", "-t", paneTarget(pane), "Enter"]);
|
|
52647
|
+
}
|
|
52648
|
+
async function cancelPane(pane) {
|
|
52649
|
+
await runOnPane(pane, ["send-keys", "-t", paneTarget(pane), "Escape"]);
|
|
52650
|
+
await Bun.sleep(CANCEL_CLEAR_DELAY_MS);
|
|
52651
|
+
await runOnPane(pane, ["send-keys", "-t", paneTarget(pane), "C-u"]);
|
|
52652
|
+
}
|
|
52653
|
+
async function paneAlive(pane) {
|
|
52654
|
+
return (await run(pane.socket, ["has-session", "-t", sessionTarget(pane)])).exitCode === 0;
|
|
52655
|
+
}
|
|
52656
|
+
async function killPane(pane) {
|
|
52657
|
+
const result = await run(pane.socket, ["kill-session", "-t", sessionTarget(pane)]);
|
|
52658
|
+
if (result.exitCode !== 0 && await paneAlive(pane)) {
|
|
52659
|
+
throw new PaneError("tmux_failed", result.stderr);
|
|
52660
|
+
}
|
|
52661
|
+
}
|
|
52662
|
+
function paneEnv(options) {
|
|
52663
|
+
const env = {
|
|
52664
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin"
|
|
52665
|
+
};
|
|
52666
|
+
for (const [name, value] of Object.entries(options.env ?? {})) {
|
|
52667
|
+
if (!DROPPED_ENV.has(name)) {
|
|
52668
|
+
env[name] = value;
|
|
52669
|
+
}
|
|
52670
|
+
}
|
|
52671
|
+
return {
|
|
52672
|
+
...env,
|
|
52673
|
+
HOME: options.home,
|
|
52674
|
+
CI: "1",
|
|
52675
|
+
TERM: "xterm-256color"
|
|
52676
|
+
};
|
|
52677
|
+
}
|
|
52678
|
+
function launchArgv(options) {
|
|
52679
|
+
const argv = [options.claudeBin ?? "claude", "--settings", options.settingsPath, "--setting-sources", "user"];
|
|
52680
|
+
if (options.model !== undefined) {
|
|
52681
|
+
argv.push("--model", options.model);
|
|
52682
|
+
}
|
|
52683
|
+
if (options.resumeSessionId !== undefined) {
|
|
52684
|
+
argv.push("--resume", options.resumeSessionId);
|
|
52685
|
+
}
|
|
52686
|
+
return argv;
|
|
52687
|
+
}
|
|
52688
|
+
async function seedTrust(dirs, cwd) {
|
|
52689
|
+
for (const dir of dirs) {
|
|
52690
|
+
const path = join6(dir, ".claude.json");
|
|
52691
|
+
const config = await readConfig(path);
|
|
52692
|
+
const projects = isRecord3(config.projects) ? config.projects : {};
|
|
52693
|
+
const project = isRecord3(projects[cwd]) ? projects[cwd] : {};
|
|
52694
|
+
projects[cwd] = {
|
|
52695
|
+
allowedTools: [],
|
|
52696
|
+
...project,
|
|
52697
|
+
hasTrustDialogAccepted: true,
|
|
52698
|
+
hasCompletedProjectOnboarding: true
|
|
52699
|
+
};
|
|
52700
|
+
const seeded = {
|
|
52701
|
+
theme: "dark",
|
|
52702
|
+
...config,
|
|
52703
|
+
hasCompletedOnboarding: true,
|
|
52704
|
+
projects
|
|
52705
|
+
};
|
|
52706
|
+
await mkdir3(dir, { recursive: true });
|
|
52707
|
+
const staging = `${path}.${process.pid}.tmp`;
|
|
52708
|
+
await writeFile2(staging, JSON.stringify(seeded, null, 2), { mode: 384 });
|
|
52709
|
+
await rename4(staging, path);
|
|
52710
|
+
}
|
|
52711
|
+
}
|
|
52712
|
+
async function readConfig(path) {
|
|
52713
|
+
const file = Bun.file(path);
|
|
52714
|
+
if (!await file.exists()) {
|
|
52715
|
+
return {};
|
|
52716
|
+
}
|
|
52717
|
+
const config = JSON.parse(await readFile3(path, "utf8"));
|
|
52718
|
+
if (!isRecord3(config)) {
|
|
52719
|
+
throw new Error(`${path} does not hold a JSON object`);
|
|
52720
|
+
}
|
|
52721
|
+
return config;
|
|
52722
|
+
}
|
|
52723
|
+
function isRecord3(value) {
|
|
52724
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52725
|
+
}
|
|
52726
|
+
function sessionTarget(pane) {
|
|
52727
|
+
return `=${pane.id}`;
|
|
52728
|
+
}
|
|
52729
|
+
function paneTarget(pane) {
|
|
52730
|
+
return `=${pane.id}:`;
|
|
52731
|
+
}
|
|
52732
|
+
function tmuxBinary() {
|
|
52733
|
+
const binary = Bun.which("tmux");
|
|
52734
|
+
if (binary === null) {
|
|
52735
|
+
throw new PaneError("tmux_failed", "tmux is not on PATH");
|
|
52736
|
+
}
|
|
52737
|
+
return binary;
|
|
52738
|
+
}
|
|
52739
|
+
function pasteKeepsControlBytes() {
|
|
52740
|
+
const binary = tmuxBinary();
|
|
52741
|
+
let known = pasteSFlags.get(binary);
|
|
52742
|
+
if (known === undefined) {
|
|
52743
|
+
known = tmuxVersion(binary).then(supportsPasteBufferS);
|
|
52744
|
+
pasteSFlags.set(binary, known);
|
|
52745
|
+
}
|
|
52746
|
+
return known;
|
|
52747
|
+
}
|
|
52748
|
+
async function tmuxVersion(binary) {
|
|
52749
|
+
const child = Bun.spawn([binary, "-V"], {
|
|
52750
|
+
env: {
|
|
52751
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin"
|
|
52752
|
+
},
|
|
52753
|
+
stdin: "ignore",
|
|
52754
|
+
stdout: "pipe",
|
|
52755
|
+
stderr: "ignore"
|
|
52756
|
+
});
|
|
52757
|
+
const [exitCode, output] = await Promise.all([child.exited, new Response(child.stdout).text()]);
|
|
52758
|
+
return exitCode === 0 ? output : "";
|
|
52759
|
+
}
|
|
52760
|
+
async function run(socket, args, options = {}) {
|
|
52761
|
+
const binary = tmuxBinary();
|
|
52762
|
+
const env = {
|
|
52763
|
+
PATH: options.path ?? process.env.PATH ?? "/usr/bin:/bin"
|
|
52764
|
+
};
|
|
52765
|
+
if (process.env.TMUX_TMPDIR !== undefined) {
|
|
52766
|
+
env.TMUX_TMPDIR = process.env.TMUX_TMPDIR;
|
|
52767
|
+
}
|
|
52768
|
+
const child = Bun.spawn([binary, "-L", socket, "-f", "/dev/null", ...args], {
|
|
52769
|
+
env,
|
|
52770
|
+
stdin: options.stdin ?? "ignore",
|
|
52771
|
+
stdout: "ignore",
|
|
52772
|
+
stderr: "pipe"
|
|
52773
|
+
});
|
|
52774
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
52775
|
+
return {
|
|
52776
|
+
exitCode,
|
|
52777
|
+
stderr: stderr.trim()
|
|
52778
|
+
};
|
|
52779
|
+
}
|
|
52780
|
+
async function runOnPane(pane, args, stdin) {
|
|
52781
|
+
const result = await run(pane.socket, args, { stdin });
|
|
52782
|
+
if (result.exitCode !== 0) {
|
|
52783
|
+
await failOnPane(pane, result.stderr);
|
|
52784
|
+
}
|
|
52785
|
+
}
|
|
52786
|
+
async function failOnPane(pane, stderr) {
|
|
52787
|
+
if (!await paneAlive(pane)) {
|
|
52788
|
+
throw new PaneError("pane_dead");
|
|
52789
|
+
}
|
|
52790
|
+
throw new PaneError("tmux_failed", stderr);
|
|
52791
|
+
}
|
|
52792
|
+
var LIVE_SOCKET = "kodizm-live", PASTE_ENTER_DELAY_MS = 328, SUBMIT_WINDOWS_MS, SESSION_START_TIMEOUT_MS = 1e4, CANCEL_CLEAR_DELAY_MS = 414, PANE_ID, RESUME_ID, DROPPED_ENV, PASTE_S_SINCE, PaneError, bufferSequence = 0, pasteSFlags;
|
|
52793
|
+
var init_pane = __esm(() => {
|
|
52794
|
+
init_input();
|
|
52795
|
+
SUBMIT_WINDOWS_MS = [328, 656, 1312, 2624];
|
|
52796
|
+
PANE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
52797
|
+
RESUME_ID = /^[A-Za-z0-9][A-Za-z0-9-]{0,127}$/;
|
|
52798
|
+
DROPPED_ENV = new Set(["CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION"]);
|
|
52799
|
+
PASTE_S_SINCE = [3, 7];
|
|
52800
|
+
PaneError = class PaneError extends Error {
|
|
52801
|
+
reason;
|
|
52802
|
+
constructor(reason, detail) {
|
|
52803
|
+
super(detail === undefined ? `pane ${reason}` : `pane ${reason}: ${detail}`);
|
|
52804
|
+
this.name = "PaneError";
|
|
52805
|
+
this.reason = reason;
|
|
52806
|
+
}
|
|
52807
|
+
};
|
|
52808
|
+
pasteSFlags = new Map;
|
|
52809
|
+
});
|
|
52810
|
+
|
|
52811
|
+
// src/live/transcript.ts
|
|
52812
|
+
import { stat as stat2 } from "fs/promises";
|
|
52813
|
+
|
|
52814
|
+
class TranscriptTail {
|
|
52815
|
+
path;
|
|
52816
|
+
skipped = 0;
|
|
52817
|
+
offset;
|
|
52818
|
+
pending = new Uint8Array(0);
|
|
52819
|
+
constructor(path, offset = 0) {
|
|
52820
|
+
this.path = path;
|
|
52821
|
+
this.offset = offset;
|
|
52822
|
+
}
|
|
52823
|
+
static async atEnd(path) {
|
|
52824
|
+
const file = Bun.file(path);
|
|
52825
|
+
return new TranscriptTail(path, await file.exists() ? file.size : 0);
|
|
52826
|
+
}
|
|
52827
|
+
async poll() {
|
|
52828
|
+
const size = await fileSize(this.path);
|
|
52829
|
+
if (size === null) {
|
|
52830
|
+
return [];
|
|
52831
|
+
}
|
|
52832
|
+
if (size < this.offset) {
|
|
52833
|
+
this.offset = 0;
|
|
52834
|
+
this.pending = new Uint8Array(0);
|
|
52835
|
+
}
|
|
52836
|
+
if (size === this.offset) {
|
|
52837
|
+
return [];
|
|
52838
|
+
}
|
|
52839
|
+
const chunk = new Uint8Array(await Bun.file(this.path).slice(this.offset, size).arrayBuffer());
|
|
52840
|
+
this.offset += chunk.byteLength;
|
|
52841
|
+
const bytes = concat(this.pending, chunk);
|
|
52842
|
+
const end = bytes.lastIndexOf(NEWLINE);
|
|
52843
|
+
if (end === -1) {
|
|
52844
|
+
this.pending = bytes;
|
|
52845
|
+
return [];
|
|
52846
|
+
}
|
|
52847
|
+
this.pending = bytes.slice(end + 1);
|
|
52848
|
+
return this.parse(new TextDecoder().decode(bytes.subarray(0, end)));
|
|
52849
|
+
}
|
|
52850
|
+
parse(text) {
|
|
52851
|
+
const records = [];
|
|
52852
|
+
for (const line of text.split(`
|
|
52853
|
+
`)) {
|
|
52854
|
+
if (line.trim() === "") {
|
|
52855
|
+
continue;
|
|
52856
|
+
}
|
|
52857
|
+
const record = parseLine(line);
|
|
52858
|
+
if (record === null) {
|
|
52859
|
+
this.skipped += 1;
|
|
52860
|
+
continue;
|
|
52861
|
+
}
|
|
52862
|
+
records.push(record);
|
|
52863
|
+
}
|
|
52864
|
+
return records;
|
|
52865
|
+
}
|
|
52866
|
+
}
|
|
52867
|
+
function classifyInterrupt(record) {
|
|
52868
|
+
if (record.type !== "user" || !isMainChain(record) || typeof record.timestamp !== "string") {
|
|
52869
|
+
return null;
|
|
52870
|
+
}
|
|
52871
|
+
const content = messageOf(record)?.content;
|
|
52872
|
+
if (!Array.isArray(content) || content.length !== 1) {
|
|
52873
|
+
return null;
|
|
52874
|
+
}
|
|
52875
|
+
const [block] = content;
|
|
52876
|
+
if (!isRecord4(block) || block.type !== "text") {
|
|
52877
|
+
return null;
|
|
52878
|
+
}
|
|
52879
|
+
if (block.text === INTERRUPT_MARKER) {
|
|
52880
|
+
return "turn";
|
|
52881
|
+
}
|
|
52882
|
+
if (block.text === INTERRUPT_TOOL_MARKER) {
|
|
52883
|
+
return "tool_use";
|
|
52884
|
+
}
|
|
52885
|
+
return null;
|
|
52886
|
+
}
|
|
52887
|
+
|
|
52888
|
+
class UsageLedger {
|
|
52889
|
+
counted = new Map;
|
|
52890
|
+
take(record) {
|
|
52891
|
+
const message = messageOf(record);
|
|
52892
|
+
if (record.type !== "assistant" || !isMainChain(record) || message === null) {
|
|
52893
|
+
return null;
|
|
52894
|
+
}
|
|
52895
|
+
if (typeof message.id !== "string" || !isRecord4(message.usage)) {
|
|
52896
|
+
return null;
|
|
52897
|
+
}
|
|
52898
|
+
const seen = this.counted.get(message.id) ?? emptyCounts();
|
|
52899
|
+
const now = {
|
|
52900
|
+
inputTokens: Math.max(seen.inputTokens, count(message.usage.input_tokens)),
|
|
52901
|
+
outputTokens: Math.max(seen.outputTokens, count(message.usage.output_tokens)),
|
|
52902
|
+
cacheReadTokens: Math.max(seen.cacheReadTokens, count(message.usage.cache_read_input_tokens)),
|
|
52903
|
+
cacheCreationTokens: Math.max(seen.cacheCreationTokens, count(message.usage.cache_creation_input_tokens))
|
|
52904
|
+
};
|
|
52905
|
+
this.counted.set(message.id, now);
|
|
52906
|
+
const delta = {
|
|
52907
|
+
inputTokens: now.inputTokens - seen.inputTokens,
|
|
52908
|
+
outputTokens: now.outputTokens - seen.outputTokens,
|
|
52909
|
+
cacheReadTokens: now.cacheReadTokens - seen.cacheReadTokens,
|
|
52910
|
+
cacheCreationTokens: now.cacheCreationTokens - seen.cacheCreationTokens
|
|
52911
|
+
};
|
|
52912
|
+
return Object.values(delta).some((value) => value > 0) ? delta : null;
|
|
52913
|
+
}
|
|
52914
|
+
}
|
|
52915
|
+
function assistantBlocks(record) {
|
|
52916
|
+
const content = messageOf(record)?.content;
|
|
52917
|
+
if (record.type !== "assistant" || !isMainChain(record) || !Array.isArray(content)) {
|
|
52918
|
+
return [];
|
|
52919
|
+
}
|
|
52920
|
+
const blocks = [];
|
|
52921
|
+
for (const block of content) {
|
|
52922
|
+
if (!isRecord4(block)) {
|
|
52923
|
+
continue;
|
|
52924
|
+
}
|
|
52925
|
+
if (block.type === "text" && typeof block.text === "string" && block.text !== "") {
|
|
52926
|
+
blocks.push({ kind: "text", text: block.text });
|
|
52927
|
+
}
|
|
52928
|
+
if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking !== "") {
|
|
52929
|
+
blocks.push({ kind: "thinking", text: block.thinking });
|
|
52930
|
+
}
|
|
52931
|
+
}
|
|
52932
|
+
return blocks;
|
|
52933
|
+
}
|
|
52934
|
+
function assistantStopReason(record) {
|
|
52935
|
+
const reason = messageOf(record)?.stop_reason;
|
|
52936
|
+
return record.type === "assistant" && typeof reason === "string" ? reason : null;
|
|
52937
|
+
}
|
|
52938
|
+
function isMainChain(record) {
|
|
52939
|
+
return record.isSidechain !== true;
|
|
52940
|
+
}
|
|
52941
|
+
function messageOf(record) {
|
|
52942
|
+
return isRecord4(record.message) ? record.message : null;
|
|
52943
|
+
}
|
|
52944
|
+
function parseLine(line) {
|
|
52945
|
+
try {
|
|
52946
|
+
const value = JSON.parse(line);
|
|
52947
|
+
return isRecord4(value) ? value : null;
|
|
52948
|
+
} catch (error) {
|
|
52949
|
+
if (error instanceof SyntaxError) {
|
|
52950
|
+
return null;
|
|
52951
|
+
}
|
|
52952
|
+
throw error;
|
|
52953
|
+
}
|
|
52954
|
+
}
|
|
52955
|
+
async function fileSize(path) {
|
|
52956
|
+
try {
|
|
52957
|
+
return (await stat2(path)).size;
|
|
52958
|
+
} catch (error) {
|
|
52959
|
+
if (error.code === "ENOENT") {
|
|
52960
|
+
return null;
|
|
52961
|
+
}
|
|
52962
|
+
throw error;
|
|
52963
|
+
}
|
|
52964
|
+
}
|
|
52965
|
+
function concat(left, right) {
|
|
52966
|
+
if (left.byteLength === 0) {
|
|
52967
|
+
return right;
|
|
52968
|
+
}
|
|
52969
|
+
const joined = new Uint8Array(left.byteLength + right.byteLength);
|
|
52970
|
+
joined.set(left);
|
|
52971
|
+
joined.set(right, left.byteLength);
|
|
52972
|
+
return joined;
|
|
52973
|
+
}
|
|
52974
|
+
function count(value) {
|
|
52975
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
52976
|
+
}
|
|
52977
|
+
function emptyCounts() {
|
|
52978
|
+
return {
|
|
52979
|
+
inputTokens: 0,
|
|
52980
|
+
outputTokens: 0,
|
|
52981
|
+
cacheReadTokens: 0,
|
|
52982
|
+
cacheCreationTokens: 0
|
|
52983
|
+
};
|
|
52984
|
+
}
|
|
52985
|
+
function isRecord4(value) {
|
|
52986
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52987
|
+
}
|
|
52988
|
+
var INTERRUPT_MARKER = "[Request interrupted by user]", INTERRUPT_TOOL_MARKER = "[Request interrupted by user for tool use]", NEWLINE = 10;
|
|
52989
|
+
var init_transcript = () => {};
|
|
52990
|
+
|
|
52991
|
+
// src/live/daemon.ts
|
|
52992
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
52993
|
+
import { chmod, mkdir as mkdir4, readFile as readFile4, stat as stat3, unlink as unlink2 } from "fs/promises";
|
|
52994
|
+
import { createServer } from "net";
|
|
52995
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join7 } from "path";
|
|
52996
|
+
function exitCodeFor(code) {
|
|
52997
|
+
return EXIT_CODES[code];
|
|
52998
|
+
}
|
|
52999
|
+
|
|
53000
|
+
class Latch {
|
|
53001
|
+
fired = false;
|
|
53002
|
+
waiters = [];
|
|
53003
|
+
arm() {
|
|
53004
|
+
this.fired = false;
|
|
53005
|
+
}
|
|
53006
|
+
fire() {
|
|
53007
|
+
this.fired = true;
|
|
53008
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
53009
|
+
waiter();
|
|
53010
|
+
}
|
|
53011
|
+
}
|
|
53012
|
+
wait(timeoutMs) {
|
|
53013
|
+
if (this.fired) {
|
|
53014
|
+
return Promise.resolve(true);
|
|
53015
|
+
}
|
|
53016
|
+
return new Promise((resolve) => {
|
|
53017
|
+
const waiter = () => {
|
|
53018
|
+
clearTimeout(timer);
|
|
53019
|
+
resolve(true);
|
|
53020
|
+
};
|
|
53021
|
+
const timer = setTimeout(() => {
|
|
53022
|
+
this.waiters = this.waiters.filter((candidate) => candidate !== waiter);
|
|
53023
|
+
resolve(false);
|
|
53024
|
+
}, timeoutMs);
|
|
53025
|
+
this.waiters.push(waiter);
|
|
53026
|
+
});
|
|
53027
|
+
}
|
|
53028
|
+
}
|
|
53029
|
+
|
|
53030
|
+
class EventQueue {
|
|
53031
|
+
options;
|
|
53032
|
+
seq = 0;
|
|
53033
|
+
pending = [];
|
|
53034
|
+
running = false;
|
|
53035
|
+
stopped = false;
|
|
53036
|
+
constructor(options) {
|
|
53037
|
+
this.options = options;
|
|
53038
|
+
}
|
|
53039
|
+
get lastSeq() {
|
|
53040
|
+
return this.seq;
|
|
53041
|
+
}
|
|
53042
|
+
get size() {
|
|
53043
|
+
return this.pending.length;
|
|
53044
|
+
}
|
|
53045
|
+
push(draft) {
|
|
53046
|
+
if (this.stopped) {
|
|
53047
|
+
return;
|
|
53048
|
+
}
|
|
53049
|
+
this.seq += 1;
|
|
53050
|
+
this.pending.push({
|
|
53051
|
+
type: draft.type,
|
|
53052
|
+
seq: this.seq,
|
|
53053
|
+
gen: this.options.gen,
|
|
53054
|
+
ts: new Date().toISOString(),
|
|
53055
|
+
payload: redactDeep(draft.payload, this.options.secrets())
|
|
53056
|
+
});
|
|
53057
|
+
this.kick();
|
|
53058
|
+
}
|
|
53059
|
+
async drain(timeoutMs) {
|
|
53060
|
+
const deadline = Date.now() + timeoutMs;
|
|
53061
|
+
while (this.pending.length > 0 && !this.stopped && Date.now() < deadline) {
|
|
53062
|
+
await Bun.sleep(20);
|
|
53063
|
+
}
|
|
53064
|
+
}
|
|
53065
|
+
stop() {
|
|
53066
|
+
this.stopped = true;
|
|
53067
|
+
}
|
|
53068
|
+
kick() {
|
|
53069
|
+
if (this.running || this.stopped) {
|
|
53070
|
+
return;
|
|
53071
|
+
}
|
|
53072
|
+
this.running = true;
|
|
53073
|
+
this.loop().finally(() => {
|
|
53074
|
+
this.running = false;
|
|
53075
|
+
});
|
|
53076
|
+
}
|
|
53077
|
+
async loop() {
|
|
53078
|
+
let failures = 0;
|
|
53079
|
+
while (!this.stopped && this.pending.length > 0) {
|
|
53080
|
+
const outcome = await this.send(this.pending.slice(0, MAX_BATCH));
|
|
53081
|
+
if (outcome === "stale") {
|
|
53082
|
+
this.stopped = true;
|
|
53083
|
+
this.pending = [];
|
|
53084
|
+
this.options.onStaleGen();
|
|
53085
|
+
return;
|
|
53086
|
+
}
|
|
53087
|
+
if (outcome === "acknowledged") {
|
|
53088
|
+
failures = 0;
|
|
53089
|
+
continue;
|
|
53090
|
+
}
|
|
53091
|
+
failures += 1;
|
|
53092
|
+
const timings = this.options.timings();
|
|
53093
|
+
await Bun.sleep(Math.min(timings.retryMaxMs, timings.retryBaseMs * 2 ** (failures - 1)));
|
|
53094
|
+
}
|
|
53095
|
+
}
|
|
53096
|
+
async send(batch) {
|
|
53097
|
+
const response = await fetchJson(this.options.endpoint, {
|
|
53098
|
+
method: "POST",
|
|
53099
|
+
bearer: this.options.bearer(),
|
|
53100
|
+
body: {
|
|
53101
|
+
events: batch
|
|
53102
|
+
},
|
|
53103
|
+
timeoutMs: this.options.timings().ingestTimeoutMs
|
|
53104
|
+
});
|
|
53105
|
+
if (response.status === 200 && isRecord(response.body) && typeof response.body.lastSeq === "number") {
|
|
53106
|
+
return this.acknowledge(response.body.lastSeq);
|
|
53107
|
+
}
|
|
53108
|
+
const error = isRecord(response.body) && isRecord(response.body.error) ? response.body.error : {};
|
|
53109
|
+
if (response.status === 409 && error.code === "stale_gen") {
|
|
53110
|
+
this.options.logger.warn("tmuxd ingest moved to a newer gen", {
|
|
53111
|
+
gen: this.options.gen,
|
|
53112
|
+
currentGen: error.currentGen
|
|
53113
|
+
});
|
|
53114
|
+
return "stale";
|
|
53115
|
+
}
|
|
53116
|
+
if (response.status === 409 && error.code === "seq_gap" && typeof error.expectedSeq === "number") {
|
|
53117
|
+
return this.acknowledge(error.expectedSeq - 1);
|
|
53118
|
+
}
|
|
53119
|
+
this.options.logger.warn("tmuxd event push failed", {
|
|
53120
|
+
gen: this.options.gen,
|
|
53121
|
+
status: response.status,
|
|
53122
|
+
code: error.code
|
|
53123
|
+
});
|
|
53124
|
+
return "retry";
|
|
53125
|
+
}
|
|
53126
|
+
acknowledge(lastSeq) {
|
|
53127
|
+
const before = this.pending.length;
|
|
53128
|
+
this.pending = this.pending.filter((event) => event.seq > lastSeq);
|
|
53129
|
+
const first = this.pending[0];
|
|
53130
|
+
if (first !== undefined && first.seq !== lastSeq + 1) {
|
|
53131
|
+
this.options.logger.error("tmuxd ingest expects an event that was already acknowledged", {
|
|
53132
|
+
gen: this.options.gen,
|
|
53133
|
+
expectedSeq: lastSeq + 1,
|
|
53134
|
+
firstPending: first.seq
|
|
53135
|
+
});
|
|
53136
|
+
return "retry";
|
|
53137
|
+
}
|
|
53138
|
+
return this.pending.length < before ? "acknowledged" : "retry";
|
|
53139
|
+
}
|
|
53140
|
+
}
|
|
53141
|
+
|
|
53142
|
+
class LivePane {
|
|
53143
|
+
pane;
|
|
53144
|
+
gen;
|
|
53145
|
+
ingestUrl;
|
|
53146
|
+
daemon;
|
|
53147
|
+
started = new Latch;
|
|
53148
|
+
submitted = new Latch;
|
|
53149
|
+
turnClosed = new Latch;
|
|
53150
|
+
queue;
|
|
53151
|
+
claudeSessionId;
|
|
53152
|
+
model;
|
|
53153
|
+
pid;
|
|
53154
|
+
turn;
|
|
53155
|
+
pendingPromptId;
|
|
53156
|
+
dialog;
|
|
53157
|
+
compacting = false;
|
|
53158
|
+
lastCloseCause;
|
|
53159
|
+
token;
|
|
53160
|
+
secrets;
|
|
53161
|
+
openTools = new Map;
|
|
53162
|
+
subagents = new Set;
|
|
53163
|
+
tail;
|
|
53164
|
+
ledger = new UsageLedger;
|
|
53165
|
+
reading;
|
|
53166
|
+
skippedLines = 0;
|
|
53167
|
+
lastAssistantText;
|
|
53168
|
+
lastStopReason = null;
|
|
53169
|
+
settled = false;
|
|
53170
|
+
lastActivityAt = Date.now();
|
|
53171
|
+
sessionEndReason;
|
|
53172
|
+
timers = [];
|
|
53173
|
+
monitoring = false;
|
|
53174
|
+
closed = false;
|
|
53175
|
+
constructor(pane, gen, ingestUrl, token, authValues, daemon) {
|
|
53176
|
+
this.pane = pane;
|
|
53177
|
+
this.gen = gen;
|
|
53178
|
+
this.ingestUrl = ingestUrl;
|
|
53179
|
+
this.daemon = daemon;
|
|
53180
|
+
this.token = token;
|
|
53181
|
+
this.secrets = [token, ...authValues.filter((value) => value.length >= MIN_SECRET_LENGTH)];
|
|
53182
|
+
this.queue = new EventQueue({
|
|
53183
|
+
gen,
|
|
53184
|
+
endpoint: `${ingestUrl}/events`,
|
|
53185
|
+
bearer: () => this.token,
|
|
53186
|
+
secrets: () => this.secrets,
|
|
53187
|
+
timings: () => daemon.timings,
|
|
53188
|
+
onStaleGen: () => void this.abandon(),
|
|
53189
|
+
logger: daemon.logger
|
|
53190
|
+
});
|
|
53191
|
+
}
|
|
53192
|
+
get id() {
|
|
53193
|
+
return this.pane.id;
|
|
53194
|
+
}
|
|
53195
|
+
get busy() {
|
|
53196
|
+
return this.turn !== undefined || this.compacting;
|
|
53197
|
+
}
|
|
53198
|
+
begin() {
|
|
53199
|
+
const timings = this.daemon.timings;
|
|
53200
|
+
this.timers = [
|
|
53201
|
+
setInterval(() => void this.guard("transcript", () => this.readTranscript()), timings.transcriptPollMs),
|
|
53202
|
+
setInterval(() => void this.guard("monitor", () => this.monitor()), timings.monitorMs),
|
|
53203
|
+
setInterval(() => void this.guard("heartbeat", () => this.beat()), timings.heartbeatMs)
|
|
53204
|
+
];
|
|
53205
|
+
}
|
|
53206
|
+
rotate(token) {
|
|
53207
|
+
this.token = token;
|
|
53208
|
+
this.secrets.push(token);
|
|
53209
|
+
}
|
|
53210
|
+
async handleHook(event, payload, signal) {
|
|
53211
|
+
if (this.closed) {
|
|
53212
|
+
return { ok: false };
|
|
53213
|
+
}
|
|
53214
|
+
this.lastActivityAt = Date.now();
|
|
53215
|
+
switch (event) {
|
|
53216
|
+
case "SessionStart":
|
|
53217
|
+
await this.onSessionStart(payload);
|
|
53218
|
+
break;
|
|
53219
|
+
case "UserPromptSubmit":
|
|
53220
|
+
this.onPromptSubmit(payload);
|
|
53221
|
+
break;
|
|
53222
|
+
case "PreToolUse":
|
|
53223
|
+
this.onPreToolUse(payload);
|
|
53224
|
+
break;
|
|
53225
|
+
case "PermissionRequest":
|
|
53226
|
+
return this.onPermissionRequest(payload, signal);
|
|
53227
|
+
case "PostToolUse":
|
|
53228
|
+
this.closeTool(payload, "completed", toolOutput(payload.tool_response));
|
|
53229
|
+
break;
|
|
53230
|
+
case "PostToolUseFailure":
|
|
53231
|
+
this.closeTool(payload, "failed", text(payload.error));
|
|
53232
|
+
break;
|
|
53233
|
+
case "PermissionDenied":
|
|
53234
|
+
this.closeTool(payload, "failed");
|
|
53235
|
+
break;
|
|
53236
|
+
case "Stop":
|
|
53237
|
+
await this.onStop(payload);
|
|
53238
|
+
break;
|
|
53239
|
+
case "StopFailure":
|
|
53240
|
+
await this.onStopFailure(payload);
|
|
53241
|
+
break;
|
|
53242
|
+
case "SessionEnd":
|
|
53243
|
+
if (payload.reason !== "clear") {
|
|
53244
|
+
this.sessionEndReason = text(payload.reason) ?? "other";
|
|
53245
|
+
}
|
|
53246
|
+
break;
|
|
53247
|
+
case "SubagentStart":
|
|
53248
|
+
this.onSubagentStart(payload);
|
|
53249
|
+
break;
|
|
53250
|
+
case "SubagentStop":
|
|
53251
|
+
this.onSubagentStop(payload);
|
|
53252
|
+
break;
|
|
53253
|
+
case "PreCompact":
|
|
53254
|
+
this.compacting = true;
|
|
53255
|
+
this.queue.push(compactionStarted(payload));
|
|
53256
|
+
break;
|
|
53257
|
+
case "PostCompact":
|
|
53258
|
+
this.compacting = false;
|
|
53259
|
+
this.queue.push(compactionCompleted(payload));
|
|
53260
|
+
break;
|
|
53261
|
+
}
|
|
53262
|
+
return { ok: true };
|
|
53263
|
+
}
|
|
53264
|
+
closeTurn(draft, cause) {
|
|
53265
|
+
if (this.turn === undefined) {
|
|
53266
|
+
return;
|
|
53267
|
+
}
|
|
53268
|
+
this.closeOpenTools();
|
|
53269
|
+
this.clearDialog();
|
|
53270
|
+
this.queue.push(draft);
|
|
53271
|
+
this.turn = undefined;
|
|
53272
|
+
this.lastCloseCause = cause;
|
|
53273
|
+
this.turnClosed.fire();
|
|
53274
|
+
}
|
|
53275
|
+
clearDialog() {
|
|
53276
|
+
this.dialog?.abort.abort();
|
|
53277
|
+
this.dialog = undefined;
|
|
53278
|
+
}
|
|
53279
|
+
interruptedTurn() {
|
|
53280
|
+
return turnCompleted("interrupted", this.lastAssistantText);
|
|
53281
|
+
}
|
|
53282
|
+
async stop() {
|
|
53283
|
+
this.close();
|
|
53284
|
+
await killPane(this.pane);
|
|
53285
|
+
this.queue.push(sessionEnded("stopped"));
|
|
53286
|
+
await this.queue.drain(STOP_DRAIN_MS);
|
|
53287
|
+
this.queue.stop();
|
|
53288
|
+
}
|
|
53289
|
+
async dispose() {
|
|
53290
|
+
this.close();
|
|
53291
|
+
this.queue.stop();
|
|
53292
|
+
await this.killLogged();
|
|
53293
|
+
}
|
|
53294
|
+
close() {
|
|
53295
|
+
this.closed = true;
|
|
53296
|
+
for (const timer of this.timers.splice(0)) {
|
|
53297
|
+
clearInterval(timer);
|
|
53298
|
+
}
|
|
53299
|
+
this.clearDialog();
|
|
53300
|
+
}
|
|
53301
|
+
async onSessionStart(payload) {
|
|
53302
|
+
const source = text(payload.source);
|
|
53303
|
+
this.model = text(payload.model) ?? this.model;
|
|
53304
|
+
if (source === "compact") {
|
|
53305
|
+
return;
|
|
53306
|
+
}
|
|
53307
|
+
const path = text(payload.transcript_path);
|
|
53308
|
+
if (path !== undefined) {
|
|
53309
|
+
this.tail = source === "resume" ? await TranscriptTail.atEnd(path) : new TranscriptTail(path);
|
|
53310
|
+
}
|
|
53311
|
+
this.claudeSessionId = text(payload.session_id) ?? this.claudeSessionId;
|
|
53312
|
+
this.queue.push(sessionStarted({
|
|
53313
|
+
...payload,
|
|
53314
|
+
model: this.model
|
|
53315
|
+
}, source === "resume" ? "resume" : "startup"));
|
|
53316
|
+
this.started.fire();
|
|
53317
|
+
}
|
|
53318
|
+
onPromptSubmit(payload) {
|
|
53319
|
+
if (this.turn !== undefined) {
|
|
53320
|
+
return;
|
|
53321
|
+
}
|
|
53322
|
+
this.turn = {
|
|
53323
|
+
promptId: this.pendingPromptId ?? text(payload.prompt_id) ?? randomUUID5()
|
|
53324
|
+
};
|
|
53325
|
+
this.pendingPromptId = undefined;
|
|
53326
|
+
this.settled = false;
|
|
53327
|
+
this.lastAssistantText = undefined;
|
|
53328
|
+
this.lastStopReason = null;
|
|
53329
|
+
this.queue.push(turnStarted(this.turn.promptId));
|
|
53330
|
+
this.submitted.fire();
|
|
53331
|
+
}
|
|
53332
|
+
onPreToolUse(payload) {
|
|
53333
|
+
if (this.turn === undefined) {
|
|
53334
|
+
return;
|
|
53335
|
+
}
|
|
53336
|
+
const toolCallId = text(payload.tool_use_id) ?? randomUUID5();
|
|
53337
|
+
this.openTools.set(toolCallId, {
|
|
53338
|
+
name: text(payload.tool_name),
|
|
53339
|
+
input: payload.tool_input
|
|
53340
|
+
});
|
|
53341
|
+
this.settled = false;
|
|
53342
|
+
this.queue.push(toolCallBegin(payload, toolCallId));
|
|
53343
|
+
}
|
|
53344
|
+
async onPermissionRequest(payload, signal) {
|
|
53345
|
+
const correlated = this.findOpenTool(payload);
|
|
53346
|
+
const dialog = {
|
|
53347
|
+
toolCallId: correlated ?? randomUUID5(),
|
|
53348
|
+
correlated: correlated !== undefined,
|
|
53349
|
+
toolName: text(payload.tool_name),
|
|
53350
|
+
input: payload.tool_input,
|
|
53351
|
+
abort: new AbortController
|
|
53352
|
+
};
|
|
53353
|
+
this.dialog?.abort.abort();
|
|
53354
|
+
this.dialog = dialog;
|
|
53355
|
+
signal.addEventListener("abort", () => dialog.abort.abort());
|
|
53356
|
+
const request = permissionRequest(payload, dialog.toolCallId);
|
|
53357
|
+
this.queue.push({
|
|
53358
|
+
type: "permission_request",
|
|
53359
|
+
payload: {
|
|
53360
|
+
...request
|
|
53361
|
+
}
|
|
53362
|
+
});
|
|
53363
|
+
const decision = await this.askIngest(request, dialog.abort.signal);
|
|
53364
|
+
if (decision === undefined || this.dialog !== dialog) {
|
|
53365
|
+
return { ok: true };
|
|
53366
|
+
}
|
|
53367
|
+
this.dialog = undefined;
|
|
53368
|
+
return {
|
|
53369
|
+
ok: true,
|
|
53370
|
+
decision
|
|
53371
|
+
};
|
|
53372
|
+
}
|
|
53373
|
+
async askIngest(request, signal) {
|
|
53374
|
+
const timings = this.daemon.timings;
|
|
53375
|
+
const opened = await fetchJson(`${this.ingestUrl}/permissions`, {
|
|
53376
|
+
method: "POST",
|
|
53377
|
+
bearer: this.token,
|
|
53378
|
+
body: redactDeep({
|
|
53379
|
+
gen: this.gen,
|
|
53380
|
+
...request
|
|
53381
|
+
}, this.secrets),
|
|
53382
|
+
timeoutMs: timings.ingestTimeoutMs,
|
|
53383
|
+
signal
|
|
53384
|
+
});
|
|
53385
|
+
if (opened.status !== 200 && opened.status !== 201) {
|
|
53386
|
+
this.daemon.logger.warn("tmuxd permission open failed", { pane: this.id, status: opened.status });
|
|
53387
|
+
return;
|
|
53388
|
+
}
|
|
53389
|
+
const immediate = decisionFrom(opened.body);
|
|
53390
|
+
if (immediate !== "pending") {
|
|
53391
|
+
return immediate;
|
|
53392
|
+
}
|
|
53393
|
+
const id = isRecord(opened.body) ? text(opened.body.id) : undefined;
|
|
53394
|
+
if (id === undefined) {
|
|
53395
|
+
return;
|
|
53396
|
+
}
|
|
53397
|
+
const deadline = Date.now() + PERMISSION_WAIT_MS;
|
|
53398
|
+
while (!signal.aborted && Date.now() < deadline) {
|
|
53399
|
+
await Bun.sleep(timings.permissionPollMs);
|
|
53400
|
+
if (signal.aborted) {
|
|
53401
|
+
return;
|
|
53402
|
+
}
|
|
53403
|
+
const polled = await fetchJson(`${this.ingestUrl}/permissions/${encodeURIComponent(id)}`, {
|
|
53404
|
+
method: "GET",
|
|
53405
|
+
bearer: this.token,
|
|
53406
|
+
timeoutMs: timings.ingestTimeoutMs,
|
|
53407
|
+
signal
|
|
53408
|
+
});
|
|
53409
|
+
if (polled.status === 404) {
|
|
53410
|
+
return;
|
|
53411
|
+
}
|
|
53412
|
+
if (polled.status !== 200) {
|
|
53413
|
+
continue;
|
|
53414
|
+
}
|
|
53415
|
+
const decision = decisionFrom(polled.body);
|
|
53416
|
+
if (decision !== "pending") {
|
|
53417
|
+
return decision;
|
|
53418
|
+
}
|
|
53419
|
+
}
|
|
53420
|
+
return;
|
|
53421
|
+
}
|
|
53422
|
+
closeTool(payload, status, output) {
|
|
53423
|
+
const toolCallId = text(payload.tool_use_id) ?? this.findOpenTool(payload);
|
|
53424
|
+
if (this.dialog !== undefined && this.isDialogOf(this.dialog, payload, toolCallId)) {
|
|
53425
|
+
this.clearDialog();
|
|
53426
|
+
}
|
|
53427
|
+
if (toolCallId === undefined || !this.openTools.delete(toolCallId)) {
|
|
53428
|
+
return;
|
|
53429
|
+
}
|
|
53430
|
+
this.queue.push(toolCallEnd(toolCallId, status, output));
|
|
53431
|
+
}
|
|
53432
|
+
isDialogOf(dialog, payload, toolCallId) {
|
|
53433
|
+
if (dialog.correlated) {
|
|
53434
|
+
return toolCallId === dialog.toolCallId;
|
|
53435
|
+
}
|
|
53436
|
+
return dialog.toolName === text(payload.tool_name) && sameJson(dialog.input, payload.tool_input);
|
|
53437
|
+
}
|
|
53438
|
+
findOpenTool(payload) {
|
|
53439
|
+
const name = text(payload.tool_name);
|
|
53440
|
+
const matches = [...this.openTools.entries()].filter(([, tool]) => tool.name === name && sameJson(tool.input, payload.tool_input));
|
|
53441
|
+
return matches.at(-1)?.[0];
|
|
53442
|
+
}
|
|
53443
|
+
closeOpenTools() {
|
|
53444
|
+
for (const toolCallId of this.openTools.keys()) {
|
|
53445
|
+
this.queue.push(toolCallEnd(toolCallId, "failed"));
|
|
53446
|
+
}
|
|
53447
|
+
this.openTools.clear();
|
|
53448
|
+
}
|
|
53449
|
+
async onStop(payload) {
|
|
53450
|
+
if (this.turn === undefined) {
|
|
53451
|
+
return;
|
|
53452
|
+
}
|
|
53453
|
+
await this.syncTranscript();
|
|
53454
|
+
this.closeTurn(turnCompleted(apiStopReason(this.lastStopReason), text(payload.last_assistant_message) ?? this.lastAssistantText), "stop");
|
|
53455
|
+
}
|
|
53456
|
+
async onStopFailure(payload) {
|
|
53457
|
+
if (this.turn === undefined) {
|
|
53458
|
+
return;
|
|
53459
|
+
}
|
|
53460
|
+
await this.syncTranscript();
|
|
53461
|
+
this.closeTurn(turnFailed(payload), "stop");
|
|
53462
|
+
}
|
|
53463
|
+
onSubagentStart(payload) {
|
|
53464
|
+
const id = text(payload.agent_id);
|
|
53465
|
+
if (id !== undefined) {
|
|
53466
|
+
this.subagents.add(id);
|
|
53467
|
+
}
|
|
53468
|
+
this.queue.push(subagentSpawn(payload));
|
|
53469
|
+
}
|
|
53470
|
+
onSubagentStop(payload) {
|
|
53471
|
+
const id = text(payload.agent_id);
|
|
53472
|
+
if (id === undefined || !this.subagents.delete(id)) {
|
|
53473
|
+
return;
|
|
53474
|
+
}
|
|
53475
|
+
this.queue.push(subagentComplete(payload));
|
|
53476
|
+
}
|
|
53477
|
+
async syncTranscript() {
|
|
53478
|
+
await this.reading;
|
|
53479
|
+
await this.readTranscript();
|
|
53480
|
+
}
|
|
53481
|
+
readTranscript() {
|
|
53482
|
+
if (this.reading === undefined) {
|
|
53483
|
+
this.reading = this.consumeTranscript().finally(() => {
|
|
53484
|
+
this.reading = undefined;
|
|
53485
|
+
});
|
|
53486
|
+
}
|
|
53487
|
+
return this.reading;
|
|
53488
|
+
}
|
|
53489
|
+
async consumeTranscript() {
|
|
53490
|
+
const tail = this.tail;
|
|
53491
|
+
if (tail === undefined || this.closed) {
|
|
53492
|
+
return;
|
|
53493
|
+
}
|
|
53494
|
+
const records = await tail.poll();
|
|
53495
|
+
if (tail.skipped > this.skippedLines) {
|
|
53496
|
+
this.daemon.logger.warn("tmuxd skipped transcript lines that are not JSON", {
|
|
53497
|
+
pane: this.id,
|
|
53498
|
+
skipped: tail.skipped - this.skippedLines
|
|
53499
|
+
});
|
|
53500
|
+
this.skippedLines = tail.skipped;
|
|
53501
|
+
}
|
|
53502
|
+
for (const record of records) {
|
|
53503
|
+
if (record.isSidechain === true) {
|
|
53504
|
+
continue;
|
|
53505
|
+
}
|
|
53506
|
+
this.lastActivityAt = Date.now();
|
|
53507
|
+
if (classifyInterrupt(record) !== null) {
|
|
53508
|
+
this.closeTurn(this.interruptedTurn(), "marker");
|
|
53509
|
+
this.clearDialog();
|
|
53510
|
+
continue;
|
|
53511
|
+
}
|
|
53512
|
+
for (const block of assistantBlocks(record)) {
|
|
53513
|
+
this.queue.push(block.kind === "text" ? outputChunk(block.text) : thinkingChunk(block.text));
|
|
53514
|
+
if (block.kind === "text") {
|
|
53515
|
+
this.lastAssistantText = block.text;
|
|
53516
|
+
}
|
|
53517
|
+
}
|
|
53518
|
+
const counts = this.ledger.take(record);
|
|
53519
|
+
if (counts !== null) {
|
|
53520
|
+
this.queue.push(usage(counts));
|
|
53521
|
+
}
|
|
53522
|
+
if (record.type === "assistant") {
|
|
53523
|
+
this.lastStopReason = assistantStopReason(record);
|
|
53524
|
+
this.settled = this.lastStopReason !== null && this.lastStopReason !== "tool_use";
|
|
53525
|
+
} else if (record.type === "user") {
|
|
53526
|
+
this.settled = false;
|
|
53527
|
+
}
|
|
53528
|
+
}
|
|
53529
|
+
}
|
|
53530
|
+
async monitor() {
|
|
53531
|
+
if (this.monitoring || this.closed) {
|
|
53532
|
+
return;
|
|
53533
|
+
}
|
|
53534
|
+
this.monitoring = true;
|
|
53535
|
+
try {
|
|
53536
|
+
if (!await this.alive()) {
|
|
53537
|
+
await this.onDeath();
|
|
53538
|
+
return;
|
|
53539
|
+
}
|
|
53540
|
+
const quiet = Date.now() - this.lastActivityAt >= this.daemon.timings.watchdogQuietMs;
|
|
53541
|
+
const idle = this.dialog === undefined && this.openTools.size === 0 && !this.compacting && this.settled;
|
|
53542
|
+
if (this.turn !== undefined && quiet && idle) {
|
|
53543
|
+
this.closeTurn(turnCompleted("watchdog", this.lastAssistantText), "watchdog");
|
|
53544
|
+
}
|
|
53545
|
+
} finally {
|
|
53546
|
+
this.monitoring = false;
|
|
53547
|
+
}
|
|
53548
|
+
}
|
|
53549
|
+
async beat() {
|
|
53550
|
+
if (!this.closed && await this.alive()) {
|
|
53551
|
+
this.queue.push(heartbeat());
|
|
53552
|
+
}
|
|
53553
|
+
}
|
|
53554
|
+
async alive() {
|
|
53555
|
+
return await paneAlive(this.pane) && (this.pid === undefined || processAlive(this.pid));
|
|
53556
|
+
}
|
|
53557
|
+
async onDeath() {
|
|
53558
|
+
if (this.closed) {
|
|
53559
|
+
return;
|
|
53560
|
+
}
|
|
53561
|
+
const turnRunning = this.turn !== undefined;
|
|
53562
|
+
this.close();
|
|
53563
|
+
if (this.sessionEndReason !== undefined) {
|
|
53564
|
+
this.queue.push(sessionEnded("stopped"));
|
|
53565
|
+
} else {
|
|
53566
|
+
this.queue.push(sessionEnded("crashed"));
|
|
53567
|
+
this.queue.push(processDied());
|
|
53568
|
+
if (turnRunning) {
|
|
53569
|
+
this.queue.push(sessionFailed("process_died", "claude exited while a turn was running."));
|
|
53570
|
+
}
|
|
53571
|
+
}
|
|
53572
|
+
await this.queue.drain(STOP_DRAIN_MS);
|
|
53573
|
+
this.queue.stop();
|
|
53574
|
+
this.daemon.remove(this);
|
|
53575
|
+
}
|
|
53576
|
+
async abandon() {
|
|
53577
|
+
if (this.closed) {
|
|
53578
|
+
return;
|
|
53579
|
+
}
|
|
53580
|
+
this.close();
|
|
53581
|
+
await this.killLogged();
|
|
53582
|
+
this.daemon.remove(this);
|
|
53583
|
+
}
|
|
53584
|
+
async killLogged() {
|
|
53585
|
+
try {
|
|
53586
|
+
await killPane(this.pane);
|
|
53587
|
+
} catch (error) {
|
|
53588
|
+
if (!(error instanceof PaneError)) {
|
|
53589
|
+
throw error;
|
|
53590
|
+
}
|
|
53591
|
+
this.daemon.logger.error("tmuxd could not kill a pane", { pane: this.id, reason: error.reason });
|
|
53592
|
+
}
|
|
53593
|
+
}
|
|
53594
|
+
async guard(task, run) {
|
|
53595
|
+
try {
|
|
53596
|
+
await run();
|
|
53597
|
+
} catch (error) {
|
|
53598
|
+
this.daemon.logger.error("tmuxd pane task failed", {
|
|
53599
|
+
pane: this.id,
|
|
53600
|
+
task,
|
|
53601
|
+
error: error instanceof Error ? error.message : String(error)
|
|
53602
|
+
});
|
|
53603
|
+
}
|
|
53604
|
+
}
|
|
53605
|
+
}
|
|
53606
|
+
|
|
53607
|
+
class LiveDaemon {
|
|
53608
|
+
options;
|
|
53609
|
+
logger;
|
|
53610
|
+
timings;
|
|
53611
|
+
panes = new Map;
|
|
53612
|
+
server;
|
|
53613
|
+
constructor(options) {
|
|
53614
|
+
this.options = options;
|
|
53615
|
+
this.logger = options.logger ?? createLogger({ env: process.env });
|
|
53616
|
+
this.timings = {
|
|
53617
|
+
...DEFAULT_TIMINGS,
|
|
53618
|
+
...options.timings
|
|
53619
|
+
};
|
|
53620
|
+
}
|
|
53621
|
+
setTimings(timings) {
|
|
53622
|
+
this.timings = {
|
|
53623
|
+
...this.timings,
|
|
53624
|
+
...timings
|
|
53625
|
+
};
|
|
53626
|
+
}
|
|
53627
|
+
async listen() {
|
|
53628
|
+
const { socketPath } = this.options;
|
|
53629
|
+
await mkdir4(dirname4(socketPath), { recursive: true, mode: 448 });
|
|
53630
|
+
if (await Bun.file(socketPath).exists()) {
|
|
53631
|
+
if (await this.answering(socketPath)) {
|
|
53632
|
+
throw new Error(`a tmuxd daemon already listens on ${socketPath}`);
|
|
53633
|
+
}
|
|
53634
|
+
await unlink2(socketPath);
|
|
53635
|
+
}
|
|
53636
|
+
const server = createServer((socket) => this.accept(socket));
|
|
53637
|
+
await new Promise((resolve, reject) => {
|
|
53638
|
+
server.once("error", reject);
|
|
53639
|
+
server.listen(socketPath, () => {
|
|
53640
|
+
server.off("error", reject);
|
|
53641
|
+
resolve();
|
|
53642
|
+
});
|
|
53643
|
+
});
|
|
53644
|
+
await chmod(socketPath, 384);
|
|
53645
|
+
this.server = server;
|
|
53646
|
+
}
|
|
53647
|
+
async close() {
|
|
53648
|
+
for (const pane of [...this.panes.values()]) {
|
|
53649
|
+
this.panes.delete(pane.id);
|
|
53650
|
+
await pane.dispose();
|
|
53651
|
+
}
|
|
53652
|
+
const server = this.server;
|
|
53653
|
+
this.server = undefined;
|
|
53654
|
+
if (server !== undefined) {
|
|
53655
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
53656
|
+
await unlink2(this.options.socketPath).catch((error) => {
|
|
53657
|
+
if (error.code !== "ENOENT") {
|
|
53658
|
+
throw error;
|
|
53659
|
+
}
|
|
53660
|
+
});
|
|
53661
|
+
}
|
|
53662
|
+
}
|
|
53663
|
+
remove(pane) {
|
|
53664
|
+
if (this.panes.get(pane.id) !== pane) {
|
|
53665
|
+
return;
|
|
53666
|
+
}
|
|
53667
|
+
this.panes.delete(pane.id);
|
|
53668
|
+
if (this.panes.size === 0 && this.options.onIdle !== undefined) {
|
|
53669
|
+
setTimeout(this.options.onIdle, 0);
|
|
53670
|
+
}
|
|
53671
|
+
}
|
|
53672
|
+
async hook(paneId, event, payload, signal) {
|
|
53673
|
+
const pane = this.panes.get(paneId);
|
|
53674
|
+
return pane === undefined ? { ok: false } : pane.handleHook(event, payload, signal);
|
|
53675
|
+
}
|
|
53676
|
+
async ctl(verb, args) {
|
|
53677
|
+
switch (verb) {
|
|
53678
|
+
case "start":
|
|
53679
|
+
return this.start(args);
|
|
53680
|
+
case "prompt":
|
|
53681
|
+
return this.prompt(args);
|
|
53682
|
+
case "command":
|
|
53683
|
+
return this.command(args);
|
|
53684
|
+
case "cancel":
|
|
53685
|
+
return this.cancel(args);
|
|
53686
|
+
case "stop":
|
|
53687
|
+
return this.stop(args);
|
|
53688
|
+
case "rotate-token":
|
|
53689
|
+
return this.rotateToken(args);
|
|
53690
|
+
case "status":
|
|
53691
|
+
return this.status(args);
|
|
53692
|
+
default:
|
|
53693
|
+
return refuse("usage_error");
|
|
53694
|
+
}
|
|
53695
|
+
}
|
|
53696
|
+
async start(args) {
|
|
53697
|
+
const envFile = args["env-file"];
|
|
53698
|
+
if (envFile === undefined || !isAbsolute4(envFile)) {
|
|
53699
|
+
return refuse("usage_error");
|
|
53700
|
+
}
|
|
53701
|
+
try {
|
|
53702
|
+
return await this.launch(args, envFile);
|
|
53703
|
+
} finally {
|
|
53704
|
+
await removeFile(envFile);
|
|
53705
|
+
}
|
|
53706
|
+
}
|
|
53707
|
+
async launch(args, envFile) {
|
|
53708
|
+
const { pane: id, cwd, model, resume } = args;
|
|
53709
|
+
const settingsBase = args["settings-base"];
|
|
53710
|
+
if (id === undefined || !PANE_ID2.test(id) || cwd === undefined || !isAbsolute4(cwd)) {
|
|
53711
|
+
return refuse("usage_error");
|
|
53712
|
+
}
|
|
53713
|
+
if (settingsBase === undefined || !isAbsolute4(settingsBase)) {
|
|
53714
|
+
return refuse("usage_error");
|
|
53715
|
+
}
|
|
53716
|
+
if (model !== undefined && !MODEL_NAME.test(model)) {
|
|
53717
|
+
return refuse("usage_error");
|
|
53718
|
+
}
|
|
53719
|
+
if (this.panes.has(id)) {
|
|
53720
|
+
return refuse("pane_exists");
|
|
53721
|
+
}
|
|
53722
|
+
if (this.panes.size >= (this.options.maxPanes ?? DEFAULT_MAX_PANES)) {
|
|
53723
|
+
return refuse("pane_cap");
|
|
53724
|
+
}
|
|
53725
|
+
const content = await readSecretFile(envFile);
|
|
53726
|
+
const env = content === null ? null : parseStartEnv(content);
|
|
53727
|
+
if (env === null) {
|
|
53728
|
+
return refuse("usage_error");
|
|
53729
|
+
}
|
|
53730
|
+
const settingsPath = join7(settingsBase, `${id}.settings.json`);
|
|
53731
|
+
await mkdir4(settingsBase, { recursive: true, mode: 448 });
|
|
53732
|
+
await writePaneSettings(settingsPath, {
|
|
53733
|
+
basePath: this.options.baseSettingsPath ?? DEFAULT_BASE_SETTINGS_PATH,
|
|
53734
|
+
helperArgv: this.options.helperArgv,
|
|
53735
|
+
socketPath: this.options.socketPath,
|
|
53736
|
+
paneId: id
|
|
53737
|
+
});
|
|
53738
|
+
const pane = {
|
|
53739
|
+
id,
|
|
53740
|
+
socket: this.options.tmuxSocket ?? LIVE_SOCKET
|
|
53741
|
+
};
|
|
53742
|
+
const live = new LivePane(pane, env.gen, env.ingestUrl, env.token, Object.values(env.authEnv), this);
|
|
53743
|
+
this.panes.set(id, live);
|
|
53744
|
+
try {
|
|
53745
|
+
await createPane({
|
|
53746
|
+
id,
|
|
53747
|
+
cwd,
|
|
53748
|
+
home: this.options.home,
|
|
53749
|
+
settingsPath,
|
|
53750
|
+
env: env.authEnv,
|
|
53751
|
+
model,
|
|
53752
|
+
resumeSessionId: resume,
|
|
53753
|
+
claudeBin: this.options.claudeBin,
|
|
53754
|
+
socket: pane.socket,
|
|
53755
|
+
confirmStarted: (timeoutMs) => live.started.wait(timeoutMs)
|
|
53756
|
+
});
|
|
53757
|
+
} catch (error) {
|
|
53758
|
+
this.panes.delete(id);
|
|
53759
|
+
live.queue.stop();
|
|
53760
|
+
return refuse(launchFailure(error));
|
|
53761
|
+
}
|
|
53762
|
+
live.pid = await panePid(pane);
|
|
53763
|
+
live.begin();
|
|
53764
|
+
return accept({
|
|
53765
|
+
pane: id,
|
|
53766
|
+
gen: env.gen,
|
|
53767
|
+
pid: live.pid,
|
|
53768
|
+
claudeSessionId: live.claudeSessionId,
|
|
53769
|
+
resumed: resume !== undefined
|
|
53770
|
+
});
|
|
53771
|
+
}
|
|
53772
|
+
async prompt(args) {
|
|
53773
|
+
const { pane: id, file } = args;
|
|
53774
|
+
if (file === undefined || !isAbsolute4(file)) {
|
|
53775
|
+
return refuse("usage_error");
|
|
53776
|
+
}
|
|
53777
|
+
const content = await takeFile(file, readFileOrNull);
|
|
53778
|
+
if (id === undefined || content === null) {
|
|
53779
|
+
return refuse("usage_error");
|
|
53780
|
+
}
|
|
53781
|
+
const live = this.panes.get(id);
|
|
53782
|
+
if (live === undefined) {
|
|
53783
|
+
return notRunning(id);
|
|
53784
|
+
}
|
|
53785
|
+
const refused = this.deliveryRefusal(live);
|
|
53786
|
+
if (refused !== null) {
|
|
53787
|
+
return refused;
|
|
53788
|
+
}
|
|
53789
|
+
let sanitized;
|
|
53790
|
+
try {
|
|
53791
|
+
sanitized = sanitizePrompt(content);
|
|
53792
|
+
} catch (error) {
|
|
53793
|
+
if (error instanceof InputRejectedError) {
|
|
53794
|
+
return refuse("prompt_rejected", { rule: REJECTION_RULES[error.reason] });
|
|
53795
|
+
}
|
|
53796
|
+
throw error;
|
|
53797
|
+
}
|
|
53798
|
+
const promptId = randomUUID5();
|
|
53799
|
+
live.pendingPromptId = promptId;
|
|
53800
|
+
live.submitted.arm();
|
|
53801
|
+
let outcome;
|
|
53802
|
+
try {
|
|
53803
|
+
outcome = await deliverPrompt(live.pane, content, (timeoutMs) => live.submitted.wait(timeoutMs), () => live.dialog !== undefined);
|
|
53804
|
+
} catch (error) {
|
|
53805
|
+
live.pendingPromptId = undefined;
|
|
53806
|
+
return deliveryFailure(error, id);
|
|
53807
|
+
}
|
|
53808
|
+
if (outcome === "aborted") {
|
|
53809
|
+
live.pendingPromptId = undefined;
|
|
53810
|
+
return refuse("dialog_open");
|
|
53811
|
+
}
|
|
53812
|
+
return accept({
|
|
53813
|
+
promptId,
|
|
53814
|
+
bytes: Buffer.byteLength(sanitized)
|
|
53815
|
+
});
|
|
53816
|
+
}
|
|
53817
|
+
async command(args) {
|
|
53818
|
+
const { pane: id, name, arg } = args;
|
|
53819
|
+
if (id === undefined || name === undefined || !COMMAND_NAMES.has(name) || name === "model" !== (arg !== undefined)) {
|
|
53820
|
+
return refuse("usage_error");
|
|
53821
|
+
}
|
|
53822
|
+
let typed;
|
|
53823
|
+
try {
|
|
53824
|
+
typed = sanitizeCommand(arg === undefined ? `/${name}` : `/${name} ${arg}`);
|
|
53825
|
+
} catch (error) {
|
|
53826
|
+
if (error instanceof InputRejectedError) {
|
|
53827
|
+
return refuse("usage_error");
|
|
53828
|
+
}
|
|
53829
|
+
throw error;
|
|
53830
|
+
}
|
|
53831
|
+
const live = this.panes.get(id);
|
|
53832
|
+
if (live === undefined) {
|
|
53833
|
+
return notRunning(id);
|
|
53834
|
+
}
|
|
53835
|
+
const refused = this.deliveryRefusal(live);
|
|
53836
|
+
if (refused !== null) {
|
|
53837
|
+
return refused;
|
|
53838
|
+
}
|
|
53839
|
+
try {
|
|
53840
|
+
await deliverCommand(live.pane, typed);
|
|
53841
|
+
} catch (error) {
|
|
53842
|
+
return deliveryFailure(error, id);
|
|
53843
|
+
}
|
|
53844
|
+
return accept(arg === undefined ? { name } : { name, arg });
|
|
53845
|
+
}
|
|
53846
|
+
async cancel(args) {
|
|
53847
|
+
const id = args.pane;
|
|
53848
|
+
if (id === undefined) {
|
|
53849
|
+
return refuse("usage_error");
|
|
53850
|
+
}
|
|
53851
|
+
const live = this.panes.get(id);
|
|
53852
|
+
if (live === undefined) {
|
|
53853
|
+
return notRunning(id);
|
|
53854
|
+
}
|
|
53855
|
+
if (live.turn === undefined) {
|
|
53856
|
+
return accept({ wasBusy: false });
|
|
53857
|
+
}
|
|
53858
|
+
live.turnClosed.arm();
|
|
53859
|
+
live.clearDialog();
|
|
53860
|
+
try {
|
|
53861
|
+
await cancelPane(live.pane);
|
|
53862
|
+
} catch (error) {
|
|
53863
|
+
return deliveryFailure(error, id);
|
|
53864
|
+
}
|
|
53865
|
+
if (await live.turnClosed.wait(this.timings.cancelTimeoutMs)) {
|
|
53866
|
+
return accept({
|
|
53867
|
+
wasBusy: true,
|
|
53868
|
+
detectedBy: live.lastCloseCause === "marker" ? "transcript_marker" : "timeout"
|
|
53869
|
+
});
|
|
53870
|
+
}
|
|
53871
|
+
live.closeTurn(live.interruptedTurn(), "timeout");
|
|
53872
|
+
return accept({
|
|
53873
|
+
wasBusy: true,
|
|
53874
|
+
detectedBy: "timeout"
|
|
53875
|
+
});
|
|
53876
|
+
}
|
|
53877
|
+
async stop(args) {
|
|
53878
|
+
const id = args.pane;
|
|
53879
|
+
if (id === undefined) {
|
|
53880
|
+
return refuse("usage_error");
|
|
53881
|
+
}
|
|
53882
|
+
const live = this.panes.get(id);
|
|
53883
|
+
if (live === undefined) {
|
|
53884
|
+
return notRunning(id);
|
|
53885
|
+
}
|
|
53886
|
+
try {
|
|
53887
|
+
await live.stop();
|
|
53888
|
+
} catch (error) {
|
|
53889
|
+
if (error instanceof PaneError) {
|
|
53890
|
+
return refuse("timeout");
|
|
53891
|
+
}
|
|
53892
|
+
throw error;
|
|
53893
|
+
} finally {
|
|
53894
|
+
this.remove(live);
|
|
53895
|
+
}
|
|
53896
|
+
return accept({ claudeSessionId: live.claudeSessionId });
|
|
53897
|
+
}
|
|
53898
|
+
async rotateToken(args) {
|
|
53899
|
+
const id = args.pane;
|
|
53900
|
+
const file = args["token-file"];
|
|
53901
|
+
if (file === undefined || !isAbsolute4(file)) {
|
|
53902
|
+
return refuse("usage_error");
|
|
53903
|
+
}
|
|
53904
|
+
const content = await takeFile(file, readSecretFile);
|
|
53905
|
+
if (id === undefined) {
|
|
53906
|
+
return refuse("usage_error");
|
|
53907
|
+
}
|
|
53908
|
+
const live = this.panes.get(id);
|
|
53909
|
+
if (live === undefined) {
|
|
53910
|
+
return notRunning(id);
|
|
53911
|
+
}
|
|
53912
|
+
const token = content?.trim() ?? "";
|
|
53913
|
+
if (token === "" || tokenGen(token) !== live.gen) {
|
|
53914
|
+
return refuse("usage_error");
|
|
53915
|
+
}
|
|
53916
|
+
live.rotate(token);
|
|
53917
|
+
return accept({ gen: live.gen });
|
|
53918
|
+
}
|
|
53919
|
+
status(args) {
|
|
53920
|
+
const id = args.pane;
|
|
53921
|
+
if (id === undefined) {
|
|
53922
|
+
return refuse("usage_error");
|
|
53923
|
+
}
|
|
53924
|
+
const live = this.panes.get(id);
|
|
53925
|
+
if (live === undefined) {
|
|
53926
|
+
return notRunning(id);
|
|
53927
|
+
}
|
|
53928
|
+
return accept({
|
|
53929
|
+
pane: id,
|
|
53930
|
+
running: true,
|
|
53931
|
+
gen: live.gen,
|
|
53932
|
+
busy: live.busy,
|
|
53933
|
+
dialogOpen: live.dialog !== undefined,
|
|
53934
|
+
lastSeq: live.queue.lastSeq,
|
|
53935
|
+
pendingEvents: live.queue.size,
|
|
53936
|
+
claudeSessionId: live.claudeSessionId,
|
|
53937
|
+
model: live.model
|
|
53938
|
+
});
|
|
53939
|
+
}
|
|
53940
|
+
deliveryRefusal(live) {
|
|
53941
|
+
if (live.dialog !== undefined) {
|
|
53942
|
+
return refuse("dialog_open");
|
|
53943
|
+
}
|
|
53944
|
+
if (live.busy || live.pendingPromptId !== undefined) {
|
|
53945
|
+
return refuse("busy");
|
|
53946
|
+
}
|
|
53947
|
+
return null;
|
|
53948
|
+
}
|
|
53949
|
+
accept(socket) {
|
|
53950
|
+
const controller = new AbortController;
|
|
53951
|
+
let buffer = "";
|
|
53952
|
+
let handled = false;
|
|
53953
|
+
socket.on("close", () => controller.abort());
|
|
53954
|
+
socket.on("error", (error) => {
|
|
53955
|
+
controller.abort();
|
|
53956
|
+
this.logger.debug("tmuxd socket client error", { error: error.message });
|
|
53957
|
+
});
|
|
53958
|
+
socket.on("data", (chunk) => {
|
|
53959
|
+
if (handled) {
|
|
53960
|
+
return;
|
|
53961
|
+
}
|
|
53962
|
+
buffer += chunk.toString("utf8");
|
|
53963
|
+
if (buffer.length > MAX_REQUEST_BYTES) {
|
|
53964
|
+
socket.destroy();
|
|
53965
|
+
return;
|
|
53966
|
+
}
|
|
53967
|
+
const newline = buffer.indexOf(`
|
|
53968
|
+
`);
|
|
53969
|
+
if (newline === -1) {
|
|
53970
|
+
return;
|
|
53971
|
+
}
|
|
53972
|
+
handled = true;
|
|
53973
|
+
this.answer(buffer.slice(0, newline), controller.signal).then((response) => {
|
|
53974
|
+
if (!socket.destroyed) {
|
|
53975
|
+
socket.end(`${JSON.stringify(response)}
|
|
53976
|
+
`);
|
|
53977
|
+
}
|
|
53978
|
+
});
|
|
53979
|
+
});
|
|
53980
|
+
}
|
|
53981
|
+
async answer(line, signal) {
|
|
53982
|
+
const request = parseRequest(line);
|
|
53983
|
+
try {
|
|
53984
|
+
if (request?.kind === "hook" && typeof request.pane === "string" && typeof request.event === "string") {
|
|
53985
|
+
return await this.hook(request.pane, request.event, isRecord(request.payload) ? request.payload : {}, signal);
|
|
53986
|
+
}
|
|
53987
|
+
if (request?.kind === "ctl" && typeof request.verb === "string") {
|
|
53988
|
+
return await this.ctl(request.verb, stringArgs(request.args));
|
|
53989
|
+
}
|
|
53990
|
+
} catch (error) {
|
|
53991
|
+
this.logger.error("tmuxd request failed", {
|
|
53992
|
+
kind: request?.kind,
|
|
53993
|
+
error: error instanceof Error ? error.message : String(error)
|
|
53994
|
+
});
|
|
53995
|
+
}
|
|
53996
|
+
return refuse("usage_error");
|
|
53997
|
+
}
|
|
53998
|
+
async answering(socketPath) {
|
|
53999
|
+
try {
|
|
54000
|
+
await socketRequest(socketPath, { kind: "ping" }, 1000);
|
|
54001
|
+
return true;
|
|
54002
|
+
} catch (error) {
|
|
54003
|
+
if (error instanceof SocketRequestError) {
|
|
54004
|
+
return error.reason !== "unreachable";
|
|
54005
|
+
}
|
|
54006
|
+
throw error;
|
|
54007
|
+
}
|
|
54008
|
+
}
|
|
54009
|
+
}
|
|
54010
|
+
function defaultSocketPath(env, tempDir) {
|
|
54011
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "agent";
|
|
54012
|
+
return join7(env.XDG_RUNTIME_DIR ?? tempDir, `kodizm-live-${uid}`, "tmuxd.sock");
|
|
54013
|
+
}
|
|
54014
|
+
function tokenGen(token) {
|
|
54015
|
+
if (!token.startsWith(TOKEN_PREFIX)) {
|
|
54016
|
+
return null;
|
|
54017
|
+
}
|
|
54018
|
+
const [payload] = token.slice(TOKEN_PREFIX.length).split(".");
|
|
54019
|
+
try {
|
|
54020
|
+
const claims = JSON.parse(Buffer.from(payload ?? "", "base64url").toString("utf8"));
|
|
54021
|
+
return isRecord(claims) && Number.isInteger(claims.gen) ? claims.gen : null;
|
|
54022
|
+
} catch (error) {
|
|
54023
|
+
if (error instanceof SyntaxError) {
|
|
54024
|
+
return null;
|
|
54025
|
+
}
|
|
54026
|
+
throw error;
|
|
54027
|
+
}
|
|
54028
|
+
}
|
|
54029
|
+
function parseStartEnv(content) {
|
|
54030
|
+
const values = {};
|
|
54031
|
+
for (const line of content.split(`
|
|
54032
|
+
`)) {
|
|
54033
|
+
if (line.trim() === "") {
|
|
54034
|
+
continue;
|
|
54035
|
+
}
|
|
54036
|
+
const separator = line.indexOf("=");
|
|
54037
|
+
const key = line.slice(0, separator);
|
|
54038
|
+
if (separator <= 0 || !ENV_KEY.test(key)) {
|
|
54039
|
+
return null;
|
|
54040
|
+
}
|
|
54041
|
+
values[key] = line.slice(separator + 1);
|
|
54042
|
+
}
|
|
54043
|
+
const ingestUrl = values.KODIZM_LIVE_INGEST_URL ?? "";
|
|
54044
|
+
const token = values.KODIZM_LIVE_TOKEN ?? "";
|
|
54045
|
+
const gen = Number(values.KODIZM_LIVE_GEN);
|
|
54046
|
+
if (!/^https?:\/\//.test(ingestUrl) || !Number.isInteger(gen) || gen < 1 || tokenGen(token) !== gen) {
|
|
54047
|
+
return null;
|
|
54048
|
+
}
|
|
54049
|
+
return {
|
|
54050
|
+
ingestUrl: ingestUrl.replace(/\/+$/, ""),
|
|
54051
|
+
token,
|
|
54052
|
+
gen,
|
|
54053
|
+
authEnv: Object.fromEntries(Object.entries(values).filter(([key]) => !key.startsWith(LIVE_KEY_PREFIX)))
|
|
54054
|
+
};
|
|
54055
|
+
}
|
|
54056
|
+
async function readSecretFile(path) {
|
|
54057
|
+
const info = await stat3(path).catch((error) => {
|
|
54058
|
+
if (error.code === "ENOENT") {
|
|
54059
|
+
return null;
|
|
54060
|
+
}
|
|
54061
|
+
throw error;
|
|
54062
|
+
});
|
|
54063
|
+
if (info === null || !info.isFile() || (info.mode & 511) !== 384) {
|
|
54064
|
+
return null;
|
|
54065
|
+
}
|
|
54066
|
+
return readFile4(path, "utf8");
|
|
54067
|
+
}
|
|
54068
|
+
async function readFileOrNull(path) {
|
|
54069
|
+
return readFile4(path, "utf8").catch((error) => {
|
|
54070
|
+
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
54071
|
+
return null;
|
|
54072
|
+
}
|
|
54073
|
+
throw error;
|
|
54074
|
+
});
|
|
54075
|
+
}
|
|
54076
|
+
async function takeFile(path, read) {
|
|
54077
|
+
try {
|
|
54078
|
+
return await read(path);
|
|
54079
|
+
} finally {
|
|
54080
|
+
await removeFile(path);
|
|
54081
|
+
}
|
|
54082
|
+
}
|
|
54083
|
+
async function removeFile(path) {
|
|
54084
|
+
await unlink2(path).catch((error) => {
|
|
54085
|
+
if (error.code !== "ENOENT") {
|
|
54086
|
+
throw error;
|
|
54087
|
+
}
|
|
54088
|
+
});
|
|
54089
|
+
}
|
|
54090
|
+
function decisionFrom(body) {
|
|
54091
|
+
const status = isRecord(body) ? body.status : undefined;
|
|
54092
|
+
switch (status) {
|
|
54093
|
+
case "pending":
|
|
54094
|
+
return "pending";
|
|
54095
|
+
case "allowed":
|
|
54096
|
+
return { behavior: "allow" };
|
|
54097
|
+
case "denied":
|
|
54098
|
+
return {
|
|
54099
|
+
behavior: "deny",
|
|
54100
|
+
message: (isRecord(body) ? text(body.message) : undefined) ?? "Denied by the operator."
|
|
54101
|
+
};
|
|
54102
|
+
case "expired":
|
|
54103
|
+
return {
|
|
54104
|
+
behavior: "deny",
|
|
54105
|
+
message: "The permission request expired."
|
|
54106
|
+
};
|
|
54107
|
+
default:
|
|
54108
|
+
return;
|
|
54109
|
+
}
|
|
54110
|
+
}
|
|
54111
|
+
async function fetchJson(url, options) {
|
|
54112
|
+
const timeout = AbortSignal.timeout(options.timeoutMs);
|
|
54113
|
+
const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout]);
|
|
54114
|
+
let status;
|
|
54115
|
+
let raw;
|
|
54116
|
+
try {
|
|
54117
|
+
const response = await fetch(url, {
|
|
54118
|
+
method: options.method,
|
|
54119
|
+
headers: {
|
|
54120
|
+
authorization: `Bearer ${options.bearer}`,
|
|
54121
|
+
accept: "application/json",
|
|
54122
|
+
...options.body === undefined ? {} : { "content-type": "application/json" }
|
|
54123
|
+
},
|
|
54124
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
54125
|
+
signal
|
|
54126
|
+
});
|
|
54127
|
+
status = response.status;
|
|
54128
|
+
raw = await response.text();
|
|
54129
|
+
} catch (error) {
|
|
54130
|
+
if (error instanceof TypeError || error instanceof DOMException && isAbort(error)) {
|
|
54131
|
+
return { status: 0, body: undefined };
|
|
54132
|
+
}
|
|
54133
|
+
throw error;
|
|
54134
|
+
}
|
|
54135
|
+
let body;
|
|
54136
|
+
try {
|
|
54137
|
+
body = raw === "" ? undefined : JSON.parse(raw);
|
|
54138
|
+
} catch (error) {
|
|
54139
|
+
if (!(error instanceof SyntaxError)) {
|
|
54140
|
+
throw error;
|
|
54141
|
+
}
|
|
54142
|
+
}
|
|
54143
|
+
return {
|
|
54144
|
+
status,
|
|
54145
|
+
body
|
|
54146
|
+
};
|
|
54147
|
+
}
|
|
54148
|
+
function isAbort(error) {
|
|
54149
|
+
return error.name === "AbortError" || error.name === "TimeoutError";
|
|
54150
|
+
}
|
|
54151
|
+
function launchFailure(error) {
|
|
54152
|
+
if (error instanceof PaneError) {
|
|
54153
|
+
return error.reason === "start_timeout" ? "timeout" : "usage_error";
|
|
54154
|
+
}
|
|
54155
|
+
if (isErrno(error)) {
|
|
54156
|
+
return "usage_error";
|
|
54157
|
+
}
|
|
54158
|
+
throw error;
|
|
54159
|
+
}
|
|
54160
|
+
function deliveryFailure(error, id) {
|
|
54161
|
+
if (!(error instanceof PaneError)) {
|
|
54162
|
+
throw error;
|
|
54163
|
+
}
|
|
54164
|
+
return error.reason === "pane_dead" ? notRunning(id) : refuse("timeout");
|
|
54165
|
+
}
|
|
54166
|
+
function isErrno(error) {
|
|
54167
|
+
return error instanceof Error && typeof error.code === "string";
|
|
54168
|
+
}
|
|
54169
|
+
async function panePid(pane) {
|
|
54170
|
+
const tmux = Bun.which("tmux");
|
|
54171
|
+
if (tmux === null) {
|
|
54172
|
+
return;
|
|
54173
|
+
}
|
|
54174
|
+
const env = {
|
|
54175
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin"
|
|
54176
|
+
};
|
|
54177
|
+
if (process.env.TMUX_TMPDIR !== undefined) {
|
|
54178
|
+
env.TMUX_TMPDIR = process.env.TMUX_TMPDIR;
|
|
54179
|
+
}
|
|
54180
|
+
const child = Bun.spawn([tmux, "-L", pane.socket, "display-message", "-p", "-t", `=${pane.id}:`, "#{pane_pid}"], {
|
|
54181
|
+
env,
|
|
54182
|
+
stdin: "ignore",
|
|
54183
|
+
stdout: "pipe",
|
|
54184
|
+
stderr: "ignore"
|
|
54185
|
+
});
|
|
54186
|
+
const [exitCode, output] = await Promise.all([child.exited, new Response(child.stdout).text()]);
|
|
54187
|
+
const pid = Number(output.trim());
|
|
54188
|
+
return exitCode === 0 && Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
54189
|
+
}
|
|
54190
|
+
function processAlive(pid) {
|
|
54191
|
+
try {
|
|
54192
|
+
process.kill(pid, 0);
|
|
54193
|
+
return true;
|
|
54194
|
+
} catch (error) {
|
|
54195
|
+
if (isErrno(error) && error.code === "ESRCH") {
|
|
54196
|
+
return false;
|
|
54197
|
+
}
|
|
54198
|
+
if (isErrno(error) && error.code === "EPERM") {
|
|
54199
|
+
return true;
|
|
54200
|
+
}
|
|
54201
|
+
throw error;
|
|
54202
|
+
}
|
|
54203
|
+
}
|
|
54204
|
+
function sameJson(left, right) {
|
|
54205
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
54206
|
+
}
|
|
54207
|
+
function parseRequest(line) {
|
|
54208
|
+
try {
|
|
54209
|
+
const value = JSON.parse(line);
|
|
54210
|
+
return isRecord(value) ? value : null;
|
|
54211
|
+
} catch (error) {
|
|
54212
|
+
if (error instanceof SyntaxError) {
|
|
54213
|
+
return null;
|
|
54214
|
+
}
|
|
54215
|
+
throw error;
|
|
54216
|
+
}
|
|
54217
|
+
}
|
|
54218
|
+
function stringArgs(value) {
|
|
54219
|
+
if (!isRecord(value)) {
|
|
54220
|
+
return {};
|
|
54221
|
+
}
|
|
54222
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
54223
|
+
}
|
|
54224
|
+
function accept(data) {
|
|
54225
|
+
return {
|
|
54226
|
+
ok: true,
|
|
54227
|
+
code: "ok",
|
|
54228
|
+
data: Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined))
|
|
54229
|
+
};
|
|
54230
|
+
}
|
|
54231
|
+
function refuse(code, data) {
|
|
54232
|
+
return data === undefined ? { ok: false, code } : { ok: false, code, data };
|
|
54233
|
+
}
|
|
54234
|
+
function notRunning(id) {
|
|
54235
|
+
return refuse("pane_not_running", {
|
|
54236
|
+
pane: id,
|
|
54237
|
+
running: false
|
|
54238
|
+
});
|
|
54239
|
+
}
|
|
54240
|
+
var HEARTBEAT_INTERVAL_MS = 20000, WATCHDOG_QUIET_MS = 30000, CANCEL_TIMEOUT_MS = 3000, PERMISSION_POLL_MS = 1500, INGEST_TIMEOUT_MS = 1e4, MAX_BATCH = 200, DEFAULT_MAX_PANES = 4, STOP_DRAIN_MS = 1e4, PERMISSION_WAIT_MS, MIN_SECRET_LENGTH = 8, MAX_REQUEST_BYTES, PANE_ID2, ENV_KEY, LIVE_KEY_PREFIX = "KODIZM_LIVE_", TOKEN_PREFIX = "kdz-int-jwt.", COMMAND_NAMES, REJECTION_RULES, EXIT_CODES, DEFAULT_TIMINGS;
|
|
54241
|
+
var init_daemon = __esm(() => {
|
|
54242
|
+
init_logger();
|
|
54243
|
+
init_events2();
|
|
54244
|
+
init_hooks();
|
|
54245
|
+
init_input();
|
|
54246
|
+
init_pane();
|
|
54247
|
+
init_transcript();
|
|
54248
|
+
PERMISSION_WAIT_MS = PERMISSION_HOOK_TIMEOUT_S * 1000 - 1e4;
|
|
54249
|
+
MAX_REQUEST_BYTES = 8 * 1024 * 1024;
|
|
54250
|
+
PANE_ID2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
54251
|
+
ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
54252
|
+
COMMAND_NAMES = new Set(["compact", "clear", "model"]);
|
|
54253
|
+
REJECTION_RULES = {
|
|
54254
|
+
slash_command: "leading_slash",
|
|
54255
|
+
bash_mode: "leading_bang",
|
|
54256
|
+
empty: "empty",
|
|
54257
|
+
unknown_command: "unknown_command",
|
|
54258
|
+
invalid_model_alias: "invalid_model_alias"
|
|
54259
|
+
};
|
|
54260
|
+
EXIT_CODES = {
|
|
54261
|
+
ok: 0,
|
|
54262
|
+
usage_error: 2,
|
|
54263
|
+
dialog_open: 3,
|
|
54264
|
+
prompt_rejected: 3,
|
|
54265
|
+
pane_cap: 3,
|
|
54266
|
+
pane_exists: 3,
|
|
54267
|
+
busy: 3,
|
|
54268
|
+
pane_not_running: 4,
|
|
54269
|
+
timeout: 5
|
|
54270
|
+
};
|
|
54271
|
+
DEFAULT_TIMINGS = {
|
|
54272
|
+
heartbeatMs: HEARTBEAT_INTERVAL_MS,
|
|
54273
|
+
watchdogQuietMs: WATCHDOG_QUIET_MS,
|
|
54274
|
+
cancelTimeoutMs: CANCEL_TIMEOUT_MS,
|
|
54275
|
+
permissionPollMs: PERMISSION_POLL_MS,
|
|
54276
|
+
ingestTimeoutMs: INGEST_TIMEOUT_MS,
|
|
54277
|
+
monitorMs: 2000,
|
|
54278
|
+
transcriptPollMs: 250,
|
|
54279
|
+
retryBaseMs: 250,
|
|
54280
|
+
retryMaxMs: 1e4
|
|
54281
|
+
};
|
|
54282
|
+
});
|
|
54283
|
+
|
|
54284
|
+
// src/live/ctl.ts
|
|
54285
|
+
import { spawn as spawn2 } from "child_process";
|
|
54286
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
54287
|
+
import { resolve as resolve5 } from "path";
|
|
54288
|
+
async function runTmuxd(args) {
|
|
54289
|
+
const [command, ...rest] = args;
|
|
54290
|
+
switch (command) {
|
|
54291
|
+
case "ctl":
|
|
54292
|
+
return runCtl(rest);
|
|
54293
|
+
case "hook":
|
|
54294
|
+
return runHook(rest);
|
|
54295
|
+
case "daemon":
|
|
54296
|
+
return runDaemon();
|
|
54297
|
+
case undefined:
|
|
54298
|
+
case "--help":
|
|
54299
|
+
case "help":
|
|
54300
|
+
process.stdout.write(USAGE);
|
|
54301
|
+
return 0;
|
|
54302
|
+
default:
|
|
54303
|
+
process.stderr.write(USAGE);
|
|
54304
|
+
return 2;
|
|
54305
|
+
}
|
|
54306
|
+
}
|
|
54307
|
+
async function runCtl(args) {
|
|
54308
|
+
const [verb, ...rest] = args;
|
|
54309
|
+
const flags = parseFlags(rest);
|
|
54310
|
+
if (verb === undefined || !VERBS.has(verb) || flags === null) {
|
|
54311
|
+
await discardHandoff(verb, rest);
|
|
54312
|
+
return print({ ok: false, code: "usage_error" });
|
|
54313
|
+
}
|
|
54314
|
+
const socketPath = socketPathFromEnv();
|
|
54315
|
+
const message = {
|
|
54316
|
+
kind: "ctl",
|
|
54317
|
+
verb,
|
|
54318
|
+
args: flags
|
|
54319
|
+
};
|
|
54320
|
+
try {
|
|
54321
|
+
return print(await ask(socketPath, message, verb === "start"));
|
|
54322
|
+
} catch (error) {
|
|
54323
|
+
if (!(error instanceof SocketRequestError)) {
|
|
54324
|
+
throw error;
|
|
54325
|
+
}
|
|
54326
|
+
process.stderr.write(`${JSON.stringify({ level: "warn", message: error.message, verb })}
|
|
54327
|
+
`);
|
|
54328
|
+
if (error.reason === "unreachable") {
|
|
54329
|
+
await discardHandoff(verb, rest);
|
|
54330
|
+
}
|
|
54331
|
+
return print(error.reason === "unreachable" ? { ok: false, code: "pane_not_running", data: { pane: flags.pane, running: false } } : { ok: false, code: "timeout" });
|
|
54332
|
+
}
|
|
54333
|
+
}
|
|
54334
|
+
async function ask(socketPath, message, spawnIfMissing) {
|
|
54335
|
+
try {
|
|
54336
|
+
return toResult(await socketRequest(socketPath, message, CTL_TIMEOUT_MS));
|
|
54337
|
+
} catch (error) {
|
|
54338
|
+
if (!spawnIfMissing || !(error instanceof SocketRequestError) || error.reason !== "unreachable") {
|
|
54339
|
+
throw error;
|
|
54340
|
+
}
|
|
54341
|
+
}
|
|
54342
|
+
spawnDaemon();
|
|
54343
|
+
const deadline = Date.now() + DAEMON_BOOT_MS;
|
|
54344
|
+
for (;; ) {
|
|
54345
|
+
try {
|
|
54346
|
+
return toResult(await socketRequest(socketPath, message, CTL_TIMEOUT_MS));
|
|
54347
|
+
} catch (error) {
|
|
54348
|
+
if (!(error instanceof SocketRequestError) || error.reason !== "unreachable" || Date.now() > deadline) {
|
|
54349
|
+
throw error;
|
|
54350
|
+
}
|
|
54351
|
+
await Bun.sleep(50);
|
|
54352
|
+
}
|
|
54353
|
+
}
|
|
54354
|
+
}
|
|
54355
|
+
async function runHook(args) {
|
|
54356
|
+
const [event, ...rest] = args;
|
|
54357
|
+
const flags = parseFlags(rest);
|
|
54358
|
+
try {
|
|
54359
|
+
if (event === undefined || flags?.pane === undefined || flags.socket === undefined) {
|
|
54360
|
+
return 0;
|
|
54361
|
+
}
|
|
54362
|
+
const output = await runHookHelper({
|
|
54363
|
+
event,
|
|
54364
|
+
paneId: flags.pane,
|
|
54365
|
+
socketPath: flags.socket,
|
|
54366
|
+
input: await readStdin()
|
|
54367
|
+
});
|
|
54368
|
+
if (output !== "") {
|
|
54369
|
+
process.stdout.write(output);
|
|
54370
|
+
}
|
|
54371
|
+
} catch (error) {
|
|
54372
|
+
process.stderr.write(`${JSON.stringify({
|
|
54373
|
+
level: "error",
|
|
54374
|
+
message: "tmuxd hook failed",
|
|
54375
|
+
event,
|
|
54376
|
+
error: error instanceof Error ? error.message : String(error)
|
|
54377
|
+
})}
|
|
54378
|
+
`);
|
|
54379
|
+
}
|
|
54380
|
+
return 0;
|
|
54381
|
+
}
|
|
54382
|
+
async function runDaemon() {
|
|
54383
|
+
const env = process.env;
|
|
54384
|
+
const maxPanes = Number(env.KODIZM_TMUXD_MAX_PANES);
|
|
54385
|
+
let shutdown = () => {};
|
|
54386
|
+
const stopped = new Promise((resolveStopped) => {
|
|
54387
|
+
shutdown = resolveStopped;
|
|
54388
|
+
});
|
|
54389
|
+
const daemon = new LiveDaemon({
|
|
54390
|
+
socketPath: socketPathFromEnv(),
|
|
54391
|
+
helperArgv: [process.execPath, process.argv[1] ?? ""],
|
|
54392
|
+
home: env.HOME ?? homedir2(),
|
|
54393
|
+
tmuxSocket: env.KODIZM_TMUXD_TMUX_SOCKET ?? LIVE_SOCKET,
|
|
54394
|
+
claudeBin: env.CLAUDE_CODE_PATH,
|
|
54395
|
+
baseSettingsPath: env.KODIZM_LIVE_BASE_SETTINGS ?? DEFAULT_BASE_SETTINGS_PATH,
|
|
54396
|
+
maxPanes: Number.isInteger(maxPanes) && maxPanes > 0 ? maxPanes : DEFAULT_MAX_PANES,
|
|
54397
|
+
logger: createLogger({ env }),
|
|
54398
|
+
onIdle: () => void daemon.close().then(shutdown)
|
|
54399
|
+
});
|
|
54400
|
+
await daemon.listen();
|
|
54401
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
54402
|
+
process.on(signal, () => void daemon.close().then(shutdown));
|
|
54403
|
+
}
|
|
54404
|
+
await stopped;
|
|
54405
|
+
return 0;
|
|
54406
|
+
}
|
|
54407
|
+
function spawnDaemon() {
|
|
54408
|
+
const child = spawn2(process.execPath, [process.argv[1] ?? "", "tmuxd", "daemon"], {
|
|
54409
|
+
detached: true,
|
|
54410
|
+
stdio: "ignore",
|
|
54411
|
+
env: process.env
|
|
54412
|
+
});
|
|
54413
|
+
child.unref();
|
|
54414
|
+
}
|
|
54415
|
+
function socketPathFromEnv() {
|
|
54416
|
+
return process.env.KODIZM_TMUXD_SOCKET ?? defaultSocketPath(process.env, tmpdir());
|
|
54417
|
+
}
|
|
54418
|
+
function parseFlags(args) {
|
|
54419
|
+
const flags = {};
|
|
54420
|
+
for (let index = 0;index < args.length; index += 2) {
|
|
54421
|
+
const flag = args[index] ?? "";
|
|
54422
|
+
const value = args[index + 1];
|
|
54423
|
+
const name = flag.slice(2);
|
|
54424
|
+
if (!flag.startsWith("--") || name === "" || value === undefined || name in flags) {
|
|
54425
|
+
return null;
|
|
54426
|
+
}
|
|
54427
|
+
flags[name] = PATH_FLAGS.has(name) ? resolve5(value) : value;
|
|
54428
|
+
}
|
|
54429
|
+
return flags;
|
|
54430
|
+
}
|
|
54431
|
+
async function discardHandoff(verb, args) {
|
|
54432
|
+
const flag = verb === undefined ? undefined : HANDOFF_FLAGS[verb];
|
|
54433
|
+
if (flag === undefined) {
|
|
54434
|
+
return;
|
|
54435
|
+
}
|
|
54436
|
+
for (let index = 0;index < args.length - 1; index += 1) {
|
|
54437
|
+
const value = args[index + 1];
|
|
54438
|
+
if (args[index] === `--${flag}` && value !== undefined) {
|
|
54439
|
+
await removeFile(resolve5(value));
|
|
54440
|
+
}
|
|
54441
|
+
}
|
|
54442
|
+
}
|
|
54443
|
+
function toResult(response) {
|
|
54444
|
+
const code = typeof response === "object" && response !== null ? response.code : undefined;
|
|
54445
|
+
if (typeof code !== "string" || typeof exitCodeFor(code) !== "number") {
|
|
54446
|
+
throw new SocketRequestError("malformed");
|
|
54447
|
+
}
|
|
54448
|
+
return response;
|
|
54449
|
+
}
|
|
54450
|
+
function print(result) {
|
|
54451
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
54452
|
+
`);
|
|
54453
|
+
return exitCodeFor(result.code);
|
|
54454
|
+
}
|
|
54455
|
+
async function readStdin() {
|
|
54456
|
+
let timer;
|
|
54457
|
+
const timeout = new Promise((resolveTimeout) => {
|
|
54458
|
+
timer = setTimeout(() => resolveTimeout(""), STDIN_TIMEOUT_MS);
|
|
54459
|
+
});
|
|
54460
|
+
const input = await Promise.race([Bun.stdin.text(), timeout]);
|
|
54461
|
+
clearTimeout(timer);
|
|
54462
|
+
return input;
|
|
54463
|
+
}
|
|
54464
|
+
var USAGE, VERBS, PATH_FLAGS, HANDOFF_FLAGS, CTL_TIMEOUT_MS = 60000, DAEMON_BOOT_MS = 5000, STDIN_TIMEOUT_MS = 2000;
|
|
54465
|
+
var init_ctl = __esm(() => {
|
|
54466
|
+
init_logger();
|
|
54467
|
+
init_daemon();
|
|
54468
|
+
init_hooks();
|
|
54469
|
+
init_pane();
|
|
54470
|
+
USAGE = `Usage: kodizm-acp tmuxd <command>
|
|
54471
|
+
|
|
54472
|
+
Commands:
|
|
54473
|
+
ctl <verb> [flags] Drive a Live pane through the daemon; prints one JSON line.
|
|
54474
|
+
Verbs: start, prompt, command, cancel, stop, rotate-token, status.
|
|
54475
|
+
hook <Event> --pane <id> --socket <path> Hook helper the pane settings run; reads the payload on stdin.
|
|
54476
|
+
daemon Run the daemon in the foreground; ctl start spawns it detached.
|
|
54477
|
+
|
|
54478
|
+
Environment:
|
|
54479
|
+
KODIZM_TMUXD_SOCKET daemon socket (default: $XDG_RUNTIME_DIR or the temp dir, kodizm-live-<uid>/tmuxd.sock)
|
|
54480
|
+
KODIZM_TMUXD_TMUX_SOCKET tmux socket name (default: ${LIVE_SOCKET})
|
|
54481
|
+
KODIZM_TMUXD_MAX_PANES panes per daemon (default: ${DEFAULT_MAX_PANES})
|
|
54482
|
+
KODIZM_LIVE_BASE_SETTINGS base settings with the deny list (default: ${DEFAULT_BASE_SETTINGS_PATH})
|
|
54483
|
+
CLAUDE_CODE_PATH claude binary (default: claude on PATH)
|
|
54484
|
+
`;
|
|
54485
|
+
VERBS = new Set(["start", "prompt", "command", "cancel", "stop", "rotate-token", "status"]);
|
|
54486
|
+
PATH_FLAGS = new Set(["settings-base", "env-file", "file", "token-file"]);
|
|
54487
|
+
HANDOFF_FLAGS = {
|
|
54488
|
+
start: "env-file",
|
|
54489
|
+
prompt: "file",
|
|
54490
|
+
"rotate-token": "token-file"
|
|
54491
|
+
};
|
|
54492
|
+
});
|
|
52134
54493
|
// package.json
|
|
52135
54494
|
var package_default = {
|
|
52136
54495
|
name: "@kodizm/acp",
|
|
52137
|
-
version: "0.
|
|
52138
|
-
description: "Custom ACP server bridging Claude Code
|
|
54496
|
+
version: "0.8.0",
|
|
54497
|
+
description: "Custom ACP server bridging Claude Code and opencode CLIs through a single Kodizm-flavored protocol surface.",
|
|
52139
54498
|
type: "module",
|
|
52140
54499
|
bin: {
|
|
52141
54500
|
"kodizm-acp": "./dist/index.js"
|
|
@@ -52167,17 +54526,18 @@ var package_default = {
|
|
|
52167
54526
|
publishConfig: {
|
|
52168
54527
|
access: "public"
|
|
52169
54528
|
},
|
|
52170
|
-
|
|
52171
|
-
|
|
52172
|
-
"@anthropic-ai/sdk": ">=0.93.0",
|
|
52173
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
52174
|
-
"@opencode-ai/sdk": "^1.18.32",
|
|
52175
|
-
zod: "^4.0.0"
|
|
54529
|
+
config: {
|
|
54530
|
+
claudeAgentSdk: "0.3.280"
|
|
52176
54531
|
},
|
|
52177
54532
|
devDependencies: {
|
|
54533
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.280",
|
|
54534
|
+
"@anthropic-ai/sdk": ">=0.93.0",
|
|
52178
54535
|
"@biomejs/biome": "^1.9.0",
|
|
54536
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54537
|
+
"@opencode-ai/sdk": "^1.18.32",
|
|
52179
54538
|
"@types/bun": "latest",
|
|
52180
|
-
typescript: "^5.6.0"
|
|
54539
|
+
typescript: "^5.6.0",
|
|
54540
|
+
zod: "^4.0.0"
|
|
52181
54541
|
},
|
|
52182
54542
|
license: "Apache-2.0",
|
|
52183
54543
|
homepage: "https://github.com/kodizm/acp",
|
|
@@ -52189,7 +54549,6 @@ var package_default = {
|
|
|
52189
54549
|
"acp",
|
|
52190
54550
|
"agent-client-protocol",
|
|
52191
54551
|
"claude",
|
|
52192
|
-
"codex",
|
|
52193
54552
|
"opencode",
|
|
52194
54553
|
"ai",
|
|
52195
54554
|
"bridge",
|
|
@@ -52215,6 +54574,8 @@ function ensureCapability(caps, required, method) {
|
|
|
52215
54574
|
// src/wire/schemas.ts
|
|
52216
54575
|
init_zod();
|
|
52217
54576
|
var AbsolutePathSchema = string2().regex(/^\//, 'path must be absolute (start with "/")');
|
|
54577
|
+
var PLUGIN_ROOT = "/opt/kodizm/plugins/";
|
|
54578
|
+
var PluginPathSchema = AbsolutePathSchema.refine((path) => path.length > PLUGIN_ROOT.length && path.startsWith(PLUGIN_ROOT) && !path.split("/").includes(".."), { message: `plugin path must live under ${PLUGIN_ROOT} with no ".." segment` });
|
|
52218
54579
|
var McpServerSchema = object({
|
|
52219
54580
|
type: literal("http"),
|
|
52220
54581
|
name: string2().min(1),
|
|
@@ -52251,7 +54612,8 @@ var CANONICAL_FIELDS_BANNED_IN_META = [
|
|
|
52251
54612
|
"heartbeatIntervalMs",
|
|
52252
54613
|
"inactivityThresholdMs",
|
|
52253
54614
|
"settingSources",
|
|
52254
|
-
"features"
|
|
54615
|
+
"features",
|
|
54616
|
+
"plugins"
|
|
52255
54617
|
];
|
|
52256
54618
|
var SettingSourcesSchema = array(_enum(["user", "project", "local"]));
|
|
52257
54619
|
var PermissionModeSchema = _enum(["default", "acceptEdits", "plan", "dontAsk", "bypassPermissions"]);
|
|
@@ -52294,6 +54656,7 @@ var InitializeRequestSchema = object({
|
|
|
52294
54656
|
});
|
|
52295
54657
|
var SessionOptionFields = {
|
|
52296
54658
|
additionalDirectories: array(AbsolutePathSchema).optional(),
|
|
54659
|
+
plugins: array(PluginPathSchema).optional(),
|
|
52297
54660
|
systemPrompt: SystemPromptSchema.optional(),
|
|
52298
54661
|
model: string2().optional(),
|
|
52299
54662
|
skills: array(string2()).optional(),
|
|
@@ -52858,6 +55221,10 @@ async function main() {
|
|
|
52858
55221
|
}
|
|
52859
55222
|
}
|
|
52860
55223
|
if (import.meta.main) {
|
|
55224
|
+
if (process.argv[2] === "tmuxd") {
|
|
55225
|
+
await Promise.resolve().then(() => init_ctl());
|
|
55226
|
+
process.exit(await runTmuxd(process.argv.slice(3)));
|
|
55227
|
+
}
|
|
52861
55228
|
await main();
|
|
52862
55229
|
}
|
|
52863
55230
|
export {
|