heyterm 0.1.0 → 0.1.2

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.
@@ -1,8 +1,8 @@
1
1
  // src/server/cli.ts
2
2
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
3
- import { readFile as readFile7 } from "node:fs/promises";
4
- import os7 from "node:os";
5
- import path17 from "node:path";
3
+ import { readFile as readFile8 } from "node:fs/promises";
4
+ import os8 from "node:os";
5
+ import path19 from "node:path";
6
6
  import { promisify as promisify5 } from "node:util";
7
7
 
8
8
  // src/server/agents/agent-adapters.ts
@@ -38,7 +38,27 @@ var gemini = {
38
38
  newSession: () => ({ program: "gemini", args: [] }),
39
39
  resume: (point) => ({ program: "gemini", args: point.agentSessionId && point.conversationSaved ? ["--resume", point.agentSessionId] : [] })
40
40
  };
41
- var adapters = [claude, codex, gemini];
41
+ var grok = {
42
+ kind: "grok",
43
+ label: "Grok Build",
44
+ program: "grok",
45
+ installHint: "Install it with curl -fsSL https://x.ai/cli/install.sh | bash, then sign in with grok login.",
46
+ choosesSessionIdAtLaunch: true,
47
+ newSession: (context) => ({ program: "grok", args: context.agentSessionId ? ["-s", context.agentSessionId] : [] }),
48
+ resume: (point, context) => {
49
+ if (point.agentSessionId && point.conversationSaved) return { program: "grok", args: ["-r", point.agentSessionId] };
50
+ return grok.newSession(context);
51
+ }
52
+ };
53
+ var cursor = {
54
+ kind: "cursor",
55
+ label: "Cursor",
56
+ program: "cursor-agent",
57
+ installHint: "Install it with curl https://cursor.com/install -fsS | bash, then sign in with cursor-agent login.",
58
+ newSession: () => ({ program: "cursor-agent", args: [] }),
59
+ resume: (point) => ({ program: "cursor-agent", args: point.agentSessionId && point.conversationSaved ? ["--resume", point.agentSessionId] : [] })
60
+ };
61
+ var adapters = [claude, codex, gemini, grok, cursor];
42
62
  var AgentAdapters = class {
43
63
  static get(kind) {
44
64
  return adapters.find((adapter) => adapter.kind === kind) ?? claude;
@@ -83,6 +103,11 @@ var HookCommands = class {
83
103
  static forSession(scriptPath, eventName) {
84
104
  return `node ${quoted(scriptPath)} heyterm ${eventName}`;
85
105
  }
106
+ // For agents that read hooks only from global files (Grok Build, Cursor). The forwarder checks the
107
+ // agent is really the one named, since Grok also runs the hooks in Cursor's file.
108
+ static forAgent(scriptPath, eventName, agent) {
109
+ return `node ${quoted(scriptPath)} agent ${eventName} ${agent}`;
110
+ }
86
111
  // For the user's own settings. If HeyTerm has been removed and the script is gone, this does
87
112
  // nothing and fails nothing, so the user never sees a hook error.
88
113
  static forExistingSessions(scriptPath, eventName) {
@@ -110,12 +135,24 @@ function forwardScript(port, token) {
110
135
  const http = require("http");
111
136
  const origin = process.argv[2];
112
137
  const eventName = process.argv[3] || "";
138
+ const agentKind = process.argv[4] || "";
113
139
  const sessionName = process.env.HEYTERM_SESSION;
140
+ // Grok and Cursor set these on every hook they run. Grok also runs the hooks in Cursor's and
141
+ // Claude's files, so a hook knows which agent really ran it only by these.
142
+ const ranByGrok = Boolean(process.env.GROK_HOOK_EVENT);
143
+ const ranByCursor = Boolean(process.env.CURSOR_VERSION) && !ranByGrok;
114
144
  const isHeyTermSession = origin === "heyterm" && Boolean(sessionName);
115
- const isOutsideSession = origin === "external" && !sessionName && !process.env.HEYTERM_ROUTER;
116
- if ((!isHeyTermSession && !isOutsideSession) || !/^[A-Za-z]+$/.test(eventName)) process.exit(0);
145
+ const isOutsideSession = origin === "external" && !sessionName && !process.env.HEYTERM_ROUTER && !ranByGrok && !process.env.CURSOR_VERSION;
146
+ const isAgentEvent = origin === "agent" && ((agentKind === "grok" && ranByGrok) || (agentKind === "cursor" && ranByCursor));
147
+ // Cursor reads a prompt hook's reply as a verdict, so its prompt always gets a "go ahead".
148
+ const defaultReply = ranByCursor && agentKind === "cursor" && eventName === "beforeSubmitPrompt" ? '{"continue":true}' : "";
149
+ const finish = (answer) => {
150
+ process.stdout.write(answer || defaultReply);
151
+ process.exit(0);
152
+ };
153
+ if ((!isHeyTermSession && !isOutsideSession && !isAgentEvent) || !/^[A-Za-z]+$/.test(eventName)) finish("");
117
154
 
118
- const query = isHeyTermSession ? "session=" + encodeURIComponent(sessionName) : "external=1";
155
+ const query = isHeyTermSession ? "session=" + encodeURIComponent(sessionName) : isAgentEvent ? "agent=" + agentKind : "external=1";
119
156
  const waitSeconds = eventName === "PermissionRequest" ? ${permissionWaitSeconds} : 3;
120
157
  const chunks = [];
121
158
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -133,14 +170,11 @@ process.stdin.on("end", () => {
133
170
  (response) => {
134
171
  const answer = [];
135
172
  response.on("data", (chunk) => answer.push(chunk));
136
- response.on("end", () => {
137
- if (response.statusCode === 200) process.stdout.write(Buffer.concat(answer));
138
- process.exit(0);
139
- });
173
+ response.on("end", () => finish(response.statusCode === 200 ? Buffer.concat(answer).toString() : ""));
140
174
  },
141
175
  );
142
176
  request.on("timeout", () => request.destroy());
143
- request.on("error", () => process.exit(0));
177
+ request.on("error", () => finish(""));
144
178
  request.end(body);
145
179
  });
146
180
  `;
@@ -250,12 +284,12 @@ var ExistingSessionConnector = class {
250
284
  }
251
285
  // A file that is not valid JSON is refused rather than overwritten: it belongs to the user.
252
286
  async readSettings() {
253
- const text = await readFile2(this.settingsPath, "utf8").catch((error) => {
287
+ const text2 = await readFile2(this.settingsPath, "utf8").catch((error) => {
254
288
  if (error.code === "ENOENT") return "{}";
255
289
  throw error;
256
290
  });
257
291
  try {
258
- return JSON.parse(text || "{}");
292
+ return JSON.parse(text2 || "{}");
259
293
  } catch {
260
294
  throw new Error(`${this.settingsPath} is not valid JSON, so HeyTerm left it alone. Fix it and try again.`);
261
295
  }
@@ -274,8 +308,8 @@ var ExistingSessionConnector = class {
274
308
 
275
309
  // src/server/main.ts
276
310
  import { execFile as execFile3 } from "node:child_process";
277
- import os5 from "node:os";
278
- import path15 from "node:path";
311
+ import os6 from "node:os";
312
+ import path17 from "node:path";
279
313
  import { promisify as promisify3 } from "node:util";
280
314
 
281
315
  // src/server/actions/action-executor.ts
@@ -340,8 +374,8 @@ var PermissionRisk = class _PermissionRisk {
340
374
  };
341
375
  }
342
376
  };
343
- function stringField(record, key) {
344
- const value = record[key];
377
+ function stringField(record2, key) {
378
+ const value = record2[key];
345
379
  return typeof value === "string" ? value : "";
346
380
  }
347
381
 
@@ -374,8 +408,11 @@ var SessionLifecycle = class _SessionLifecycle {
374
408
  }
375
409
  case "SessionEnd":
376
410
  return _SessionLifecycle.moveTo({ ...session, pending: void 0 }, "stopped", event.receivedAt);
377
- case "Notification":
378
- return session;
411
+ case "Notification": {
412
+ if (payload.waits_in_terminal === true) return _SessionLifecycle.moveTo({ ...session, blockedByTerminalDialog: true }, "asked", event.receivedAt);
413
+ const reply2 = stringField2(payload, "assistant_message");
414
+ return reply2 ? { ...session, lastMessage: reply2 } : session;
415
+ }
379
416
  }
380
417
  }
381
418
  static applyTrustDialog(session, now, trustSubject = "folder") {
@@ -384,8 +421,8 @@ var SessionLifecycle = class _SessionLifecycle {
384
421
  static markSeen(session, now) {
385
422
  return { ...session, lastSeenAt: now, activitySinceSeen: [] };
386
423
  }
387
- static recordDecision(session, text, now) {
388
- return _SessionLifecycle.addActivity(session, "decision", text, now);
424
+ static recordDecision(session, text2, now) {
425
+ return _SessionLifecycle.addActivity(session, "decision", text2, now);
389
426
  }
390
427
  static applyToolStart(session, event) {
391
428
  const toolName = stringField2(event.payload, "tool_name");
@@ -415,7 +452,8 @@ var SessionLifecycle = class _SessionLifecycle {
415
452
  return _SessionLifecycle.moveTo({ ...session, pending }, "needs_permission", event.receivedAt);
416
453
  }
417
454
  static applyTurnEnd(session, event) {
418
- const lastMessage = stringField2(event.payload, "last_assistant_message");
455
+ if (event.payload.turn_cancelled === true) return _SessionLifecycle.moveTo({ ...session, pending: void 0 }, "idle", event.receivedAt);
456
+ const lastMessage = event.payload.keep_last_message === true ? session.lastMessage ?? "" : stringField2(event.payload, "last_assistant_message");
419
457
  const withMessage = _SessionLifecycle.addActivity({ ...session, lastMessage, pending: void 0 }, "message", lastMessage, event.receivedAt);
420
458
  return _SessionLifecycle.moveTo(withMessage, endsWithQuestion(lastMessage) ? "asked" : "done", event.receivedAt);
421
459
  }
@@ -424,9 +462,9 @@ var SessionLifecycle = class _SessionLifecycle {
424
462
  const waitedMillisecondsToday = leavingWait ? session.waitedMillisecondsToday + Math.max(0, now - session.stateSince) : session.waitedMillisecondsToday;
425
463
  return { ...session, state, stateSince: now, waitedMillisecondsToday };
426
464
  }
427
- static addActivity(session, kind, text, at) {
428
- if (!text) return session;
429
- const entries = [...session.activitySinceSeen, { kind, text, at }];
465
+ static addActivity(session, kind, text2, at) {
466
+ if (!text2) return session;
467
+ const entries = [...session.activitySinceSeen, { kind, text: text2, at }];
430
468
  return { ...session, activitySinceSeen: entries.slice(-_SessionLifecycle.maximumActivityEntries) };
431
469
  }
432
470
  };
@@ -444,12 +482,12 @@ function endsWithQuestion(message) {
444
482
  const paragraphs = message.split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
445
483
  return paragraphs.at(-1)?.includes("?") ?? false;
446
484
  }
447
- function stringField2(record, key) {
448
- const value = record[key];
485
+ function stringField2(record2, key) {
486
+ const value = record2[key];
449
487
  return typeof value === "string" ? value : "";
450
488
  }
451
- function recordField(record, key) {
452
- const value = record[key];
489
+ function recordField(record2, key) {
490
+ const value = record2[key];
453
491
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
454
492
  }
455
493
 
@@ -582,8 +620,8 @@ var PendingControls = class {
582
620
  var longestUtteranceCharacters = 4e3;
583
621
  var greeting = /^hey[\s-]?term(?:inal)?\b[\s,.:!-]*/i;
584
622
  var UtteranceLimits = class {
585
- static check(text) {
586
- const utterance = text.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").trim().replace(greeting, "").trim();
623
+ static check(text2) {
624
+ const utterance = text2.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").trim().replace(greeting, "").trim();
587
625
  if (!utterance) return { ok: false, reason: "empty" };
588
626
  if (utterance.length > longestUtteranceCharacters) return { ok: false, reason: "too_long" };
589
627
  return { ok: true, utterance };
@@ -640,7 +678,7 @@ var CatchUpDigest = class _CatchUpDigest {
640
678
  parts.push("Nothing new since you looked.");
641
679
  } else {
642
680
  const counts = [plural(commands.length, "command"), changedFiles.length > 0 ? `${plural(changedFiles.length, "file")} changed` : ""];
643
- const countText = counts.filter((text) => text && !text.startsWith("0 ")).join(", ");
681
+ const countText = counts.filter((text2) => text2 && !text2.startsWith("0 ")).join(", ");
644
682
  if (countText) parts.push(`${countText}.`);
645
683
  }
646
684
  if (lead) parts.push(lead);
@@ -692,9 +730,9 @@ var SessionActions = class {
692
730
  return { text: "I did not catch what to do.", tone: "warning", speak: true };
693
731
  }
694
732
  }
695
- async deliver(sessionName, text) {
733
+ async deliver(sessionName, text2) {
696
734
  this.markSeen(sessionName);
697
- const result = await this.launcher.deliverText(sessionName, text);
735
+ const result = await this.launcher.deliverText(sessionName, text2);
698
736
  if (result === "submitted") return { text: `Sent to ${sessionName}.`, tone: "success", speak: false };
699
737
  if (result === "queued") return { text: `Queued for ${sessionName}; it reads it after its current step.`, tone: "info", speak: false };
700
738
  return { text: `${sessionName} did not take the message. Check its terminal.`, tone: "error", speak: true };
@@ -825,8 +863,8 @@ function permissionSpoken(sessionName, pending) {
825
863
  if (editingTools.includes(pending.toolName)) return `${sessionName} wants to edit a file. Say approve or deny.`;
826
864
  return `${sessionName} wants to use a tool. Say approve or deny.`;
827
865
  }
828
- function endSentence(text) {
829
- return /[.?!]$/.test(text.trim()) ? text.trim() : `${text.trim()}.`;
866
+ function endSentence(text2) {
867
+ return /[.?!]$/.test(text2.trim()) ? text2.trim() : `${text2.trim()}.`;
830
868
  }
831
869
  function questionHeadline(sessionName, pending) {
832
870
  const [first, ...rest] = pending.questions;
@@ -839,24 +877,59 @@ var countWords = ["zero", "one", "two", "three", "four", "five", "six", "seven",
839
877
  function countWord(count) {
840
878
  return countWords[count] ?? String(count);
841
879
  }
842
- function capitalize(text) {
843
- return text.charAt(0).toUpperCase() + text.slice(1);
880
+ function capitalize(text2) {
881
+ return text2.charAt(0).toUpperCase() + text2.slice(1);
844
882
  }
845
883
 
884
+ // src/shared/request-thread.ts
885
+ var decisionKinds = ["permission", "question", "trust", "failed"];
886
+ var RequestThread = class _RequestThread {
887
+ static isDecision(kind) {
888
+ return decisionKinds.includes(kind);
889
+ }
890
+ // The same session asking the same kind of thing since the same moment is the same request.
891
+ static isStillWaiting(request, sessions) {
892
+ return AttentionQueue.build(sessions).some((item) => item.sessionName === request.session && item.kind === request.kind && item.since === request.since);
893
+ }
894
+ static waitingDecisions(sessions) {
895
+ return AttentionQueue.build(sessions).filter((item) => _RequestThread.isDecision(item.kind));
896
+ }
897
+ // Requests the conversation has no line for, so they can still be shown and answered.
898
+ static waitingWithoutEntry(entries, sessions) {
899
+ const told = entries.flatMap((entry) => entry.request ? [entry.request] : []);
900
+ return _RequestThread.waitingDecisions(sessions).filter((item) => !told.some((request) => request.session === item.sessionName && request.kind === item.kind && request.since === item.since));
901
+ }
902
+ // A click on a request's button, put the way the user would have said it.
903
+ static replyWords(action) {
904
+ switch (action.type) {
905
+ case "approve":
906
+ return `Approve ${action.session}`;
907
+ case "deny":
908
+ return `Deny ${action.session}`;
909
+ case "trust":
910
+ return `Trust ${action.session}`;
911
+ case "answer":
912
+ return `${action.session}: ${Object.values(action.answers).join(", ")}`;
913
+ default:
914
+ return void 0;
915
+ }
916
+ }
917
+ };
918
+
846
919
  // src/server/router/utterance-text.ts
847
- function wordSpans(text) {
848
- return [...text.matchAll(/[a-z0-9][a-z0-9'-]*/gi)].map((match) => ({
920
+ function wordSpans(text2) {
921
+ return [...text2.matchAll(/[a-z0-9][a-z0-9'-]*/gi)].map((match) => ({
849
922
  word: match[0].toLowerCase(),
850
923
  start: match.index,
851
924
  end: match.index + match[0].length
852
925
  }));
853
926
  }
854
- function normalizedPhrase(text) {
855
- return wordSpans(text).map((span) => span.word).join(" ");
927
+ function normalizedPhrase(text2) {
928
+ return wordSpans(text2).map((span) => span.word).join(" ");
856
929
  }
857
- function textFromWord(text, spans, wordIndex) {
930
+ function textFromWord(text2, spans, wordIndex) {
858
931
  const span = spans[wordIndex];
859
- return span ? text.slice(span.start).trim() : "";
932
+ return span ? text2.slice(span.start).trim() : "";
860
933
  }
861
934
  var SpokenPhrases = {
862
935
  yes: /^(yes|yeah|yep|yup|sure|ok|okay|approve|approved|allow|accept|fine|sounds good|looks good|go ahead|do it)( (it|that|this|please|go ahead|do it))*$/,
@@ -948,8 +1021,8 @@ var SpokenNameMatcher = class _SpokenNameMatcher {
948
1021
  return mentions;
949
1022
  }
950
1023
  };
951
- function compactForm(text) {
952
- return text.toLowerCase().replace(/[^a-z0-9]/g, "");
1024
+ function compactForm(text2) {
1025
+ return text2.toLowerCase().replace(/[^a-z0-9]/g, "");
953
1026
  }
954
1027
  function spanLength(mention) {
955
1028
  return mention.lastWordIndex - mention.firstWordIndex;
@@ -1063,7 +1136,7 @@ var sessionCommands = [
1063
1136
  { pattern: /^(?:stop|interrupt|pause|halt|hold) (.+)$/, build: (session) => ({ type: "interrupt", session }) },
1064
1137
  { pattern: /^(?:bring back|resume|reopen|restart) (.+)$/, build: (session) => ({ type: "resume", session }), isResume: true }
1065
1138
  ];
1066
- var spawnPattern = /^(?:start|open|spin up|create|launch|begin)(?: a| an)?(?: new)?(?: (claude|codex|gemini))?(?: (?:session|agent|terminal))? (?:in|on|for|at) /;
1139
+ var spawnPattern = /^(?:start|open|spin up|create|launch|begin)(?: a| an)?(?: new)?(?: (claude|codex|gemini|grok(?: build)?|cursor))?(?: (?:session|agent|terminal))? (?:in|on|for|at) /;
1067
1140
  var taskJoinWords = /* @__PURE__ */ new Set(["and", "to", "then", "so"]);
1068
1141
  var FastPathRouter = class {
1069
1142
  constructor(options = { sendUnaddressedWordsToFocusedSession: true }) {
@@ -1111,7 +1184,7 @@ var FastPathRouter = class {
1111
1184
  let taskWordIndex = firstProjectWord + wordCount;
1112
1185
  while (spans[taskWordIndex] && taskJoinWords.has(spans[taskWordIndex].word)) taskWordIndex += 1;
1113
1186
  const task = textFromWord(utterance, spans, taskWordIndex);
1114
- const agent = prefix[1] === "codex" || prefix[1] === "gemini" ? prefix[1] : void 0;
1187
+ const agent = namedAgent(prefix[1]);
1115
1188
  return { type: "spawn", project: project.name, ...agent ? { agent } : {}, ...task ? { task } : {} };
1116
1189
  }
1117
1190
  return void 0;
@@ -1134,16 +1207,16 @@ var FastPathRouter = class {
1134
1207
  if (!mention) return void 0;
1135
1208
  let textWordIndex = mention.lastWordIndex + 1;
1136
1209
  while (spans[textWordIndex] && ["session", "agent", "one", "to"].includes(spans[textWordIndex].word)) textWordIndex += 1;
1137
- const text = textFromWord(utterance, spans, textWordIndex);
1138
- return text ? { type: "send", session: mention.name, text, needsPreview: true } : void 0;
1210
+ const text2 = textFromWord(utterance, spans, textWordIndex);
1211
+ return text2 ? { type: "send", session: mention.name, text: text2, needsPreview: true } : void 0;
1139
1212
  }
1140
1213
  // "Peeker, run the tests again."
1141
1214
  routeAddressed(utterance, spans, sessionMatcher) {
1142
1215
  const mentions = sessionMatcher.findMentions(utterance);
1143
1216
  const [first] = mentions;
1144
1217
  if (!first || first.firstWordIndex !== 0 || mentions.length > 1) return void 0;
1145
- const text = textFromWord(utterance, spans, first.lastWordIndex + 1);
1146
- return text ? { type: "send", session: first.name, text, needsPreview: true } : void 0;
1218
+ const text2 = textFromWord(utterance, spans, first.lastWordIndex + 1);
1219
+ return text2 ? { type: "send", session: first.name, text: text2, needsPreview: true } : void 0;
1147
1220
  }
1148
1221
  routeToFocusedSession(utterance, phrase, context, sessionMatcher) {
1149
1222
  if (sessionMatcher.findMentions(utterance).length > 0) return void 0;
@@ -1156,6 +1229,11 @@ var FastPathRouter = class {
1156
1229
  function acceptsText(session) {
1157
1230
  return !["needs_permission", "needs_trust", "stopped", "starting"].includes(session.state) && session.pending === void 0 && !session.blockedByTerminalDialog;
1158
1231
  }
1232
+ function namedAgent(spoken) {
1233
+ if (spoken === "codex" || spoken === "gemini" || spoken === "cursor") return spoken;
1234
+ if (spoken?.startsWith("grok")) return "grok";
1235
+ return void 0;
1236
+ }
1159
1237
 
1160
1238
  // src/server/conductor/conductor-rules.ts
1161
1239
  var longestSpokenReply = 280;
@@ -1184,9 +1262,9 @@ var NarrationRules = class {
1184
1262
  function itemKey(item) {
1185
1263
  return `${item.sessionName}|${item.kind}|${item.since}`;
1186
1264
  }
1187
- function shortenedForSpeech(text) {
1188
- if (text.length <= longestSpokenReply) return text;
1189
- const sentences = text.match(/[^.!?]+[.!?]+/g) ?? [text];
1265
+ function shortenedForSpeech(text2) {
1266
+ if (text2.length <= longestSpokenReply) return text2;
1267
+ const sentences = text2.match(/[^.!?]+[.!?]+/g) ?? [text2];
1190
1268
  return `${sentences.slice(0, 2).join("").trim()} The rest is on screen.`;
1191
1269
  }
1192
1270
  var ConductorSafety = class {
@@ -1195,8 +1273,8 @@ var ConductorSafety = class {
1195
1273
  return /privatekeys/i.test(JSON.stringify(input ?? {}));
1196
1274
  }
1197
1275
  };
1198
- function speakable(text) {
1199
- return text.replace(/```[\s\S]*?```/g, " ").replace(/[`*_#>]/g, "").replace(/^\s*[-•]\s+/gm, "").replace(/\s+/g, " ").trim();
1276
+ function speakable(text2) {
1277
+ return text2.replace(/```[\s\S]*?```/g, " ").replace(/[`*_#>]/g, "").replace(/^\s*[-•]\s+/gm, "").replace(/\s+/g, " ").trim();
1200
1278
  }
1201
1279
 
1202
1280
  // src/server/actions/action-executor.ts
@@ -1246,8 +1324,8 @@ var ActionExecutor = class extends EventEmitter {
1246
1324
  currentMode() {
1247
1325
  return this.mode;
1248
1326
  }
1249
- async handleUtterance(text, source, focusOverride) {
1250
- const checked = UtteranceLimits.check(text);
1327
+ async handleUtterance(text2, source, focusOverride) {
1328
+ const checked = UtteranceLimits.check(text2);
1251
1329
  if (!checked.ok) {
1252
1330
  if (checked.reason === "too_long") this.tell({ text: "That was far longer than speech, so I left it. Say it in a sentence or two.", tone: "warning", speak: true });
1253
1331
  return;
@@ -1265,6 +1343,12 @@ var ActionExecutor = class extends EventEmitter {
1265
1343
  if (this.controls.preview && action.type !== "cancel" && action.type !== "send_now") await this.sendPreviewNow();
1266
1344
  await this.perform(action, source === "voice" ? utterance : "keyboard", utterance);
1267
1345
  }
1346
+ // A button on the page. An answer to a request also shows in the conversation as the user's reply.
1347
+ async performClick(action) {
1348
+ const replyWords = RequestThread.replyWords(action);
1349
+ if (replyWords) this.assistant?.recordReply(replyWords);
1350
+ return this.perform(action, "click");
1351
+ }
1268
1352
  async perform(action, decidedBy, utterance = "") {
1269
1353
  const decision = { decidedBy };
1270
1354
  switch (action.type) {
@@ -1305,14 +1389,14 @@ var ActionExecutor = class extends EventEmitter {
1305
1389
  return this.tell(await this.sessionActions.perform(action, (sessionName) => this.setFocus(sessionName)));
1306
1390
  }
1307
1391
  }
1308
- async send(sessionName, text, needsPreview) {
1392
+ async send(sessionName, text2, needsPreview) {
1309
1393
  const session = this.registry.get(sessionName);
1310
1394
  if (!session) return this.tell({ text: `There is no session called ${sessionName}.`, tone: "warning", speak: true });
1311
1395
  if (session.origin === "external") return this.tell({ text: outsideSessionReason(sessionName), tone: "warning", speak: true });
1312
1396
  if (session.blockedByTerminalDialog) return this.tell({ text: `${sessionName} is showing a dialog in its terminal. Answer it there.`, tone: "warning", speak: true });
1313
1397
  if (!acceptsText(session)) return this.tell({ text: blockedReason(sessionName, session.state), tone: "warning", speak: true });
1314
- if (!needsPreview) return this.tell(await this.sessionActions.deliver(sessionName, text));
1315
- this.controls.startPreview({ session: sessionName, text }, () => void this.sendPreviewNow());
1398
+ if (!needsPreview) return this.tell(await this.sessionActions.deliver(sessionName, text2));
1399
+ this.controls.startPreview({ session: sessionName, text: text2 }, () => void this.sendPreviewNow());
1316
1400
  }
1317
1401
  async sendPreviewNow() {
1318
1402
  const preview = this.controls.takePreview();
@@ -1433,8 +1517,8 @@ function describeSession(session) {
1433
1517
  }
1434
1518
  return parts.join("; ");
1435
1519
  }
1436
- function clip(text) {
1437
- const oneLine = text.replace(/\s+/g, " ").replaceAll("</session_text>", "");
1520
+ function clip(text2) {
1521
+ const oneLine = text2.replace(/\s+/g, " ").replaceAll("</session_text>", "");
1438
1522
  return oneLine.length > 300 ? `${oneLine.slice(0, 300)}\u2026` : oneLine;
1439
1523
  }
1440
1524
 
@@ -1519,7 +1603,7 @@ var Assistant = class extends EventEmitter2 {
1519
1603
  this.conductor = conductor;
1520
1604
  this.situation = situation;
1521
1605
  this.announcementMode = announcementMode;
1522
- conductor.on("activity", (kind, text) => this.record(kind, text));
1606
+ conductor.on("activity", (kind, text2) => this.record(kind, text2));
1523
1607
  conductor.on("busy", (busy) => this.emit("thinking", busy));
1524
1608
  }
1525
1609
  conductor;
@@ -1533,16 +1617,20 @@ var Assistant = class extends EventEmitter2 {
1533
1617
  // One announcement at a time; what arrives meanwhile is told together after it.
1534
1618
  narrating = false;
1535
1619
  // Said straight away, outside a turn: the assistant's own request for a confirm.
1536
- sayNow(text) {
1537
- this.record("assistant", text);
1538
- this.emit("say", text);
1620
+ sayNow(text2) {
1621
+ this.record("assistant", text2);
1622
+ this.emit("say", text2);
1539
1623
  }
1540
1624
  // Something HeyTerm did at once, without the assistant, still belongs in the conversation.
1541
1625
  recordInstant(utterance) {
1542
1626
  this.record("user", utterance);
1543
1627
  }
1544
- recordOutcome(text) {
1545
- this.record("event", text);
1628
+ // A request answered with a click, shown as the user's reply under it.
1629
+ recordReply(words) {
1630
+ this.record("user", words);
1631
+ }
1632
+ recordOutcome(text2) {
1633
+ this.record("event", text2);
1546
1634
  }
1547
1635
  close() {
1548
1636
  clearTimeout(this.groupTimer);
@@ -1564,6 +1652,10 @@ var Assistant = class extends EventEmitter2 {
1564
1652
  noteSessions(sessions) {
1565
1653
  const arrived = this.arrivals.note(AttentionQueue.build(sessions));
1566
1654
  if (arrived.length === 0) return;
1655
+ for (const item of arrived) {
1656
+ if (RequestThread.isDecision(item.kind)) this.record("request", item.headline, { session: item.sessionName, kind: item.kind, since: item.since });
1657
+ else this.record("event", item.headline);
1658
+ }
1567
1659
  this.waitingArrivals.push(...arrived);
1568
1660
  this.scheduleNarration();
1569
1661
  }
@@ -1584,7 +1676,6 @@ var Assistant = class extends EventEmitter2 {
1584
1676
  async narrate() {
1585
1677
  const items = this.waitingArrivals;
1586
1678
  this.waitingArrivals = [];
1587
- for (const item of items) this.record("event", item.headline);
1588
1679
  const mode = this.announcementMode();
1589
1680
  const told = mode === "mute" ? [] : mode === "quiet" ? items.filter((item) => kindsToldInQuietMode.includes(item.kind)) : items;
1590
1681
  if (told.length === 0) return;
@@ -1623,8 +1714,9 @@ var Assistant = class extends EventEmitter2 {
1623
1714
  this.record("error", message);
1624
1715
  if (this.announcementMode() !== "mute") this.emit("say", "Something went wrong on my side. It's on screen.");
1625
1716
  }
1626
- record(kind, text) {
1627
- this.entries = [...this.entries, { id: this.nextEntryId++, at: Date.now(), kind, text }].slice(-keptEntries);
1717
+ record(kind, text2, request) {
1718
+ const entry = request ? { id: this.nextEntryId++, at: Date.now(), kind, text: text2, request } : { id: this.nextEntryId++, at: Date.now(), kind, text: text2 };
1719
+ this.entries = [...this.entries, entry].slice(-keptEntries);
1628
1720
  this.emit("conversation", this.entries);
1629
1721
  }
1630
1722
  };
@@ -1685,9 +1777,9 @@ var Conductor = class extends EventEmitter3 {
1685
1777
  sendToSession;
1686
1778
  turnTimer;
1687
1779
  // Resolves with the assistant's reply to this turn. Turns run one at a time, in order.
1688
- ask(text, priority) {
1780
+ ask(text2, priority) {
1689
1781
  return new Promise((resolve, reject) => {
1690
- this.turns.add({ text, resolve, reject }, priority);
1782
+ this.turns.add({ text: text2, resolve, reject }, priority);
1691
1783
  this.startNextTurn();
1692
1784
  });
1693
1785
  }
@@ -1850,7 +1942,7 @@ function createConductorTools(dependencies) {
1850
1942
  "send_to_session",
1851
1943
  "Give a session its next prompt, as if the developer typed it.",
1852
1944
  { name: z.string(), text: z.string() },
1853
- ({ name, text }) => withSession(name, (session) => ownSessionOnly(session, async () => (await sessionActions.deliver(name, text)).text))
1945
+ ({ name, text: text2 }) => withSession(name, (session) => ownSessionOnly(session, async () => (await sessionActions.deliver(name, text2)).text))
1854
1946
  ),
1855
1947
  tool(
1856
1948
  "answer_question",
@@ -1881,11 +1973,11 @@ function createConductorTools(dependencies) {
1881
1973
  ),
1882
1974
  tool(
1883
1975
  "start_session",
1884
- "Start a new coding agent session in a folder, with its first task if there is one. The folder is a project name from list_projects or any folder path on this Mac, such as ~/Documents/Projects. The agent is Claude Code unless the developer asks for Codex or Gemini.",
1976
+ "Start a new coding agent session in a folder, with its first task if there is one. The folder is a project name from list_projects or any folder path on this Mac, such as ~/Documents/Projects. The agent is Claude Code unless the developer asks for Codex, Gemini, Grok (Grok Build) or Cursor.",
1885
1977
  {
1886
1978
  project: z.string().describe("A project name, or a folder path"),
1887
1979
  task: z.string().optional(),
1888
- agent: z.enum(["claude", "codex", "gemini"]).optional()
1980
+ agent: z.enum(["claude", "codex", "gemini", "grok", "cursor"]).optional()
1889
1981
  },
1890
1982
  async ({ project, task, agent }) => reply((await sessionActions.perform({ type: "spawn", project, ...task ? { task } : {}, ...agent ? { agent } : {} }, (sessionName) => executor.setFocus(sessionName))).text)
1891
1983
  ),
@@ -1940,8 +2032,8 @@ function createConductorTools(dependencies) {
1940
2032
  ]
1941
2033
  });
1942
2034
  }
1943
- function reply(text) {
1944
- return { content: [{ type: "text", text }] };
2035
+ function reply(text2) {
2036
+ return { content: [{ type: "text", text: text2 }] };
1945
2037
  }
1946
2038
 
1947
2039
  // src/server/conductor/create-assistant.ts
@@ -2098,6 +2190,102 @@ var HeldHookRequests = class {
2098
2190
  }
2099
2191
  };
2100
2192
 
2193
+ // src/server/hooks/agent-hook-files.ts
2194
+ import { copyFile as copyFile2, mkdir as mkdir5, readFile as readFile4, rename as rename3, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
2195
+ import os3 from "node:os";
2196
+ import path8 from "node:path";
2197
+
2198
+ // src/server/hooks/global-agent-hooks.ts
2199
+ var grokEvents = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop", "StopCancelled", "StopFailure", "Notification", "SessionEnd"];
2200
+ var cursorEvents = ["sessionStart", "sessionEnd", "beforeSubmitPrompt", "postToolUse", "afterAgentResponse", "stop"];
2201
+ var hookTimeoutSeconds = 10;
2202
+ var GlobalAgentHooks = class _GlobalAgentHooks {
2203
+ static grokHookFile(scriptPath) {
2204
+ const hooks = Object.fromEntries(
2205
+ grokEvents.map((eventName) => [eventName, [{ hooks: [{ type: "command", command: HookCommands.forAgent(scriptPath, eventName, "grok"), timeout: hookTimeoutSeconds }] }]])
2206
+ );
2207
+ return { hooks };
2208
+ }
2209
+ static cursorWithHeyTerm(file, scriptPath) {
2210
+ const cleaned = _GlobalAgentHooks.cursorWithoutHeyTerm(file);
2211
+ const hooks = { ...cleaned.hooks ?? {} };
2212
+ for (const eventName of cursorEvents) hooks[eventName] = [...hooks[eventName] ?? [], { command: HookCommands.forAgent(scriptPath, eventName, "cursor"), timeout: hookTimeoutSeconds }];
2213
+ return { ...cleaned, version: cleaned.version ?? 1, hooks };
2214
+ }
2215
+ static cursorWithoutHeyTerm(file) {
2216
+ if (!file.hooks) return file;
2217
+ const remaining = {};
2218
+ for (const [eventName, entries] of Object.entries(file.hooks)) {
2219
+ const kept = entries.filter((entry) => !HookCommands.isHeyTerm(entry.command ?? ""));
2220
+ if (kept.length > 0) remaining[eventName] = kept;
2221
+ }
2222
+ const { hooks: _removedHooks, ...rest } = file;
2223
+ return Object.keys(remaining).length > 0 ? { ...rest, hooks: remaining } : rest;
2224
+ }
2225
+ };
2226
+
2227
+ // src/server/hooks/agent-hook-files.ts
2228
+ function agentHookPaths() {
2229
+ return {
2230
+ grokPath: process.env.HEYTERM_GROK_HOOKS_PATH ?? path8.join(process.env.GROK_HOME ?? path8.join(os3.homedir(), ".grok"), "hooks", "heyterm.json"),
2231
+ cursorPath: process.env.HEYTERM_CURSOR_HOOKS_PATH ?? path8.join(os3.homedir(), ".cursor", "hooks.json")
2232
+ };
2233
+ }
2234
+ var AgentHookFiles = class {
2235
+ constructor(scriptPath, backupDirectory, paths = agentHookPaths()) {
2236
+ this.scriptPath = scriptPath;
2237
+ this.backupDirectory = backupDirectory;
2238
+ this.paths = paths;
2239
+ }
2240
+ scriptPath;
2241
+ backupDirectory;
2242
+ paths;
2243
+ async install() {
2244
+ if (await isFolder2(path8.dirname(path8.dirname(this.paths.grokPath)))) {
2245
+ await mkdir5(path8.dirname(this.paths.grokPath), { recursive: true });
2246
+ await writeAtomically(this.paths.grokPath, GlobalAgentHooks.grokHookFile(this.scriptPath));
2247
+ }
2248
+ if (await isFolder2(path8.dirname(this.paths.cursorPath))) {
2249
+ const current = await readJson(this.paths.cursorPath);
2250
+ if (!current) return;
2251
+ await this.backUp(this.paths.cursorPath);
2252
+ await writeAtomically(this.paths.cursorPath, GlobalAgentHooks.cursorWithHeyTerm(current, this.scriptPath));
2253
+ }
2254
+ }
2255
+ async remove() {
2256
+ await rm2(this.paths.grokPath, { force: true });
2257
+ const current = await readJson(this.paths.cursorPath);
2258
+ if (!current || !current.hooks) return;
2259
+ const cleaned = GlobalAgentHooks.cursorWithoutHeyTerm(current);
2260
+ if (JSON.stringify(cleaned) === JSON.stringify(current)) return;
2261
+ const holdsNothingElse = Object.keys(cleaned).every((key) => key === "version");
2262
+ if (holdsNothingElse) await rm2(this.paths.cursorPath, { force: true });
2263
+ else await writeAtomically(this.paths.cursorPath, cleaned);
2264
+ }
2265
+ async backUp(filePath) {
2266
+ await mkdir5(this.backupDirectory, { recursive: true });
2267
+ await copyFile2(filePath, path8.join(this.backupDirectory, `cursor-hooks-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}.json`)).catch(() => void 0);
2268
+ }
2269
+ };
2270
+ async function isFolder2(folder) {
2271
+ return stat2(folder).then((info) => info.isDirectory(), () => false);
2272
+ }
2273
+ async function readJson(filePath) {
2274
+ const text2 = await readFile4(filePath, "utf8").catch((error) => error.code === "ENOENT" ? "{}" : void 0);
2275
+ if (text2 === void 0) return void 0;
2276
+ try {
2277
+ return JSON.parse(text2 || "{}");
2278
+ } catch {
2279
+ return void 0;
2280
+ }
2281
+ }
2282
+ async function writeAtomically(filePath, content) {
2283
+ const temporaryPath = `${filePath}.heyterm.tmp`;
2284
+ await writeFile4(temporaryPath, `${JSON.stringify(content, null, 2)}
2285
+ `);
2286
+ await rename3(temporaryPath, filePath);
2287
+ }
2288
+
2101
2289
  // src/server/http/heyterm-http-server.ts
2102
2290
  import { existsSync as existsSync2 } from "node:fs";
2103
2291
  import { createServer } from "node:http";
@@ -2113,7 +2301,7 @@ var HeyTermHttpServer = class {
2113
2301
  this.hub = hub;
2114
2302
  this.terminals = terminals;
2115
2303
  this.tickets = tickets;
2116
- this.serveWeb = existsSync2(webDirectory) ? sirv(webDirectory, { single: true }) : void 0;
2304
+ this.serveWeb = existsSync2(webDirectory) ? sirv(webDirectory, { single: true, setHeaders: cachingHeaders }) : void 0;
2117
2305
  this.server.on("upgrade", (request, socket, head) => {
2118
2306
  if (!this.guard.isFromThisMac(request) || !this.guard.hasToken(request)) return socket.destroy();
2119
2307
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
@@ -2178,6 +2366,8 @@ var HeyTermHttpServer = class {
2178
2366
  async handleHook(request, response, url) {
2179
2367
  if (!this.guard.hasToken(request)) return respond(response, 401, "");
2180
2368
  const eventName = url.pathname.slice("/hooks/".length);
2369
+ const agent = url.searchParams.get("agent");
2370
+ if (agent === "grok" || agent === "cursor") return this.handleAgentHook(request, response, agent, eventName);
2181
2371
  const sessionName = url.searchParams.get("session") ?? "";
2182
2372
  const isExternal = url.searchParams.get("external") === "1";
2183
2373
  if (!forwardedHookEvents.includes(eventName) || !sessionName && !isExternal) return respond(response, 400, "");
@@ -2190,7 +2380,21 @@ var HeyTermHttpServer = class {
2190
2380
  const answer = isExternal ? await this.registry.applyExternalHookEvent(eventName, payload, Date.now()) : await this.registry.applyHookEvent({ name: eventName, sessionName, payload, receivedAt: Date.now() });
2191
2381
  respond(response, 200, answer, "application/json");
2192
2382
  }
2383
+ // Grok Build and Cursor name events their own way; the registry translates them.
2384
+ async handleAgentHook(request, response, agent, eventName) {
2385
+ if (!/^[A-Za-z]+$/.test(eventName)) return respond(response, 400, "");
2386
+ let payload;
2387
+ try {
2388
+ payload = JSON.parse(await readBody(request, largestHookBodyBytes));
2389
+ } catch {
2390
+ return respond(response, 400, "");
2391
+ }
2392
+ respond(response, 200, await this.registry.applyAgentHookEvent(agent, eventName, payload, Date.now()), "application/json");
2393
+ }
2193
2394
  };
2395
+ function cachingHeaders(response, pathname) {
2396
+ response.setHeader("Cache-Control", pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache");
2397
+ }
2194
2398
  function securityHeaders(port) {
2195
2399
  const ownSockets = `ws://127.0.0.1:${port} ws://localhost:${port}`;
2196
2400
  return {
@@ -2212,9 +2416,9 @@ function securityHeaders(port) {
2212
2416
  "Cross-Origin-Opener-Policy": "same-origin"
2213
2417
  };
2214
2418
  }
2215
- function safeJson(text) {
2419
+ function safeJson(text2) {
2216
2420
  try {
2217
- return JSON.parse(text);
2421
+ return JSON.parse(text2);
2218
2422
  } catch {
2219
2423
  return void 0;
2220
2424
  }
@@ -2253,7 +2457,7 @@ var EventHub = class {
2253
2457
  this.macVoice = macVoice;
2254
2458
  this.assistant = assistant;
2255
2459
  assistant.on("conversation", (entries) => this.broadcast({ type: "conversation", entries }));
2256
- assistant.on("say", (text) => this.broadcast({ type: "say", text }));
2460
+ assistant.on("say", (text2) => this.broadcast({ type: "say", text: text2 }));
2257
2461
  registry.on("changed", () => this.scheduleSnapshot());
2258
2462
  executor.on("changed", () => this.scheduleSnapshot());
2259
2463
  executor.on("feedback", (feedback) => this.broadcast({ type: "feedback", feedback }));
@@ -2286,16 +2490,16 @@ var EventHub = class {
2286
2490
  this.onPageCountChange(this.sockets.size);
2287
2491
  });
2288
2492
  }
2289
- async receive(text, socket) {
2493
+ async receive(text2, socket) {
2290
2494
  let message;
2291
2495
  try {
2292
- message = JSON.parse(text);
2496
+ message = JSON.parse(text2);
2293
2497
  } catch {
2294
2498
  return;
2295
2499
  }
2296
2500
  try {
2297
2501
  if (message.type === "utterance") await this.executor.handleUtterance(String(message.text), message.source === "voice" ? "voice" : "keyboard");
2298
- if (message.type === "action") await this.executor.perform(message.action, "click");
2502
+ if (message.type === "action") await this.executor.performClick(message.action);
2299
2503
  if (message.type === "focus") this.executor.setFocus(String(message.session));
2300
2504
  if (message.type === "setup_refresh") await this.sendSetup();
2301
2505
  if (message.type === "set_project_roots") await this.changeProjectRoots(message.roots);
@@ -2329,8 +2533,8 @@ var EventHub = class {
2329
2533
  }
2330
2534
  async connectExistingSessions(connected) {
2331
2535
  await this.setup.setExistingSessionsConnected(connected);
2332
- const text = connected ? "Connected. Claude sessions you start in any terminal now show up here." : "Disconnected. HeyTerm's hooks are out of your Claude settings.";
2333
- this.broadcast({ type: "feedback", feedback: { text, tone: "success", speak: false } });
2536
+ const text2 = connected ? "Connected. Claude sessions you start in any terminal now show up here." : "Disconnected. HeyTerm's hooks are out of your Claude settings.";
2537
+ this.broadcast({ type: "feedback", feedback: { text: text2, tone: "success", speak: false } });
2334
2538
  await this.sendSetup();
2335
2539
  }
2336
2540
  async refreshProjects() {
@@ -2354,8 +2558,8 @@ var EventHub = class {
2354
2558
  }, snapshotThrottleMilliseconds);
2355
2559
  }
2356
2560
  broadcast(message) {
2357
- const text = JSON.stringify(message);
2358
- for (const socket of this.sockets) if (socket.readyState === socket.OPEN) socket.send(text);
2561
+ const text2 = JSON.stringify(message);
2562
+ for (const socket of this.sockets) if (socket.readyState === socket.OPEN) socket.send(text2);
2359
2563
  }
2360
2564
  };
2361
2565
 
@@ -2445,18 +2649,18 @@ var TerminalBridge = class {
2445
2649
  return;
2446
2650
  }
2447
2651
  socket.on("message", (raw) => {
2448
- const text = raw.toString();
2449
- if (text.length > largestInputBytes) return;
2450
- const message = parseMessage(text);
2652
+ const text2 = raw.toString();
2653
+ if (text2.length > largestInputBytes) return;
2654
+ const message = parseMessage(text2);
2451
2655
  if (message?.type === "input") attachment.write(message.data);
2452
2656
  if (message?.type === "resize") attachment.resize(message.columns, message.rows);
2453
2657
  });
2454
2658
  socket.on("close", () => attachment.detach());
2455
2659
  }
2456
2660
  };
2457
- function parseMessage(text) {
2661
+ function parseMessage(text2) {
2458
2662
  try {
2459
- const message = JSON.parse(text);
2663
+ const message = JSON.parse(text2);
2460
2664
  if (message.type === "input" && typeof message.data === "string") return message;
2461
2665
  if (message.type === "resize" && Number.isFinite(message.columns) && Number.isFinite(message.rows)) return message;
2462
2666
  return void 0;
@@ -2466,9 +2670,9 @@ function parseMessage(text) {
2466
2670
  }
2467
2671
 
2468
2672
  // src/server/projects/project-catalog.ts
2469
- import { readdir, stat as stat2 } from "node:fs/promises";
2470
- import os3 from "node:os";
2471
- import path8 from "node:path";
2673
+ import { readdir, stat as stat3 } from "node:fs/promises";
2674
+ import os4 from "node:os";
2675
+ import path9 from "node:path";
2472
2676
  var deepestFolderLevel = 3;
2473
2677
  var skippedFolders = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "Pods", "DerivedData", "target"]);
2474
2678
  var refreshMilliseconds = 5 * 6e4;
@@ -2495,9 +2699,9 @@ var ProjectCatalog = class {
2495
2699
  const listed = (await this.list()).find((project) => project.name === nameOrPath);
2496
2700
  if (listed) return listed;
2497
2701
  if (!looksLikePath(nameOrPath)) return void 0;
2498
- const folder = path8.resolve(nameOrPath.replace(/^~(?=$|[\\/])/, os3.homedir()));
2499
- const isFolder3 = await stat2(folder).then((info) => info.isDirectory(), () => false);
2500
- return isFolder3 ? { name: path8.basename(folder).toLowerCase() || "root", path: folder } : void 0;
2702
+ const folder = path9.resolve(nameOrPath.replace(/^~(?=$|[\\/])/, os4.homedir()));
2703
+ const isFolder4 = await stat3(folder).then((info) => info.isDirectory(), () => false);
2704
+ return isFolder4 ? { name: path9.basename(folder).toLowerCase() || "root", path: folder } : void 0;
2501
2705
  }
2502
2706
  async refresh() {
2503
2707
  const found = [];
@@ -2506,49 +2710,50 @@ var ProjectCatalog = class {
2506
2710
  this.refreshedAt = Date.now();
2507
2711
  }
2508
2712
  };
2509
- function looksLikePath(text) {
2510
- return text.startsWith("~") || path8.isAbsolute(text) || text.includes("/") || text.includes("\\");
2713
+ function looksLikePath(text2) {
2714
+ return text2.startsWith("~") || path9.isAbsolute(text2) || text2.includes("/") || text2.includes("\\");
2511
2715
  }
2512
2716
  async function collectRepositories(folder, level, found) {
2513
2717
  const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
2514
2718
  if (entries.some((entry) => entry.name === ".git")) {
2515
- found.push({ name: path8.basename(folder), path: folder });
2719
+ found.push({ name: path9.basename(folder), path: folder });
2516
2720
  return;
2517
2721
  }
2518
2722
  if (level >= deepestFolderLevel) return;
2519
2723
  for (const entry of entries) {
2520
2724
  if (entry.name.startsWith(".") || skippedFolders.has(entry.name)) continue;
2521
- const childPath = path8.join(folder, entry.name);
2522
- const isFolder3 = entry.isDirectory() || entry.isSymbolicLink() && await stat2(childPath).then((info) => info.isDirectory(), () => false);
2523
- if (isFolder3) await collectRepositories(childPath, level + 1, found);
2725
+ const childPath = path9.join(folder, entry.name);
2726
+ const isFolder4 = entry.isDirectory() || entry.isSymbolicLink() && await stat3(childPath).then((info) => info.isDirectory(), () => false);
2727
+ if (isFolder4) await collectRepositories(childPath, level + 1, found);
2524
2728
  }
2525
2729
  }
2526
2730
  function withUniqueNames(projects) {
2527
2731
  const seen = /* @__PURE__ */ new Set();
2528
2732
  return projects.map((project) => {
2529
2733
  const baseName = project.name.toLowerCase();
2530
- const name = seen.has(baseName) ? `${path8.basename(path8.dirname(project.path)).toLowerCase()}-${baseName}` : baseName;
2734
+ const name = seen.has(baseName) ? `${path9.basename(path9.dirname(project.path)).toLowerCase()}-${baseName}` : baseName;
2531
2735
  seen.add(name);
2532
2736
  return { ...project, name };
2533
2737
  });
2534
2738
  }
2535
2739
 
2536
2740
  // src/server/safety/audit-log.ts
2537
- import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
2538
- import path9 from "node:path";
2741
+ import { appendFile, mkdir as mkdir6 } from "node:fs/promises";
2742
+ import path10 from "node:path";
2539
2743
  var AuditLog = class {
2540
2744
  filePath;
2541
2745
  constructor(dataDirectory2) {
2542
- this.filePath = path9.join(dataDirectory2, "audit.jsonl");
2746
+ this.filePath = path10.join(dataDirectory2, "audit.jsonl");
2543
2747
  }
2544
2748
  async record(entry) {
2545
- await mkdir5(path9.dirname(this.filePath), { recursive: true });
2749
+ await mkdir6(path10.dirname(this.filePath), { recursive: true });
2546
2750
  await appendFile(this.filePath, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
2547
2751
  `, { mode: 384 });
2548
2752
  }
2549
2753
  };
2550
2754
 
2551
2755
  // src/server/sessions/session-launcher.ts
2756
+ import { randomUUID } from "node:crypto";
2552
2757
  import { execFile } from "node:child_process";
2553
2758
  import { access } from "node:fs/promises";
2554
2759
  import { promisify } from "node:util";
@@ -2556,6 +2761,8 @@ import { promisify } from "node:util";
2556
2761
  // src/server/sessions/screen-reader.ts
2557
2762
  var trustOptionText = /Yes, I trust this folder|\bTrust (?:all )?and continue\b/;
2558
2763
  var hookReviewText = /Hooks need review/;
2764
+ var grokTrustText = /Do you trust the contents of this directory\?[\s\S]*Yes, proceed\s+y\b/;
2765
+ var cursorTrustText = /Do you trust the contents of this directory\?[\s\S]*\[a\] Trust this workspace[\s\S]*press the key shown/;
2559
2766
  var selectionMarkers = ["\u276F", "\u203A"];
2560
2767
  var greatestOptionDistance = 4;
2561
2768
  var ScreenReader = class _ScreenReader {
@@ -2564,6 +2771,8 @@ var ScreenReader = class _ScreenReader {
2564
2771
  }
2565
2772
  // Keys that select "Yes, I trust this folder" and confirm it, or undefined when the dialog is not showing.
2566
2773
  static trustDialogKeys(screenText) {
2774
+ if (grokTrustText.test(screenText)) return ["y"];
2775
+ if (cursorTrustText.test(screenText)) return ["a"];
2567
2776
  const lines = screenText.split("\n");
2568
2777
  const trustLineIndex = lines.findIndex((line) => trustOptionText.test(line));
2569
2778
  if (trustLineIndex === -1) return void 0;
@@ -2631,10 +2840,12 @@ var SessionLauncher = class {
2631
2840
  createdAt: now,
2632
2841
  lastSeenAt: now,
2633
2842
  activitySinceSeen: [],
2634
- waitedMillisecondsToday: 0
2843
+ waitedMillisecondsToday: 0,
2844
+ // Grok's hooks come from a global file; the id HeyTerm gives it is how its events find this session.
2845
+ agentSessionId: AgentAdapters.get(agent).choosesSessionIdAtLaunch ? randomUUID() : void 0
2635
2846
  };
2636
2847
  this.registry.add(session);
2637
- this.launch(session, AgentAdapters.get(agent).newSession(this.launchContext(name)));
2848
+ this.launch(session, AgentAdapters.get(agent).newSession({ ...this.launchContext(name), agentSessionId: session.agentSessionId }));
2638
2849
  if (task) void this.sendWhenReady(name, task);
2639
2850
  return session;
2640
2851
  }
@@ -2645,7 +2856,10 @@ var SessionLauncher = class {
2645
2856
  this.registry.add(session);
2646
2857
  const conversationSaved = closed.transcriptPath ? await fileExists(closed.transcriptPath) : closed.lastPrompt !== void 0;
2647
2858
  const point = { agentSessionId: closed.agentSessionId, conversationSaved };
2648
- this.launch(session, AgentAdapters.get(closed.agent).resume(point, this.launchContext(session.name)));
2859
+ const adapter = AgentAdapters.get(closed.agent);
2860
+ if (adapter.choosesSessionIdAtLaunch && !conversationSaved) this.registry.update(session.name, (current) => ({ ...current, agentSessionId: randomUUID() }));
2861
+ const launchId = this.registry.get(session.name)?.agentSessionId;
2862
+ this.launch(session, adapter.resume(point, { ...this.launchContext(session.name), agentSessionId: launchId }));
2649
2863
  return session;
2650
2864
  }
2651
2865
  async close(name) {
@@ -2666,11 +2880,11 @@ var SessionLauncher = class {
2666
2880
  this.registry.update(name, (session) => ({ ...session, state: session.trustSubject === "hooks" ? "idle" : "starting", stateSince: Date.now(), trustSubject: void 0 }));
2667
2881
  return true;
2668
2882
  }
2669
- async deliverText(name, text) {
2883
+ async deliverText(name, text2) {
2670
2884
  const session = this.registry.get(name);
2671
2885
  if (!session || !this.host.hasSession(name)) return "failed";
2672
2886
  const isWorking = session.state === "working";
2673
- this.host.pasteText(name, text);
2887
+ this.host.pasteText(name, text2);
2674
2888
  await delay(pasteSettleMilliseconds);
2675
2889
  const firstConfirmation = this.registry.waitForPromptSubmitted(name, promptConfirmationMilliseconds);
2676
2890
  this.host.pressKeys(name, ["Enter"]);
@@ -2772,15 +2986,28 @@ function delay(milliseconds) {
2772
2986
  }
2773
2987
 
2774
2988
  // src/server/sessions/session-registry.ts
2775
- import { randomUUID } from "node:crypto";
2989
+ import { randomUUID as randomUUID2 } from "node:crypto";
2776
2990
  import { EventEmitter as EventEmitter4 } from "node:events";
2991
+ import path13 from "node:path";
2992
+
2993
+ // src/server/sessions/agent-session-match.ts
2777
2994
  import path11 from "node:path";
2995
+ var AgentSessionMatch = class {
2996
+ static find(sessions, agent, agentSessionId, workingDirectory) {
2997
+ const ours = sessions.filter((session) => session.origin !== "external" && session.agent === agent);
2998
+ const known = ours.find((session) => session.agentSessionId === agentSessionId);
2999
+ if (known) return { session: known, claimsId: false };
3000
+ const folder = path11.resolve(workingDirectory || "/");
3001
+ const unclaimed = ours.filter((session) => !session.agentSessionId && path11.resolve(session.projectPath) === folder).sort((left, right) => right.createdAt - left.createdAt);
3002
+ return unclaimed[0] ? { session: unclaimed[0], claimsId: true } : void 0;
3003
+ }
3004
+ };
2778
3005
 
2779
3006
  // src/server/sessions/external-session-naming.ts
2780
- import path10 from "node:path";
3007
+ import path12 from "node:path";
2781
3008
  var ExternalSessionNaming = class {
2782
3009
  static nameFor(facts, takenNames) {
2783
- const base = slug(facts.sessionTitle ?? "") || slug(path10.basename(facts.workingDirectory)) || "session";
3010
+ const base = slug(facts.sessionTitle ?? "") || slug(path12.basename(facts.workingDirectory)) || "session";
2784
3011
  if (!takenNames.includes(base)) return base;
2785
3012
  for (let suffix = 2; ; suffix += 1) {
2786
3013
  const candidate = `${base}-${suffix}`;
@@ -2788,8 +3015,101 @@ var ExternalSessionNaming = class {
2788
3015
  }
2789
3016
  }
2790
3017
  };
2791
- function slug(text) {
2792
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
3018
+ function slug(text2) {
3019
+ return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
3020
+ }
3021
+
3022
+ // src/server/agents/agent-hook-translator.ts
3023
+ var grokToolNames = {
3024
+ run_terminal_command: "Bash",
3025
+ search_replace: "Edit",
3026
+ // Seen in a recorded session; the guide's alias list does not name it.
3027
+ write: "Write",
3028
+ read_file: "Read",
3029
+ list_dir: "Glob",
3030
+ grep: "Grep",
3031
+ web_search: "WebSearch",
3032
+ spawn_subagent: "Task",
3033
+ ask_user_question: "AskUserQuestion"
3034
+ };
3035
+ var cursorToolNames = { Shell: "Bash", Write: "Edit", Delete: "Edit" };
3036
+ var AgentHookTranslator = class {
3037
+ static translate(agent, eventName, payload) {
3038
+ return agent === "grok" ? fromGrok(eventName, payload) : fromCursor(eventName, payload);
3039
+ }
3040
+ // What the hook prints when HeyTerm has nothing to say. Cursor reads a prompt hook's reply as a
3041
+ // verdict, so it gets an explicit "go ahead"; everything else stays silent and the agent decides.
3042
+ static defaultReply(agent, eventName) {
3043
+ return agent === "cursor" && eventName === "beforeSubmitPrompt" ? '{"continue":true}' : "";
3044
+ }
3045
+ };
3046
+ function fromGrok(eventName, payload) {
3047
+ const agentSessionId = text(payload.session_id) || text(payload.sessionId);
3048
+ if (!agentSessionId) return void 0;
3049
+ const base = { session_id: agentSessionId, cwd: text(payload.cwd) || text(payload.workspaceRoot) };
3050
+ const event = (name, extra = {}) => ({ name, agentSessionId, payload: { ...base, ...extra } });
3051
+ switch (eventName) {
3052
+ case "SessionStart":
3053
+ case "SessionEnd":
3054
+ return event(eventName);
3055
+ case "UserPromptSubmit":
3056
+ return event("UserPromptSubmit", { prompt: text(payload.prompt) || text(payload.userPrompt) });
3057
+ case "PreToolUse":
3058
+ case "PostToolUse":
3059
+ return event(eventName, grokTool(payload));
3060
+ case "Stop":
3061
+ if (text(payload.reason) === "shutdown") return void 0;
3062
+ return event("Stop", { last_assistant_message: text(payload.last_assistant_message) || text(payload.lastAssistantMessage) });
3063
+ case "StopCancelled":
3064
+ return event("Stop", { turn_cancelled: true });
3065
+ case "StopFailure":
3066
+ return event("StopFailure", { error: text(payload.error_details) || text(payload.errorDetails) || text(payload.error) });
3067
+ case "Notification": {
3068
+ const notificationType = text(payload.notification_type) || text(payload.notificationType);
3069
+ return notificationType === "permission_prompt" ? event("Notification", { waits_in_terminal: true }) : void 0;
3070
+ }
3071
+ default:
3072
+ return void 0;
3073
+ }
3074
+ }
3075
+ function grokTool(payload) {
3076
+ const toolName = text(payload.tool_name) || text(payload.toolName);
3077
+ return { tool_name: grokToolNames[toolName] ?? toolName, tool_input: record(payload.tool_input) ?? record(payload.toolInput) ?? {} };
3078
+ }
3079
+ function fromCursor(eventName, payload) {
3080
+ const agentSessionId = text(payload.conversation_id) || text(payload.session_id);
3081
+ if (!agentSessionId) return void 0;
3082
+ const roots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.filter((root) => typeof root === "string") : [];
3083
+ const base = { session_id: agentSessionId, cwd: roots[0] ?? text(payload.cwd) };
3084
+ const event = (name, extra = {}) => ({ name, agentSessionId, payload: { ...base, ...extra } });
3085
+ switch (eventName) {
3086
+ case "sessionStart":
3087
+ return event("SessionStart");
3088
+ case "sessionEnd":
3089
+ return event("SessionEnd");
3090
+ case "beforeSubmitPrompt":
3091
+ return event("UserPromptSubmit", { prompt: text(payload.prompt) });
3092
+ case "postToolUse": {
3093
+ const toolName = text(payload.tool_name);
3094
+ return event("PostToolUse", { tool_name: cursorToolNames[toolName] ?? toolName, tool_input: record(payload.tool_input) ?? {} });
3095
+ }
3096
+ case "afterAgentResponse":
3097
+ return event("Notification", { assistant_message: text(payload.text) });
3098
+ case "stop": {
3099
+ const status = text(payload.status);
3100
+ if (status === "error") return event("StopFailure", { error: "the turn ended on an error" });
3101
+ if (status === "aborted") return event("Stop", { turn_cancelled: true });
3102
+ return event("Stop", { keep_last_message: true });
3103
+ }
3104
+ default:
3105
+ return void 0;
3106
+ }
3107
+ }
3108
+ function text(value) {
3109
+ return typeof value === "string" ? value : "";
3110
+ }
3111
+ function record(value) {
3112
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2793
3113
  }
2794
3114
 
2795
3115
  // src/server/sessions/session-registry.ts
@@ -2852,7 +3172,7 @@ var SessionRegistry = class extends EventEmitter4 {
2852
3172
  this.resetWaitsOnNewDay(event.receivedAt);
2853
3173
  const before = this.sessions.get(event.sessionName);
2854
3174
  if (!before) return "";
2855
- const requestId = event.name === "PermissionRequest" ? randomUUID() : void 0;
3175
+ const requestId = event.name === "PermissionRequest" ? randomUUID2() : void 0;
2856
3176
  const after = SessionLifecycle.apply(before, event, { requestId });
2857
3177
  this.set(after);
2858
3178
  if (before.pending && before.pending.requestId !== after.pending?.requestId) {
@@ -2861,6 +3181,18 @@ var SessionRegistry = class extends EventEmitter4 {
2861
3181
  if (event.name === "UserPromptSubmit") this.resolvePromptWaiters(event.sessionName);
2862
3182
  return requestId ? this.heldRequests.hold(requestId, event.sessionName) : "";
2863
3183
  }
3184
+ // An event from Grok Build or Cursor, which run HeyTerm's hooks from a global file in every session.
3185
+ // Only the sessions HeyTerm started move; the reply is what the hook prints.
3186
+ async applyAgentHookEvent(agent, eventName, rawPayload, receivedAt) {
3187
+ const defaultReply = AgentHookTranslator.defaultReply(agent, eventName);
3188
+ const event = AgentHookTranslator.translate(agent, eventName, rawPayload);
3189
+ if (!event) return defaultReply;
3190
+ const found = AgentSessionMatch.find(this.list(), agent, event.agentSessionId, String(event.payload.cwd ?? ""));
3191
+ if (!found) return defaultReply;
3192
+ if (found.claimsId) this.set({ ...found.session, agentSessionId: event.agentSessionId });
3193
+ const answer = await this.applyHookEvent({ name: event.name, sessionName: found.session.name, payload: event.payload, receivedAt });
3194
+ return answer || defaultReply;
3195
+ }
2864
3196
  // An event from a session the user started in their own terminal, known by its Claude session id.
2865
3197
  async applyExternalHookEvent(name, payload, receivedAt) {
2866
3198
  const agentSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
@@ -2883,7 +3215,7 @@ var SessionRegistry = class extends EventEmitter4 {
2883
3215
  name,
2884
3216
  origin: "external",
2885
3217
  projectPath: workingDirectory,
2886
- projectName: path11.basename(workingDirectory) || name,
3218
+ projectName: path13.basename(workingDirectory) || name,
2887
3219
  agent: "claude",
2888
3220
  state: "idle",
2889
3221
  stateSince: now,
@@ -2949,9 +3281,9 @@ function localDayKey(time) {
2949
3281
  }
2950
3282
 
2951
3283
  // src/server/sessions/state-store.ts
2952
- import { randomUUID as randomUUID2 } from "node:crypto";
2953
- import { mkdir as mkdir6, readFile as readFile4, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
2954
- import path12 from "node:path";
3284
+ import { randomUUID as randomUUID3 } from "node:crypto";
3285
+ import { mkdir as mkdir7, readFile as readFile5, rename as rename4, writeFile as writeFile5 } from "node:fs/promises";
3286
+ import path14 from "node:path";
2955
3287
  function upgraded(saved) {
2956
3288
  const { claudeSessionId, ...session } = saved;
2957
3289
  return { ...session, agent: session.agent ?? "claude", agentSessionId: session.agentSessionId ?? claudeSessionId };
@@ -2959,13 +3291,13 @@ function upgraded(saved) {
2959
3291
  var StateStore = class {
2960
3292
  filePath;
2961
3293
  constructor(dataDirectory2) {
2962
- this.filePath = path12.join(dataDirectory2, "state.json");
3294
+ this.filePath = path14.join(dataDirectory2, "state.json");
2963
3295
  }
2964
3296
  async load() {
2965
- const text = await readFile4(this.filePath, "utf8").catch(() => "");
2966
- if (!text) return { sessions: [], recentlyClosed: [] };
3297
+ const text2 = await readFile5(this.filePath, "utf8").catch(() => "");
3298
+ if (!text2) return { sessions: [], recentlyClosed: [] };
2967
3299
  try {
2968
- const parsed = JSON.parse(text);
3300
+ const parsed = JSON.parse(text2);
2969
3301
  return { sessions: (parsed.sessions ?? []).map(upgraded), recentlyClosed: (parsed.recentlyClosed ?? []).map(upgraded) };
2970
3302
  } catch {
2971
3303
  return { sessions: [], recentlyClosed: [] };
@@ -2980,10 +3312,10 @@ var StateStore = class {
2980
3312
  }
2981
3313
  saving = Promise.resolve();
2982
3314
  async write(state) {
2983
- await mkdir6(path12.dirname(this.filePath), { recursive: true });
2984
- const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID2()}.tmp`;
2985
- await writeFile4(temporaryPath, JSON.stringify(state), { mode: 384 });
2986
- await rename3(temporaryPath, this.filePath);
3315
+ await mkdir7(path14.dirname(this.filePath), { recursive: true });
3316
+ const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID3()}.tmp`;
3317
+ await writeFile5(temporaryPath, JSON.stringify(state), { mode: 384 });
3318
+ await rename4(temporaryPath, this.filePath);
2987
3319
  }
2988
3320
  };
2989
3321
 
@@ -3023,9 +3355,9 @@ var TrustDialogWatcher = class {
3023
3355
 
3024
3356
  // src/server/setup/setup-service.ts
3025
3357
  import { execFile as execFile2 } from "node:child_process";
3026
- import { stat as stat3 } from "node:fs/promises";
3027
- import os4 from "node:os";
3028
- import path13 from "node:path";
3358
+ import { stat as stat4 } from "node:fs/promises";
3359
+ import os5 from "node:os";
3360
+ import path15 from "node:path";
3029
3361
  import { promisify as promisify2 } from "node:util";
3030
3362
  var execFileAsync2 = promisify2(execFile2);
3031
3363
  var testedClaudeVersion = "2.1.310";
@@ -3074,7 +3406,7 @@ var SetupService = class {
3074
3406
  async setProjectRoots(roots) {
3075
3407
  const expanded = roots.map((root) => root.trim()).filter(Boolean).map(expandHome);
3076
3408
  const existing = [];
3077
- for (const root of expanded) if (await isFolder2(root)) existing.push(root);
3409
+ for (const root of expanded) if (await isFolder3(root)) existing.push(root);
3078
3410
  await this.userConfig.update({ projectRoots: existing });
3079
3411
  await this.projects.setRoots(existing);
3080
3412
  return expanded.filter((root) => !existing.includes(root));
@@ -3105,38 +3437,38 @@ function compareVersions(left, right) {
3105
3437
  return 0;
3106
3438
  }
3107
3439
  function expandHome(folder) {
3108
- return folder === "~" || folder.startsWith("~/") ? path13.join(os4.homedir(), folder.slice(1)) : path13.resolve(folder);
3440
+ return folder === "~" || folder.startsWith("~/") ? path15.join(os5.homedir(), folder.slice(1)) : path15.resolve(folder);
3109
3441
  }
3110
- async function isFolder2(folder) {
3111
- return stat3(folder).then((info) => info.isDirectory(), () => false);
3442
+ async function isFolder3(folder) {
3443
+ return stat4(folder).then((info) => info.isDirectory(), () => false);
3112
3444
  }
3113
3445
 
3114
3446
  // src/server/setup/input-log.ts
3115
- import { appendFile as appendFile2, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
3116
- import path14 from "node:path";
3447
+ import { appendFile as appendFile2, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
3448
+ import path16 from "node:path";
3117
3449
  var keptLines = 1e3;
3118
3450
  var InputLog = class {
3119
3451
  filePath;
3120
3452
  writing = Promise.resolve();
3121
3453
  constructor(dataDirectory2) {
3122
- this.filePath = path14.join(dataDirectory2, "input-log.jsonl");
3454
+ this.filePath = path16.join(dataDirectory2, "input-log.jsonl");
3123
3455
  }
3124
3456
  record(records) {
3125
- const safe = records.slice(0, 500).map((record) => JSON.stringify(withoutText(record))).join("\n");
3457
+ const safe = records.slice(0, 500).map((record2) => JSON.stringify(withoutText(record2))).join("\n");
3126
3458
  if (!safe) return;
3127
3459
  this.writing = this.writing.then(async () => {
3128
3460
  await appendFile2(this.filePath, `${safe}
3129
3461
  `, { mode: 384 });
3130
- const lines = (await readFile5(this.filePath, "utf8")).split("\n").filter(Boolean);
3131
- if (lines.length > keptLines * 2) await writeFile5(this.filePath, `${lines.slice(-keptLines).join("\n")}
3462
+ const lines = (await readFile6(this.filePath, "utf8")).split("\n").filter(Boolean);
3463
+ if (lines.length > keptLines * 2) await writeFile6(this.filePath, `${lines.slice(-keptLines).join("\n")}
3132
3464
  `, { mode: 384 });
3133
3465
  }).catch(() => void 0);
3134
3466
  }
3135
3467
  };
3136
- function withoutText(record) {
3468
+ function withoutText(record2) {
3137
3469
  const kept = {};
3138
3470
  for (const key of ["at", "type", "inputType", "length", "key", "target", "windowHasFocus"]) {
3139
- const value = record[key];
3471
+ const value = record2[key];
3140
3472
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "string" && value.length <= 40) kept[key] = value;
3141
3473
  }
3142
3474
  return kept;
@@ -3160,7 +3492,10 @@ var keySequences = {
3160
3492
  Up: "\x1B[A",
3161
3493
  Down: "\x1B[B",
3162
3494
  Right: "\x1B[C",
3163
- Left: "\x1B[D"
3495
+ Left: "\x1B[D",
3496
+ // Grok Build's and Cursor's dialogs answer to a letter.
3497
+ y: "y",
3498
+ a: "a"
3164
3499
  };
3165
3500
  var pasteStart = "\x1B[200~";
3166
3501
  var pasteEnd = "\x1B[201~";
@@ -3174,8 +3509,8 @@ var TerminalInput = class {
3174
3509
  }
3175
3510
  // Dictated text as the program would get it from a real paste. Control characters are dropped,
3176
3511
  // so text can never press keys; without bracketed paste, line breaks become spaces.
3177
- static paste(text, { bracketedPaste }) {
3178
- const withoutMarkers = text.replaceAll(pasteStart, "").replaceAll(pasteEnd, "");
3512
+ static paste(text2, { bracketedPaste }) {
3513
+ const withoutMarkers = text2.replaceAll(pasteStart, "").replaceAll(pasteEnd, "");
3179
3514
  const lines = withoutMarkers.split(/\r\n|\r|\n/).map((line) => line.replace(/[\x00-\x1f\x7f]/g, ""));
3180
3515
  if (!bracketedPaste) return lines.join(" ");
3181
3516
  return `${pasteStart}${lines.join("\r")}${pasteEnd}`;
@@ -3229,10 +3564,10 @@ var TerminalHost = class extends EventEmitter5 {
3229
3564
  killQuietly(hosted.process);
3230
3565
  }
3231
3566
  // Text as one paste, so line breaks and symbols arrive as text, never as key presses.
3232
- pasteText(name, text) {
3567
+ pasteText(name, text2) {
3233
3568
  const hosted = this.terminals.get(name);
3234
3569
  if (!hosted) return;
3235
- hosted.process.write(TerminalInput.paste(text, { bracketedPaste: hosted.screen.modes.bracketedPasteMode }));
3570
+ hosted.process.write(TerminalInput.paste(text2, { bracketedPaste: hosted.screen.modes.bracketedPasteMode }));
3236
3571
  }
3237
3572
  pressKeys(name, keys) {
3238
3573
  this.terminals.get(name)?.process.write(TerminalInput.keys(keys));
@@ -3310,15 +3645,15 @@ var MacVoice = class {
3310
3645
  available = process.platform === "darwin";
3311
3646
  speaking;
3312
3647
  deduplicator = new SpokenLineDeduplicator(duplicateWindowMilliseconds);
3313
- speak(text, done) {
3314
- if (!this.available || !text.trim() || !this.deduplicator.shouldSpeak(text, Date.now())) return done();
3648
+ speak(text2, done) {
3649
+ if (!this.available || !text2.trim() || !this.deduplicator.shouldSpeak(text2, Date.now())) return done();
3315
3650
  this.stop();
3316
3651
  const child = spawn("say", ["-f", "-"], { stdio: ["pipe", "ignore", "ignore"] });
3317
3652
  const line = { child, done };
3318
3653
  child.on("error", () => this.finish(line));
3319
3654
  child.on("exit", () => this.finish(line));
3320
3655
  child.stdin.on("error", () => void 0);
3321
- child.stdin.end(text);
3656
+ child.stdin.end(text2);
3322
3657
  this.speaking = line;
3323
3658
  }
3324
3659
  stop() {
@@ -3339,8 +3674,8 @@ var SpokenLineDeduplicator = class {
3339
3674
  }
3340
3675
  windowMilliseconds;
3341
3676
  lastSaidAt = /* @__PURE__ */ new Map();
3342
- shouldSpeak(text, now) {
3343
- const key = text.trim();
3677
+ shouldSpeak(text2, now) {
3678
+ const key = text2.trim();
3344
3679
  const last = this.lastSaidAt.get(key);
3345
3680
  for (const [line, at] of this.lastSaidAt) if (now - at > this.windowMilliseconds) this.lastSaidAt.delete(line);
3346
3681
  if (last !== void 0 && now - last <= this.windowMilliseconds) return false;
@@ -3359,6 +3694,8 @@ async function startHeyTerm() {
3359
3694
  const userConfig = new UserConfig(config.dataDirectory);
3360
3695
  const savedChoices = await userConfig.load();
3361
3696
  const hooks = await new HookInstaller(config.dataDirectory, config.port).install();
3697
+ const agentHookFiles = new AgentHookFiles(hooks.forwardScriptPath, path17.join(config.dataDirectory, "backups"));
3698
+ await agentHookFiles.install().catch((error) => console.error(`Could not add HeyTerm's Grok and Cursor hooks: ${error.message}`));
3362
3699
  await stopSessionsFromOlderVersion(config.tmuxSocketName);
3363
3700
  const host = new TerminalHost();
3364
3701
  const heldRequests = new HeldHookRequests();
@@ -3375,12 +3712,12 @@ async function startHeyTerm() {
3375
3712
  const decisions = new DecisionActions(registry, heldRequests, audit);
3376
3713
  const sessionActions = new SessionActions(registry, launcher, projects, audit);
3377
3714
  const executor = new ActionExecutor(registry, projects, new FastPathRouter({ sendUnaddressedWordsToFocusedSession: false }), decisions, sessionActions);
3378
- const assistant = createAssistant({ model: config.routerModel, workingDirectory: os5.homedir(), registry, launcher, projects, sessionActions, decisions, executor, audit });
3715
+ const assistant = createAssistant({ model: config.routerModel, workingDirectory: os6.homedir(), registry, launcher, projects, sessionActions, decisions, executor, audit });
3379
3716
  executor.attachAssistant(assistant);
3380
3717
  executor.on("feedback", (feedback) => assistant.recordOutcome(feedback.text));
3381
3718
  registry.on("changed", () => assistant.noteSessions(registry.list()));
3382
3719
  assistant.noteSessions(registry.list());
3383
- const connector = new ExistingSessionConnector(hooks.forwardScriptPath, path15.join(config.dataDirectory, "backups"));
3720
+ const connector = new ExistingSessionConnector(hooks.forwardScriptPath, path17.join(config.dataDirectory, "backups"));
3384
3721
  const showOwnTerminalSessions = savedChoices.showOwnTerminalSessions ?? await connector.isConnected();
3385
3722
  if (savedChoices.showOwnTerminalSessions === void 0 && showOwnTerminalSessions) await userConfig.update({ showOwnTerminalSessions });
3386
3723
  if (showOwnTerminalSessions) await connector.connect().catch((error) => console.error(error.message));
@@ -3402,6 +3739,7 @@ Stopping HeyTerm (${reason}).`);
3402
3739
  assistant.close();
3403
3740
  launcher.shutdown();
3404
3741
  if ((await userConfig.load()).showOwnTerminalSessions) await connector.disconnect().catch(() => void 0);
3742
+ await agentHookFiles.remove().catch(() => void 0);
3405
3743
  await registry.saveNow().catch(() => void 0);
3406
3744
  await server.close();
3407
3745
  console.log("HeyTerm stopped. Its sessions are under Recently closed next time.");
@@ -3440,30 +3778,30 @@ async function stopSessionsFromOlderVersion(tmuxSocketName) {
3440
3778
 
3441
3779
  // src/server/setup/trash.ts
3442
3780
  import { execFile as execFile4 } from "node:child_process";
3443
- import { mkdir as mkdir7, rename as rename4, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
3444
- import os6 from "node:os";
3445
- import path16 from "node:path";
3781
+ import { mkdir as mkdir8, rename as rename5, stat as stat5, writeFile as writeFile7 } from "node:fs/promises";
3782
+ import os7 from "node:os";
3783
+ import path18 from "node:path";
3446
3784
  import { promisify as promisify4 } from "node:util";
3447
3785
  var execFileAsync4 = promisify4(execFile4);
3448
3786
  async function moveToTrash(target) {
3449
- if (process.platform === "darwin") return moveIntoFolder(target, path16.join(os6.homedir(), ".Trash"));
3787
+ if (process.platform === "darwin") return moveIntoFolder(target, path18.join(os7.homedir(), ".Trash"));
3450
3788
  if (process.platform === "win32") return moveToRecycleBin(target);
3451
3789
  return moveToFreedesktopTrash(target);
3452
3790
  }
3453
3791
  async function moveIntoFolder(target, trashFolder) {
3454
- await rename4(target, await freeName(trashFolder, path16.basename(target)));
3792
+ await rename5(target, await freeName(trashFolder, path18.basename(target)));
3455
3793
  }
3456
3794
  async function moveToFreedesktopTrash(target) {
3457
- const trashHome = path16.join(process.env.XDG_DATA_HOME ?? path16.join(os6.homedir(), ".local", "share"), "Trash");
3458
- await mkdir7(path16.join(trashHome, "files"), { recursive: true });
3459
- await mkdir7(path16.join(trashHome, "info"), { recursive: true });
3460
- const destination = await freeName(path16.join(trashHome, "files"), path16.basename(target));
3795
+ const trashHome = path18.join(process.env.XDG_DATA_HOME ?? path18.join(os7.homedir(), ".local", "share"), "Trash");
3796
+ await mkdir8(path18.join(trashHome, "files"), { recursive: true });
3797
+ await mkdir8(path18.join(trashHome, "info"), { recursive: true });
3798
+ const destination = await freeName(path18.join(trashHome, "files"), path18.basename(target));
3461
3799
  const info = `[Trash Info]
3462
3800
  Path=${encodeURI(target)}
3463
3801
  DeletionDate=${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19)}
3464
3802
  `;
3465
- await writeFile6(path16.join(trashHome, "info", `${path16.basename(destination)}.trashinfo`), info);
3466
- await rename4(target, destination);
3803
+ await writeFile7(path18.join(trashHome, "info", `${path18.basename(destination)}.trashinfo`), info);
3804
+ await rename5(target, destination);
3467
3805
  }
3468
3806
  async function moveToRecycleBin(target) {
3469
3807
  const quoted2 = target.replaceAll("'", "''");
@@ -3471,19 +3809,19 @@ async function moveToRecycleBin(target) {
3471
3809
  await execFileAsync4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 3e4 });
3472
3810
  }
3473
3811
  async function freeName(folder, name) {
3474
- let candidate = path16.join(folder, name);
3475
- for (let number = 2; await exists2(candidate); number += 1) candidate = path16.join(folder, `${name} ${number}`);
3812
+ let candidate = path18.join(folder, name);
3813
+ for (let number = 2; await exists2(candidate); number += 1) candidate = path18.join(folder, `${name} ${number}`);
3476
3814
  return candidate;
3477
3815
  }
3478
3816
  async function exists2(target) {
3479
- return stat4(target).then(
3817
+ return stat5(target).then(
3480
3818
  () => true,
3481
3819
  () => false
3482
3820
  );
3483
3821
  }
3484
3822
 
3485
3823
  // src/server/setup/uninstaller.ts
3486
- import { readFile as readFile6, rename as rename5, writeFile as writeFile7 } from "node:fs/promises";
3824
+ import { readFile as readFile7, rename as rename6, writeFile as writeFile8 } from "node:fs/promises";
3487
3825
  var Uninstaller = class {
3488
3826
  constructor(options) {
3489
3827
  this.options = options;
@@ -3491,6 +3829,7 @@ var Uninstaller = class {
3491
3829
  options;
3492
3830
  async run() {
3493
3831
  const removedHooks = await this.removeHooks();
3832
+ await new AgentHookFiles("", "", this.options.agentHookPaths ?? agentHookPaths()).remove().catch(() => void 0);
3494
3833
  const report = { removedHooks, trashed: [], leftBehind: [] };
3495
3834
  try {
3496
3835
  await this.options.moveToTrash(this.options.dataDirectory);
@@ -3501,19 +3840,19 @@ var Uninstaller = class {
3501
3840
  return report;
3502
3841
  }
3503
3842
  async removeHooks() {
3504
- const text = await readFile6(this.options.claudeSettingsPath, "utf8").catch(() => void 0);
3505
- if (text === void 0) return false;
3843
+ const text2 = await readFile7(this.options.claudeSettingsPath, "utf8").catch(() => void 0);
3844
+ if (text2 === void 0) return false;
3506
3845
  let settings;
3507
3846
  try {
3508
- settings = JSON.parse(text || "{}");
3847
+ settings = JSON.parse(text2 || "{}");
3509
3848
  } catch {
3510
3849
  return false;
3511
3850
  }
3512
3851
  if (!UserSettingsHooks.isConnected(settings)) return false;
3513
3852
  const temporaryPath = `${this.options.claudeSettingsPath}.heyterm.tmp`;
3514
- await writeFile7(temporaryPath, `${JSON.stringify(UserSettingsHooks.withoutHeyTerm(settings), null, 2)}
3853
+ await writeFile8(temporaryPath, `${JSON.stringify(UserSettingsHooks.withoutHeyTerm(settings), null, 2)}
3515
3854
  `);
3516
- await rename5(temporaryPath, this.options.claudeSettingsPath);
3855
+ await rename6(temporaryPath, this.options.claudeSettingsPath);
3517
3856
  return true;
3518
3857
  }
3519
3858
  };
@@ -3616,12 +3955,12 @@ function baseUrl() {
3616
3955
  return `http://127.0.0.1:${process.env.HEYTERM_PORT ?? 4747}`;
3617
3956
  }
3618
3957
  function dataDirectory() {
3619
- return process.env.HEYTERM_DATA_DIRECTORY ?? path17.join(os7.homedir(), ".heyterm");
3958
+ return process.env.HEYTERM_DATA_DIRECTORY ?? path19.join(os8.homedir(), ".heyterm");
3620
3959
  }
3621
3960
  async function readToken() {
3622
- const folders = [dataDirectory(), path17.join(os7.homedir(), ".callsign")];
3961
+ const folders = [dataDirectory(), path19.join(os8.homedir(), ".callsign")];
3623
3962
  for (const folder of folders) {
3624
- const token = (await readFile7(path17.join(folder, "token"), "utf8").catch(() => "")).trim();
3963
+ const token = (await readFile8(path19.join(folder, "token"), "utf8").catch(() => "")).trim();
3625
3964
  if (token) return token;
3626
3965
  }
3627
3966
  return void 0;