harnery 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +54 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +11 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +57 -13
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/hooks/cli.js +87 -18
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/package.json +2 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +97 -11
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/core/agents/render/prompt-context.ts +25 -8
- package/src/core/agents/render/session-context.ts +83 -16
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/hooks/cli.ts +106 -17
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/storage/process-log.ts +17 -2
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
package/dist/core/hooks/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationComman
|
|
|
30
30
|
import { readHeartbeat, setAssignedNameCache, stampSessionNameReminded, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
|
|
31
31
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
32
32
|
import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.js";
|
|
33
|
-
import { assignName } from "../agents/state/names.js";
|
|
33
|
+
import { assignName, readForkParent, readForkState, recordForkDetection, resolveName, } from "../agents/state/names.js";
|
|
34
34
|
import { writePidmapRow } from "../agents/state/pidmap.js";
|
|
35
35
|
import { agentsRequireGitFinalization, coordFreshnessSeconds, hostPromptContextConfig, resolveBinName, } from "../config.js";
|
|
36
36
|
import { checkpointContext, completeContextRecovery, extractContextSample, markContextCompactionCompleted, prepareContextRecovery, readContextState, recordContextSample, } from "../context/index.js";
|
|
@@ -48,6 +48,7 @@ import { hookTimeoutFor } from "./adapter/wiring.js";
|
|
|
48
48
|
import { scheduleBackupSnapshot } from "./backup-schedule.js";
|
|
49
49
|
import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, recordImageArtifactsV3, resetSoundCounters, runSessionSyncExtension, soundForEvent, } from "./effects/index.js";
|
|
50
50
|
import { toolInputHash, toolTargetHash } from "./events/input-hash.js";
|
|
51
|
+
import { detectClaudeCodeFork } from "./fork-lineage.js";
|
|
51
52
|
import { canonicalize } from "./guard-path.js";
|
|
52
53
|
import { beginHookHealth, observeHookDebug, observeHookError, writeHookHealthCompletion, } from "./health.js";
|
|
53
54
|
import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
|
|
@@ -56,6 +57,7 @@ import { resolveOwner } from "./resolve/owner.js";
|
|
|
56
57
|
import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
|
|
57
58
|
import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.js";
|
|
58
59
|
import { sessionNamePresence } from "./session-name-presence.js";
|
|
60
|
+
import { shellRmReason } from "./shell-rm.js";
|
|
59
61
|
import { shellWaiterReason } from "./shell-waiter.js";
|
|
60
62
|
let hookHealthState;
|
|
61
63
|
let hookRunMarkerPath = null;
|
|
@@ -155,11 +157,13 @@ function logError(coordRoot, err, context) {
|
|
|
155
157
|
* Mint or recover the hurricane-style name for this owner in-process. Returns
|
|
156
158
|
* null on any failure so session.started emission never breaks adapter flow.
|
|
157
159
|
*/
|
|
158
|
-
function assignNameInProcess(coordRoot, instanceId, kind,
|
|
160
|
+
function assignNameInProcess(coordRoot, instanceId, kind, fork) {
|
|
159
161
|
try {
|
|
160
162
|
const name = assignName(coordRoot, instanceId, kind, {
|
|
161
163
|
freshnessSecs: coordFreshnessSeconds(coordRoot),
|
|
162
|
-
...(forkedFrom ? { forkedFrom } : {}),
|
|
164
|
+
...(fork?.forkedFrom ? { forkedFrom: fork.forkedFrom } : {}),
|
|
165
|
+
...(fork?.fork ? { fork: true } : {}),
|
|
166
|
+
...(fork?.forkChecked ? { forkChecked: true } : {}),
|
|
163
167
|
});
|
|
164
168
|
return { name, kind };
|
|
165
169
|
}
|
|
@@ -167,9 +171,60 @@ function assignNameInProcess(coordRoot, instanceId, kind, forkedFrom) {
|
|
|
167
171
|
return null;
|
|
168
172
|
}
|
|
169
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Name a main session, recording fork lineage when the instance is new. Every
|
|
176
|
+
* main-session naming path goes through here: whichever hook first sees a new
|
|
177
|
+
* instance writes its history row, and lineage can only ride that first row
|
|
178
|
+
* (later detection appends a `fork.detected` row instead).
|
|
179
|
+
*/
|
|
180
|
+
function assignSessionNameInProcess(coordRoot, instanceId, payload, adapter) {
|
|
181
|
+
let fork;
|
|
182
|
+
try {
|
|
183
|
+
if (adapter === "claude-code" &&
|
|
184
|
+
payload?.session_id === instanceId &&
|
|
185
|
+
!resolveName(coordRoot, instanceId)) {
|
|
186
|
+
const detected = detectClaudeCodeFork({
|
|
187
|
+
coordRoot,
|
|
188
|
+
transcriptPath: payload.transcript_path,
|
|
189
|
+
sessionId: instanceId,
|
|
190
|
+
source: payload.source,
|
|
191
|
+
});
|
|
192
|
+
fork = {
|
|
193
|
+
...(detected.forkedFrom ? { forkedFrom: detected.forkedFrom } : {}),
|
|
194
|
+
fork: detected.fork,
|
|
195
|
+
forkChecked: detected.checked,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
fork = undefined;
|
|
201
|
+
}
|
|
202
|
+
return assignNameInProcess(coordRoot, instanceId, "session", fork);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Finish fork detection that session start could not: a CLI fork starts before
|
|
206
|
+
* its transcript exists, and a mid-flight instance may never see SessionStart.
|
|
207
|
+
* Runs once per instance; the appended `fork_checked` row stops later calls.
|
|
208
|
+
*/
|
|
209
|
+
function healForkLineage(coordRoot, instanceId, payload) {
|
|
210
|
+
if (!payload?.transcript_path || payload.session_id !== instanceId)
|
|
211
|
+
return;
|
|
212
|
+
const state = readForkState(coordRoot, instanceId);
|
|
213
|
+
if (!state || state.checked)
|
|
214
|
+
return;
|
|
215
|
+
const detected = detectClaudeCodeFork({
|
|
216
|
+
coordRoot,
|
|
217
|
+
transcriptPath: payload.transcript_path,
|
|
218
|
+
sessionId: instanceId,
|
|
219
|
+
knownFork: state.fork,
|
|
220
|
+
});
|
|
221
|
+
if (!detected.checked)
|
|
222
|
+
return;
|
|
223
|
+
recordForkDetection(coordRoot, instanceId, detected);
|
|
224
|
+
}
|
|
170
225
|
/** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
|
|
171
226
|
* Stamp the disposable cache so Codec does not render the 8-character id. */
|
|
172
|
-
function ensureSessionDisplayName(coordRoot, instanceId, sessionId, adapter) {
|
|
227
|
+
function ensureSessionDisplayName(coordRoot, instanceId, sessionId, adapter, payload) {
|
|
173
228
|
if (coordEnv("WORKFLOW_CHILD") === "1")
|
|
174
229
|
return;
|
|
175
230
|
const row = readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
|
|
@@ -177,7 +232,7 @@ function ensureSessionDisplayName(coordRoot, instanceId, sessionId, adapter) {
|
|
|
177
232
|
return;
|
|
178
233
|
if (row?.name?.trim())
|
|
179
234
|
return;
|
|
180
|
-
const assigned =
|
|
235
|
+
const assigned = assignSessionNameInProcess(coordRoot, instanceId, payload, adapter);
|
|
181
236
|
if (!assigned?.name)
|
|
182
237
|
return;
|
|
183
238
|
if (!readHeartbeat(coordRoot, instanceId)) {
|
|
@@ -230,21 +285,21 @@ function buildEventData(eventType, ctx) {
|
|
|
230
285
|
// The platform label is the adapter id itself; enumerating and defaulting
|
|
231
286
|
// to codex silently mislabels any adapter not in the list.
|
|
232
287
|
const adapterPlatform = ctx.adapter;
|
|
233
|
-
// Recorded fork lineage is NOT detected here. On claude-code a fork
|
|
234
|
-
// never fires its own session.started — SessionStart fires under the
|
|
235
|
-
// PARENT's session id (source=resume) before the fork id is minted
|
|
236
|
-
// (verified 2026-08-05) — so detection at this point can only mislabel
|
|
237
|
-
// the resumed parent. The fork's new instance is caught by the
|
|
238
|
-
// tool.requested heal path instead. The forkedFrom plumbing below stays
|
|
239
|
-
// for adapters that DO report a parent at session start.
|
|
240
|
-
const forkedFrom = undefined;
|
|
241
288
|
// Assign (or recover) name + kind in-process. Idempotent: resume
|
|
242
|
-
// returns the original name; new owner consumes a counter slot
|
|
289
|
+
// returns the original name; new owner consumes a counter slot and,
|
|
290
|
+
// on claude-code, runs fork detection (fork-lineage.ts). Since Claude
|
|
291
|
+
// Code 2.1.283 both fork flows fire SessionStart under the FORK's own
|
|
292
|
+
// id (verified 2026-10-07), superseding the 2026-08-05 finding that a
|
|
293
|
+
// fork never started on its own id. A resumed parent keeps its existing
|
|
294
|
+
// history row, so it is never relabeled here.
|
|
243
295
|
// Workflow children stay unnamed, as on the tool path: a pool name
|
|
244
296
|
// would make them the addressee of mail held for that name.
|
|
245
297
|
const assigned = coordEnv("WORKFLOW_CHILD") === "1"
|
|
246
298
|
? undefined
|
|
247
|
-
:
|
|
299
|
+
: assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
|
|
300
|
+
const forkedFrom = assigned
|
|
301
|
+
? readForkParent(ctx.coordRoot, ctx.instanceId)?.instance_id
|
|
302
|
+
: undefined;
|
|
248
303
|
// Write the adapter pid-map row so `harn agents whoami` ppid-walks find
|
|
249
304
|
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
250
305
|
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
@@ -309,7 +364,7 @@ function buildEventData(eventType, ctx) {
|
|
|
309
364
|
// 8-character native id.
|
|
310
365
|
if (coordEnv("WORKFLOW_CHILD") !== "1" &&
|
|
311
366
|
!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
|
|
312
|
-
|
|
367
|
+
assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
|
|
313
368
|
}
|
|
314
369
|
const prompt = p?.prompt ?? "";
|
|
315
370
|
const { value, truncated } = clampString(prompt, 4000);
|
|
@@ -645,7 +700,7 @@ async function main() {
|
|
|
645
700
|
priorCoordination?.kind !== "transient" &&
|
|
646
701
|
!priorCoordination?.workflow_run_id &&
|
|
647
702
|
!priorCoordination?.name?.trim()) {
|
|
648
|
-
|
|
703
|
+
assignSessionNameInProcess(coordRoot, owner.instance_id, payload, adapter);
|
|
649
704
|
}
|
|
650
705
|
const data = buildEventData(norm.event_type, {
|
|
651
706
|
coordRoot,
|
|
@@ -820,12 +875,20 @@ function recordSignal(run) {
|
|
|
820
875
|
}
|
|
821
876
|
if (norm.event_type !== "session.ended") {
|
|
822
877
|
try {
|
|
823
|
-
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
|
|
878
|
+
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter, payload);
|
|
824
879
|
}
|
|
825
880
|
catch (error) {
|
|
826
881
|
logError(coordRoot, error, { phase: "session-display-name" });
|
|
827
882
|
}
|
|
828
883
|
}
|
|
884
|
+
if (norm.event_type === "tool.requested" && adapter === "claude-code" && run.topLevelSession) {
|
|
885
|
+
try {
|
|
886
|
+
healForkLineage(coordRoot, owner.instance_id, payload);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
logError(coordRoot, error, { phase: "fork-lineage-heal" });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
829
892
|
const v3EventId = v3Result && "event" in v3Result
|
|
830
893
|
? v3Result.event.event_id
|
|
831
894
|
: v3Result && "event_id" in v3Result
|
|
@@ -1281,6 +1344,12 @@ async function handleToolRequested(run) {
|
|
|
1281
1344
|
emitDeny(adapter, waiterReason);
|
|
1282
1345
|
return 0;
|
|
1283
1346
|
}
|
|
1347
|
+
const rmReason = shellRmReason(payload?.tool_name, payload?.tool_input, resolveBinName(coordRoot));
|
|
1348
|
+
if (rmReason) {
|
|
1349
|
+
const { emitDeny } = await import("./adapter/output.js");
|
|
1350
|
+
emitDeny(adapter, rmReason);
|
|
1351
|
+
return 0;
|
|
1352
|
+
}
|
|
1284
1353
|
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
1285
1354
|
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
1286
1355
|
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code fork detection: decide whether a new main-session instance is a
|
|
3
|
+
* branch of another session, and which one.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes exist as of Claude Code 2.1.283 (verified 2026-10-07):
|
|
6
|
+
*
|
|
7
|
+
* - CLI `--resume <id> --fork-session` fires SessionStart under the FORK's id
|
|
8
|
+
* with `source: "fork"`, before its transcript exists, and names no parent.
|
|
9
|
+
* The transcript later holds the parent's message uuids with every row's
|
|
10
|
+
* `sessionId` rewritten, so the parent is found by uuid containment
|
|
11
|
+
* (`detectForkParent`) once the transcript is on disk.
|
|
12
|
+
* - The desktop app's "Fork from here" copies the parent's rows into the
|
|
13
|
+
* fork's transcript with their original `sessionId`, so inherited rows
|
|
14
|
+
* name the parent directly (`detectInheritedSessionParent`).
|
|
15
|
+
*
|
|
16
|
+
* Inherited rows alone do not prove a fork: a continuation of a finished
|
|
17
|
+
* session can carry them too. The parent's liveness separates the two. A
|
|
18
|
+
* parent with an unended generation is still running (or resumable) and owns
|
|
19
|
+
* its name, task, and claims, so the new instance is a fork. A finished
|
|
20
|
+
* parent makes it a continuation, which keeps the generic notice.
|
|
21
|
+
*
|
|
22
|
+
* Everything here fails open: an unreadable transcript or an unknown layout
|
|
23
|
+
* yields "not a fork", never an error.
|
|
24
|
+
*/
|
|
25
|
+
export interface ForkDetection {
|
|
26
|
+
fork: boolean;
|
|
27
|
+
forkedFrom?: string;
|
|
28
|
+
/** Detection had the evidence it needs; the tool-path heal can skip it. */
|
|
29
|
+
checked: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function detectClaudeCodeFork(input: {
|
|
32
|
+
coordRoot: string;
|
|
33
|
+
transcriptPath: string | undefined;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
source?: string;
|
|
36
|
+
/** The instance was already recorded as a fork without a parent. */
|
|
37
|
+
knownFork?: boolean;
|
|
38
|
+
isLive?: (instanceId: string) => boolean;
|
|
39
|
+
}): ForkDetection;
|
|
40
|
+
//# sourceMappingURL=fork-lineage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fork-lineage.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/fork-lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;CAClB;AAKD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1C,GAAG,aAAa,CAkChB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code fork detection: decide whether a new main-session instance is a
|
|
3
|
+
* branch of another session, and which one.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes exist as of Claude Code 2.1.283 (verified 2026-10-07):
|
|
6
|
+
*
|
|
7
|
+
* - CLI `--resume <id> --fork-session` fires SessionStart under the FORK's id
|
|
8
|
+
* with `source: "fork"`, before its transcript exists, and names no parent.
|
|
9
|
+
* The transcript later holds the parent's message uuids with every row's
|
|
10
|
+
* `sessionId` rewritten, so the parent is found by uuid containment
|
|
11
|
+
* (`detectForkParent`) once the transcript is on disk.
|
|
12
|
+
* - The desktop app's "Fork from here" copies the parent's rows into the
|
|
13
|
+
* fork's transcript with their original `sessionId`, so inherited rows
|
|
14
|
+
* name the parent directly (`detectInheritedSessionParent`).
|
|
15
|
+
*
|
|
16
|
+
* Inherited rows alone do not prove a fork: a continuation of a finished
|
|
17
|
+
* session can carry them too. The parent's liveness separates the two. A
|
|
18
|
+
* parent with an unended generation is still running (or resumable) and owns
|
|
19
|
+
* its name, task, and claims, so the new instance is a fork. A finished
|
|
20
|
+
* parent makes it a continuation, which keeps the generic notice.
|
|
21
|
+
*
|
|
22
|
+
* Everything here fails open: an unreadable transcript or an unknown layout
|
|
23
|
+
* yields "not a fork", never an error.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
27
|
+
import { detectForkParent, detectInheritedSessionParent } from "./resolve/transcript.js";
|
|
28
|
+
/** Sources after which a brand-new instance cannot hold inherited history. */
|
|
29
|
+
const FRESH_SOURCES = new Set(["startup", "clear"]);
|
|
30
|
+
export function detectClaudeCodeFork(input) {
|
|
31
|
+
try {
|
|
32
|
+
const declared = input.source === "fork" || input.knownFork === true;
|
|
33
|
+
const transcriptReady = !!input.transcriptPath && existsSync(input.transcriptPath);
|
|
34
|
+
const isLive = input.isLive ?? ((id) => readLiveCoordinationRow(input.coordRoot, id) !== null);
|
|
35
|
+
const inherited = transcriptReady
|
|
36
|
+
? detectInheritedSessionParent(input.transcriptPath, input.sessionId)
|
|
37
|
+
: undefined;
|
|
38
|
+
if (inherited && inherited !== input.sessionId) {
|
|
39
|
+
if (declared || isLive(inherited)) {
|
|
40
|
+
return { fork: true, forkedFrom: inherited, checked: true };
|
|
41
|
+
}
|
|
42
|
+
return { fork: false, checked: true };
|
|
43
|
+
}
|
|
44
|
+
if (declared) {
|
|
45
|
+
if (!transcriptReady)
|
|
46
|
+
return { fork: true, checked: false };
|
|
47
|
+
const parent = detectForkParent(input.transcriptPath, input.sessionId);
|
|
48
|
+
return {
|
|
49
|
+
fork: true,
|
|
50
|
+
...(parent && parent !== input.sessionId ? { forkedFrom: parent } : {}),
|
|
51
|
+
checked: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
fork: false,
|
|
56
|
+
checked: transcriptReady || FRESH_SOURCES.has(input.source ?? ""),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { fork: false, checked: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -92,6 +92,22 @@ export interface TranscriptRuntimeScan {
|
|
|
92
92
|
* SessionStart).
|
|
93
93
|
*/
|
|
94
94
|
export declare function scanTranscriptRuntime(transcriptPath: string | undefined): TranscriptRuntimeScan | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Detect inherited history from row session ids: the session whose rows were
|
|
97
|
+
* copied into this transcript, or undefined when every row is this session's.
|
|
98
|
+
*
|
|
99
|
+
* Empirical basis (verified 2026-10-07, Claude Code 2.1.283): the desktop
|
|
100
|
+
* app's "Fork from here" copies the parent's rows into the fork's transcript
|
|
101
|
+
* WITHOUT rewriting their `sessionId`, so the fork's file opens with thousands
|
|
102
|
+
* of rows stamped with the parent's id. The CLI `--fork-session` path rewrites
|
|
103
|
+
* the ids instead and is covered by {@link detectForkParent}.
|
|
104
|
+
*
|
|
105
|
+
* When the history is itself several generations deep, the LAST foreign row is
|
|
106
|
+
* the nearest ancestor, so the tail is scanned first. Whether the foreign
|
|
107
|
+
* session is a fork parent or a dead predecessor (a continuation) is the
|
|
108
|
+
* caller's call. Fail-soft: never throws, undefined on any read problem.
|
|
109
|
+
*/
|
|
110
|
+
export declare function detectInheritedSessionParent(transcriptPath: string | undefined, sessionId: string): string | undefined;
|
|
95
111
|
/**
|
|
96
112
|
* Detect the parent session of a forked CC conversation from its transcript.
|
|
97
113
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/transcript.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,wBAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GACb,OAAO,CAiCT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB9F;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,OAAO,CAIT;AAED,MAAM,MAAM,4BAA4B,GACpC;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IACE,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;CAClF,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,4BAA4B,CAqD9B;AAgBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAUrF;AAwDD;4CAC4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd;+EAC2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,qBAAqB,GAAG,SAAS,CA8BnC;
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/transcript.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,wBAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GACb,OAAO,CAiCT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB9F;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,OAAO,CAIT;AAED,MAAM,MAAM,4BAA4B,GACpC;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IACE,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;CAClF,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,4BAA4B,CAqD9B;AAgBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAUrF;AAwDD;4CAC4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd;+EAC2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,qBAAqB,GAAG,SAAS,CA8BnC;AAgBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAiBpB;AA6CD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkDpB"}
|
|
@@ -318,6 +318,90 @@ const SESSION_FILE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
|
|
|
318
318
|
const FORK_SCAN_MAX_BYTES = 128 * 1024 * 1024;
|
|
319
319
|
/** How many message uuids to sample across the fork's copied prefix. */
|
|
320
320
|
const FORK_SAMPLE_SIZE = 24;
|
|
321
|
+
/** Tail window scanned for inherited rows; a fork's own rows are few when the
|
|
322
|
+
* scan runs (session start or the first tool call), so the copied history ends
|
|
323
|
+
* well inside it. */
|
|
324
|
+
const INHERITED_TAIL_BYTES = 4 * 1024 * 1024;
|
|
325
|
+
const INHERITED_HEAD_BYTES = 64 * 1024;
|
|
326
|
+
const SESSION_ID_FIELD_RE = /"sessionId":"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/gi;
|
|
327
|
+
/**
|
|
328
|
+
* Detect inherited history from row session ids: the session whose rows were
|
|
329
|
+
* copied into this transcript, or undefined when every row is this session's.
|
|
330
|
+
*
|
|
331
|
+
* Empirical basis (verified 2026-10-07, Claude Code 2.1.283): the desktop
|
|
332
|
+
* app's "Fork from here" copies the parent's rows into the fork's transcript
|
|
333
|
+
* WITHOUT rewriting their `sessionId`, so the fork's file opens with thousands
|
|
334
|
+
* of rows stamped with the parent's id. The CLI `--fork-session` path rewrites
|
|
335
|
+
* the ids instead and is covered by {@link detectForkParent}.
|
|
336
|
+
*
|
|
337
|
+
* When the history is itself several generations deep, the LAST foreign row is
|
|
338
|
+
* the nearest ancestor, so the tail is scanned first. Whether the foreign
|
|
339
|
+
* session is a fork parent or a dead predecessor (a continuation) is the
|
|
340
|
+
* caller's call. Fail-soft: never throws, undefined on any read problem.
|
|
341
|
+
*/
|
|
342
|
+
export function detectInheritedSessionParent(transcriptPath, sessionId) {
|
|
343
|
+
try {
|
|
344
|
+
const readablePath = resolveTranscriptPath(transcriptPath);
|
|
345
|
+
if (!readablePath || !sessionId)
|
|
346
|
+
return undefined;
|
|
347
|
+
const size = statSync(readablePath).size;
|
|
348
|
+
const fromTail = lastForeignSessionId(readRange(readablePath, Math.max(0, size - INHERITED_TAIL_BYTES), size), sessionId);
|
|
349
|
+
if (fromTail || size <= INHERITED_TAIL_BYTES)
|
|
350
|
+
return fromTail;
|
|
351
|
+
return lastForeignSessionId(readRange(readablePath, 0, Math.min(size, INHERITED_HEAD_BYTES)), sessionId);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function lastForeignSessionId(text, sessionId) {
|
|
358
|
+
if (!text)
|
|
359
|
+
return undefined;
|
|
360
|
+
const own = sessionId.toLowerCase();
|
|
361
|
+
const lines = text.split("\n");
|
|
362
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
363
|
+
const line = lines[i];
|
|
364
|
+
if (!line.includes('"sessionId":"'))
|
|
365
|
+
continue;
|
|
366
|
+
let foreign = false;
|
|
367
|
+
for (const m of line.matchAll(SESSION_ID_FIELD_RE)) {
|
|
368
|
+
if (m[1].toLowerCase() !== own)
|
|
369
|
+
foreign = true;
|
|
370
|
+
}
|
|
371
|
+
if (!foreign)
|
|
372
|
+
continue;
|
|
373
|
+
// Confirm on the parsed row: a nested object can carry another session's
|
|
374
|
+
// id, but only the row's own top-level field says whose row it is. A
|
|
375
|
+
// partial line at a window edge fails to parse and is skipped.
|
|
376
|
+
try {
|
|
377
|
+
const row = JSON.parse(line);
|
|
378
|
+
if (typeof row.sessionId === "string" && row.sessionId.toLowerCase() !== own) {
|
|
379
|
+
return row.sessionId;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* torn line */
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
function readRange(path, start, end) {
|
|
389
|
+
try {
|
|
390
|
+
const fs = require("node:fs");
|
|
391
|
+
const fd = fs.openSync(path, "r");
|
|
392
|
+
try {
|
|
393
|
+
const buf = Buffer.alloc(Math.max(0, end - start));
|
|
394
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
395
|
+
return buf.toString("utf8");
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
fs.closeSync(fd);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
321
405
|
/**
|
|
322
406
|
* Detect the parent session of a forked CC conversation from its transcript.
|
|
323
407
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-rm.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/shell-rm.ts"],"names":[],"mappings":"AAsFA,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAYf"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deny a shell `rm` and name the guarded replacement. The instructions have
|
|
4
|
+
* said "use <bin> rm instead of shell rm" since the command shipped, and
|
|
5
|
+
* agents still reached for bare rm several times a day, often on unquoted
|
|
6
|
+
* variable paths that Claude Code then stops on for a human prompt. A
|
|
7
|
+
* repeated, machine-detectable failure belongs in a gate, not more prose.
|
|
8
|
+
*
|
|
9
|
+
* This is a guardrail against the reflex, not a security boundary: it reads
|
|
10
|
+
* command positions only (so `git rm`, `docker rm`, and quoted examples pass),
|
|
11
|
+
* plus the `xargs rm`, `find -exec rm`, and `bash -c '...rm...'` shapes.
|
|
12
|
+
* HARNERY_ALLOW_RM=1 on the command is the deliberate override.
|
|
13
|
+
*/
|
|
14
|
+
const OVERRIDE = "HARNERY_ALLOW_RM=1";
|
|
15
|
+
const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
|
|
16
|
+
const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
|
|
17
|
+
const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
|
|
18
|
+
// xargs options that consume the following token as their value.
|
|
19
|
+
const XARGS_VALUE_FLAGS = new Set(["-n", "-I", "-L", "-P", "-d", "-E", "-s", "-a"]);
|
|
20
|
+
function isRm(value, quoted) {
|
|
21
|
+
return !quoted && basename(value) === "rm";
|
|
22
|
+
}
|
|
23
|
+
function invokesRm(source, depth = 0) {
|
|
24
|
+
if (depth > 8)
|
|
25
|
+
return false;
|
|
26
|
+
const tokens = tokenize(source);
|
|
27
|
+
if (tokens.some((t) => !t.quoted && t.value === OVERRIDE))
|
|
28
|
+
return false;
|
|
29
|
+
let start = true;
|
|
30
|
+
let afterWrapper = false;
|
|
31
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
32
|
+
const token = tokens[i];
|
|
33
|
+
if (token.operator) {
|
|
34
|
+
if (SEPARATORS.has(token.value)) {
|
|
35
|
+
start = true;
|
|
36
|
+
afterWrapper = false;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!start)
|
|
41
|
+
continue;
|
|
42
|
+
if (!token.quoted) {
|
|
43
|
+
if (KEYWORDS.has(token.value))
|
|
44
|
+
continue;
|
|
45
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value))
|
|
46
|
+
continue;
|
|
47
|
+
if (afterWrapper && token.value.startsWith("-"))
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const name = basename(token.value);
|
|
51
|
+
if (WRAPPERS.has(name)) {
|
|
52
|
+
afterWrapper = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
start = false;
|
|
56
|
+
afterWrapper = false;
|
|
57
|
+
if (isRm(token.value, token.quoted))
|
|
58
|
+
return true;
|
|
59
|
+
if (name === "xargs") {
|
|
60
|
+
for (let j = i + 1; j < tokens.length && !tokens[j].operator; j++) {
|
|
61
|
+
const arg = tokens[j];
|
|
62
|
+
if (arg.value.startsWith("-")) {
|
|
63
|
+
if (XARGS_VALUE_FLAGS.has(arg.value))
|
|
64
|
+
j++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isRm(arg.value, arg.quoted))
|
|
68
|
+
return true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (name === "find") {
|
|
73
|
+
for (let j = i + 1; j < tokens.length && !tokens[j].operator; j++) {
|
|
74
|
+
if (!/^-(?:exec|execdir|ok|okdir)$/.test(tokens[j].value))
|
|
75
|
+
continue;
|
|
76
|
+
const next = tokens[j + 1];
|
|
77
|
+
if (next && !next.operator && isRm(next.value, next.quoted))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
|
|
82
|
+
for (let j = i + 1; j < tokens.length && !tokens[j].operator; j++) {
|
|
83
|
+
if (/^-[a-z]*c[a-z]*$/.test(tokens[j].value)) {
|
|
84
|
+
const script = tokens[j + 1];
|
|
85
|
+
if (script && !script.operator && invokesRm(script.value, depth + 1))
|
|
86
|
+
return true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export function shellRmReason(toolName, toolInput, binName) {
|
|
95
|
+
const command = shellCommandFromTool(toolName, toolInput);
|
|
96
|
+
if (command === null || !invokesRm(command))
|
|
97
|
+
return null;
|
|
98
|
+
return (`Shell rm blocked: it skips the resolved-path, Git-ownership, and peer-claim checks, and an ` +
|
|
99
|
+
`unquoted variable path can expand to the wrong target. Use the guarded paths instead: ` +
|
|
100
|
+
`\`${binName} rm --root <directory> <paths...>\` previews removal of untracked files (add ` +
|
|
101
|
+
`--yes to delete, -r for a directory); \`git rm\` removes tracked source; ` +
|
|
102
|
+
`\`${binName} artifacts discard <id>\` retires managed evidence. To replace a file, overwrite ` +
|
|
103
|
+
`it in place or \`mv\` the new file over the old one, so nothing needs deleting. If none of ` +
|
|
104
|
+
`these fit, prefix the command with ${OVERRIDE}.`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Shared, deliberately small shell lexer for PreToolUse command guards. */
|
|
2
|
+
export interface Token {
|
|
3
|
+
value: string;
|
|
4
|
+
quoted: boolean;
|
|
5
|
+
operator?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Keep quoted arguments opaque, including examples and heredoc bodies. */
|
|
8
|
+
export declare function tokenize(source: string): Token[];
|
|
9
|
+
export declare function basename(value: string): string;
|
|
10
|
+
/** The command string of a Bash-style tool call, or null for any other tool. */
|
|
11
|
+
export declare function shellCommandFromTool(toolName: unknown, toolInput: unknown): string | null;
|
|
12
|
+
//# sourceMappingURL=shell-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-tokens.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/shell-tokens.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,CA+DhD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgBzF"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Shared, deliberately small shell lexer for PreToolUse command guards. */
|
|
2
|
+
/** Keep quoted arguments opaque, including examples and heredoc bodies. */
|
|
3
|
+
export function tokenize(source) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const heredocs = [];
|
|
6
|
+
for (let i = 0; i < source.length;) {
|
|
7
|
+
const ch = source[i];
|
|
8
|
+
if (ch === "\\" && source[i + 1] === "\n") {
|
|
9
|
+
i += 2;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (ch === "\n") {
|
|
13
|
+
out.push({ value: ";", quoted: false, operator: true });
|
|
14
|
+
i++;
|
|
15
|
+
for (const doc of heredocs.splice(0)) {
|
|
16
|
+
while (i < source.length) {
|
|
17
|
+
const end = source.indexOf("\n", i);
|
|
18
|
+
const stop = end === -1 ? source.length : end;
|
|
19
|
+
const line = source.slice(i, stop);
|
|
20
|
+
i = end === -1 ? source.length : end + 1;
|
|
21
|
+
if ((doc.tabs ? line.replace(/^\t+/, "") : line) === doc.end)
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (/\s/.test(ch)) {
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === "#") {
|
|
32
|
+
const end = source.indexOf("\n", i);
|
|
33
|
+
i = end === -1 ? source.length : end;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (";|&(){}<>".includes(ch)) {
|
|
37
|
+
const op = source.slice(i).match(/^(?:<<-?|>>|&&|\|\||[;|&(){}<>])/)[0];
|
|
38
|
+
out.push({ value: op, quoted: false, operator: true });
|
|
39
|
+
i += op.length;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let value = "";
|
|
43
|
+
let quoted = false;
|
|
44
|
+
while (i < source.length && !/[\s;|&(){}<>]/.test(source[i])) {
|
|
45
|
+
const next = source[i++];
|
|
46
|
+
if (next === "'" || next === '"' || next === "`") {
|
|
47
|
+
quoted = true;
|
|
48
|
+
while (i < source.length && source[i] !== next) {
|
|
49
|
+
if (source[i] === "\\" && next !== "'" && i + 1 < source.length)
|
|
50
|
+
i++;
|
|
51
|
+
value += source[i++];
|
|
52
|
+
}
|
|
53
|
+
if (source[i] === next)
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
else if (next === "\\" && i < source.length) {
|
|
57
|
+
const escaped = source[i++];
|
|
58
|
+
if (escaped !== "\n")
|
|
59
|
+
value += escaped;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
value += next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const previous = out.at(-1)?.value;
|
|
66
|
+
if (previous === "<<" || previous === "<<-") {
|
|
67
|
+
heredocs.push({ end: value, tabs: previous === "<<-" });
|
|
68
|
+
}
|
|
69
|
+
out.push({ value, quoted });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function basename(value) {
|
|
74
|
+
return value.split(/[\\/]/).at(-1) ?? value;
|
|
75
|
+
}
|
|
76
|
+
/** The command string of a Bash-style tool call, or null for any other tool. */
|
|
77
|
+
export function shellCommandFromTool(toolName, toolInput) {
|
|
78
|
+
if (typeof toolName !== "string")
|
|
79
|
+
return null;
|
|
80
|
+
const name = toolName.toLowerCase().replace(/^functions\./, "");
|
|
81
|
+
if (!["bash", "shell", "shell_command", "exec_command"].includes(name))
|
|
82
|
+
return null;
|
|
83
|
+
let input = toolInput;
|
|
84
|
+
if (typeof input === "string") {
|
|
85
|
+
try {
|
|
86
|
+
input = JSON.parse(input);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!input || typeof input !== "object")
|
|
93
|
+
return null;
|
|
94
|
+
const args = input;
|
|
95
|
+
const command = args.command ?? args.cmd;
|
|
96
|
+
return typeof command === "string" ? command : null;
|
|
97
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-waiter.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/shell-waiter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shell-waiter.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/shell-waiter.ts"],"names":[],"mappings":"AA2EA,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGtF"}
|