@shardflux/cli 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/README.md +27 -0
  3. package/dist/account-args.js +1 -0
  4. package/dist/agent-commands.d.ts +1 -0
  5. package/dist/agent-commands.js +15 -5
  6. package/dist/agent-config-files.d.ts +16 -2
  7. package/dist/agent-config-files.js +48 -61
  8. package/dist/agent-config-sync.d.ts +12 -1
  9. package/dist/agent-config-sync.js +228 -54
  10. package/dist/agent-repository.d.ts +39 -9
  11. package/dist/agent-repository.js +309 -92
  12. package/dist/agent-state.d.ts +3 -0
  13. package/dist/agent-state.js +13 -0
  14. package/dist/agent-target-commands.d.ts +2 -0
  15. package/dist/agent-target-commands.js +45 -15
  16. package/dist/args.d.ts +1 -1
  17. package/dist/args.js +14 -2
  18. package/dist/conversation-router.js +7 -11
  19. package/dist/conversation-stash.js +64 -18
  20. package/dist/conversation-terminal.d.ts +1 -1
  21. package/dist/conversation-terminal.js +9 -10
  22. package/dist/conversations.d.ts +9 -2
  23. package/dist/conversations.js +23 -6
  24. package/dist/errors.d.ts +1 -1
  25. package/dist/errors.js +1 -1
  26. package/dist/files-mirror.d.ts +1 -9
  27. package/dist/files-mirror.js +19 -3
  28. package/dist/format.js +37 -2
  29. package/dist/handlers.js +3 -1
  30. package/dist/initial-prompt.d.ts +15 -2
  31. package/dist/initial-prompt.js +29 -4
  32. package/dist/local-sessions.d.ts +60 -0
  33. package/dist/local-sessions.js +445 -0
  34. package/dist/main.js +12 -0
  35. package/dist/move.d.ts +44 -0
  36. package/dist/move.js +248 -0
  37. package/dist/moved-sessions.d.ts +33 -0
  38. package/dist/moved-sessions.js +91 -0
  39. package/dist/onboarding.d.ts +18 -3
  40. package/dist/onboarding.js +72 -46
  41. package/dist/project-sync-manifest.d.ts +31 -3
  42. package/dist/project-sync-manifest.js +192 -108
  43. package/dist/project-sync-transfer.d.ts +50 -4
  44. package/dist/project-sync-transfer.js +404 -106
  45. package/dist/project-sync.js +30 -33
  46. package/dist/seed-phases.d.ts +29 -0
  47. package/dist/seed-phases.js +83 -0
  48. package/dist/t3-mode.d.ts +7 -2
  49. package/dist/t3-mode.js +39 -5
  50. package/dist/thread-on-computer.js +4 -0
  51. package/dist/worktree-twin.d.ts +2 -6
  52. package/dist/worktree-twin.js +16 -51
  53. package/package.json +5 -5
