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.
- package/README.md +23 -5
- package/THIRD_PARTY_NOTICES.md +565 -28
- package/assets/logo-mark.svg +5 -5
- package/dist/server/cli.js +526 -187
- package/dist/web/assets/index-B8NXrT8_.css +1 -0
- package/dist/web/assets/index-DN5rav7f.js +108 -0
- package/dist/web/favicon.svg +5 -5
- package/dist/web/index.html +4 -3
- package/dist/web/theme-boot.js +9 -0
- package/package.json +8 -3
- package/dist/web/assets/index-B6Nt0upF.js +0 -106
- package/dist/web/assets/index-DG46Czg4.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -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
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
|
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
|
-
|
|
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", () =>
|
|
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
|
|
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(
|
|
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
|
|
278
|
-
import
|
|
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(
|
|
344
|
-
const value =
|
|
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,
|
|
388
|
-
return _SessionLifecycle.addActivity(session, "decision",
|
|
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
|
-
|
|
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,
|
|
428
|
-
if (!
|
|
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(
|
|
448
|
-
const value =
|
|
485
|
+
function stringField2(record2, key) {
|
|
486
|
+
const value = record2[key];
|
|
449
487
|
return typeof value === "string" ? value : "";
|
|
450
488
|
}
|
|
451
|
-
function recordField(
|
|
452
|
-
const value =
|
|
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(
|
|
586
|
-
const utterance =
|
|
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((
|
|
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,
|
|
733
|
+
async deliver(sessionName, text2) {
|
|
696
734
|
this.markSeen(sessionName);
|
|
697
|
-
const result = await this.launcher.deliverText(sessionName,
|
|
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(
|
|
829
|
-
return /[.?!]$/.test(
|
|
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(
|
|
843
|
-
return
|
|
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(
|
|
848
|
-
return [...
|
|
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(
|
|
855
|
-
return wordSpans(
|
|
927
|
+
function normalizedPhrase(text2) {
|
|
928
|
+
return wordSpans(text2).map((span) => span.word).join(" ");
|
|
856
929
|
}
|
|
857
|
-
function textFromWord(
|
|
930
|
+
function textFromWord(text2, spans, wordIndex) {
|
|
858
931
|
const span = spans[wordIndex];
|
|
859
|
-
return span ?
|
|
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(
|
|
952
|
-
return
|
|
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]
|
|
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
|
|
1138
|
-
return
|
|
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
|
|
1146
|
-
return
|
|
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(
|
|
1188
|
-
if (
|
|
1189
|
-
const sentences =
|
|
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(
|
|
1199
|
-
return
|
|
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(
|
|
1250
|
-
const checked = UtteranceLimits.check(
|
|
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,
|
|
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,
|
|
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(
|
|
1437
|
-
const oneLine =
|
|
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,
|
|
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(
|
|
1537
|
-
this.record("assistant",
|
|
1538
|
-
this.emit("say",
|
|
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
|
-
|
|
1545
|
-
|
|
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,
|
|
1627
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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(
|
|
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(
|
|
2419
|
+
function safeJson(text2) {
|
|
2216
2420
|
try {
|
|
2217
|
-
return JSON.parse(
|
|
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", (
|
|
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(
|
|
2493
|
+
async receive(text2, socket) {
|
|
2290
2494
|
let message;
|
|
2291
2495
|
try {
|
|
2292
|
-
message = JSON.parse(
|
|
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.
|
|
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
|
|
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
|
|
2358
|
-
for (const socket of this.sockets) if (socket.readyState === socket.OPEN) socket.send(
|
|
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
|
|
2449
|
-
if (
|
|
2450
|
-
const message = parseMessage(
|
|
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(
|
|
2661
|
+
function parseMessage(text2) {
|
|
2458
2662
|
try {
|
|
2459
|
-
const message = JSON.parse(
|
|
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
|
|
2470
|
-
import
|
|
2471
|
-
import
|
|
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 =
|
|
2499
|
-
const
|
|
2500
|
-
return
|
|
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(
|
|
2510
|
-
return
|
|
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:
|
|
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 =
|
|
2522
|
-
const
|
|
2523
|
-
if (
|
|
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) ? `${
|
|
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
|
|
2538
|
-
import
|
|
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 =
|
|
2746
|
+
this.filePath = path10.join(dataDirectory2, "audit.jsonl");
|
|
2543
2747
|
}
|
|
2544
2748
|
async record(entry) {
|
|
2545
|
-
await
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
3007
|
+
import path12 from "node:path";
|
|
2781
3008
|
var ExternalSessionNaming = class {
|
|
2782
3009
|
static nameFor(facts, takenNames) {
|
|
2783
|
-
const base = slug(facts.sessionTitle ?? "") || slug(
|
|
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(
|
|
2792
|
-
return
|
|
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" ?
|
|
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:
|
|
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
|
|
2953
|
-
import { mkdir as
|
|
2954
|
-
import
|
|
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 =
|
|
3294
|
+
this.filePath = path14.join(dataDirectory2, "state.json");
|
|
2963
3295
|
}
|
|
2964
3296
|
async load() {
|
|
2965
|
-
const
|
|
2966
|
-
if (!
|
|
3297
|
+
const text2 = await readFile5(this.filePath, "utf8").catch(() => "");
|
|
3298
|
+
if (!text2) return { sessions: [], recentlyClosed: [] };
|
|
2967
3299
|
try {
|
|
2968
|
-
const parsed = JSON.parse(
|
|
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
|
|
2984
|
-
const temporaryPath = `${this.filePath}.${process.pid}.${
|
|
2985
|
-
await
|
|
2986
|
-
await
|
|
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
|
|
3027
|
-
import
|
|
3028
|
-
import
|
|
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
|
|
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("~/") ?
|
|
3440
|
+
return folder === "~" || folder.startsWith("~/") ? path15.join(os5.homedir(), folder.slice(1)) : path15.resolve(folder);
|
|
3109
3441
|
}
|
|
3110
|
-
async function
|
|
3111
|
-
return
|
|
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
|
|
3116
|
-
import
|
|
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 =
|
|
3454
|
+
this.filePath = path16.join(dataDirectory2, "input-log.jsonl");
|
|
3123
3455
|
}
|
|
3124
3456
|
record(records) {
|
|
3125
|
-
const safe = records.slice(0, 500).map((
|
|
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
|
|
3131
|
-
if (lines.length > keptLines * 2) await
|
|
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(
|
|
3468
|
+
function withoutText(record2) {
|
|
3137
3469
|
const kept = {};
|
|
3138
3470
|
for (const key of ["at", "type", "inputType", "length", "key", "target", "windowHasFocus"]) {
|
|
3139
|
-
const value =
|
|
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(
|
|
3178
|
-
const withoutMarkers =
|
|
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,
|
|
3567
|
+
pasteText(name, text2) {
|
|
3233
3568
|
const hosted = this.terminals.get(name);
|
|
3234
3569
|
if (!hosted) return;
|
|
3235
|
-
hosted.process.write(TerminalInput.paste(
|
|
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(
|
|
3314
|
-
if (!this.available || !
|
|
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(
|
|
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(
|
|
3343
|
-
const key =
|
|
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:
|
|
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,
|
|
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
|
|
3444
|
-
import
|
|
3445
|
-
import
|
|
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,
|
|
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
|
|
3792
|
+
await rename5(target, await freeName(trashFolder, path18.basename(target)));
|
|
3455
3793
|
}
|
|
3456
3794
|
async function moveToFreedesktopTrash(target) {
|
|
3457
|
-
const trashHome =
|
|
3458
|
-
await
|
|
3459
|
-
await
|
|
3460
|
-
const destination = await freeName(
|
|
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
|
|
3466
|
-
await
|
|
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 =
|
|
3475
|
-
for (let number = 2; await exists2(candidate); number += 1) candidate =
|
|
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
|
|
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
|
|
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
|
|
3505
|
-
if (
|
|
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(
|
|
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
|
|
3853
|
+
await writeFile8(temporaryPath, `${JSON.stringify(UserSettingsHooks.withoutHeyTerm(settings), null, 2)}
|
|
3515
3854
|
`);
|
|
3516
|
-
await
|
|
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 ??
|
|
3958
|
+
return process.env.HEYTERM_DATA_DIRECTORY ?? path19.join(os8.homedir(), ".heyterm");
|
|
3620
3959
|
}
|
|
3621
3960
|
async function readToken() {
|
|
3622
|
-
const folders = [dataDirectory(),
|
|
3961
|
+
const folders = [dataDirectory(), path19.join(os8.homedir(), ".callsign")];
|
|
3623
3962
|
for (const folder of folders) {
|
|
3624
|
-
const token = (await
|
|
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;
|