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.
Files changed (2) hide show
  1. package/dist/cli.mjs +597 -69
  2. 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
- return "Update Todos";
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 = 2, 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, DaemonToPhone, DaemonInner, ClientHello, PermissionReply, RulesSync, UserMessage, SpawnMode, SpawnEffort, Spawn, Stop, ListDirs, Ping, PushRegister, PhoneToDaemon, PhoneInner, clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
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 existsSync2 } from "node:fs";
11616
- import { homedir as homedir2 } from "node:os";
11617
- import { join as join2, basename } from "node:path";
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
- action: task ? task.slice(0, 60) : "starting…",
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 = homedir2();
11822
- const roots = [home, ...["Projects", "code", "dev", "src", "repos", "Developer", "work"].map((d) => join2(home, 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: basename(p) || p, git: existsSync2(join2(p, ".git")) });
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 = readdirSync(root);
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 = join2(root, e);
12209
+ const p = join3(root, e);
11841
12210
  try {
11842
- if (!statSync(p).isDirectory())
12211
+ if (!statSync2(p).isDirectory())
11843
12212
  continue;
11844
12213
  } catch {
11845
12214
  continue;
11846
12215
  }
11847
- if (root !== home || existsSync2(join2(p, ".git")))
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 sessionId = body.session_id ?? "unknown";
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 toolUseId = this.registerOpenTool(sessionId, body);
12119
- const mark = (d) => this.emit(sessionId, { kind: "tool_decision", id: crypto.randomUUID(), toolUseId, decision: d });
12120
- const ruled = verdict(this.cfg.rules, rawCommand, this.cfg.host);
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
- this.closeOpenTool(sessionId, toolUseId);
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((resolve) => {
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
- resolve({ allow: false, by: "timeout" });
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
- return String(input?.command ?? "").slice(0, 500);
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 readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
12453
- import { homedir as homedir3 } from "node:os";
12454
- import { join as join3 } from "node:path";
12455
- var claudeDir = () => process.env.CLAUDE_CONFIG_DIR || join3(homedir3(), ".claude");
12456
- var settingsPath = () => join3(claudeDir(), "settings.json");
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(claudeDir(), { recursive: true });
12987
+ mkdirSync2(claudeDir2(), { recursive: true });
12460
12988
  let settings = {};
12461
- if (existsSync3(settingsPath())) {
12989
+ if (existsSync4(settingsPath())) {
12462
12990
  try {
12463
- settings = JSON.parse(readFileSync2(settingsPath(), "utf8"));
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 (!existsSync3(settingsPath()))
13026
+ if (!existsSync4(settingsPath()))
12499
13027
  return;
12500
13028
  try {
12501
- const settings = JSON.parse(readFileSync2(settingsPath(), "utf8"));
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((resolve) => {
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
- resolve(data);
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 homedir4 } from "node:os";
12564
- import { join as join4, dirname } from "node:path";
12565
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, copyFileSync, chmodSync, existsSync as existsSync4, rmSync } from "node:fs";
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 = homedir4();
12568
- var HATCHEE_DIR = join4(HOME, ".hatchee");
12569
- var BIN_DIR = join4(HATCHEE_DIR, "bin");
12570
- var STABLE_BIN = join4(BIN_DIR, "hatchee.mjs");
12571
- var LOG = join4(HATCHEE_DIR, "daemon.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 = join4(HOME, "Library", "LaunchAgents", `${LABEL}.plist`);
12574
- var UNIT = join4(HOME, ".config", "systemd", "user", "hatchee.service");
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
- join4(HOME, ".bun/bin"),
12582
- join4(HOME, ".npm-global/bin"),
12583
- join4(HOME, ".local/bin")
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 (existsSync4(PLIST))
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 (existsSync4(UNIT)) {
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 (existsSync4(STABLE_BIN))
13275
+ if (existsSync5(STABLE_BIN))
12748
13276
  rmSync(STABLE_BIN);
12749
13277
  }
12750
13278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hatchee",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Your coding agents, alive on your iPhone lock screen — Hatchee daemon. Approve Claude Code / Codex from your phone.",
5
5
  "type": "module",
6
6
  "bin": {