@@ -675,8 +675,19 @@ export class FilesMirror {
675
675
  * as refs/worktree/shard/seed and in the hub as refs/shard/seed/<name>. Mirrors leave out what it already contains,
676
676
  * so the user's own uncommitted edits never show as the conversation's change. Threads on one computer are worktrees
677
677
  * of one repository (0.16.0+), so the seed is per worktree; the shared refs/shard/seed of earlier seeds is read, and
678
- * still written, only in the repository's main worktree.
678
+ * still written, only in the repository's main worktree. With $4 = background (0.19.1+) the hub push runs detached
679
+ * in the VM and the script returns once the seed is recorded: the hub is on the project's network volume, where the
680
+ * push took 6-10 s on every start (staging, coding 37), on the way to the first prompt.
679
681
  */
682
+ // It is embedded in single quotes below: never a single quote in it. A blobless partial clone (0.19.0+) pushes only
683
+ // when the hub already has the seed history; otherwise the push would fetch every historic blob first. Its background
684
+ // backfill completes the repository for later pushes.
685
+ const SEED_PUSH = `set -u
686
+ seed=$1; name=$2; hub=$3; cd "$4"
687
+ push=1; if [ "$(git config --bool remote.origin.promisor 2>/dev/null || true)" = true ]; then push=0
688
+ for c in "$seed" "$seed^1"; do o=$(git rev-parse -q --verify "$c^{commit}" 2>/dev/null) && git --git-dir="$hub" cat-file -e "$o^{commit}" 2>/dev/null && push=1 && break; done; fi
689
+ if [ -d "$hub" ] && [ "$push" = 1 ]; then git -c core.hooksPath=/dev/null push -q "$hub" "+$seed:refs/shard/seed/$name" >/dev/null 2>&1 || true; fi
690
+ `;
680
691
  export const SEED_SNAPSHOT = `set -eu
681
692
  cd "$1"
682
693
  main=0; [ "$(git rev-parse --absolute-git-dir)" = "$(git rev-parse --path-format=absolute --git-common-dir)" ] && main=1
@@ -691,13 +702,18 @@ seed=$(GIT_AUTHOR_NAME=Shardflux GIT_AUTHOR_EMAIL=seed@shardflux.dev GIT_COMMITT
691
702
  git update-ref refs/worktree/shard/seed "$seed"
692
703
  if [ "$main" = 1 ]; then git update-ref refs/shard/seed "$seed"; fi
693
704
  fi
694
- if [ -d "$3" ]; then git -c core.hooksPath=/dev/null push -q "$3" "+$seed:refs/shard/seed/$2" >/dev/null 2>&1 || true; fi
705
+ push='${SEED_PUSH}'
706
+ if [ "\${4:-}" = background ]; then
707
+ if command -v setsid >/dev/null 2>&1; then (setsid sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD" >/dev/null 2>&1 </dev/null &)
708
+ else (sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD" >/dev/null 2>&1 </dev/null &); fi
709
+ else sh -c "$push" shard-seed-push "$seed" "$2" "$3" "$PWD"; fi
695
710
  printf '%s\n' "$seed"
696
711
  `;
697
712
  /** Best effort: without a snapshot the folder also lists the start state's own edits. */
698
713
  export async function recordSeedSnapshot(ws, c, signal) {
699
714
  try {
700
- const r = await ws.cell().exec.run(['sh', '-c', SEED_SNAPSHOT, 'shard-seed', c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH], { timeoutMs: 120_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
715
+ // The hub push runs detached in the VM: it is on the project volume and never on the way to the first prompt.
716
+ const r = await ws.cell().exec.run(['sh', '-c', SEED_SNAPSHOT, 'shard-seed', c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'], { timeoutMs: 120_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
701
717
  return r.exitCode === 0 ? r.stdout.trim() : null;
702
718
  }
703
719
  catch (error) {
package/dist/format.js CHANGED
@@ -376,7 +376,7 @@ function objectTable(rows, columns) {
376
376
  const cols = columns.filter(([k]) => objs.some((o) => k in o));
377
377
  return table(cols.map(([, h]) => h), objs.map((o) => cols.map(([k]) => (k === 'percent_used' && typeof o[k] === 'number' ? `${o[k]}%` : scalar(o[k])))));
378
378
  }
379
- const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'exhausted_reason', 'egress_override', 'meters', 'allowances', 'exhausted', 'alert_thresholds', 'spend_cap']);
379
+ const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'exhausted_reason', 'egress_override', 'meters', 'allowances', 'exhausted', 'alert_thresholds', 'spend_cap', 'gpu']);
380
380
  /** Minor units as money (the overage block): `$3.00` for usd, else `3.00 EUR`. A fractional minor amount (a sub-cent rate) keeps its digits. */
381
381
  export function capMoney(minor, currency) {
382
382
  if (typeof minor !== 'number')
@@ -432,6 +432,39 @@ function spendCapText(cap) {
432
432
  }
433
433
  return out;
434
434
  }
435
+ /** Hours for people: at most two decimals (`25`, `3.25`, `0.2`). */
436
+ const hoursText = (h) => String(Math.round(h * 100) / 100);
437
+ /**
438
+ * The GPU-hours lines of the summary (0.19.0; `gpu`, SDK 0.25.0): used of included with the overage past them, then,
439
+ * while the GPU is stopped (cap_state gpu_blocked), why and what brings it back. Workspaces keep running meanwhile.
440
+ * Nothing without `gpu` (plans without GPU-hours).
441
+ */
442
+ function gpuLines(gpu, resetsAt) {
443
+ if (!isObject(gpu) || typeof gpu.used_hours !== 'number')
444
+ return [];
445
+ const included = typeof gpu.included_hours === 'number' ? gpu.included_hours : null;
446
+ let used = included === null ? `${hoursText(gpu.used_hours)} used (the plan includes none)` : `${hoursText(gpu.used_hours)} of ${hoursText(included)} used`;
447
+ const over = typeof gpu.overage_hours === 'number' ? gpu.overage_hours : 0;
448
+ if (included !== null && over > 0) {
449
+ const billed = typeof gpu.billed_overage_hours === 'number' ? gpu.billed_overage_hours : 0;
450
+ used += `, ${hoursText(over)} past the allowance${billed > 0 ? ` (${hoursText(billed)} billed as overage)` : ''}`;
451
+ }
452
+ const rows = [['GPU-hours', used]];
453
+ if (gpu.cap_state !== 'gpu_blocked')
454
+ return rows;
455
+ const resets = resetsAt ? ` (the period resets ${time(resetsAt)})` : '';
456
+ switch (gpu.blocked_reason) {
457
+ case 'overage_paused':
458
+ rows.push(['GPU', 'stopped (overage_paused): workspaces keep running; pay the past-due invoice (shard billing portal --open) and the GPU renders again']);
459
+ break;
460
+ case 'spend_cap_reached':
461
+ rows.push(['GPU', `stopped (spend_cap_reached): workspaces keep running; raise the spend cap (shard billing overage cap <dollars>), or wait for the reset${resets}`]);
462
+ break;
463
+ default:
464
+ rows.push(['GPU', `stopped (allowance_used): workspaces keep running; turn on overage (shard billing overage on --cap <dollars>), upgrade (shard billing plans), or wait for the reset${resets}`]);
465
+ }
466
+ return rows;
467
+ }
435
468
  /**
436
469
  * The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
437
470
  * freshness, then meters and allowances as tables (cap state `overage` past an allowance while opt-in overage is on),
@@ -464,6 +497,8 @@ export function usageText(summary) {
464
497
  if (Array.isArray(s.allowances) && s.allowances.some((a) => isObject(a) && a.cap_state === 'storage_blocked')) {
465
498
  head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
466
499
  }
500
+ const resetsAt = isObject(s.period) ? [s.period.resets_at, s.period.end].find((t) => typeof t === 'string') : undefined;
501
+ head.push(...gpuLines(s.gpu, resetsAt));
467
502
  for (const [k, v] of Object.entries(s))
468
503
  if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
469
504
  head.push([k.replace(/_/g, ' '), scalar(v)]);
@@ -1019,7 +1054,7 @@ export function egressVersionTable(rows) {
1019
1054
  return table(['VERSION', 'KIND', 'POLICY', 'BY', 'CREATED'], rows.map((v) => [String(v.version), v.kind, v.kind === 'cleared' ? '-' : egressPolicyLine({ mode: v.mode ?? 'allow_all', rules: v.rules, cidrs: v.cidrs }), `${v.created_by.type}:${v.created_by.id}`, time(v.created_at)]));
1020
1055
  }
1021
1056
  // ---- Overage settings (0.5.1; shard billing overage) -----------------------------------------------------------
1022
- const RATE_UNITS = { ram_gib_hours: 'RAM GiB-hour', cpu_hours: 'CPU-hour' };
1057
+ const RATE_UNITS = { ram_gib_hours: 'RAM GiB-hour', cpu_hours: 'CPU-hour', gpu_hours: 'GPU-hour' };
1023
1058
  /**
1024
1059
  * The spend policy's overage settings for people (GET/PUT /v1/organizations/{id}/spend-policy): the state, the spend
1025
1060
  * cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
package/dist/handlers.js CHANGED
@@ -21,4 +21,6 @@ import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
21
21
  import { THREAD_HANDLERS } from "./thread-on-computer.js";
22
22
  import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
23
23
  import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
24
- export const HANDLERS = { ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS };
24
+ import { MOVE_HANDLERS } from "./move.js";
25
+ import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
26
+ export const HANDLERS = { ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS, 'agents config-sync': AGENT_CONFIG_HANDLERS['agents config-sync'] };
@@ -22,5 +22,18 @@ export declare function readInitialImages(ctx: Ctx, values: Values, prompt: stri
22
22
  export declare function promptWithImages(prompt: string, images: readonly InitialImage[]): string;
23
23
  /** Writes the images into the conversation VM before the first message refers to them. */
24
24
  export declare function uploadInitialImages(cell: Pick<CellClient, 'files'>, images: readonly InitialImage[], signal?: AbortSignal): Promise<void>;
25
- /** Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay. */
26
- export declare function submitInitialPrompt(connect: (signal: AbortSignal) => Promise<TerminalConnection>, prompt: string, workspaceKey: string, signal: AbortSignal, app?: 'claude' | 'codex', write?: (bytes: Uint8Array) => Promise<unknown>): Promise<void>;
25
+ /** The conversation inbox takes a message of at most this many characters (the API's text limit). */
26
+ export declare const INBOX_MAX_CHARS = 65536;
27
+ /**
28
+ * The harness's input box is the last thing that drew its prompt glyph (0.19.1+). Claude Code and Codex draw the same
29
+ * glyph as the selection cursor of their dialogs ("❯ 1. Yes, I trust this folder"); a prompt pasted there would pick
30
+ * options. The guest's own first-message delivery reads the screen the same way (coding 39+).
31
+ */
32
+ export declare function inputBoxReady(tail: string, glyph: string): boolean;
33
+ /**
34
+ * Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
35
+ * When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
36
+ * (its inbox, which waits for the harness and retries Enter) and the result is `queued` (0.19.1+): a first prompt is
37
+ * never lost to a slow start.
38
+ */
39
+ export declare function submitInitialPrompt(connect: (signal: AbortSignal) => Promise<TerminalConnection>, prompt: string, workspaceKey: string, signal: AbortSignal, app?: 'claude' | 'codex', write?: (bytes: Uint8Array) => Promise<unknown>, queue?: () => Promise<unknown>): Promise<'submitted' | 'queued'>;
@@ -137,8 +137,26 @@ export async function uploadInitialImages(cell, images, signal) {
137
137
  await cell.files.write(image.path, image.data, { mode: '0600', createParents: true });
138
138
  }
139
139
  }
140
- /** Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay. */
141
- export async function submitInitialPrompt(connect, prompt, workspaceKey, signal, app = 'claude', write) {
140
+ /** The conversation inbox takes a message of at most this many characters (the API's text limit). */
141
+ export const INBOX_MAX_CHARS = 65536;
142
+ /**
143
+ * The harness's input box is the last thing that drew its prompt glyph (0.19.1+). Claude Code and Codex draw the same
144
+ * glyph as the selection cursor of their dialogs ("❯ 1. Yes, I trust this folder"); a prompt pasted there would pick
145
+ * options. The guest's own first-message delivery reads the screen the same way (coding 39+).
146
+ */
147
+ export function inputBoxReady(tail, glyph) {
148
+ // eslint-disable-next-line no-control-regex -- Native terminal escapes are removed before reading the screen text.
149
+ const plain = tail.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, ' ');
150
+ const at = plain.lastIndexOf(glyph);
151
+ return at >= 0 && !/^[\s\u00a0]*\d+\./.test(plain.slice(at + glyph.length));
152
+ }
153
+ /**
154
+ * Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
155
+ * When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
156
+ * (its inbox, which waits for the harness and retries Enter) and the result is `queued` (0.19.1+): a first prompt is
157
+ * never lost to a slow start.
158
+ */
159
+ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal, app = 'claude', write, queue) {
142
160
  const bounded = AbortSignal.any([signal, AbortSignal.timeout(20000)]);
143
161
  let connection;
144
162
  let sent = false, pasted = false;
@@ -159,7 +177,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
159
177
  // eslint-disable-next-line no-control-regex -- Strip native terminal ANSI before matching prompt echo.
160
178
  const plain = tail.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\s/g, '');
161
179
  if (sent && bytes.length)
162
- return;
180
+ return 'submitted';
163
181
  if (pasted) {
164
182
  // A long prompt overflows the input box: a 24-row terminal shows only its end, so either end confirms the paste.
165
183
  const compact = prompt.replace(/\s/g, '');
@@ -174,7 +192,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
174
192
  tail = '';
175
193
  continue;
176
194
  }
177
- const ready = app === 'claude' ? tail.includes('❯') : tail.includes('›');
195
+ const ready = inputBoxReady(tail, app === 'claude' ? '❯' : '›');
178
196
  if (!ready || !pasteEnabled)
179
197
  continue;
180
198
  const paste = Buffer.from(`\x1b[200~${prompt}\x1b[201~`);
@@ -188,6 +206,13 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
188
206
  throw new Error('The agent terminal closed before accepting a prompt.');
189
207
  }
