hatchee 0.2.0 → 0.4.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/cli.mjs +597 -69
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7925,8 +7925,13 @@ function toolTitle(name, input) {
|
|
|
7925
7925
|
return `WebSearch(${clip(String(input?.query ?? "?"), 64)})`;
|
|
7926
7926
|
case "Task":
|
|
7927
7927
|
return `Task(${clip(String(input?.description ?? "agent"), 64)})`;
|
|
7928
|
-
case "TodoWrite":
|
|
7929
|
-
|
|
7928
|
+
case "TodoWrite": {
|
|
7929
|
+
const todos = Array.isArray(input?.todos) ? input.todos : [];
|
|
7930
|
+
const done = todos.filter((t) => t?.status === "completed").length;
|
|
7931
|
+
return todos.length ? `Update Todos (${done}/${todos.length} done)` : "Update Todos";
|
|
7932
|
+
}
|
|
7933
|
+
case "ExitPlanMode":
|
|
7934
|
+
return "Plan ready for review";
|
|
7930
7935
|
case "NotebookEdit":
|
|
7931
7936
|
return `Edit(${clip(String(input?.notebook_path ?? "?"), 64)})`;
|
|
7932
7937
|
default:
|
|
@@ -7953,6 +7958,14 @@ function toolDetail(name, input) {
|
|
|
7953
7958
|
return String(input?.query ?? "");
|
|
7954
7959
|
case "Task":
|
|
7955
7960
|
return String(input?.prompt ?? input?.description ?? "").slice(0, 600);
|
|
7961
|
+
case "TodoWrite": {
|
|
7962
|
+
const todos = Array.isArray(input?.todos) ? input.todos : [];
|
|
7963
|
+
const box = (s) => s === "completed" ? "☑" : s === "in_progress" ? "◐" : "☐";
|
|
7964
|
+
return todos.map((t) => `${box(String(t?.status))} ${String(t?.content ?? "")}`).join(`
|
|
7965
|
+
`).slice(0, 1200);
|
|
7966
|
+
}
|
|
7967
|
+
case "ExitPlanMode":
|
|
7968
|
+
return String(input?.plan ?? "").slice(0, 2000);
|
|
7956
7969
|
default:
|
|
7957
7970
|
try {
|
|
7958
7971
|
const s = JSON.stringify(input);
|
|
@@ -8005,7 +8018,7 @@ function flattenItem(item) {
|
|
|
8005
8018
|
return { text: item.text, color: item.color };
|
|
8006
8019
|
}
|
|
8007
8020
|
}
|
|
8008
|
-
var PROTOCOL_VERSION =
|
|
8021
|
+
var PROTOCOL_VERSION = 4, PROTOCOL_MIN = 1, AgentState, RuleAction, Rule, DiffStat, LineColor, TermLine, ToolDecision, TranscriptItem, SessionSnapshot, ServerHello, Encrypted, SessionUpsert, SessionRemoved, Line, Item, PermissionRequest, PermissionResolved, ErrorMsg, Pong, RulesUpdate, DirEntry, DirsUpdate, RecentSession, SessionsUpdate, DiffFile, DiffUpdate, DaemonToPhone, DaemonInner, ClientHello, PermissionReply, RulesSync, UserMessage, SpawnMode, SpawnEffort, Spawn, Stop, SetSession, Interrupt, ListDirs, ListSessions, GetDiff, Ping, PushRegister, PhoneToDaemon, PhoneInner, clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
8009
8022
|
var init_src = __esm(() => {
|
|
8010
8023
|
init_zod();
|
|
8011
8024
|
AgentState = exports_external.enum([
|
|
@@ -8076,6 +8089,7 @@ var init_src = __esm(() => {
|
|
|
8076
8089
|
plan: exports_external.array(exports_external.string()).default([]),
|
|
8077
8090
|
diff: exports_external.array(DiffStat).default([]),
|
|
8078
8091
|
spawned: exports_external.boolean().default(false),
|
|
8092
|
+
mode: exports_external.string().nullable().default(null),
|
|
8079
8093
|
startedAt: exports_external.number(),
|
|
8080
8094
|
updatedAt: exports_external.number()
|
|
8081
8095
|
});
|
|
@@ -8138,6 +8152,26 @@ var init_src = __esm(() => {
|
|
|
8138
8152
|
t: exports_external.literal("dirs"),
|
|
8139
8153
|
dirs: exports_external.array(DirEntry)
|
|
8140
8154
|
});
|
|
8155
|
+
RecentSession = exports_external.object({
|
|
8156
|
+
id: exports_external.string(),
|
|
8157
|
+
cwd: exports_external.string(),
|
|
8158
|
+
name: exports_external.string(),
|
|
8159
|
+
summary: exports_external.string(),
|
|
8160
|
+
updatedAt: exports_external.number()
|
|
8161
|
+
});
|
|
8162
|
+
SessionsUpdate = exports_external.object({
|
|
8163
|
+
t: exports_external.literal("sessions"),
|
|
8164
|
+
sessions: exports_external.array(RecentSession)
|
|
8165
|
+
});
|
|
8166
|
+
DiffFile = exports_external.object({
|
|
8167
|
+
file: exports_external.string(),
|
|
8168
|
+
patch: exports_external.string()
|
|
8169
|
+
});
|
|
8170
|
+
DiffUpdate = exports_external.object({
|
|
8171
|
+
t: exports_external.literal("diff"),
|
|
8172
|
+
sessionId: exports_external.string(),
|
|
8173
|
+
files: exports_external.array(DiffFile)
|
|
8174
|
+
});
|
|
8141
8175
|
DaemonToPhone = exports_external.discriminatedUnion("t", [
|
|
8142
8176
|
ServerHello,
|
|
8143
8177
|
SessionUpsert,
|
|
@@ -8150,6 +8184,8 @@ var init_src = __esm(() => {
|
|
|
8150
8184
|
Pong,
|
|
8151
8185
|
RulesUpdate,
|
|
8152
8186
|
DirsUpdate,
|
|
8187
|
+
SessionsUpdate,
|
|
8188
|
+
DiffUpdate,
|
|
8153
8189
|
Encrypted
|
|
8154
8190
|
]);
|
|
8155
8191
|
DaemonInner = exports_external.discriminatedUnion("t", [
|
|
@@ -8162,7 +8198,9 @@ var init_src = __esm(() => {
|
|
|
8162
8198
|
ErrorMsg,
|
|
8163
8199
|
Pong,
|
|
8164
8200
|
RulesUpdate,
|
|
8165
|
-
DirsUpdate
|
|
8201
|
+
DirsUpdate,
|
|
8202
|
+
SessionsUpdate,
|
|
8203
|
+
DiffUpdate
|
|
8166
8204
|
]);
|
|
8167
8205
|
ClientHello = exports_external.object({
|
|
8168
8206
|
t: exports_external.literal("hello"),
|
|
@@ -8175,7 +8213,8 @@ var init_src = __esm(() => {
|
|
|
8175
8213
|
t: exports_external.literal("permission_reply"),
|
|
8176
8214
|
requestId: exports_external.string(),
|
|
8177
8215
|
allow: exports_external.boolean(),
|
|
8178
|
-
always: exports_external.boolean().default(false)
|
|
8216
|
+
always: exports_external.boolean().default(false),
|
|
8217
|
+
answer: exports_external.string().nullable().default(null)
|
|
8179
8218
|
});
|
|
8180
8219
|
RulesSync = exports_external.object({
|
|
8181
8220
|
t: exports_external.literal("rules_sync"),
|
|
@@ -8194,13 +8233,26 @@ var init_src = __esm(() => {
|
|
|
8194
8233
|
cwd: exports_external.string().nullable().default(null),
|
|
8195
8234
|
mode: SpawnMode.default("edits"),
|
|
8196
8235
|
model: exports_external.string().nullable().default(null),
|
|
8197
|
-
effort: SpawnEffort.nullable().default(null)
|
|
8236
|
+
effort: SpawnEffort.nullable().default(null),
|
|
8237
|
+
resume: exports_external.string().nullable().default(null)
|
|
8198
8238
|
});
|
|
8199
8239
|
Stop = exports_external.object({
|
|
8200
8240
|
t: exports_external.literal("stop"),
|
|
8201
8241
|
sessionId: exports_external.string()
|
|
8202
8242
|
});
|
|
8243
|
+
SetSession = exports_external.object({
|
|
8244
|
+
t: exports_external.literal("set_session"),
|
|
8245
|
+
sessionId: exports_external.string(),
|
|
8246
|
+
mode: SpawnMode.nullable().default(null),
|
|
8247
|
+
model: exports_external.string().nullable().default(null)
|
|
8248
|
+
});
|
|
8249
|
+
Interrupt = exports_external.object({
|
|
8250
|
+
t: exports_external.literal("interrupt"),
|
|
8251
|
+
sessionId: exports_external.string()
|
|
8252
|
+
});
|
|
8203
8253
|
ListDirs = exports_external.object({ t: exports_external.literal("list_dirs") });
|
|
8254
|
+
ListSessions = exports_external.object({ t: exports_external.literal("list_sessions") });
|
|
8255
|
+
GetDiff = exports_external.object({ t: exports_external.literal("get_diff"), sessionId: exports_external.string() });
|
|
8204
8256
|
Ping = exports_external.object({ t: exports_external.literal("ping") });
|
|
8205
8257
|
PushRegister = exports_external.object({
|
|
8206
8258
|
t: exports_external.literal("push_register"),
|
|
@@ -8215,7 +8267,11 @@ var init_src = __esm(() => {
|
|
|
8215
8267
|
UserMessage,
|
|
8216
8268
|
Spawn,
|
|
8217
8269
|
Stop,
|
|
8270
|
+
SetSession,
|
|
8271
|
+
Interrupt,
|
|
8218
8272
|
ListDirs,
|
|
8273
|
+
ListSessions,
|
|
8274
|
+
GetDiff,
|
|
8219
8275
|
Ping,
|
|
8220
8276
|
PushRegister,
|
|
8221
8277
|
Encrypted
|
|
@@ -8226,7 +8282,11 @@ var init_src = __esm(() => {
|
|
|
8226
8282
|
UserMessage,
|
|
8227
8283
|
Spawn,
|
|
8228
8284
|
Stop,
|
|
8285
|
+
SetSession,
|
|
8286
|
+
Interrupt,
|
|
8229
8287
|
ListDirs,
|
|
8288
|
+
ListSessions,
|
|
8289
|
+
GetDiff,
|
|
8230
8290
|
Ping,
|
|
8231
8291
|
PushRegister
|
|
8232
8292
|
]);
|
|
@@ -8251,6 +8311,7 @@ class SpawnedSession {
|
|
|
8251
8311
|
mode = "edits";
|
|
8252
8312
|
claudeSessionId = null;
|
|
8253
8313
|
stopped = false;
|
|
8314
|
+
busy = false;
|
|
8254
8315
|
constructor(daemon, sessionId, cwd, model = null, effort = null, claudeBin = process.env.DROVER_CLAUDE_BIN || "claude") {
|
|
8255
8316
|
this.daemon = daemon;
|
|
8256
8317
|
this.sessionId = sessionId;
|
|
@@ -8263,6 +8324,29 @@ class SpawnedSession {
|
|
|
8263
8324
|
this.mode = mode;
|
|
8264
8325
|
this.spawnProc(firstTask);
|
|
8265
8326
|
}
|
|
8327
|
+
resumeFrom(claudeSessionId) {
|
|
8328
|
+
this.claudeSessionId = claudeSessionId;
|
|
8329
|
+
}
|
|
8330
|
+
reconfigure(mode, model) {
|
|
8331
|
+
if (mode)
|
|
8332
|
+
this.mode = mode;
|
|
8333
|
+
if (model)
|
|
8334
|
+
this.model = model;
|
|
8335
|
+
if (this.proc && !this.busy) {
|
|
8336
|
+
try {
|
|
8337
|
+
this.proc.stdin.end();
|
|
8338
|
+
} catch {}
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
interrupt() {
|
|
8342
|
+
if (!this.proc?.stdin.writable)
|
|
8343
|
+
return;
|
|
8344
|
+
const msg = { type: "control_request", request_id: crypto.randomUUID(), request: { subtype: "interrupt" } };
|
|
8345
|
+
try {
|
|
8346
|
+
this.proc.stdin.write(JSON.stringify(msg) + `
|
|
8347
|
+
`);
|
|
8348
|
+
} catch {}
|
|
8349
|
+
}
|
|
8266
8350
|
spawnProc(firstMessage) {
|
|
8267
8351
|
const args = [
|
|
8268
8352
|
"--print",
|
|
@@ -8297,6 +8381,7 @@ class SpawnedSession {
|
|
|
8297
8381
|
this.send(firstMessage);
|
|
8298
8382
|
}
|
|
8299
8383
|
send(text) {
|
|
8384
|
+
this.busy = true;
|
|
8300
8385
|
if (this.proc?.stdin.writable) {
|
|
8301
8386
|
const msg = { type: "user", message: { role: "user", content: text } };
|
|
8302
8387
|
this.proc.stdin.write(JSON.stringify(msg) + `
|
|
@@ -8307,6 +8392,7 @@ class SpawnedSession {
|
|
|
8307
8392
|
}
|
|
8308
8393
|
onProcExit(_code) {
|
|
8309
8394
|
this.proc = null;
|
|
8395
|
+
this.busy = false;
|
|
8310
8396
|
if (this.stopped)
|
|
8311
8397
|
return;
|
|
8312
8398
|
this.daemon.spawnState(this.sessionId, "waiting", "ready — send your next message");
|
|
@@ -8331,8 +8417,10 @@ class SpawnedSession {
|
|
|
8331
8417
|
switch (m.type) {
|
|
8332
8418
|
case "system":
|
|
8333
8419
|
if (m.subtype === "init") {
|
|
8334
|
-
if (typeof m.session_id === "string")
|
|
8420
|
+
if (typeof m.session_id === "string") {
|
|
8335
8421
|
this.claudeSessionId = m.session_id;
|
|
8422
|
+
this.daemon.adoptClaudeSession(this.sessionId, m.session_id);
|
|
8423
|
+
}
|
|
8336
8424
|
this.daemon.spawnState(this.sessionId, "working", "session ready");
|
|
8337
8425
|
}
|
|
8338
8426
|
break;
|
|
@@ -8377,13 +8465,23 @@ class SpawnedSession {
|
|
|
8377
8465
|
break;
|
|
8378
8466
|
}
|
|
8379
8467
|
case "result": {
|
|
8468
|
+
this.busy = false;
|
|
8380
8469
|
const ok = m.is_error !== true;
|
|
8470
|
+
const u = m.usage;
|
|
8471
|
+
if (u && (u.input_tokens || u.output_tokens)) {
|
|
8472
|
+
const secs = m.duration_ms ? `${(m.duration_ms / 1000).toFixed(1)}s · ` : "";
|
|
8473
|
+
this.daemon.spawnLine(this.sessionId, `⏱ ${secs}${fmtTokens(u.input_tokens)} in / ${fmtTokens(u.output_tokens)} out`, "muted");
|
|
8474
|
+
}
|
|
8381
8475
|
this.daemon.spawnState(this.sessionId, ok ? "waiting" : "error", ok ? "waiting for your next message" : String(m.result ?? "error"));
|
|
8382
8476
|
break;
|
|
8383
8477
|
}
|
|
8384
8478
|
}
|
|
8385
8479
|
}
|
|
8386
8480
|
}
|
|
8481
|
+
function fmtTokens(n) {
|
|
8482
|
+
const v = Number(n) || 0;
|
|
8483
|
+
return v >= 1000 ? `${(v / 1000).toFixed(1)}k` : String(v);
|
|
8484
|
+
}
|
|
8387
8485
|
function contentText(content) {
|
|
8388
8486
|
if (typeof content === "string")
|
|
8389
8487
|
return content;
|
|
@@ -11612,9 +11710,9 @@ function lanIPv4() {
|
|
|
11612
11710
|
|
|
11613
11711
|
// src/server.ts
|
|
11614
11712
|
import { createServer } from "node:http";
|
|
11615
|
-
import { readdirSync, statSync, existsSync as
|
|
11616
|
-
import { homedir as
|
|
11617
|
-
import { join as
|
|
11713
|
+
import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync3 } from "node:fs";
|
|
11714
|
+
import { homedir as homedir3 } from "node:os";
|
|
11715
|
+
import { join as join3, basename as basename2 } from "node:path";
|
|
11618
11716
|
|
|
11619
11717
|
// ../../node_modules/ws/wrapper.mjs
|
|
11620
11718
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -11688,6 +11786,258 @@ class Registry {
|
|
|
11688
11786
|
});
|
|
11689
11787
|
return { branch, branches, diff };
|
|
11690
11788
|
}
|
|
11789
|
+
gitDiff(cwd) {
|
|
11790
|
+
let raw = "";
|
|
11791
|
+
try {
|
|
11792
|
+
raw = execSync("git diff", { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 3000, maxBuffer: 4 * 1024 * 1024 }).toString();
|
|
11793
|
+
} catch {
|
|
11794
|
+
return [];
|
|
11795
|
+
}
|
|
11796
|
+
if (!raw.trim())
|
|
11797
|
+
return [];
|
|
11798
|
+
const files = [];
|
|
11799
|
+
for (const chunk of raw.split(/^diff --git /m).slice(1)) {
|
|
11800
|
+
if (files.length >= 12)
|
|
11801
|
+
break;
|
|
11802
|
+
const header = chunk.split(`
|
|
11803
|
+
`, 1)[0] ?? "";
|
|
11804
|
+
const file = header.match(/ b\/(.*)$/)?.[1] ?? header.split(" ").pop() ?? "?";
|
|
11805
|
+
const body = chunk.split(`
|
|
11806
|
+
`).filter((l) => l.startsWith("@@") || l.startsWith("+") || l.startsWith("-") || l.startsWith(" ")).join(`
|
|
11807
|
+
`);
|
|
11808
|
+
const lines = body.split(`
|
|
11809
|
+
`);
|
|
11810
|
+
const clipped = lines.length > 120 ? lines.slice(0, 120).join(`
|
|
11811
|
+
`) + `
|
|
11812
|
+
… (+${lines.length - 120} more lines)` : body;
|
|
11813
|
+
files.push({ file, patch: clipped.slice(0, 6000) });
|
|
11814
|
+
}
|
|
11815
|
+
return files;
|
|
11816
|
+
}
|
|
11817
|
+
prune(now = Date.now(), keepAlive = () => false) {
|
|
11818
|
+
const ENDED_TTL = 60 * 60000;
|
|
11819
|
+
const IDLE_TTL = 12 * 60 * 60000;
|
|
11820
|
+
const removed = [];
|
|
11821
|
+
for (const [id, s] of this.sessions) {
|
|
11822
|
+
if (keepAlive(id))
|
|
11823
|
+
continue;
|
|
11824
|
+
const ttl = ["done", "canceled", "error"].includes(s.state) ? ENDED_TTL : IDLE_TTL;
|
|
11825
|
+
if (now - s.updatedAt > ttl)
|
|
11826
|
+
removed.push(id);
|
|
11827
|
+
}
|
|
11828
|
+
for (const id of removed) {
|
|
11829
|
+
this.sessions.delete(id);
|
|
11830
|
+
this.items.delete(id);
|
|
11831
|
+
}
|
|
11832
|
+
return removed;
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
|
|
11836
|
+
// src/transcript.ts
|
|
11837
|
+
init_src();
|
|
11838
|
+
import { openSync, readSync, closeSync, fstatSync, readdirSync, statSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
11839
|
+
import { homedir as homedir2 } from "node:os";
|
|
11840
|
+
import { join as join2, basename, resolve, sep } from "node:path";
|
|
11841
|
+
var claudeDir = () => process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude");
|
|
11842
|
+
function isSafeTranscriptPath(path) {
|
|
11843
|
+
if (!path.endsWith(".jsonl"))
|
|
11844
|
+
return false;
|
|
11845
|
+
const real = resolve(path);
|
|
11846
|
+
const root = resolve(claudeDir());
|
|
11847
|
+
return real === root || real.startsWith(root + sep);
|
|
11848
|
+
}
|
|
11849
|
+
|
|
11850
|
+
class TranscriptReader {
|
|
11851
|
+
offsets = new Map;
|
|
11852
|
+
static MAX_READ = 256 * 1024;
|
|
11853
|
+
read(sessionId, path) {
|
|
11854
|
+
if (typeof path !== "string" || !path || !isSafeTranscriptPath(path))
|
|
11855
|
+
return [];
|
|
11856
|
+
if (basename(path) !== `${sessionId}.jsonl`)
|
|
11857
|
+
return [];
|
|
11858
|
+
let fd;
|
|
11859
|
+
try {
|
|
11860
|
+
fd = openSync(path, "r");
|
|
11861
|
+
} catch {
|
|
11862
|
+
return [];
|
|
11863
|
+
}
|
|
11864
|
+
try {
|
|
11865
|
+
const size = fstatSync(fd).size;
|
|
11866
|
+
let off = this.offsets.get(sessionId) ?? 0;
|
|
11867
|
+
if (off > size)
|
|
11868
|
+
off = 0;
|
|
11869
|
+
if (size <= off)
|
|
11870
|
+
return [];
|
|
11871
|
+
if (size - off > TranscriptReader.MAX_READ)
|
|
11872
|
+
off = size - TranscriptReader.MAX_READ;
|
|
11873
|
+
const buf = Buffer.alloc(size - off);
|
|
11874
|
+
readSync(fd, buf, 0, buf.length, off);
|
|
11875
|
+
const text = buf.toString("utf8");
|
|
11876
|
+
const lastNL = text.lastIndexOf(`
|
|
11877
|
+
`);
|
|
11878
|
+
if (lastNL < 0)
|
|
11879
|
+
return [];
|
|
11880
|
+
this.offsets.set(sessionId, off + Buffer.byteLength(text.slice(0, lastNL + 1)));
|
|
11881
|
+
const items = [];
|
|
11882
|
+
for (const line of text.slice(0, lastNL).split(`
|
|
11883
|
+
`)) {
|
|
11884
|
+
parseEntry(line, items, { proseOnly: true });
|
|
11885
|
+
}
|
|
11886
|
+
return items;
|
|
11887
|
+
} catch {
|
|
11888
|
+
return [];
|
|
11889
|
+
} finally {
|
|
11890
|
+
try {
|
|
11891
|
+
closeSync(fd);
|
|
11892
|
+
} catch {}
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
}
|
|
11896
|
+
function parseEntry(line, out, opts) {
|
|
11897
|
+
if (!line.trim())
|
|
11898
|
+
return;
|
|
11899
|
+
let e;
|
|
11900
|
+
try {
|
|
11901
|
+
e = JSON.parse(line);
|
|
11902
|
+
} catch {
|
|
11903
|
+
return;
|
|
11904
|
+
}
|
|
11905
|
+
if (e.isSidechain)
|
|
11906
|
+
return;
|
|
11907
|
+
const uuid = String(e.uuid ?? "");
|
|
11908
|
+
if (!uuid)
|
|
11909
|
+
return;
|
|
11910
|
+
const content = e.message?.content;
|
|
11911
|
+
if (e.type === "user" && !opts.proseOnly) {
|
|
11912
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join(`
|
|
11913
|
+
`) : "";
|
|
11914
|
+
if (text.trim() && !text.startsWith("<")) {
|
|
11915
|
+
out.push({ kind: "user", id: `${uuid}:u`, text: clipOutput(text.trim(), 10, 500) });
|
|
11916
|
+
}
|
|
11917
|
+
return;
|
|
11918
|
+
}
|
|
11919
|
+
if (e.type !== "assistant" || !Array.isArray(content))
|
|
11920
|
+
return;
|
|
11921
|
+
content.forEach((b, idx) => {
|
|
11922
|
+
if (b?.type === "text" && String(b.text ?? "").trim()) {
|
|
11923
|
+
out.push({ kind: "text", id: `${uuid}:${idx}`, text: clipOutput(String(b.text).trim(), 60, 4000) });
|
|
11924
|
+
} else if (b?.type === "thinking" && String(b.thinking ?? "").trim()) {
|
|
11925
|
+
out.push({ kind: "thinking", id: `${uuid}:${idx}`, text: clipOutput(String(b.thinking).trim(), 40, 2000) });
|
|
11926
|
+
}
|
|
11927
|
+
});
|
|
11928
|
+
}
|
|
11929
|
+
function listRecentSessions(limit = 20) {
|
|
11930
|
+
const root = join2(claudeDir(), "projects");
|
|
11931
|
+
let dirs = [];
|
|
11932
|
+
try {
|
|
11933
|
+
dirs = readdirSync(root);
|
|
11934
|
+
} catch {
|
|
11935
|
+
return [];
|
|
11936
|
+
}
|
|
11937
|
+
const found = [];
|
|
11938
|
+
for (const d of dirs) {
|
|
11939
|
+
const dir = join2(root, d);
|
|
11940
|
+
let files = [];
|
|
11941
|
+
try {
|
|
11942
|
+
files = readdirSync(dir);
|
|
11943
|
+
} catch {
|
|
11944
|
+
continue;
|
|
11945
|
+
}
|
|
11946
|
+
for (const f of files) {
|
|
11947
|
+
if (!f.endsWith(".jsonl"))
|
|
11948
|
+
continue;
|
|
11949
|
+
const p = join2(dir, f);
|
|
11950
|
+
let mtime = 0, size = 0;
|
|
11951
|
+
try {
|
|
11952
|
+
const st = statSync(p);
|
|
11953
|
+
mtime = st.mtimeMs;
|
|
11954
|
+
size = st.size;
|
|
11955
|
+
} catch {
|
|
11956
|
+
continue;
|
|
11957
|
+
}
|
|
11958
|
+
if (size < 200)
|
|
11959
|
+
continue;
|
|
11960
|
+
found.push({ id: f.slice(0, -6), cwd: "", name: "", summary: "", updatedAt: Math.round(mtime), mtime });
|
|
11961
|
+
found[found.length - 1].path = p;
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
found.sort((a, b) => b.mtime - a.mtime);
|
|
11965
|
+
const top = found.slice(0, limit);
|
|
11966
|
+
for (const s of top) {
|
|
11967
|
+
const meta = sessionMeta(s.path);
|
|
11968
|
+
s.cwd = meta.cwd;
|
|
11969
|
+
s.name = meta.cwd ? basename(meta.cwd) : "";
|
|
11970
|
+
s.summary = meta.summary;
|
|
11971
|
+
}
|
|
11972
|
+
return top.filter((s) => s.summary).map(({ id, cwd, name, summary, updatedAt }) => ({ id, cwd, name, summary, updatedAt }));
|
|
11973
|
+
}
|
|
11974
|
+
function sessionMeta(path) {
|
|
11975
|
+
let head = "";
|
|
11976
|
+
try {
|
|
11977
|
+
const fd = openSync(path, "r");
|
|
11978
|
+
try {
|
|
11979
|
+
const buf = Buffer.alloc(32 * 1024);
|
|
11980
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
11981
|
+
head = buf.toString("utf8", 0, n);
|
|
11982
|
+
} finally {
|
|
11983
|
+
closeSync(fd);
|
|
11984
|
+
}
|
|
11985
|
+
} catch {
|
|
11986
|
+
return { cwd: "", summary: "" };
|
|
11987
|
+
}
|
|
11988
|
+
let cwd = "", summary = "";
|
|
11989
|
+
for (const line of head.split(`
|
|
11990
|
+
`)) {
|
|
11991
|
+
if (!line.trim())
|
|
11992
|
+
continue;
|
|
11993
|
+
let e;
|
|
11994
|
+
try {
|
|
11995
|
+
e = JSON.parse(line);
|
|
11996
|
+
} catch {
|
|
11997
|
+
continue;
|
|
11998
|
+
}
|
|
11999
|
+
if (!cwd && typeof e.cwd === "string")
|
|
12000
|
+
cwd = e.cwd;
|
|
12001
|
+
if (!summary && e.type === "summary" && typeof e.summary === "string")
|
|
12002
|
+
summary = e.summary;
|
|
12003
|
+
if (!summary && e.type === "user" && !e.isSidechain) {
|
|
12004
|
+
const c = e.message?.content;
|
|
12005
|
+
const text = typeof c === "string" ? c : Array.isArray(c) ? c.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join(" ") : "";
|
|
12006
|
+
if (text.trim() && !text.startsWith("<"))
|
|
12007
|
+
summary = text.trim();
|
|
12008
|
+
}
|
|
12009
|
+
if (cwd && summary)
|
|
12010
|
+
break;
|
|
12011
|
+
}
|
|
12012
|
+
return { cwd, summary: summary.replace(/\s+/g, " ").slice(0, 120) };
|
|
12013
|
+
}
|
|
12014
|
+
function backfillItems(resumeId, maxItems = 20) {
|
|
12015
|
+
const root = join2(claudeDir(), "projects");
|
|
12016
|
+
let path = "";
|
|
12017
|
+
try {
|
|
12018
|
+
for (const d of readdirSync(root)) {
|
|
12019
|
+
const p = join2(root, d, `${resumeId}.jsonl`);
|
|
12020
|
+
if (existsSync2(p)) {
|
|
12021
|
+
path = p;
|
|
12022
|
+
break;
|
|
12023
|
+
}
|
|
12024
|
+
}
|
|
12025
|
+
} catch {
|
|
12026
|
+
return [];
|
|
12027
|
+
}
|
|
12028
|
+
if (!path)
|
|
12029
|
+
return [];
|
|
12030
|
+
let raw = "";
|
|
12031
|
+
try {
|
|
12032
|
+
raw = readFileSync2(path, "utf8");
|
|
12033
|
+
} catch {
|
|
12034
|
+
return [];
|
|
12035
|
+
}
|
|
12036
|
+
const items = [];
|
|
12037
|
+
for (const line of raw.split(`
|
|
12038
|
+
`))
|
|
12039
|
+
parseEntry(line, items, { proseOnly: false });
|
|
12040
|
+
return items.slice(-maxItems);
|
|
11691
12041
|
}
|
|
11692
12042
|
|
|
11693
12043
|
// src/server.ts
|
|
@@ -11704,6 +12054,8 @@ class Daemon {
|
|
|
11704
12054
|
relayPush = null;
|
|
11705
12055
|
spawned = new Map;
|
|
11706
12056
|
openTools = new Map;
|
|
12057
|
+
aliases = new Map;
|
|
12058
|
+
transcripts = new TranscriptReader;
|
|
11707
12059
|
defaultCwd = process.cwd();
|
|
11708
12060
|
ephemeral;
|
|
11709
12061
|
constructor(cfg, opts = {}) {
|
|
@@ -11748,6 +12100,11 @@ class Daemon {
|
|
|
11748
12100
|
if (p.authed)
|
|
11749
12101
|
this.sendTo(p, msg);
|
|
11750
12102
|
}
|
|
12103
|
+
broadcastV(minProto, msg) {
|
|
12104
|
+
for (const p of this.phones)
|
|
12105
|
+
if (p.authed && p.proto >= minProto)
|
|
12106
|
+
this.sendTo(p, msg);
|
|
12107
|
+
}
|
|
11751
12108
|
emit(sessionId, item) {
|
|
11752
12109
|
this.registry.appendItem(sessionId, item);
|
|
11753
12110
|
for (const p of this.phones) {
|
|
@@ -11797,7 +12154,7 @@ class Daemon {
|
|
|
11797
12154
|
relayLeave(conn) {
|
|
11798
12155
|
this.phones.delete(conn);
|
|
11799
12156
|
}
|
|
11800
|
-
async spawnSession(task, cwd, mode = "edits", model = null, effort = null) {
|
|
12157
|
+
async spawnSession(task, cwd, mode = "edits", model = null, effort = null, resume = null) {
|
|
11801
12158
|
const { SpawnedSession: SpawnedSession2 } = await Promise.resolve().then(() => (init_spawn(), exports_spawn));
|
|
11802
12159
|
const id = `spawn-${crypto.randomUUID()}`;
|
|
11803
12160
|
const dir = cwd || this.defaultCwd;
|
|
@@ -11807,44 +12164,56 @@ class Daemon {
|
|
|
11807
12164
|
agentName: "Claude Code",
|
|
11808
12165
|
state: "working",
|
|
11809
12166
|
spawned: true,
|
|
11810
|
-
|
|
12167
|
+
mode,
|
|
12168
|
+
action: task ? task.slice(0, 60) : resume ? "continuing previous session…" : "starting…",
|
|
11811
12169
|
...this.registry.gitInfo(dir)
|
|
11812
12170
|
}, this.cfg.host);
|
|
11813
12171
|
this.broadcast({ t: "session_upsert", session: s });
|
|
12172
|
+
if (resume) {
|
|
12173
|
+
for (const item of backfillItems(resume))
|
|
12174
|
+
this.emit(id, item);
|
|
12175
|
+
this.aliases.set(resume, id);
|
|
12176
|
+
}
|
|
11814
12177
|
if (task)
|
|
11815
12178
|
this.emit(id, { kind: "user", id: crypto.randomUUID(), text: task });
|
|
11816
12179
|
const session = new SpawnedSession2(this, id, dir, model, effort);
|
|
12180
|
+
if (resume)
|
|
12181
|
+
session.resumeFrom(resume);
|
|
11817
12182
|
this.spawned.set(id, session);
|
|
11818
12183
|
session.start(task, mode);
|
|
11819
12184
|
}
|
|
12185
|
+
adoptClaudeSession(spawnId, claudeId) {
|
|
12186
|
+
if (claudeId && claudeId !== spawnId)
|
|
12187
|
+
this.aliases.set(claudeId, spawnId);
|
|
12188
|
+
}
|
|
11820
12189
|
listProjectDirs() {
|
|
11821
|
-
const home =
|
|
11822
|
-
const roots = [home, ...["Projects", "code", "dev", "src", "repos", "Developer", "work"].map((d) =>
|
|
12190
|
+
const home = homedir3();
|
|
12191
|
+
const roots = [home, ...["Projects", "code", "dev", "src", "repos", "Developer", "work"].map((d) => join3(home, d))];
|
|
11823
12192
|
const out = new Map;
|
|
11824
12193
|
const add2 = (p) => {
|
|
11825
12194
|
if (out.has(p))
|
|
11826
12195
|
return;
|
|
11827
|
-
out.set(p, { path: p, name:
|
|
12196
|
+
out.set(p, { path: p, name: basename2(p) || p, git: existsSync3(join3(p, ".git")) });
|
|
11828
12197
|
};
|
|
11829
12198
|
add2(this.defaultCwd);
|
|
11830
12199
|
for (const root of roots) {
|
|
11831
12200
|
let entries = [];
|
|
11832
12201
|
try {
|
|
11833
|
-
entries =
|
|
12202
|
+
entries = readdirSync2(root);
|
|
11834
12203
|
} catch {
|
|
11835
12204
|
continue;
|
|
11836
12205
|
}
|
|
11837
12206
|
for (const e of entries) {
|
|
11838
12207
|
if (e.startsWith(".") || out.size > 80)
|
|
11839
12208
|
continue;
|
|
11840
|
-
const p =
|
|
12209
|
+
const p = join3(root, e);
|
|
11841
12210
|
try {
|
|
11842
|
-
if (!
|
|
12211
|
+
if (!statSync2(p).isDirectory())
|
|
11843
12212
|
continue;
|
|
11844
12213
|
} catch {
|
|
11845
12214
|
continue;
|
|
11846
12215
|
}
|
|
11847
|
-
if (root !== home ||
|
|
12216
|
+
if (root !== home || existsSync3(join3(p, ".git")))
|
|
11848
12217
|
add2(p);
|
|
11849
12218
|
}
|
|
11850
12219
|
}
|
|
@@ -11869,6 +12238,14 @@ class Daemon {
|
|
|
11869
12238
|
this.spawnState(sessionId, "error", "couldn't start Claude Code");
|
|
11870
12239
|
this.spawned.delete(sessionId);
|
|
11871
12240
|
}
|
|
12241
|
+
pruneSessions(now = Date.now()) {
|
|
12242
|
+
const removed = this.registry.prune(now, (id) => this.spawned.has(id));
|
|
12243
|
+
for (const id of removed) {
|
|
12244
|
+
this.openTools.delete(id);
|
|
12245
|
+
this.broadcast({ t: "session_removed", sessionId: id });
|
|
12246
|
+
}
|
|
12247
|
+
return removed;
|
|
12248
|
+
}
|
|
11872
12249
|
wakePhones(server, danger) {
|
|
11873
12250
|
if (!this.relayPush)
|
|
11874
12251
|
return;
|
|
@@ -11973,20 +12350,20 @@ class Daemon {
|
|
|
11973
12350
|
return;
|
|
11974
12351
|
this.pending.delete(msg.requestId);
|
|
11975
12352
|
clearTimeout(req.timer);
|
|
11976
|
-
if (msg.always && msg.allow && !req.danger) {
|
|
12353
|
+
if (msg.always && msg.allow && !req.danger && req.kind === "tool") {
|
|
11977
12354
|
const pattern = req.command.split(/\s+/).slice(0, 2).join(" ");
|
|
11978
12355
|
if (pattern && !this.cfg.rules.some((r) => r.pattern === pattern && r.scope === this.cfg.host)) {
|
|
11979
12356
|
this.cfg.rules.unshift({ pattern, action: "allow", scope: this.cfg.host });
|
|
11980
12357
|
this.persist();
|
|
11981
12358
|
}
|
|
11982
12359
|
}
|
|
11983
|
-
req.resolve({ allow: msg.allow, always: msg.always, by: "phone" });
|
|
12360
|
+
req.resolve({ allow: msg.allow, always: msg.always, by: "phone", answer: msg.answer ?? null });
|
|
11984
12361
|
this.broadcast({ t: "permission_resolved", requestId: msg.requestId, allowed: msg.allow, by: "phone" });
|
|
11985
12362
|
const s = this.registry.sessions.get(req.sessionId);
|
|
11986
|
-
if (s) {
|
|
12363
|
+
if (s && req.kind === "tool") {
|
|
11987
12364
|
this.broadcast({
|
|
11988
12365
|
t: "session_upsert",
|
|
11989
|
-
session: this.registry.upsert({ id: s.id, state: msg.allow ? "working" : "canceled", action: msg.allow ? "continuing…" : "denied from phone", command: "" }, this.cfg.host)
|
|
12366
|
+
session: this.registry.upsert({ id: s.id, state: msg.allow ? "working" : "canceled", action: msg.allow ? "continuing…" : "denied from phone", command: "", plan: [] }, this.cfg.host)
|
|
11990
12367
|
});
|
|
11991
12368
|
}
|
|
11992
12369
|
break;
|
|
@@ -12025,12 +12402,38 @@ class Daemon {
|
|
|
12025
12402
|
break;
|
|
12026
12403
|
}
|
|
12027
12404
|
case "spawn": {
|
|
12028
|
-
this.spawnSession(msg.task ?? "", msg.cwd ?? null, msg.mode ?? "edits", msg.model ?? null, msg.effort ?? null);
|
|
12405
|
+
this.spawnSession(msg.task ?? "", msg.cwd ?? null, msg.mode ?? "edits", msg.model ?? null, msg.effort ?? null, msg.resume ?? null);
|
|
12406
|
+
break;
|
|
12407
|
+
}
|
|
12408
|
+
case "set_session": {
|
|
12409
|
+
const owned = this.spawned.get(msg.sessionId);
|
|
12410
|
+
if (!owned)
|
|
12411
|
+
break;
|
|
12412
|
+
owned.reconfigure(msg.mode ?? null, msg.model ?? null);
|
|
12413
|
+
if (msg.mode) {
|
|
12414
|
+
const s = this.registry.upsert({ id: msg.sessionId, mode: msg.mode }, this.cfg.host);
|
|
12415
|
+
this.broadcast({ t: "session_upsert", session: s });
|
|
12416
|
+
}
|
|
12417
|
+
const what = [msg.mode && `mode → ${msg.mode}`, msg.model && `model → ${msg.model}`].filter(Boolean).join(" · ");
|
|
12418
|
+
if (what)
|
|
12419
|
+
this.note(msg.sessionId, `⏵ ${what} (applies from the next turn)`, "muted");
|
|
12420
|
+
break;
|
|
12421
|
+
}
|
|
12422
|
+
case "interrupt": {
|
|
12423
|
+
this.spawned.get(msg.sessionId)?.interrupt();
|
|
12029
12424
|
break;
|
|
12030
12425
|
}
|
|
12031
12426
|
case "list_dirs":
|
|
12032
12427
|
this.sendTo(p, { t: "dirs", dirs: this.listProjectDirs() });
|
|
12033
12428
|
break;
|
|
12429
|
+
case "list_sessions":
|
|
12430
|
+
this.sendTo(p, { t: "sessions", sessions: listRecentSessions() });
|
|
12431
|
+
break;
|
|
12432
|
+
case "get_diff": {
|
|
12433
|
+
const cwd = this.registry.sessions.get(msg.sessionId)?.cwd;
|
|
12434
|
+
this.sendTo(p, { t: "diff", sessionId: msg.sessionId, files: cwd ? this.registry.gitDiff(cwd) : [] });
|
|
12435
|
+
break;
|
|
12436
|
+
}
|
|
12034
12437
|
case "stop": {
|
|
12035
12438
|
const owned = this.spawned.get(msg.sessionId);
|
|
12036
12439
|
if (owned) {
|
|
@@ -12048,10 +12451,21 @@ class Daemon {
|
|
|
12048
12451
|
}
|
|
12049
12452
|
async handleHookEvent(body) {
|
|
12050
12453
|
const event = body.hook_event_name ?? "";
|
|
12051
|
-
const
|
|
12454
|
+
const rawId = body.session_id ?? "unknown";
|
|
12455
|
+
const sessionId = this.aliases.get(rawId) ?? rawId;
|
|
12456
|
+
const isSpawned = sessionId !== rawId || this.spawned.has(sessionId);
|
|
12052
12457
|
const cwd = body.cwd ?? "";
|
|
12458
|
+
if (!isSpawned) {
|
|
12459
|
+
for (const item of this.transcripts.read(sessionId, body.transcript_path)) {
|
|
12460
|
+
const seen = this.registry.items.get(sessionId)?.some((i) => i.id === item.id);
|
|
12461
|
+
if (!seen)
|
|
12462
|
+
this.emit(sessionId, item);
|
|
12463
|
+
}
|
|
12464
|
+
}
|
|
12053
12465
|
switch (event) {
|
|
12054
12466
|
case "SessionStart": {
|
|
12467
|
+
if (isSpawned)
|
|
12468
|
+
return { body: { ok: true } };
|
|
12055
12469
|
const git = this.registry.gitInfo(cwd);
|
|
12056
12470
|
const s = this.registry.upsert({
|
|
12057
12471
|
id: sessionId,
|
|
@@ -12066,6 +12480,8 @@ class Daemon {
|
|
|
12066
12480
|
return { body: { ok: true } };
|
|
12067
12481
|
}
|
|
12068
12482
|
case "UserPromptSubmit": {
|
|
12483
|
+
if (isSpawned)
|
|
12484
|
+
return { body: { ok: true } };
|
|
12069
12485
|
const prompt = String(body.prompt ?? "").slice(0, 500);
|
|
12070
12486
|
this.emit(sessionId, { kind: "user", id: crypto.randomUUID(), text: prompt });
|
|
12071
12487
|
const s = this.registry.upsert({ id: sessionId, cwd, state: "working", action: prompt.slice(0, 80) }, this.cfg.host);
|
|
@@ -12073,8 +12489,10 @@ class Daemon {
|
|
|
12073
12489
|
return { body: { ok: true } };
|
|
12074
12490
|
}
|
|
12075
12491
|
case "PreToolUse":
|
|
12076
|
-
return this.handlePreToolUse(body, sessionId, cwd);
|
|
12492
|
+
return this.handlePreToolUse(body, sessionId, cwd, isSpawned);
|
|
12077
12493
|
case "PostToolUse": {
|
|
12494
|
+
if (isSpawned)
|
|
12495
|
+
return { body: { ok: true } };
|
|
12078
12496
|
const toolUseId = this.takeOpenTool(sessionId, body);
|
|
12079
12497
|
const out = extractToolOutput(body.tool_response);
|
|
12080
12498
|
this.emit(sessionId, {
|
|
@@ -12103,6 +12521,8 @@ class Daemon {
|
|
|
12103
12521
|
return { body: { ok: true } };
|
|
12104
12522
|
}
|
|
12105
12523
|
case "Stop": {
|
|
12524
|
+
if (isSpawned)
|
|
12525
|
+
return { body: { ok: true } };
|
|
12106
12526
|
const s = this.registry.upsert({ id: sessionId, cwd, state: "done", action: "finished", command: "" }, this.cfg.host);
|
|
12107
12527
|
this.broadcast({ t: "session_upsert", session: s });
|
|
12108
12528
|
return { body: { ok: true } };
|
|
@@ -12111,20 +12531,34 @@ class Daemon {
|
|
|
12111
12531
|
return { body: { ok: true } };
|
|
12112
12532
|
}
|
|
12113
12533
|
}
|
|
12114
|
-
async handlePreToolUse(body, sessionId, cwd) {
|
|
12534
|
+
async handlePreToolUse(body, sessionId, cwd, isSpawned = false) {
|
|
12535
|
+
if (body.tool_name === "AskUserQuestion")
|
|
12536
|
+
return this.handleQuestion(body, sessionId, cwd);
|
|
12115
12537
|
const command = toolSummary(body.tool_name, body.tool_input);
|
|
12116
12538
|
const rawCommand = body.tool_name === "Bash" ? String(body.tool_input?.command ?? "") : command;
|
|
12117
12539
|
const danger = isDangerous(rawCommand) || isDangerousTool(body.tool_name, body.tool_input);
|
|
12118
|
-
const
|
|
12119
|
-
const
|
|
12120
|
-
const
|
|
12540
|
+
const isPlan = body.tool_name === "ExitPlanMode";
|
|
12541
|
+
const toolUseId = isPlan || isSpawned ? null : this.registerOpenTool(sessionId, body);
|
|
12542
|
+
const mark = (d) => {
|
|
12543
|
+
if (toolUseId) {
|
|
12544
|
+
this.emit(sessionId, { kind: "tool_decision", id: crypto.randomUUID(), toolUseId, decision: d });
|
|
12545
|
+
} else if (isSpawned && !isPlan) {
|
|
12546
|
+
this.markStreamDecision(sessionId, body, d);
|
|
12547
|
+
} else {
|
|
12548
|
+
const line = flattenItem({ kind: "tool_decision", id: "", toolUseId: "", decision: d });
|
|
12549
|
+
if (line)
|
|
12550
|
+
this.note(sessionId, line.text, line.color);
|
|
12551
|
+
}
|
|
12552
|
+
};
|
|
12553
|
+
const ruled = isPlan ? null : verdict(this.cfg.rules, rawCommand, this.cfg.host);
|
|
12121
12554
|
if (!danger && ruled === "allow") {
|
|
12122
12555
|
mark("auto");
|
|
12123
12556
|
return decision("allow", "drover rule");
|
|
12124
12557
|
}
|
|
12125
12558
|
if (ruled === "deny") {
|
|
12126
12559
|
mark("blocked");
|
|
12127
|
-
|
|
12560
|
+
if (toolUseId)
|
|
12561
|
+
this.closeOpenTool(sessionId, toolUseId);
|
|
12128
12562
|
const s2 = this.registry.upsert({ id: sessionId, cwd, state: "working", action: "blocked by your rules" }, this.cfg.host);
|
|
12129
12563
|
this.broadcast({ t: "session_upsert", session: s2 });
|
|
12130
12564
|
return decision("deny", "blocked by drover rule");
|
|
@@ -12135,13 +12569,17 @@ class Daemon {
|
|
|
12135
12569
|
return decision("ask", "no phone connected");
|
|
12136
12570
|
}
|
|
12137
12571
|
const requestId = crypto.randomUUID();
|
|
12572
|
+
const plan = isPlan ? String(body.tool_input?.plan ?? "").split(`
|
|
12573
|
+
`).map((l) => l.trim()).filter(Boolean).slice(0, 30) : [];
|
|
12574
|
+
const displayCommand = isPlan ? "Review the plan" : command;
|
|
12138
12575
|
const s = this.registry.upsert({
|
|
12139
12576
|
id: sessionId,
|
|
12140
12577
|
cwd,
|
|
12141
12578
|
state: "approval",
|
|
12142
|
-
action: danger ? "wants to run a DANGEROUS command" : `wants to run: ${command.slice(0, 60)}`,
|
|
12143
|
-
command,
|
|
12144
|
-
danger
|
|
12579
|
+
action: isPlan ? "plan ready — review before anything runs" : danger ? "wants to run a DANGEROUS command" : `wants to run: ${command.slice(0, 60)}`,
|
|
12580
|
+
command: displayCommand,
|
|
12581
|
+
danger,
|
|
12582
|
+
plan
|
|
12145
12583
|
}, this.cfg.host);
|
|
12146
12584
|
this.broadcast({ t: "session_upsert", session: s });
|
|
12147
12585
|
this.broadcast({
|
|
@@ -12149,20 +12587,20 @@ class Daemon {
|
|
|
12149
12587
|
requestId,
|
|
12150
12588
|
sessionId,
|
|
12151
12589
|
toolName: String(body.tool_name ?? ""),
|
|
12152
|
-
command,
|
|
12590
|
+
command: displayCommand,
|
|
12153
12591
|
danger,
|
|
12154
12592
|
expiresAt: Date.now() + HOOK_WAIT_MS
|
|
12155
12593
|
});
|
|
12156
12594
|
this.wakePhones(s.server, danger);
|
|
12157
|
-
const result = await new Promise((
|
|
12595
|
+
const result = await new Promise((resolve2) => {
|
|
12158
12596
|
const timer = setTimeout(() => {
|
|
12159
12597
|
this.pending.delete(requestId);
|
|
12160
12598
|
this.broadcast({ t: "permission_resolved", requestId, allowed: false, by: "timeout" });
|
|
12161
|
-
const back = this.registry.upsert({ id: sessionId, state: "working", action: "answer in the terminal (phone timed out)", command: "" }, this.cfg.host);
|
|
12599
|
+
const back = this.registry.upsert({ id: sessionId, state: "working", action: "answer in the terminal (phone timed out)", command: "", plan: [] }, this.cfg.host);
|
|
12162
12600
|
this.broadcast({ t: "session_upsert", session: back });
|
|
12163
|
-
|
|
12601
|
+
resolve2({ allow: false, by: "timeout" });
|
|
12164
12602
|
}, HOOK_WAIT_MS);
|
|
12165
|
-
this.pending.set(requestId, { requestId, sessionId, command, danger, timer, resolve });
|
|
12603
|
+
this.pending.set(requestId, { requestId, sessionId, command, danger, kind: "tool", timer, resolve: resolve2 });
|
|
12166
12604
|
});
|
|
12167
12605
|
if (result.by === "timeout") {
|
|
12168
12606
|
mark("timeout");
|
|
@@ -12176,6 +12614,64 @@ class Daemon {
|
|
|
12176
12614
|
note(sessionId, text, color) {
|
|
12177
12615
|
this.emit(sessionId, { kind: "note", id: crypto.randomUUID(), text, color });
|
|
12178
12616
|
}
|
|
12617
|
+
async handleQuestion(body, sessionId, cwd) {
|
|
12618
|
+
const qs = Array.isArray(body.tool_input?.questions) ? body.tool_input.questions : [];
|
|
12619
|
+
const q = qs[0] ?? {};
|
|
12620
|
+
const question = String(q.question ?? "Claude has a question").slice(0, 300);
|
|
12621
|
+
const options = Array.isArray(q.options) ? q.options.map((o) => String(o?.label ?? o ?? "")).filter(Boolean).slice(0, 4) : [];
|
|
12622
|
+
const connected = [...this.phones].some((p) => p.authed && p.proto >= 4);
|
|
12623
|
+
const wakeable = [...this.pushTokens.values()].some((t) => t.apnsToken);
|
|
12624
|
+
if (!connected && !wakeable)
|
|
12625
|
+
return decision("ask", "no phone connected");
|
|
12626
|
+
const requestId = crypto.randomUUID();
|
|
12627
|
+
this.emit(sessionId, { kind: "note", id: crypto.randomUUID(), text: `? ${question}`, color: "warn" });
|
|
12628
|
+
const s = this.registry.upsert({
|
|
12629
|
+
id: sessionId,
|
|
12630
|
+
cwd,
|
|
12631
|
+
state: "waiting",
|
|
12632
|
+
action: question.slice(0, 120),
|
|
12633
|
+
answers: options,
|
|
12634
|
+
command: ""
|
|
12635
|
+
}, this.cfg.host);
|
|
12636
|
+
this.broadcast({ t: "session_upsert", session: s });
|
|
12637
|
+
this.broadcastV(4, {
|
|
12638
|
+
t: "permission_request",
|
|
12639
|
+
requestId,
|
|
12640
|
+
sessionId,
|
|
12641
|
+
toolName: "AskUserQuestion",
|
|
12642
|
+
command: question,
|
|
12643
|
+
danger: false,
|
|
12644
|
+
expiresAt: Date.now() + HOOK_WAIT_MS
|
|
12645
|
+
});
|
|
12646
|
+
this.wakePhones(s.server, false);
|
|
12647
|
+
const result = await new Promise((resolve2) => {
|
|
12648
|
+
const timer = setTimeout(() => {
|
|
12649
|
+
this.pending.delete(requestId);
|
|
12650
|
+
this.broadcast({ t: "permission_resolved", requestId, allowed: false, by: "timeout" });
|
|
12651
|
+
const back2 = this.registry.upsert({ id: sessionId, state: "working", action: "answer in the terminal (phone timed out)", answers: [] }, this.cfg.host);
|
|
12652
|
+
this.broadcast({ t: "session_upsert", session: back2 });
|
|
12653
|
+
resolve2({ allow: false, by: "timeout" });
|
|
12654
|
+
}, HOOK_WAIT_MS);
|
|
12655
|
+
this.pending.set(requestId, {
|
|
12656
|
+
requestId,
|
|
12657
|
+
sessionId,
|
|
12658
|
+
command: question,
|
|
12659
|
+
danger: false,
|
|
12660
|
+
kind: "question",
|
|
12661
|
+
timer,
|
|
12662
|
+
resolve: resolve2
|
|
12663
|
+
});
|
|
12664
|
+
});
|
|
12665
|
+
if (result.by === "timeout")
|
|
12666
|
+
return decision("ask", "phone did not answer");
|
|
12667
|
+
const answer = (result.answer ?? "").trim();
|
|
12668
|
+
const back = this.registry.upsert({ id: sessionId, state: "working", action: "continuing…", answers: [] }, this.cfg.host);
|
|
12669
|
+
this.broadcast({ t: "session_upsert", session: back });
|
|
12670
|
+
if (!answer)
|
|
12671
|
+
return decision("ask", "answer it in the terminal");
|
|
12672
|
+
this.emit(sessionId, { kind: "user", id: crypto.randomUUID(), text: answer });
|
|
12673
|
+
return decision("deny", `The user answered the question from their phone: "${answer}". Proceed with that answer — do not ask again.`);
|
|
12674
|
+
}
|
|
12179
12675
|
registerOpenTool(sessionId, body) {
|
|
12180
12676
|
const id = typeof body.tool_use_id === "string" && body.tool_use_id ? body.tool_use_id : crypto.randomUUID();
|
|
12181
12677
|
const tool = String(body.tool_name ?? "tool");
|
|
@@ -12193,6 +12689,34 @@ class Daemon {
|
|
|
12193
12689
|
this.openTools.set(sessionId, open2);
|
|
12194
12690
|
return id;
|
|
12195
12691
|
}
|
|
12692
|
+
streamToolTarget(sessionId, tool, detail) {
|
|
12693
|
+
const items = this.registry.items.get(sessionId) ?? [];
|
|
12694
|
+
const decided = new Set;
|
|
12695
|
+
for (const i of items)
|
|
12696
|
+
if (i.kind === "tool_decision")
|
|
12697
|
+
decided.add(i.toolUseId);
|
|
12698
|
+
for (let k = items.length - 1;k >= 0; k--) {
|
|
12699
|
+
const i = items[k];
|
|
12700
|
+
if (i.kind === "tool_use" && !decided.has(i.id) && i.tool === tool && i.input === detail)
|
|
12701
|
+
return i.id;
|
|
12702
|
+
}
|
|
12703
|
+
return null;
|
|
12704
|
+
}
|
|
12705
|
+
async markStreamDecision(sessionId, body, d) {
|
|
12706
|
+
const tool = String(body.tool_name ?? "");
|
|
12707
|
+
const detail = toolDetail(tool, body.tool_input);
|
|
12708
|
+
for (let attempt = 0;attempt < 6; attempt++) {
|
|
12709
|
+
const target = this.streamToolTarget(sessionId, tool, detail);
|
|
12710
|
+
if (target) {
|
|
12711
|
+
this.emit(sessionId, { kind: "tool_decision", id: crypto.randomUUID(), toolUseId: target, decision: d });
|
|
12712
|
+
return;
|
|
12713
|
+
}
|
|
12714
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
12715
|
+
}
|
|
12716
|
+
const line = flattenItem({ kind: "tool_decision", id: "", toolUseId: "", decision: d });
|
|
12717
|
+
if (line)
|
|
12718
|
+
this.note(sessionId, line.text, line.color);
|
|
12719
|
+
}
|
|
12196
12720
|
closeOpenTool(sessionId, toolUseId) {
|
|
12197
12721
|
const open2 = this.openTools.get(sessionId);
|
|
12198
12722
|
if (!open2)
|
|
@@ -12220,6 +12744,8 @@ class Daemon {
|
|
|
12220
12744
|
}
|
|
12221
12745
|
listen(onListenError) {
|
|
12222
12746
|
const self = this;
|
|
12747
|
+
const sweeper = setInterval(() => self.pruneSessions(), 600000);
|
|
12748
|
+
sweeper.unref?.();
|
|
12223
12749
|
const wss = new import_websocket_server.default({ port: this.cfg.wsPort });
|
|
12224
12750
|
if (onListenError)
|
|
12225
12751
|
wss.on("error", onListenError);
|
|
@@ -12332,8 +12858,10 @@ function extractToolOutput(resp) {
|
|
|
12332
12858
|
}
|
|
12333
12859
|
function toolSummary(toolName, input) {
|
|
12334
12860
|
switch (toolName) {
|
|
12335
|
-
case "Bash":
|
|
12336
|
-
|
|
12861
|
+
case "Bash": {
|
|
12862
|
+
const cmd = String(input?.command ?? "");
|
|
12863
|
+
return cmd.length > 500 ? cmd.slice(0, 480) + " …[truncated]" : cmd;
|
|
12864
|
+
}
|
|
12337
12865
|
case "Edit":
|
|
12338
12866
|
case "MultiEdit":
|
|
12339
12867
|
return `Edit ${input?.file_path ?? "?"}`;
|
|
@@ -12449,18 +12977,18 @@ function b64url2(bytes) {
|
|
|
12449
12977
|
}
|
|
12450
12978
|
|
|
12451
12979
|
// src/hooks.ts
|
|
12452
|
-
import { readFileSync as
|
|
12453
|
-
import { homedir as
|
|
12454
|
-
import { join as
|
|
12455
|
-
var
|
|
12456
|
-
var settingsPath = () =>
|
|
12980
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
|
|
12981
|
+
import { homedir as homedir4 } from "node:os";
|
|
12982
|
+
import { join as join4 } from "node:path";
|
|
12983
|
+
var claudeDir2 = () => process.env.CLAUDE_CONFIG_DIR || join4(homedir4(), ".claude");
|
|
12984
|
+
var settingsPath = () => join4(claudeDir2(), "settings.json");
|
|
12457
12985
|
var EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Notification", "Stop"];
|
|
12458
12986
|
function installHooks(droverBin) {
|
|
12459
|
-
mkdirSync2(
|
|
12987
|
+
mkdirSync2(claudeDir2(), { recursive: true });
|
|
12460
12988
|
let settings = {};
|
|
12461
|
-
if (
|
|
12989
|
+
if (existsSync4(settingsPath())) {
|
|
12462
12990
|
try {
|
|
12463
|
-
settings = JSON.parse(
|
|
12991
|
+
settings = JSON.parse(readFileSync3(settingsPath(), "utf8"));
|
|
12464
12992
|
} catch {
|
|
12465
12993
|
settings = {};
|
|
12466
12994
|
}
|
|
@@ -12495,10 +13023,10 @@ function isOurs(x) {
|
|
|
12495
13023
|
return cmd.includes("hatchee") || cmd.includes("drover") || cmd.includes("daemon/src/cli.ts");
|
|
12496
13024
|
}
|
|
12497
13025
|
function uninstallHooks() {
|
|
12498
|
-
if (!
|
|
13026
|
+
if (!existsSync4(settingsPath()))
|
|
12499
13027
|
return;
|
|
12500
13028
|
try {
|
|
12501
|
-
const settings = JSON.parse(
|
|
13029
|
+
const settings = JSON.parse(readFileSync3(settingsPath(), "utf8"));
|
|
12502
13030
|
for (const event of Object.keys(settings.hooks ?? {})) {
|
|
12503
13031
|
settings.hooks[event] = settings.hooks[event].filter((h) => !h?.hooks?.some(isOurs));
|
|
12504
13032
|
if (settings.hooks[event].length === 0)
|
|
@@ -12508,13 +13036,13 @@ function uninstallHooks() {
|
|
|
12508
13036
|
} catch {}
|
|
12509
13037
|
}
|
|
12510
13038
|
function readStdin() {
|
|
12511
|
-
return new Promise((
|
|
13039
|
+
return new Promise((resolve2) => {
|
|
12512
13040
|
let data = "";
|
|
12513
13041
|
let done = false;
|
|
12514
13042
|
const finish = () => {
|
|
12515
13043
|
if (!done) {
|
|
12516
13044
|
done = true;
|
|
12517
|
-
|
|
13045
|
+
resolve2(data);
|
|
12518
13046
|
}
|
|
12519
13047
|
};
|
|
12520
13048
|
process.stdin.setEncoding("utf8");
|
|
@@ -12560,27 +13088,27 @@ async function runHook(hookPort) {
|
|
|
12560
13088
|
}
|
|
12561
13089
|
|
|
12562
13090
|
// src/service.ts
|
|
12563
|
-
import { homedir as
|
|
12564
|
-
import { join as
|
|
12565
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, copyFileSync, chmodSync, existsSync as
|
|
13091
|
+
import { homedir as homedir5 } from "node:os";
|
|
13092
|
+
import { join as join5, dirname } from "node:path";
|
|
13093
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, copyFileSync, chmodSync, existsSync as existsSync5, rmSync } from "node:fs";
|
|
12566
13094
|
import { execFileSync } from "node:child_process";
|
|
12567
|
-
var HOME =
|
|
12568
|
-
var HATCHEE_DIR =
|
|
12569
|
-
var BIN_DIR =
|
|
12570
|
-
var STABLE_BIN =
|
|
12571
|
-
var LOG =
|
|
13095
|
+
var HOME = homedir5();
|
|
13096
|
+
var HATCHEE_DIR = join5(HOME, ".hatchee");
|
|
13097
|
+
var BIN_DIR = join5(HATCHEE_DIR, "bin");
|
|
13098
|
+
var STABLE_BIN = join5(BIN_DIR, "hatchee.mjs");
|
|
13099
|
+
var LOG = join5(HATCHEE_DIR, "daemon.log");
|
|
12572
13100
|
var LABEL = "cloud.hatchee.daemon";
|
|
12573
|
-
var PLIST =
|
|
12574
|
-
var UNIT =
|
|
13101
|
+
var PLIST = join5(HOME, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
13102
|
+
var UNIT = join5(HOME, ".config", "systemd", "user", "hatchee.service");
|
|
12575
13103
|
function servicePath(node) {
|
|
12576
13104
|
const system = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
12577
13105
|
const userTool = [
|
|
12578
13106
|
dirname(node),
|
|
12579
13107
|
"/opt/homebrew/bin",
|
|
12580
13108
|
"/usr/local/bin",
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
13109
|
+
join5(HOME, ".bun/bin"),
|
|
13110
|
+
join5(HOME, ".npm-global/bin"),
|
|
13111
|
+
join5(HOME, ".local/bin")
|
|
12584
13112
|
];
|
|
12585
13113
|
const seen = new Set;
|
|
12586
13114
|
return [...system, ...userTool].filter((p) => p && !seen.has(p) && seen.add(p)).join(":");
|
|
@@ -12731,12 +13259,12 @@ function uninstallService() {
|
|
|
12731
13259
|
if (process.platform === "darwin") {
|
|
12732
13260
|
const uid = String(process.getuid?.() ?? "");
|
|
12733
13261
|
run("launchctl", ["bootout", `gui/${uid}/${LABEL}`]);
|
|
12734
|
-
if (
|
|
13262
|
+
if (existsSync5(PLIST))
|
|
12735
13263
|
rmSync(PLIST);
|
|
12736
13264
|
console.log(` ✓ background service removed (launchd: ${LABEL})`);
|
|
12737
13265
|
} else if (process.platform === "linux") {
|
|
12738
13266
|
run("systemctl", ["--user", "disable", "--now", "hatchee.service"]);
|
|
12739
|
-
if (
|
|
13267
|
+
if (existsSync5(UNIT)) {
|
|
12740
13268
|
rmSync(UNIT);
|
|
12741
13269
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
12742
13270
|
}
|
|
@@ -12744,7 +13272,7 @@ function uninstallService() {
|
|
|
12744
13272
|
} else {
|
|
12745
13273
|
console.log(` no background service on ${process.platform}.`);
|
|
12746
13274
|
}
|
|
12747
|
-
if (
|
|
13275
|
+
if (existsSync5(STABLE_BIN))
|
|
12748
13276
|
rmSync(STABLE_BIN);
|
|
12749
13277
|
}
|
|
12750
13278
|
|