litmus-cli 1.4.72 → 1.4.74
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 +13 -0
- package/dist/commands/connect.d.ts +17 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +51 -4
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/forward.d.ts +3 -0
- package/dist/commands/forward.d.ts.map +1 -1
- package/dist/commands/forward.js +81 -8
- package/dist/commands/forward.js.map +1 -1
- package/dist/lib/api.d.ts +7 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +38 -8
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auto-forward.d.ts +3 -1
- package/dist/lib/auto-forward.d.ts.map +1 -1
- package/dist/lib/auto-forward.js +32 -3
- package/dist/lib/auto-forward.js.map +1 -1
- package/dist/lib/backfill-tools.d.ts +3 -0
- package/dist/lib/backfill-tools.d.ts.map +1 -1
- package/dist/lib/backfill-tools.js +9 -2
- package/dist/lib/backfill-tools.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/connection-attempt.d.ts +13 -2
- package/dist/lib/connection-attempt.d.ts.map +1 -1
- package/dist/lib/connection-attempt.js +16 -3
- package/dist/lib/connection-attempt.js.map +1 -1
- package/dist/lib/hook-logger.cjs +99 -6
- package/dist/lib/hosted-capture.d.ts +21 -0
- package/dist/lib/hosted-capture.d.ts.map +1 -0
- package/dist/lib/hosted-capture.js +231 -0
- package/dist/lib/hosted-capture.js.map +1 -0
- package/dist/lib/hosted-transcripts.d.ts +31 -0
- package/dist/lib/hosted-transcripts.d.ts.map +1 -0
- package/dist/lib/hosted-transcripts.js +364 -0
- package/dist/lib/hosted-transcripts.js.map +1 -0
- package/dist/lib/hosted-watcher.d.ts +2 -0
- package/dist/lib/hosted-watcher.d.ts.map +1 -0
- package/dist/lib/hosted-watcher.js +200 -0
- package/dist/lib/hosted-watcher.js.map +1 -0
- package/dist/lib/session-state.d.ts +61 -0
- package/dist/lib/session-state.d.ts.map +1 -0
- package/dist/lib/session-state.js +92 -0
- package/dist/lib/session-state.js.map +1 -0
- package/dist/lib/ssh-client.d.ts +5 -1
- package/dist/lib/ssh-client.d.ts.map +1 -1
- package/dist/lib/ssh-client.js +22 -3
- package/dist/lib/ssh-client.js.map +1 -1
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +7 -2
- package/dist/lib/tracker.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/hook-logger.cjs
CHANGED
|
@@ -1876,7 +1876,58 @@ function claimPrompt(generationId) {
|
|
|
1876
1876
|
* session's) and is the only pointer left once a copied explicit path has
|
|
1877
1877
|
* been rejected.
|
|
1878
1878
|
*/
|
|
1879
|
+
// Hosted capture is explicitly machine-wide for the disclosed assessment
|
|
1880
|
+
// window. An expired lease never falls back to this registration's credentials.
|
|
1881
|
+
function hostedCaptureWindow(now = Date.now()) {
|
|
1882
|
+
try {
|
|
1883
|
+
const window = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".litmus", "hosted-capture.json"), "utf8"))
|
|
1884
|
+
if (typeof window.projectDir !== "string" || !Number.isFinite(window.startedAt)
|
|
1885
|
+
|| !Number.isFinite(window.expiresAt) || now < window.startedAt
|
|
1886
|
+
|| now >= window.expiresAt || window.expiresAt - now > 10000) return null
|
|
1887
|
+
const dir = path.join(window.projectDir, ".litmus")
|
|
1888
|
+
if (sessionOverMarked(dir)) return null
|
|
1889
|
+
const config = readConfig(dir)
|
|
1890
|
+
if (!config || config.captureMode !== "hosted-local") return null
|
|
1891
|
+
if (tokenDigest(config.token) !== window.tokenKey) return null
|
|
1892
|
+
return { ...window, litmusDir: dir }
|
|
1893
|
+
} catch { return null }
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function ownedHostedTranscript(window, effectiveTool, id) {
|
|
1897
|
+
if (!window || !id || !["claude", "codex"].includes(effectiveTool)) return null
|
|
1898
|
+
try {
|
|
1899
|
+
const state = JSON.parse(fs.readFileSync(path.join(window.litmusDir, "transcript-observer.json"), "utf8"))
|
|
1900
|
+
if (state.version !== 1 || state.startedAt !== window.startedAt) return null
|
|
1901
|
+
return state.cursors.find(c => c.tool === effectiveTool && c.sessionId === id) || null
|
|
1902
|
+
} catch { return null }
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function markHostedClaudeCompletion(window, cursor, parsed) {
|
|
1906
|
+
try {
|
|
1907
|
+
// Stop can precede the final disk flush. Use the same bounded settling
|
|
1908
|
+
// reader as normal responses, and match the hook's final text when given.
|
|
1909
|
+
const entryId = readSettledTranscript(cursor.file, raw => {
|
|
1910
|
+
if (!raw.endsWith("\n")) return null
|
|
1911
|
+
const reply = parseClaudeTranscript(raw)
|
|
1912
|
+
if (!reply.tailComplete) return null
|
|
1913
|
+
const final = reply.blocks[reply.blocks.length - 1]
|
|
1914
|
+
const finalText = reply.blocks.filter(block => block.id === final?.id).map(block => block.text).join("\n")
|
|
1915
|
+
if (typeof parsed.last_assistant_message === "string" && finalText !== parsed.last_assistant_message) return null
|
|
1916
|
+
return final?.id || null
|
|
1917
|
+
})
|
|
1918
|
+
if (!entryId || hostedCaptureWindow()?.litmusDir !== window.litmusDir) return
|
|
1919
|
+
const dir = path.join(window.litmusDir, "transcript-completions")
|
|
1920
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
1921
|
+
const key = crypto.createHash("sha256").update(JSON.stringify([cursor.sessionId])).digest("hex")
|
|
1922
|
+
const file = path.join(dir, `${key}.json`), temp = `${file}.${crypto.randomUUID()}.tmp`
|
|
1923
|
+
fs.writeFileSync(temp, JSON.stringify({ startedAt: window.startedAt, completedAt: Date.now(), entryId }), { mode: 0o600 })
|
|
1924
|
+
fs.renameSync(temp, file)
|
|
1925
|
+
} catch { /* completion metadata is best-effort; terminal transcript rows still work */ }
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1879
1928
|
function resolveAssessmentDir(parsed, workspaceRoots) {
|
|
1929
|
+
const hosted = hostedCaptureWindow()
|
|
1930
|
+
if (hosted) return hosted.litmusDir
|
|
1880
1931
|
const sessionDirs = [
|
|
1881
1932
|
...(workspaceRoots || []),
|
|
1882
1933
|
...(parsed && typeof parsed.cwd === "string" && parsed.cwd ? [parsed.cwd] : []),
|
|
@@ -1886,7 +1937,10 @@ function resolveAssessmentDir(parsed, workspaceRoots) {
|
|
|
1886
1937
|
const hintDirs = trustExplicit
|
|
1887
1938
|
? [explicitAssessmentDir, ...sessionDirs]
|
|
1888
1939
|
: sessionDirs
|
|
1889
|
-
|
|
1940
|
+
const found = findLitmusDir(hintDirs)
|
|
1941
|
+
// Folder lookup must not resurrect an expired hosted window.
|
|
1942
|
+
if (found && readConfig(found)?.captureMode === "hosted-local") return null
|
|
1943
|
+
return found
|
|
1890
1944
|
}
|
|
1891
1945
|
|
|
1892
1946
|
async function main() {
|
|
@@ -1905,6 +1959,7 @@ async function main() {
|
|
|
1905
1959
|
let parsed = null
|
|
1906
1960
|
try { parsed = JSON.parse(raw) } catch { /* extractPrompt reports the failure */ }
|
|
1907
1961
|
const cursorShaped = isCursorPayload(parsed)
|
|
1962
|
+
const effectiveTool = (tool === "claude" && (cursorShaped || process.env.CURSOR_TRACE_ID)) ? "cursor" : tool
|
|
1908
1963
|
|
|
1909
1964
|
// ENG-2768: an assessment whose session is over records nothing more. Asked
|
|
1910
1965
|
// here, before any transcript read, against the same placement the event
|
|
@@ -1914,6 +1969,24 @@ async function main() {
|
|
|
1914
1969
|
const earlyRoots = parsed && Array.isArray(parsed.workspace_roots) ? parsed.workspace_roots : []
|
|
1915
1970
|
const earlyLitmusDir = resolveAssessmentDir(parsed, earlyRoots)
|
|
1916
1971
|
if (earlyLitmusDir && sessionOverMarked(earlyLitmusDir)) return
|
|
1972
|
+
const hostedAtStart = hostedCaptureWindow()
|
|
1973
|
+
const owned = ownedHostedTranscript(hostedAtStart, effectiveTool, parsed && (parsed.session_id || parsed.sessionId))
|
|
1974
|
+
if (owned && effectiveTool === "claude" && parsed?.hook_event_name === "Stop") {
|
|
1975
|
+
markHostedClaudeCompletion(hostedAtStart, owned, parsed)
|
|
1976
|
+
return
|
|
1977
|
+
}
|
|
1978
|
+
// Bind occurrence time to the SAME receipt before potentially slow transcript
|
|
1979
|
+
// settling. Renewal during the read must not move this event past its grant.
|
|
1980
|
+
const hostedCapturedAt = hostedAtStart
|
|
1981
|
+
? hostedAtStart.serverTime + Date.now() - hostedAtStart.leaseResponseReceivedAt
|
|
1982
|
+
: null
|
|
1983
|
+
// Responses must have an observed prompt in THIS capture window. Do not
|
|
1984
|
+
// read/import a transcript for a conversation that predates setup.
|
|
1985
|
+
if (hostedAtStart && isResponseEvent(parsed)) {
|
|
1986
|
+
const id = parsed && (parsed.session_id || parsed.sessionId || parsed.conversation_id)
|
|
1987
|
+
const turn = currentTurnRecord(id)
|
|
1988
|
+
if (!turn || !turn.turnId || turn.ts < hostedAtStart.startedAt) return
|
|
1989
|
+
}
|
|
1917
1990
|
|
|
1918
1991
|
// ENG-2477: a capture SELF-TEST probe. `litmus init` (and `litmus doctor`)
|
|
1919
1992
|
// run this script the way the tool would — the tool's own payload shape, on
|
|
@@ -1938,9 +2011,13 @@ async function main() {
|
|
|
1938
2011
|
// The relabel is what makes the claim below label-safe: whichever invocation
|
|
1939
2012
|
// wins the race, a Cursor-shaped payload is recorded as tool="cursor", so the
|
|
1940
2013
|
// event never blames Claude Code for a prompt it had no part in.
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2014
|
+
|
|
2015
|
+
// Existing hosted sessions are emitted by the transcript observer even when
|
|
2016
|
+
// this session's hooks reload later. New sessions retain the hook lane. The
|
|
2017
|
+
// immutable setup boundary gives each session exactly one writer.
|
|
2018
|
+
if (hostedAtStart && !selftestNonce && (effectiveTool === "codex" || effectiveTool === "claude")) {
|
|
2019
|
+
if (owned) return
|
|
2020
|
+
}
|
|
1944
2021
|
|
|
1945
2022
|
// Post-response events (Stop / stop / afterAgentResponse) produce an
|
|
1946
2023
|
// ai_response event; everything else stays on the pre-ENG-1323 prompt path.
|
|
@@ -2004,6 +2081,7 @@ async function main() {
|
|
|
2004
2081
|
|
|
2005
2082
|
const litmusDir = resolveAssessmentDir(parsed, workspaceRoots)
|
|
2006
2083
|
if (!litmusDir) return
|
|
2084
|
+
if (hostedAtStart && hostedCaptureWindow()?.litmusDir !== hostedAtStart.litmusDir) return
|
|
2007
2085
|
if (litmusDir !== earlyLitmusDir && sessionOverMarked(litmusDir)) return
|
|
2008
2086
|
|
|
2009
2087
|
// Claim as LATE as possible — only once we know we have content and somewhere
|
|
@@ -2068,6 +2146,11 @@ async function main() {
|
|
|
2068
2146
|
// no second mechanism; and it survives hostd's `strip_credentials`, whose key
|
|
2069
2147
|
// list would have deleted anything spelled session/token/secret.
|
|
2070
2148
|
event.id = crypto.randomUUID()
|
|
2149
|
+
if (hostedAtStart) {
|
|
2150
|
+
event.capture_location = "local"
|
|
2151
|
+
event.capture_receipt = hostedAtStart.receipt
|
|
2152
|
+
event.ts = new Date(hostedCapturedAt).toISOString()
|
|
2153
|
+
}
|
|
2071
2154
|
if (sessionId) event.sessionId = sessionId
|
|
2072
2155
|
if (turnId) event.turnId = turnId
|
|
2073
2156
|
// ENG-2344: the files Cursor's `afterFileEdit` hook reported the agent
|
|
@@ -2100,6 +2183,7 @@ async function main() {
|
|
|
2100
2183
|
const MAX_SERIALIZED_BYTES = 60000
|
|
2101
2184
|
const bodyField = responseEvent ? "text" : "prompt"
|
|
2102
2185
|
if (!fullCursorPrompt) trimEventToBytes(event, bodyField, MAX_SERIALIZED_BYTES)
|
|
2186
|
+
else if (hostedAtStart && !trimEventToBytes(event, bodyField, 6_010_000)) return
|
|
2103
2187
|
|
|
2104
2188
|
// Record that this turn's reply is captured, so the sibling `stop` hook
|
|
2105
2189
|
// doesn't record it a second time (IDE only — see the claim helpers).
|
|
@@ -2107,6 +2191,9 @@ async function main() {
|
|
|
2107
2191
|
markCursorResponseCaptured(cursorConversation)
|
|
2108
2192
|
}
|
|
2109
2193
|
|
|
2194
|
+
// Recheck after transcript reads/settling: teardown may have happened meanwhile.
|
|
2195
|
+
if (hostedAtStart && hostedCaptureWindow()?.litmusDir !== litmusDir) return
|
|
2196
|
+
|
|
2110
2197
|
// 1. Append to activity.jsonl (for submission ZIP)
|
|
2111
2198
|
const logPath = path.join(litmusDir, "activity.jsonl")
|
|
2112
2199
|
let logged = false
|
|
@@ -2131,7 +2218,12 @@ async function main() {
|
|
|
2131
2218
|
// cannot edit or delete it afterwards (ENG-1962). Additive — step 1 above
|
|
2132
2219
|
// stays the submission-zip path, so a container whose drain lane is down
|
|
2133
2220
|
// still grades exactly as it does today.
|
|
2134
|
-
emitToDrain(event, parsed && parsed.hook_event_name)
|
|
2221
|
+
if (!hostedAtStart) emitToDrain(event, parsed && parsed.hook_event_name)
|
|
2222
|
+
|
|
2223
|
+
// Hosted hooks only append to the private local outbox. The supervisor
|
|
2224
|
+
// batches uploads below the activity endpoint's rate limit and persists its
|
|
2225
|
+
// acknowledged cursor. Never spend one HTTP request for each agent call.
|
|
2226
|
+
if (hostedAtStart) return
|
|
2135
2227
|
|
|
2136
2228
|
// 2. Upload to the server. Awaited, bounded — see uploadEvent.
|
|
2137
2229
|
const config = readConfig(litmusDir)
|
|
@@ -2149,7 +2241,7 @@ async function main() {
|
|
|
2149
2241
|
// work — this is a courtesy, and it must never cost the prompt anything.
|
|
2150
2242
|
// Not on a self-test probe: init is about to start the tracker itself, and
|
|
2151
2243
|
// doctor has already dealt with it.
|
|
2152
|
-
if (!selftestNonce) maybeReviveWatcher(litmusDir, config)
|
|
2244
|
+
if (!selftestNonce && !hostedAtStart) maybeReviveWatcher(litmusDir, config)
|
|
2153
2245
|
|
|
2154
2246
|
// No explicit process.exit() — the event loop drains naturally once the upload
|
|
2155
2247
|
// has settled and its timers are cleared (the revival spawn is unref'd).
|
|
@@ -2157,6 +2249,7 @@ async function main() {
|
|
|
2157
2249
|
|
|
2158
2250
|
// Exported for unit tests only; every hook invocation runs this file directly.
|
|
2159
2251
|
module.exports = {
|
|
2252
|
+
hostedCaptureWindow,
|
|
2160
2253
|
nativeSessionPath,
|
|
2161
2254
|
watcherPresumedDead,
|
|
2162
2255
|
isFinalRefusal,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InitMetadata } from "./schemas.js";
|
|
2
|
+
export declare const HOSTED_CAPTURE_DISCLOSURE_VERSION = "hosted_local_v1";
|
|
3
|
+
export declare const HOSTED_CAPTURE_DISCLOSURE = "During this assessment, Litmus records prompts and responses from supported AI agents on this computer, across all folders and projects, alongside activity captured on the Litmus workspace. The hiring company can review these conversations. Capture stops when the coding session ends; if this computer loses contact with Litmus, capture pauses within ten seconds. Browser AI, Copilot and unsupported tools are not covered. Restart any already running local AI tools after setup so they load the capture hooks.";
|
|
4
|
+
export declare const HOSTED_CAPTURE_FILE: string;
|
|
5
|
+
export declare function atomicPrivateJson(file: string, value: unknown): void;
|
|
6
|
+
export declare function captureLease(backendUrl: string, token: string, accept?: boolean): Promise<{
|
|
7
|
+
active: boolean;
|
|
8
|
+
leaseSeconds: number;
|
|
9
|
+
receipt?: string;
|
|
10
|
+
serverTime?: number;
|
|
11
|
+
receivedAt: number;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function acquireCaptureLock(lock: string): () => void;
|
|
14
|
+
/** OS-owned exclusion makes stale-file recovery a serialized operation.
|
|
15
|
+
* The loopback guard carries no protocol/data and is released by the OS on crash.
|
|
16
|
+
* A fixed per-lock port ensures contenders cannot bind independent guards.
|
|
17
|
+
*/
|
|
18
|
+
export declare function acquireCaptureSetupLock(lock: string): Promise<() => Promise<void>>;
|
|
19
|
+
export declare function askCaptureConsent(): Promise<string>;
|
|
20
|
+
export declare function startHostedCapture(meta: InitMetadata, token: string, apiBase: string): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=hosted-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-capture.d.ts","sourceRoot":"","sources":["../../src/lib/hosted-capture.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAKhD,eAAO,MAAM,iCAAiC,oBAAoB,CAAA;AAClE,eAAO,MAAM,yBAAyB,kgBAAkgB,CAAA;AACxiB,eAAO,MAAM,mBAAmB,QAA4D,CAAA;AAC5F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAKpE;AACD,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAC,CAAC,CAWjM;AACD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,IAAI,CA6B3D;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAgBxF;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAqBzD;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD1G"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./version.js";
|
|
2
|
+
import { createHash, randomUUID } from "crypto";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, rmSync } from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import readline from "readline";
|
|
7
|
+
import tty from "tty";
|
|
8
|
+
import { createServer } from "net";
|
|
9
|
+
import { openSync, linkSync, unlinkSync, realpathSync } from "fs";
|
|
10
|
+
import { startTracker, waitForTrackerReady, isTrackerHealthy } from "./tracker.js";
|
|
11
|
+
import { setupAiTracking } from "../commands/init.js";
|
|
12
|
+
import { releaseAiTrackingForAssessment, readRegistrySnapshot } from "./ai-tracking.js";
|
|
13
|
+
import { retireAiCaptureForEndedSession, sessionOverMarked } from "./session-over.js";
|
|
14
|
+
import { prepareHostedTranscripts } from "./hosted-transcripts.js";
|
|
15
|
+
export const HOSTED_CAPTURE_DISCLOSURE_VERSION = "hosted_local_v1";
|
|
16
|
+
export const HOSTED_CAPTURE_DISCLOSURE = "During this assessment, Litmus records prompts and responses from supported AI agents on this computer, across all folders and projects, alongside activity captured on the Litmus workspace. The hiring company can review these conversations. Capture stops when the coding session ends; if this computer loses contact with Litmus, capture pauses within ten seconds. Browser AI, Copilot and unsupported tools are not covered. Restart any already running local AI tools after setup so they load the capture hooks.";
|
|
17
|
+
export const HOSTED_CAPTURE_FILE = path.join(os.homedir(), ".litmus", "hosted-capture.json");
|
|
18
|
+
export function atomicPrivateJson(file, value) {
|
|
19
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
20
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(temp, JSON.stringify(value), { mode: 0o600, flag: "wx" });
|
|
23
|
+
renameSync(temp, file);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
rmSync(temp, { force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function captureLease(backendUrl, token, accept = false) {
|
|
30
|
+
const response = await fetch(`${backendUrl}/cli/capture-session${accept ? "/accept" : ""}`, {
|
|
31
|
+
method: accept ? "POST" : "GET", redirect: "error", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
32
|
+
...(accept ? { body: JSON.stringify({ disclosureVersion: HOSTED_CAPTURE_DISCLOSURE_VERSION }) } : {}),
|
|
33
|
+
signal: AbortSignal.timeout(4000),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(`Local capture authorization failed (${response.status})`);
|
|
37
|
+
const value = await response.json();
|
|
38
|
+
if (typeof value.active !== "boolean" || typeof value.leaseSeconds !== "number" || value.leaseSeconds < 0 || value.leaseSeconds > 10)
|
|
39
|
+
throw new Error("Invalid capture authorization");
|
|
40
|
+
if (value.active && (typeof value.receipt !== "string" || value.receipt.length > 2048 || typeof value.serverTime !== "number" || !Number.isFinite(value.serverTime)))
|
|
41
|
+
throw new Error("Missing local capture receipt");
|
|
42
|
+
return { active: value.active, leaseSeconds: value.leaseSeconds, receipt: value.receipt, serverTime: value.serverTime, receivedAt: Date.now() };
|
|
43
|
+
}
|
|
44
|
+
export function acquireCaptureLock(lock) {
|
|
45
|
+
const nonce = randomUUID();
|
|
46
|
+
const temp = `${lock}.${nonce}.tmp`;
|
|
47
|
+
writeFileSync(temp, JSON.stringify({ pid: process.pid, nonce }), { mode: 0o600, flag: "wx" });
|
|
48
|
+
try {
|
|
49
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
50
|
+
try {
|
|
51
|
+
// Publish a COMPLETE owner record atomically, including on Windows.
|
|
52
|
+
linkSync(temp, lock);
|
|
53
|
+
return () => {
|
|
54
|
+
try {
|
|
55
|
+
if (JSON.parse(readFileSync(lock, "utf8")).nonce === nonce)
|
|
56
|
+
unlinkSync(lock);
|
|
57
|
+
}
|
|
58
|
+
catch { /* replaced */ }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== "EEXIST")
|
|
63
|
+
throw error;
|
|
64
|
+
let owner;
|
|
65
|
+
try {
|
|
66
|
+
owner = JSON.parse(readFileSync(lock, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch (readError) {
|
|
69
|
+
if (readError.code === "ENOENT")
|
|
70
|
+
continue;
|
|
71
|
+
throw new Error("Local capture setup has an invalid lock. Contact support.");
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string")
|
|
74
|
+
throw new Error("Invalid capture setup owner. Contact support.");
|
|
75
|
+
try {
|
|
76
|
+
process.kill(owner.pid, 0);
|
|
77
|
+
throw new Error("Local capture setup is already running. Retry connect.");
|
|
78
|
+
}
|
|
79
|
+
catch (probe) {
|
|
80
|
+
if (probe.code !== "ESRCH")
|
|
81
|
+
throw probe;
|
|
82
|
+
try {
|
|
83
|
+
if (JSON.parse(readFileSync(lock, "utf8")).nonce === owner.nonce)
|
|
84
|
+
unlinkSync(lock);
|
|
85
|
+
}
|
|
86
|
+
catch { /* another recoverer won */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error("Could not recover local capture setup. Retry connect.");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
rmSync(temp, { force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** OS-owned exclusion makes stale-file recovery a serialized operation.
|
|
97
|
+
* The loopback guard carries no protocol/data and is released by the OS on crash.
|
|
98
|
+
* A fixed per-lock port ensures contenders cannot bind independent guards.
|
|
99
|
+
*/
|
|
100
|
+
export async function acquireCaptureSetupLock(lock) {
|
|
101
|
+
const port = 49152 + parseInt(createHash("sha256").update(path.join(realpathSync(path.dirname(lock)), path.basename(lock))).digest("hex").slice(0, 8), 16) % 16384;
|
|
102
|
+
const guard = createServer(socket => socket.destroy());
|
|
103
|
+
try {
|
|
104
|
+
await new Promise((resolve, reject) => {
|
|
105
|
+
guard.once("error", reject);
|
|
106
|
+
guard.listen({ host: "127.0.0.1", port, exclusive: true }, () => { guard.removeListener("error", reject); resolve(); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new Error("Capture setup is already running, or its local coordination port is occupied. Retry after the other setup or application closes.");
|
|
111
|
+
}
|
|
112
|
+
let releaseFile;
|
|
113
|
+
try {
|
|
114
|
+
releaseFile = acquireCaptureLock(lock);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
await new Promise(resolve => guard.close(() => resolve()));
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return async () => {
|
|
121
|
+
try {
|
|
122
|
+
releaseFile();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await new Promise(resolve => guard.close(() => resolve()));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function askCaptureConsent() {
|
|
130
|
+
let input = process.stdin;
|
|
131
|
+
let output = process.stdout;
|
|
132
|
+
let inputFd, outputFd;
|
|
133
|
+
try {
|
|
134
|
+
if (!process.stdin.isTTY) {
|
|
135
|
+
// stdin may already have carried `connect -`'s token. Consent is a separate
|
|
136
|
+
// interaction with the controlling terminal, never the token pipe.
|
|
137
|
+
inputFd = openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r");
|
|
138
|
+
input = new tty.ReadStream(inputFd);
|
|
139
|
+
outputFd = openSync(process.platform === "win32" ? "CONOUT$" : "/dev/tty", "w");
|
|
140
|
+
output = new tty.WriteStream(outputFd);
|
|
141
|
+
}
|
|
142
|
+
output.write(`\n${HOSTED_CAPTURE_DISCLOSURE}\n\n`);
|
|
143
|
+
const rl = readline.createInterface({ input, output, terminal: false });
|
|
144
|
+
return await new Promise(resolve => { rl.once("close", () => resolve("")); rl.question("Allow local AI capture for this assessment? [y/N] ", resolve); }).finally(() => rl.close());
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error("Open an interactive terminal to review local AI capture, then retry litmus connect.");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (inputFd !== undefined) {
|
|
151
|
+
input.destroy();
|
|
152
|
+
}
|
|
153
|
+
if (outputFd !== undefined) {
|
|
154
|
+
output.destroy();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function startHostedCapture(meta, token, apiBase) {
|
|
159
|
+
const key = createHash("sha256").update(token).digest("hex");
|
|
160
|
+
const projectDir = path.join(os.homedir(), ".litmus", "hosted-sessions", key);
|
|
161
|
+
// One machine-wide window: never silently mix two candidates' conversations.
|
|
162
|
+
const lock = `${HOSTED_CAPTURE_FILE}.lock`;
|
|
163
|
+
mkdirSync(path.dirname(lock), { recursive: true, mode: 0o700 });
|
|
164
|
+
const releaseLock = await acquireCaptureSetupLock(lock);
|
|
165
|
+
try {
|
|
166
|
+
let prior = {};
|
|
167
|
+
try {
|
|
168
|
+
prior = JSON.parse(readFileSync(HOSTED_CAPTURE_FILE, "utf8"));
|
|
169
|
+
}
|
|
170
|
+
catch { /* none */ }
|
|
171
|
+
if (prior.projectDir && sessionOverMarked(prior.projectDir)) {
|
|
172
|
+
// Cleanup may have been interrupted after deleting old credentials but
|
|
173
|
+
// before removing the expired routing record. The ended marker survives.
|
|
174
|
+
releaseAiTrackingForAssessment(prior.projectDir);
|
|
175
|
+
rmSync(HOSTED_CAPTURE_FILE, { force: true });
|
|
176
|
+
prior = {};
|
|
177
|
+
}
|
|
178
|
+
if (prior.projectDir && prior.tokenKey !== key) {
|
|
179
|
+
const oldConfig = JSON.parse(readFileSync(path.join(prior.projectDir, ".litmus", "config.json"), "utf8"));
|
|
180
|
+
const oldLease = await captureLease(oldConfig.backendUrl, oldConfig.token);
|
|
181
|
+
if (oldLease.active)
|
|
182
|
+
throw new Error("Another assessment is active on this computer. Finish it before starting this one.");
|
|
183
|
+
// Confirmed server closure, not mere lease expiry, permits replacing a window.
|
|
184
|
+
retireAiCaptureForEndedSession(prior.projectDir, oldConfig.token);
|
|
185
|
+
rmSync(HOSTED_CAPTURE_FILE, { force: true });
|
|
186
|
+
}
|
|
187
|
+
const otherSessions = (readRegistrySnapshot() ?? []).filter(dir => dir !== projectDir && dir !== prior.projectDir);
|
|
188
|
+
if (otherSessions.length)
|
|
189
|
+
throw new Error("Another local assessment is registered on this computer. Finish its capture session before starting hosted local capture.");
|
|
190
|
+
if (prior.projectDir === projectDir && isTrackerHealthy(projectDir))
|
|
191
|
+
return;
|
|
192
|
+
const answer = await askCaptureConsent();
|
|
193
|
+
if (!/^y(es)?$/i.test(answer.trim()))
|
|
194
|
+
throw new Error("Local capture was not enabled. No local prompts have been recorded.");
|
|
195
|
+
const requestStarted = Date.now();
|
|
196
|
+
const lease = await captureLease(meta.backendUrl, token, true);
|
|
197
|
+
if (!lease.active)
|
|
198
|
+
throw new Error("The assessment coding session has ended.");
|
|
199
|
+
mkdirSync(path.join(projectDir, ".litmus"), { recursive: true, mode: 0o700 });
|
|
200
|
+
atomicPrivateJson(path.join(projectDir, ".litmus", "config.json"), { ...meta, apiBase, token, cliVersion: CLI_VERSION, captureMode: "hosted-local" });
|
|
201
|
+
// A setup failure from an earlier attempt is not a terminal session marker.
|
|
202
|
+
const endedMarker = path.join(projectDir, ".litmus", "session-over");
|
|
203
|
+
if (existsSync(endedMarker))
|
|
204
|
+
throw new Error("This local capture session is already retired");
|
|
205
|
+
prepareHostedTranscripts(projectDir, requestStarted);
|
|
206
|
+
// Only enable the routing window after consent is persisted by the server.
|
|
207
|
+
atomicPrivateJson(HOSTED_CAPTURE_FILE, { projectDir, tokenKey: key, startedAt: requestStarted, expiresAt: requestStarted + lease.leaseSeconds * 1000, receipt: lease.receipt, serverTime: lease.serverTime, leaseRequestStarted: requestStarted, leaseResponseReceivedAt: lease.receivedAt });
|
|
208
|
+
try {
|
|
209
|
+
const hooks = setupAiTracking(projectDir);
|
|
210
|
+
if (hooks.mode !== "user")
|
|
211
|
+
throw new Error("User-level AI hooks could not be installed");
|
|
212
|
+
startTracker(projectDir);
|
|
213
|
+
if (await waitForTrackerReady(projectDir) !== "ready")
|
|
214
|
+
throw new Error("Local capture watcher did not become ready");
|
|
215
|
+
const missing = [!hooks.codexInstalled && "Codex", !hooks.geminiInstalled && "Gemini"].filter(Boolean);
|
|
216
|
+
if (missing.length)
|
|
217
|
+
console.log(`Capture hooks could not be installed for: ${missing.join(", ")}. Use another supported tool or contact support.`);
|
|
218
|
+
console.log("Local hooks enabled for Claude Code, Codex, Cursor and Gemini where supported. Copilot and browser AI are not covered by this local window. Restart already running local AI tools to load their hooks.");
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
atomicPrivateJson(HOSTED_CAPTURE_FILE, { projectDir, tokenKey: key, expiresAt: 0 });
|
|
222
|
+
releaseAiTrackingForAssessment(projectDir);
|
|
223
|
+
rmSync(HOSTED_CAPTURE_FILE, { force: true });
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await releaseLock();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=hosted-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-capture.js","sourceRoot":"","sources":["../../src/lib/hosted-capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,CAAA;AAC3F,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,QAAQ,MAAM,UAAU,CAAA;AAC/B,OAAO,GAAG,MAAM,KAAK,CAAA;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjE,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,OAAO,EAAE,8BAA8B,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAElE,MAAM,CAAC,MAAM,iCAAiC,GAAG,iBAAiB,CAAA;AAClE,MAAM,CAAC,MAAM,yBAAyB,GAAG,+fAA+f,CAAA;AACxiB,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAA;AAC5F,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAc;IAC5D,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/D,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAA;IAC1C,IAAI,CAAC;QAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAAC,CAAC;YAC/F,CAAC;QAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAAC,CAAC;AAC3C,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAkB,EAAE,KAAa,EAAE,MAAM,GAAG,KAAK;IAClF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,UAAU,uBAAuB,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE;QAC1F,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QACrI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,iBAAiB,EAAE,iCAAiC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrG,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;KAClC,CAAC,CAAA;IACF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;IAC5F,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAyF,CAAA;IAC1H,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAA;IACtL,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAA;IACtN,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,OAAO,EAAE,KAAK,CAAC,OAA6B,EAAE,UAAU,EAAE,KAAK,CAAC,UAAgC,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;AAC7L,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,UAAU,EAAE,CAAA;IAC1B,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,KAAK,MAAM,CAAA;IACnC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7F,IAAI,CAAC;QACH,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,oEAAoE;gBACpE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;gBACpB,OAAO,GAAG,EAAE;oBACV,IAAI,CAAC;wBAAC,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK;4BAAE,UAAU,CAAC,IAAI,CAAC,CAAA;oBAAC,CAAC;oBAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;gBAC/G,CAAC,CAAA;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,MAAM,KAAK,CAAA;gBACnE,IAAI,KAAmC,CAAA;gBACvC,IAAI,CAAC;oBAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;gBAAC,CAAC;gBAAC,OAAO,SAAS,EAAE,CAAC;oBACxE,IAAK,SAAmC,CAAC,IAAI,KAAK,QAAQ;wBAAE,SAAQ;oBACpE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;gBAC9E,CAAC;gBACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;oBAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;gBACvJ,IAAI,CAAC;oBAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBAAC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;gBAAC,CAAC;gBAC7G,OAAO,KAAK,EAAE,CAAC;oBACb,IAAK,KAA+B,CAAC,IAAI,KAAK,OAAO;wBAAE,MAAM,KAAK,CAAA;oBAClE,IAAI,CAAC;wBAAC,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;4BAAE,UAAU,CAAC,IAAI,CAAC,CAAA;oBAAC,CAAC;oBAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;gBAClI,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;IAC1E,CAAC;YAAS,CAAC;QAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,IAAY;IACxD,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAC,CAAC,CAAC,EAAC,EAAE,CAAC,GAAG,KAAK,CAAA;IAC/J,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAC,MAAM,EAAE,EAAE;YACzC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAC,MAAM,CAAC,CAAA;YAC1B,KAAK,CAAC,MAAM,CAAC,EAAC,IAAI,EAAC,WAAW,EAAC,IAAI,EAAC,SAAS,EAAC,IAAI,EAAC,EAAC,GAAE,EAAE,GAAC,KAAK,CAAC,cAAc,CAAC,OAAO,EAAC,MAAM,CAAC,CAAC,CAAA,OAAO,EAAE,CAAA,CAAA,CAAC,CAAC,CAAA;QAC3G,CAAC,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QAAC,MAAM,IAAI,KAAK,CAAC,kIAAkI,CAAC,CAAA;IAAC,CAAC;IAC/J,IAAI,WAAuB,CAAA;IAC3B,IAAI,CAAC;QAAC,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAA;IAAC,CAAC;IAC9C,OAAM,KAAK,EAAE,CAAC;QAAC,MAAM,IAAI,OAAO,CAAO,OAAO,CAAA,EAAE,CAAA,KAAK,CAAC,KAAK,CAAC,GAAE,EAAE,CAAA,OAAO,EAAE,CAAC,CAAC,CAAC;QAAC,MAAM,KAAK,CAAA;IAAC,CAAC;IAC1F,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,CAAC;YAAC,WAAW,EAAE,CAAA;QAAC,CAAC;gBACb,CAAC;YAAC,MAAM,IAAI,OAAO,CAAO,OAAO,CAAA,EAAE,CAAA,KAAK,CAAC,KAAK,CAAC,GAAE,EAAE,CAAA,OAAO,EAAE,CAAC,CAAC,CAAA;QAAC,CAAC;IAC1E,CAAC,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,KAAK,GAA0B,OAAO,CAAC,KAAK,CAAA;IAChD,IAAI,MAAM,GAA0B,OAAO,CAAC,MAAM,CAAA;IAClD,IAAI,OAA2B,EAAE,QAA4B,CAAA;IAC7D,IAAI,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACzB,4EAA4E;YAC5E,mEAAmE;YACnE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;YAC7E,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;YACnC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;YAC/E,MAAM,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAA;QACxC,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,KAAK,yBAAyB,MAAM,CAAC,CAAA;QAClD,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;QACvE,OAAO,MAAM,IAAI,OAAO,CAAS,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,oDAAoD,EAAE,OAAO,CAAC,CAAA,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAA;IAC5L,CAAC;IAAC,MAAM,CAAC;QAAC,MAAM,IAAI,KAAK,CAAC,qFAAqF,CAAC,CAAA;IAAC,CAAC;YAC1G,CAAC;QACP,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAAE,KAAwB,CAAC,OAAO,EAAE,CAAA;QAAC,CAAC;QAClE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAAE,MAA0B,CAAC,OAAO,EAAE,CAAA;QAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAkB,EAAE,KAAa,EAAE,OAAe;IACzF,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,iBAAiB,EAAE,GAAG,CAAC,CAAA;IAC7E,6EAA6E;IAC7E,MAAM,IAAI,GAAG,GAAG,mBAAmB,OAAO,CAAA;IAC1C,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/D,MAAM,WAAW,GAAG,MAAM,uBAAuB,CAAC,IAAI,CAAC,CAAA;IACvD,IAAI,CAAC;QACH,IAAI,KAAK,GAAiE,EAAE,CAAA;QAC5E,IAAI,CAAC;YAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;QAC1F,IAAI,KAAK,CAAC,UAAU,IAAI,iBAAiB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,uEAAuE;YACvE,yEAAyE;YACzE,8BAA8B,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YAChD,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAC5C,KAAK,GAAG,EAAE,CAAA;QACZ,CAAC;QACD,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,EAAE,CAAC;YAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;YACzG,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAA;YAC1E,IAAI,QAAQ,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAA;YAC1H,+EAA+E;YAC/E,8BAA8B,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAA;YACjE,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC9C,CAAC;QACD,MAAM,aAAa,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,KAAK,CAAC,UAAU,CAAC,CAAA;QAClH,IAAI,aAAa,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,2HAA2H,CAAC,CAAA;QACtK,IAAI,KAAK,CAAC,UAAU,KAAK,UAAU,IAAI,gBAAgB,CAAC,UAAU,CAAC;YAAE,OAAM;QAC3E,MAAM,MAAM,GAAG,MAAM,iBAAiB,EAAE,CAAA;QACxC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;QAC5H,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACjC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;QAC9D,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;QAC9E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QAC7E,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAA;QACrJ,4EAA4E;QAC5E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,cAAc,CAAC,CAAA;QACpE,IAAI,UAAU,CAAC,WAAW,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;QAC7F,wBAAwB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;QACpD,2EAA2E;QAC3E,iBAAiB,CAAC,mBAAmB,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,mBAAmB,EAAE,cAAc,EAAE,uBAAuB,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;QAC7R,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;YACxF,YAAY,CAAC,UAAU,CAAC,CAAA;YACxB,IAAI,MAAM,mBAAmB,CAAC,UAAU,CAAC,KAAK,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;YACpH,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,IAAI,OAAO,EAAE,CAAC,KAAK,CAAC,eAAe,IAAI,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YACtG,IAAI,OAAO,CAAC,MAAM;gBAAE,OAAO,CAAC,GAAG,CAAC,6CAA6C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAA;YAClJ,OAAO,CAAC,GAAG,CAAC,yMAAyM,CAAC,CAAA;QACxN,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,iBAAiB,CAAC,mBAAmB,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;YACnF,8BAA8B,CAAC,UAAU,CAAC,CAAA;YAC1C,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAC5C,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;YAAS,CAAC;QAAC,MAAM,WAAW,EAAE,CAAA;IAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface TranscriptLease {
|
|
2
|
+
startedAt: number;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
receipt: string;
|
|
5
|
+
serverTime: number;
|
|
6
|
+
leaseResponseReceivedAt: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function prepareHostedTranscripts(projectDir: string, startedAt: number): void;
|
|
9
|
+
export declare class HostedTranscriptObserver {
|
|
10
|
+
private projectDir;
|
|
11
|
+
private activityLog;
|
|
12
|
+
private state;
|
|
13
|
+
private stateFile;
|
|
14
|
+
private pending;
|
|
15
|
+
private claudeReplies;
|
|
16
|
+
private previousExpiry;
|
|
17
|
+
constructor(projectDir: string, activityLog: string);
|
|
18
|
+
/** Offsets advance even while unauthorized: a later lease cannot backfill
|
|
19
|
+
* messages sent during a disconnect. No content is checkpointed in state.
|
|
20
|
+
*/
|
|
21
|
+
poll(lease: TranscriptLease | null, now?: number): void;
|
|
22
|
+
private record;
|
|
23
|
+
/** Some Claude versions persist the final assistant row with stop_reason:
|
|
24
|
+
* null. Their Stop hook records only the final native entry id, never text.
|
|
25
|
+
* Wait until that exact entry has been read before publishing one reply.
|
|
26
|
+
* A terminal stop_reason remains sufficient when hooks have not reloaded.
|
|
27
|
+
*/
|
|
28
|
+
private finishClaude;
|
|
29
|
+
private emit;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=hosted-transcripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-transcripts.d.ts","sourceRoot":"","sources":["../../src/lib/hosted-transcripts.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,uBAAuB,EAAE,MAAM,CAAA;CAC3G;AAqED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAcpF;AAQD,qBAAa,wBAAwB;IAMvB,OAAO,CAAC,UAAU;IAAU,OAAO,CAAC,WAAW;IAL3D,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,aAAa,CAAkH;IACvI,OAAO,CAAC,cAAc,CAAI;gBACN,UAAU,EAAE,MAAM,EAAU,WAAW,EAAE,MAAM;IAOnE;;OAEG;IACH,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,EAAE,GAAG,SAAa,GAAG,IAAI;IAiF3D,OAAO,CAAC,MAAM;IAqDd;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,IAAI;CAqBb"}
|