190
208
  catch {
209
+ if (!pasted && queue && !signal.aborted && prompt.length <= INBOX_MAX_CHARS) {
210
+ try {
211
+ await queue();
212
+ return 'queued';
213
+ }
214
+ catch { /* reported as not submitted below */ }
215
+ }
191
216
  throw new CodedError('initial_prompt_failed', `Created ${workspaceKey}, but the initial prompt ${sent ? 'could not be confirmed' : 'was not submitted'}. Open that conversation before starting another.`, undefined, { workspace_key: workspaceKey, submitted: sent });
192
217
  }
193
218
  finally {
@@ -0,0 +1,60 @@
1
+ export type SessionHarness = 'claude' | 'codex';
2
+ export interface LocalSession {
3
+ harness: SessionHarness;
4
+ session_id: string;
5
+ /** The transcript (Claude) or rollout (Codex) file. */
6
+ file: string;
7
+ /** The session's working directory, as its transcript records it. */
8
+ cwd: string;
9
+ title: string;
10
+ updated_at: string;
11
+ bytes: number;
12
+ /** Where it was started: Claude's entrypoint (cli, claude-desktop, sdk-cli…) or Codex's originator (codex-tui…). */
13
+ source: string;
14
+ }
15
+ export interface SessionState {
16
+ /** A process has the session open right now. */
17
+ running: boolean;
18
+ /** working: a turn is in progress (never cut off); idle: between turns, or no process is running it. */
19
+ turn: 'working' | 'idle';
20
+ pid?: number;
21
+ }
22
+ type Env = Record<string, string | undefined>;
23
+ type Rec = Record<string, unknown>;
24
+ export declare const isSessionId: (id: string) => boolean;
25
+ export declare const claudeConfigDir: (env: Env) => string;
26
+ export declare const codexHome: (env: Env) => string;
27
+ /** Claude Code's folder name under projects/ for a working directory (measured: a 268-character path → 200 + "-1qkjv5"). */
28
+ export declare function claudeProjectName(cwd: string): string;
29
+ /** Records appended after `offset` (whole lines only). */
30
+ export declare function recordsAfter(file: string, offset: number): Rec[];
31
+ /** Whether the transcript's last turn is still running, from its records (see the module comment). */
32
+ export declare function claudeTurnFromRecords(records: Rec[]): 'working' | 'idle';
33
+ export declare function codexTurnFromRecords(records: Rec[]): 'working' | 'idle';
34
+ /** A project's sessions on this computer, newest first: those started in the folder or below it. */
35
+ export declare function listLocalSessions(folder: string, env: Env, options?: {
36
+ harness?: SessionHarness;
37
+ limit?: number;
38
+ }): LocalSession[];
39
+ /** One session by id, wherever it was started. */
40
+ export declare function findLocalSession(id: string, env: Env): LocalSession | null;
41
+ /** Whether a turn is running now. A session no process has open is between turns, whatever its last record says. */
42
+ export declare function sessionState(s: Pick<LocalSession, 'harness' | 'session_id' | 'file'>, env: Env, now?: number): SessionState;
43
+ /** Whether someone typed in the local copy after it moved: a new prompt or turn after `offset`. */
44
+ export declare function continuedAfter(s: Pick<LocalSession, 'harness' | 'file'>, offset: number): boolean;
45
+ /**
46
+ * The transcript as whole lines, cut after its last complete record: a harness may be appending a record while a
47
+ * move reads it (between turns that is metadata, never part of a turn).
48
+ */
49
+ export declare function completeRecords(bytes: Buffer): Buffer;
50
+ /**
51
+ * The session's records with its working directory moved from `from` to `to`: Claude's and Codex's `cwd` fields (and
52
+ * Codex's workdir and workspace roots). Lines that do not mention `from` keep their exact bytes; message text,
53
+ * tool output and signed thinking are never changed.
54
+ */
55
+ export declare function rewriteSessionPaths(bytes: Buffer, from: string, to: string): Buffer;
56
+ /** Where the session's transcript lives under a home folder, relative to it (the VM's and the project space's layout). */
57
+ export declare function homeRelativeTranscript(s: Pick<LocalSession, 'harness' | 'session_id' | 'file'>, cwd: string): string;
58
+ /** The transcript file is a regular file: never follow a link out of the harness's store. */
59
+ export declare function readTranscript(file: string): Buffer;
60
+ export {};