@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +156 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +69 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L27 (2026-05-27) — Auto-update channel + last-check persistence.
|
|
3
|
+
*
|
|
4
|
+
* Two pieces of disk state are managed here:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Channel selection** — `~/.pugi/config.json::updateChannel`.
|
|
7
|
+
* Persisted across sessions so `pugi update` keeps polling the
|
|
8
|
+
* same track the operator opted into via `pugi update --channel
|
|
9
|
+
* <name>`. Mirrors the read/write pattern used by
|
|
10
|
+
* `core/permissions/state.ts::getGlobalDefaultMode` (passthrough
|
|
11
|
+
* schema, atomic tmp+rename, defensive parse).
|
|
12
|
+
*
|
|
13
|
+
* 2. **Last-check timestamp** — `~/.pugi/.last-update-check` (ISO
|
|
14
|
+
* string, single-line). Read by the cold-start banner gate so
|
|
15
|
+
* operators only see the "update available" hint once per
|
|
16
|
+
* `UPDATE_CHECK_INTERVAL_HOURS` (default 24h). Living on its own
|
|
17
|
+
* file (NOT a JSON object inside config.json) is intentional:
|
|
18
|
+
* the timestamp is a hot path — every CLI invocation touches it —
|
|
19
|
+
* and a single-line read+write is materially faster than the
|
|
20
|
+
* JSON parse + serialise of the broader config doc, with no
|
|
21
|
+
* schema coupling cost.
|
|
22
|
+
*
|
|
23
|
+
* Module contract:
|
|
24
|
+
*
|
|
25
|
+
* - Every file path resolver accepts a `homeDir` override so the
|
|
26
|
+
* test suite can drive the module through a per-test mkdtemp
|
|
27
|
+
* directory without polluting the real `~/.pugi/`.
|
|
28
|
+
*
|
|
29
|
+
* - Parse / read helpers NEVER throw on a malformed file. A
|
|
30
|
+
* corrupted JSON blob, a missing field, or an unreadable file all
|
|
31
|
+
* collapse to "no persisted value" so the next layer (the CLI
|
|
32
|
+
* flag or the hard default `beta`) takes over. A future-self
|
|
33
|
+
* debugging an update flow against a corrupt config never has the
|
|
34
|
+
* CLI crash on them.
|
|
35
|
+
*
|
|
36
|
+
* - Write helpers use the atomic tmp+rename idiom so a kill mid-
|
|
37
|
+
* write never produces a half-flushed JSON document. The
|
|
38
|
+
* timestamp file is small enough that POSIX `rename` is itself
|
|
39
|
+
* atomic in practice, but we keep the idiom uniform with the
|
|
40
|
+
* config write so reviewers do not have to context-switch.
|
|
41
|
+
*/
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
import { resolve, dirname } from 'node:path';
|
|
45
|
+
import { z } from 'zod';
|
|
46
|
+
import { DEFAULT_UPDATE_CHANNEL, UPDATE_CHANNELS, } from './channels.js';
|
|
47
|
+
/**
|
|
48
|
+
* Default rate-limit window between registry probes. Operators see the
|
|
49
|
+
* cold-start banner at most once per window. Override per call via
|
|
50
|
+
* `shouldCheckForUpdate({ intervalHours })` — the cron-style scheduler
|
|
51
|
+
* passes 0 to force a check on every invocation, the doctor probe
|
|
52
|
+
* passes 24 to match the operator-visible cadence.
|
|
53
|
+
*/
|
|
54
|
+
export const UPDATE_CHECK_INTERVAL_HOURS = 24;
|
|
55
|
+
/** Filename of the per-user channel + misc config. Mirrors L6 / L25. */
|
|
56
|
+
const CONFIG_FILE = '.pugi/config.json';
|
|
57
|
+
/** Filename of the standalone last-check ISO timestamp. */
|
|
58
|
+
const LAST_CHECK_FILE = '.pugi/.last-update-check';
|
|
59
|
+
/**
|
|
60
|
+
* Zod schema for the channel slice of `~/.pugi/config.json`. The
|
|
61
|
+
* passthrough lets sibling skills (L6 `defaultPermissionMode`, L25
|
|
62
|
+
* onboarding marker, etc.) coexist in the same JSON document without
|
|
63
|
+
* dropping their fields on a channel write.
|
|
64
|
+
*/
|
|
65
|
+
const channelConfigSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
updateChannel: z.enum(['stable', 'beta', 'canary']).optional(),
|
|
68
|
+
})
|
|
69
|
+
.partial()
|
|
70
|
+
.passthrough();
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the absolute path of the per-user config file. Defaults to
|
|
73
|
+
* the real home dir, but every caller in the spec passes an explicit
|
|
74
|
+
* tmpdir so the persisted writes never escape the test sandbox.
|
|
75
|
+
*/
|
|
76
|
+
export function configPath(homeDir = homedir()) {
|
|
77
|
+
return resolve(homeDir, CONFIG_FILE);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the absolute path of the single-line last-check file.
|
|
81
|
+
*/
|
|
82
|
+
export function lastCheckPath(homeDir = homedir()) {
|
|
83
|
+
return resolve(homeDir, LAST_CHECK_FILE);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the persisted channel selection. Returns `null` when the
|
|
87
|
+
* config file is absent, the field is unset, or the file is unparse-
|
|
88
|
+
* able. The caller layers in the CLI flag + the hard default
|
|
89
|
+
* `DEFAULT_UPDATE_CHANNEL`.
|
|
90
|
+
*
|
|
91
|
+
* Defensive parse is intentional — a half-written config from a
|
|
92
|
+
* crashed session should never block `pugi update` from finishing the
|
|
93
|
+
* channel switch.
|
|
94
|
+
*/
|
|
95
|
+
export function getUpdateChannel(homeDir = homedir()) {
|
|
96
|
+
const path = configPath(homeDir);
|
|
97
|
+
if (!existsSync(path))
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const raw = readFileSync(path, 'utf8');
|
|
101
|
+
const parsed = channelConfigSchema.parse(JSON.parse(raw));
|
|
102
|
+
return parsed.updateChannel ?? null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the effective channel for an invocation. Resolution order:
|
|
110
|
+
*
|
|
111
|
+
* 1. `cliFlag` (when provided + parses to a known channel).
|
|
112
|
+
* 2. `~/.pugi/config.json::updateChannel`.
|
|
113
|
+
* 3. `DEFAULT_UPDATE_CHANNEL` (currently `beta`).
|
|
114
|
+
*
|
|
115
|
+
* An invalid `cliFlag` (e.g. `--channel yolo`) falls through to the
|
|
116
|
+
* next layer rather than crashing — the dispatcher already validates
|
|
117
|
+
* the flag up front and surfaces a deterministic error for unknown
|
|
118
|
+
* names. This helper exists for code paths (the doctor probe, the
|
|
119
|
+
* cold-start banner) where no CLI flag is in play and a silent fall-
|
|
120
|
+
* through is the correct behaviour.
|
|
121
|
+
*/
|
|
122
|
+
export function resolveEffectiveChannel(options = {}) {
|
|
123
|
+
const cli = options.cliFlag;
|
|
124
|
+
if (cli && typeof cli === 'string') {
|
|
125
|
+
const trimmed = cli.trim().toLowerCase();
|
|
126
|
+
for (const channel of UPDATE_CHANNELS) {
|
|
127
|
+
if (channel === trimmed)
|
|
128
|
+
return channel;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const persisted = getUpdateChannel(options.homeDir ?? homedir());
|
|
132
|
+
if (persisted)
|
|
133
|
+
return persisted;
|
|
134
|
+
return DEFAULT_UPDATE_CHANNEL;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Persist the channel to `~/.pugi/config.json::updateChannel`. Creates
|
|
138
|
+
* `~/.pugi/` when missing; preserves any unrelated keys in the file
|
|
139
|
+
* (passthrough schema). Atomic tmp+rename so a kill mid-write never
|
|
140
|
+
* leaves the config half-flushed.
|
|
141
|
+
*/
|
|
142
|
+
export function setUpdateChannel(channel, homeDir = homedir()) {
|
|
143
|
+
const path = configPath(homeDir);
|
|
144
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
145
|
+
const existing = existsSync(path)
|
|
146
|
+
? safeParseObject(readFileSync(path, 'utf8'))
|
|
147
|
+
: {};
|
|
148
|
+
const next = { ...existing, updateChannel: channel };
|
|
149
|
+
const tmpPath = `${path}.tmp`;
|
|
150
|
+
writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
mode: 0o600,
|
|
153
|
+
});
|
|
154
|
+
renameSync(tmpPath, path);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read the ISO timestamp of the most recent registry probe. Returns
|
|
158
|
+
* `null` when the file is absent or the contents do not parse as a
|
|
159
|
+
* valid Date. The caller treats `null` as "never checked" and runs an
|
|
160
|
+
* immediate probe.
|
|
161
|
+
*/
|
|
162
|
+
export function readLastCheckedAt(homeDir = homedir()) {
|
|
163
|
+
const path = lastCheckPath(homeDir);
|
|
164
|
+
if (!existsSync(path))
|
|
165
|
+
return null;
|
|
166
|
+
try {
|
|
167
|
+
const raw = readFileSync(path, 'utf8').trim();
|
|
168
|
+
if (raw.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
const ts = Date.parse(raw);
|
|
171
|
+
if (!Number.isFinite(ts))
|
|
172
|
+
return null;
|
|
173
|
+
return new Date(ts);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Persist the timestamp of the most recent registry probe. Atomic
|
|
181
|
+
* tmp+rename for the same reasons as `setUpdateChannel` — the file is
|
|
182
|
+
* small but we keep the idiom uniform.
|
|
183
|
+
*/
|
|
184
|
+
export function writeLastCheckedAt(when, homeDir = homedir()) {
|
|
185
|
+
const path = lastCheckPath(homeDir);
|
|
186
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
187
|
+
const tmpPath = `${path}.tmp`;
|
|
188
|
+
writeFileSync(tmpPath, `${when.toISOString()}\n`, {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
mode: 0o600,
|
|
191
|
+
});
|
|
192
|
+
renameSync(tmpPath, path);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Decide whether the cold-start hint should run a fresh registry
|
|
196
|
+
* probe. Returns true when the last probe was more than
|
|
197
|
+
* `intervalHours` ago OR the timestamp file is missing entirely.
|
|
198
|
+
*
|
|
199
|
+
* Pass `intervalHours = 0` to force a probe on every call (used by
|
|
200
|
+
* the `pugi update --check` JSON surface where the operator is
|
|
201
|
+
* explicitly asking for a fresh result).
|
|
202
|
+
*/
|
|
203
|
+
export function shouldCheckForUpdate(options = {}) {
|
|
204
|
+
const now = options.now ? options.now() : Date.now();
|
|
205
|
+
const intervalHours = options.intervalHours ?? UPDATE_CHECK_INTERVAL_HOURS;
|
|
206
|
+
if (intervalHours <= 0)
|
|
207
|
+
return true;
|
|
208
|
+
const last = readLastCheckedAt(options.homeDir ?? homedir());
|
|
209
|
+
if (!last)
|
|
210
|
+
return true;
|
|
211
|
+
const ageMs = now - last.getTime();
|
|
212
|
+
const windowMs = intervalHours * 60 * 60 * 1_000;
|
|
213
|
+
return ageMs >= windowMs;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Defensive helper — parse JSON to an object; non-object payloads
|
|
217
|
+
* (top-level array, primitive) collapse to an empty object so the
|
|
218
|
+
* channel-write merge does not surface a TypeError. Mirrors the
|
|
219
|
+
* `safeParseObject` in `core/permissions/state.ts` — duplicating the
|
|
220
|
+
* 10 lines is cheaper than threading a shared util module through
|
|
221
|
+
* two unrelated leak surfaces.
|
|
222
|
+
*/
|
|
223
|
+
function safeParseObject(raw) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(raw);
|
|
226
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
227
|
+
return parsed;
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L22 (2026-05-27) — `--bare` mode predicate.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of Claude Code's `--bare` flag: when active the CLI behaves
|
|
5
|
+
* like a plain LLM frontend with NO project auto-discovery. Useful for:
|
|
6
|
+
*
|
|
7
|
+
* - headless scripting where the operator wants deterministic, repo-
|
|
8
|
+
* independent behavior (`pugi --bare --print "..."`),
|
|
9
|
+
* - dropping into a workspace without auto-creating `.pugi/`,
|
|
10
|
+
* - REPL sessions that should NOT inject ambient `PUGI.md` / `CLAUDE.md`
|
|
11
|
+
* into the model prompt,
|
|
12
|
+
* - support / triage flows where the engineer needs the CLI to act
|
|
13
|
+
* like a fresh install regardless of where it's invoked.
|
|
14
|
+
*
|
|
15
|
+
* Discovery surfaces gated by `isBareMode()`:
|
|
16
|
+
*
|
|
17
|
+
* 1. `PUGI.md` / `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` parent-dir
|
|
18
|
+
* walk-up (`loadTraversedMarkdown` in `core/context/markdown-traverse.ts`).
|
|
19
|
+
* 2. Workspace-root markdown context (`loadMarkdownContext` consumers).
|
|
20
|
+
* 3. Auto-init `.pugi/` scaffold on REPL boot in untouched dirs.
|
|
21
|
+
* 4. Persona / skill auto-load from `.pugi/skills/`.
|
|
22
|
+
* 5. Workspace summary (`readPugiSummary`) read on REPL session start.
|
|
23
|
+
*
|
|
24
|
+
* Activation precedence — the bare bit is "sticky" once set so any
|
|
25
|
+
* subprocess the CLI spawns inherits it without re-passing the flag:
|
|
26
|
+
*
|
|
27
|
+
* 1. Top-level `--bare` arg parsed by `parseArgs` in `runtime/cli.ts`.
|
|
28
|
+
* The parser sets `process.env.PUGI_BARE='1'` BEFORE the dispatch
|
|
29
|
+
* flows so callsites checking the env see the activated state.
|
|
30
|
+
* 2. `PUGI_BARE=1` env var (any value matching `/^(1|true|yes|on)$/i`).
|
|
31
|
+
* 3. Default: bare mode OFF — full auto-discovery as before.
|
|
32
|
+
*
|
|
33
|
+
* This mirrors the existing `PUGI_SKIP_SPLASH` / `PUGI_NO_AUTO_INIT`
|
|
34
|
+
* env-flag pattern so the bare module fits the rest of the runtime
|
|
35
|
+
* configuration grammar without inventing a new wire.
|
|
36
|
+
*
|
|
37
|
+
* Test surface: `apps/pugi-cli/test/bare-mode.spec.ts` exercises the
|
|
38
|
+
* env precedence, value parsing, and the explicit-set / clear helpers.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Env var consulted by `isBareMode()`. Kept as an export so the spec
|
|
42
|
+
* + the runtime CLI can use the same constant — no string-typing of
|
|
43
|
+
* the wire name across modules.
|
|
44
|
+
*/
|
|
45
|
+
export const PUGI_BARE_ENV = 'PUGI_BARE';
|
|
46
|
+
/**
|
|
47
|
+
* Truthy values recognised on the `PUGI_BARE` env. Anything else
|
|
48
|
+
* (empty string, `0`, `false`, `no`, `off`, `disabled`, undefined) is
|
|
49
|
+
* treated as bare-mode OFF. The list is intentionally short — the
|
|
50
|
+
* value is set by the CLI parser and is not customer-typed prose, so
|
|
51
|
+
* we do not need a permissive boolean coercion.
|
|
52
|
+
*/
|
|
53
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
54
|
+
/**
|
|
55
|
+
* Return true when bare mode is active for the current process. Reads
|
|
56
|
+
* `process.env[PUGI_BARE_ENV]` and applies the truthy-value match.
|
|
57
|
+
*
|
|
58
|
+
* Safe to call from any module (no FS, no side-effects). The runtime
|
|
59
|
+
* cost is a single env-var lookup + lower-case + set membership, so
|
|
60
|
+
* gating hot-path callsites with `if (isBareMode()) return ...` adds
|
|
61
|
+
* effectively zero overhead in the default (non-bare) case.
|
|
62
|
+
*/
|
|
63
|
+
export function isBareMode(env = process.env) {
|
|
64
|
+
const raw = env[PUGI_BARE_ENV];
|
|
65
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
return TRUTHY.has(raw.toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Explicitly activate bare mode for the current process. Called by
|
|
71
|
+
* `parseArgs` in `runtime/cli.ts` when `--bare` is seen on the command
|
|
72
|
+
* line so downstream modules (engine, REPL bootstrap, doctor probe)
|
|
73
|
+
* see a consistent activated state via `isBareMode()` regardless of
|
|
74
|
+
* whether the operator set the env var manually or used the flag.
|
|
75
|
+
*
|
|
76
|
+
* Subprocess inheritance is the reason we mutate `process.env` rather
|
|
77
|
+
* than threading a `bare: boolean` field through every call signature
|
|
78
|
+
* — every Node child_process spawn inherits `process.env` by default,
|
|
79
|
+
* so the bare bit propagates to MCP servers / hook scripts / git
|
|
80
|
+
* subprocesses without ceremony.
|
|
81
|
+
*/
|
|
82
|
+
export function setBareMode(env = process.env) {
|
|
83
|
+
env[PUGI_BARE_ENV] = '1';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Clear bare mode for the current process. Provided primarily for the
|
|
87
|
+
* spec so adjacent tests do not leak state between cases. Production
|
|
88
|
+
* code does NOT call this — bare mode is a one-shot per process.
|
|
89
|
+
*/
|
|
90
|
+
export function clearBareMode(env = process.env) {
|
|
91
|
+
delete env[PUGI_BARE_ENV];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Human-readable one-line banner printed by the dispatcher when bare
|
|
95
|
+
* mode is active and the invocation is NOT JSON-only. Kept as a single
|
|
96
|
+
* constant so the spec can assert the exact wording and downstream
|
|
97
|
+
* tools (status bars, doctor row, REPL header) stay in lockstep.
|
|
98
|
+
*/
|
|
99
|
+
export const BARE_MODE_BANNER = 'Pugi --bare mode: project auto-discovery disabled.';
|
|
100
|
+
/**
|
|
101
|
+
* Short label rendered inside the `pugi doctor` table when bare mode
|
|
102
|
+
* is active. The doctor probe surfaces `BARE MODE` as a separate row
|
|
103
|
+
* so operators triaging "why is Pugi ignoring my PUGI.md" see the
|
|
104
|
+
* cause without grep'ing the env.
|
|
105
|
+
*/
|
|
106
|
+
export const BARE_MODE_DOCTOR_LABEL = 'BARE MODE';
|
|
107
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -367,7 +367,7 @@ const WRITE_WORKSPACE_PREFIXES = [
|
|
|
367
367
|
* the class is `write_protected` regardless of the operation type.
|
|
368
368
|
*
|
|
369
369
|
* Wildcards are handled as substring matches (e.g. `/.ssh/` matches
|
|
370
|
-
* `~/.ssh/foo` and
|
|
370
|
+
* `~/.ssh/foo` and `[HOME]/USER/.ssh/bar`).
|
|
371
371
|
*/
|
|
372
372
|
const PROTECTED_PATH_SUBSTRINGS = [
|
|
373
373
|
'/.ssh/',
|
|
@@ -388,6 +388,40 @@ const PROTECTED_PATH_SUBSTRINGS = [
|
|
|
388
388
|
'/usr/',
|
|
389
389
|
'/var/',
|
|
390
390
|
];
|
|
391
|
+
/**
|
|
392
|
+
* Protected basename triggers — files whose CONTENT must never leak
|
|
393
|
+
* through the bash surface, even when the literal path is workspace-
|
|
394
|
+
* local. Mirrors `permission.ts::protectedBasenames` and `.env.*`
|
|
395
|
+
* pattern so the read-tool gate (which fires on `read .env`) and the
|
|
396
|
+
* bash gate (which fires on `cat .env`) stay symmetric.
|
|
397
|
+
*
|
|
398
|
+
* P0 fix 2026-05-28 (Codex audit): before this list existed, the
|
|
399
|
+
* engine model could circumvent the `read` tool's `protectedTargetReason`
|
|
400
|
+
* check by emitting `bash cat .env` — the classifier saw `cat` (read
|
|
401
|
+
* token) + `.env` (not in PROTECTED_PATH_SUBSTRINGS) and returned class
|
|
402
|
+
* `read`, which the permission matrix allows under every mode. The
|
|
403
|
+
* `local-first-invariants` spec proved the leak: `pugi explain .env`
|
|
404
|
+
* surfaced `SECRET=should_never_leak` in the engine summary.
|
|
405
|
+
*
|
|
406
|
+
* Match shape: the substring must touch a `.` boundary (`/.env`,
|
|
407
|
+
* ` .env`, `.env\b`) or appear as the full token so a path like
|
|
408
|
+
* `apps/codeforge/file.env-template` (no real secret) does not
|
|
409
|
+
* over-trigger.
|
|
410
|
+
*/
|
|
411
|
+
const PROTECTED_BASENAME_PATTERNS = [
|
|
412
|
+
// `.env`, `.env.production`, `.env.local` — anywhere in the command.
|
|
413
|
+
// Boundary on the left is start/whitespace/quote/`/`, on the right
|
|
414
|
+
// start/whitespace/end/quote/`>`/`|`/`;`.
|
|
415
|
+
/(^|[\s'"\/=])\.env(\.[A-Za-z0-9_-]+)?($|[\s'"<>|;&])/,
|
|
416
|
+
// SSH key basenames (covers both `id_rsa` and `id_ed25519` even
|
|
417
|
+
// outside `~/.ssh/`). The `/.ssh/` substring above gates the
|
|
418
|
+
// directory case; this catches a key file copied to the workspace.
|
|
419
|
+
/(^|[\s'"\/])id_(rsa|ed25519|ecdsa|dsa)(\.pub)?($|[\s'"<>|;&])/,
|
|
420
|
+
// Other credential basenames mirrored from permission.ts.
|
|
421
|
+
/(^|[\s'"\/])\.npmrc($|[\s'"<>|;&])/,
|
|
422
|
+
/(^|[\s'"\/])\.pypirc($|[\s'"<>|;&])/,
|
|
423
|
+
/(^|[\s'"\/])\.gitconfig($|[\s'"<>|;&])/,
|
|
424
|
+
];
|
|
391
425
|
/**
|
|
392
426
|
* Obfuscation triggers — any of these forces the `unknown` class so
|
|
393
427
|
* the permission engine can fail closed.
|
|
@@ -469,6 +503,26 @@ function classifyComponent(cmd, ctx) {
|
|
|
469
503
|
matched: protectedRead.matched,
|
|
470
504
|
};
|
|
471
505
|
}
|
|
506
|
+
// 4a-bis. Parent-traversal in read arguments. The file-tools layer
|
|
507
|
+
// refuses `..` segments via `resolveWorkspacePath`, but the bash
|
|
508
|
+
// surface had no equivalent gate — the engine could emit
|
|
509
|
+
// `cat ../README.md` or `ls ..` to enumerate / read outside the
|
|
510
|
+
// workspace, sidestepping the path-security check that the `read`
|
|
511
|
+
// and `glob` tools enforce.
|
|
512
|
+
//
|
|
513
|
+
// P0 fix 2026-05-28 (Codex audit): treat `..` as a path segment
|
|
514
|
+
// (`../`, ` ..`, `..\n`) in any read-class command as a workspace
|
|
515
|
+
// escape. We classify it as `write_protected` so the auto/dontAsk
|
|
516
|
+
// modes refuse, mirroring the `Path escapes workspace` semantics
|
|
517
|
+
// the file-tools layer already provides.
|
|
518
|
+
const traversal = detectParentTraversalRead(trimmed);
|
|
519
|
+
if (traversal) {
|
|
520
|
+
return {
|
|
521
|
+
class: 'write_protected',
|
|
522
|
+
reason: traversal.reason,
|
|
523
|
+
matched: traversal.matched,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
472
526
|
// 4b. .env writes are always protected, even inside the workspace
|
|
473
527
|
// (CEO directive feedback_never_delete_untracked_env.md).
|
|
474
528
|
const envWrite = detectEnvWrite(trimmed);
|
|
@@ -785,6 +839,59 @@ function detectProtectedRead(cmd) {
|
|
|
785
839
|
};
|
|
786
840
|
}
|
|
787
841
|
}
|
|
842
|
+
// P0 fix 2026-05-28: extend protected-read detection to credential
|
|
843
|
+
// basenames (`cat .env`, `head id_rsa`, `grep TOKEN .env.production`).
|
|
844
|
+
// Without this branch, the engine model can bypass the `read` tool's
|
|
845
|
+
// `protectedTargetReason` gate by emitting a bash `cat` — the read
|
|
846
|
+
// tool refuses, the model falls back to bash, and the classifier
|
|
847
|
+
// (which only knew about full-path substrings) classified `cat .env`
|
|
848
|
+
// as benign `read`. The `local-first-invariants` spec proved the leak.
|
|
849
|
+
for (const pattern of PROTECTED_BASENAME_PATTERNS) {
|
|
850
|
+
const match = cmd.match(pattern);
|
|
851
|
+
if (match) {
|
|
852
|
+
return {
|
|
853
|
+
reason: `Read from protected basename: ${match[0].trim()}`,
|
|
854
|
+
matched: match[0].trim(),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Detect parent-traversal segments (`..`) inside read-class commands.
|
|
862
|
+
* The file-tools layer (`resolveWorkspacePath`) refuses these for the
|
|
863
|
+
* `read`/`glob`/`grep` tools, but bash had no equivalent gate. We
|
|
864
|
+
* trigger on the SAME shape `path-security.ts` rejects: a `..` segment
|
|
865
|
+
* separated by `/` or whitespace. Quoted/escaped variants get the same
|
|
866
|
+
* treatment.
|
|
867
|
+
*
|
|
868
|
+
* Returns null on the safe path (no `..` segment) so the caller falls
|
|
869
|
+
* through to the regular read classification.
|
|
870
|
+
*/
|
|
871
|
+
function detectParentTraversalRead(cmd) {
|
|
872
|
+
const firstToken = cmd.split(/\s+/)[0] ?? '';
|
|
873
|
+
const isReadTool = READ_TOKENS.has(firstToken) ||
|
|
874
|
+
READ_PREFIX_TOKENS.has(firstToken) ||
|
|
875
|
+
firstToken === 'sed' ||
|
|
876
|
+
firstToken === 'awk' ||
|
|
877
|
+
firstToken === 'find';
|
|
878
|
+
if (!isReadTool)
|
|
879
|
+
return null;
|
|
880
|
+
// Match `..` as a path segment: preceded by start/whitespace/quote/`/`
|
|
881
|
+
// and followed by `/`, end-of-string, whitespace, or shell metas.
|
|
882
|
+
// Avoids over-matching `v1..v2` (range syntax inside a single token)
|
|
883
|
+
// and `1..5` (numeric ranges) because those lack the path boundary.
|
|
884
|
+
const traversalPattern = /(^|[\s'"\/])\.\.(\/|$|[\s'"<>|;&])/;
|
|
885
|
+
const m = cmd.match(traversalPattern);
|
|
886
|
+
if (m) {
|
|
887
|
+
return {
|
|
888
|
+
reason: 'Read command escapes workspace via parent traversal',
|
|
889
|
+
matched: '..',
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
// Absolute path read of /etc, /usr, /var, etc is already covered by
|
|
893
|
+
// PROTECTED_PATH_SUBSTRINGS in detectProtectedRead — no extra branch
|
|
894
|
+
// needed here.
|
|
788
895
|
return null;
|
|
789
896
|
}
|
|
790
897
|
function detectEnvWrite(cmd) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumer — read SessionStore events for a session, apply the L8
|
|
3
|
+
* compact mask + the L9 rewind mask, and return the visible transcript
|
|
4
|
+
* the REPL bootstrap (or a programmatic consumer) should render.
|
|
5
|
+
*
|
|
6
|
+
* Separation of concerns:
|
|
7
|
+
*
|
|
8
|
+
* - This module owns the READ path: list sessions, load events,
|
|
9
|
+
* reconstruct a clean transcript stream. No writes.
|
|
10
|
+
* - The WRITE path (append a rewind-marker, undo-rewind) lives in
|
|
11
|
+
* `./rewinder.ts`.
|
|
12
|
+
* - The REPL session lifecycle (lockfile, Ink mount, dispatch FSM)
|
|
13
|
+
* stays in `core/repl/session.ts`. We do NOT spin up the REPL here.
|
|
14
|
+
*
|
|
15
|
+
* Why route resume through this module at all (vs. operators using
|
|
16
|
+
* `core/repl/store/*` directly):
|
|
17
|
+
*
|
|
18
|
+
* The store returns RAW events. Most consumers want masked events —
|
|
19
|
+
* i.e. the chronological list after compact-boundary masking AND
|
|
20
|
+
* rewind-marker masking. Doing both passes inline at every call site
|
|
21
|
+
* would scatter the mask logic; centralising it here means a future
|
|
22
|
+
* third mask (named checkpoints? selective edit?) lands in one place.
|
|
23
|
+
*/
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { applyCompactMask } from '../compact/buffer-rewriter.js';
|
|
26
|
+
import { SqliteSessionStore, resolveProjectStoreDir, } from '../repl/store/index.js';
|
|
27
|
+
import { applyRewindMask, findLatestActiveRewind } from './rewinder.js';
|
|
28
|
+
/**
|
|
29
|
+
* Composed mask: compact-mask first (collapses summarised slices into
|
|
30
|
+
* boundary markers + kept tail), then rewind-mask (drops everything
|
|
31
|
+
* inside an active rewind range, including any compaction markers that
|
|
32
|
+
* fell inside it).
|
|
33
|
+
*
|
|
34
|
+
* Order matters: compact-mask reads `coversUntilOffset` against the
|
|
35
|
+
* RAW event indices. Running rewind-mask first would shift indices and
|
|
36
|
+
* break the compact replay anchor. The result is the chronological
|
|
37
|
+
* stream the operator should SEE, with infra rows (rewind markers)
|
|
38
|
+
* stripped.
|
|
39
|
+
*/
|
|
40
|
+
export function applyAllMasks(events) {
|
|
41
|
+
return applyRewindMask(applyCompactMask(events));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* List sessions a `pugi resume` invocation could open. Uses the
|
|
45
|
+
* READ-ONLY store view so the call never takes the lockfile — safe to
|
|
46
|
+
* run alongside a live REPL. Each row carries derived metadata
|
|
47
|
+
* (`visibleEventCount`, `hasActiveRewind`) so the renderer does not
|
|
48
|
+
* need to re-walk events.
|
|
49
|
+
*
|
|
50
|
+
* Returns an empty array when the project store does not exist (no
|
|
51
|
+
* sessions ever started for this project slug). Callers surface a
|
|
52
|
+
* "nothing to resume" message in that branch.
|
|
53
|
+
*/
|
|
54
|
+
export async function listResumableSessions(input) {
|
|
55
|
+
const dir = input.storeDir ?? resolveProjectStoreDir(input.projectSlug, input.home ?? homedir());
|
|
56
|
+
const limit = clampLimit(input.limit ?? 10, 1, 50);
|
|
57
|
+
let view;
|
|
58
|
+
try {
|
|
59
|
+
view = await SqliteSessionStore.openReadOnly(dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const rows = await view.list({ project: input.projectSlug, limit });
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const events = await view.events(row.id);
|
|
69
|
+
const visible = applyAllMasks(events);
|
|
70
|
+
const latest = findLatestActiveRewind(events);
|
|
71
|
+
out.push({
|
|
72
|
+
row,
|
|
73
|
+
visibleEventCount: visible.length,
|
|
74
|
+
hasActiveRewind: latest !== null,
|
|
75
|
+
updatedAt: row.updatedAt,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await view.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Load one session for replay. The caller (REPL bootstrap, tests,
|
|
89
|
+
* future programmatic exporters) gets BOTH the raw event stream and
|
|
90
|
+
* the masked view so it can choose its rendering strategy. Returns
|
|
91
|
+
* null when the session does not exist; throws when the store cannot
|
|
92
|
+
* be opened (the caller surfaces a one-line error).
|
|
93
|
+
*
|
|
94
|
+
* The PID lockfile contention is NOT relevant here — we use the
|
|
95
|
+
* read-only view. Concurrent writers from a live REPL are safe.
|
|
96
|
+
*/
|
|
97
|
+
export async function loadSessionForReplay(input) {
|
|
98
|
+
const dir = input.storeDir ?? resolveProjectStoreDir(input.projectSlug, input.home ?? homedir());
|
|
99
|
+
const view = await SqliteSessionStore.openReadOnly(dir);
|
|
100
|
+
try {
|
|
101
|
+
const row = await view.get(input.sessionId);
|
|
102
|
+
if (!row)
|
|
103
|
+
return null;
|
|
104
|
+
const rawEvents = await view.events(row.id);
|
|
105
|
+
const visibleEvents = applyAllMasks(rawEvents);
|
|
106
|
+
const latest = findLatestActiveRewind(rawEvents);
|
|
107
|
+
return {
|
|
108
|
+
row,
|
|
109
|
+
rawEvents,
|
|
110
|
+
visibleEvents,
|
|
111
|
+
hasActiveRewind: latest !== null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await view.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Load raw + masked events through an already-open SessionStore.
|
|
120
|
+
*
|
|
121
|
+
* Used by the in-REPL `/rewind` slash handler — the live REPL already
|
|
122
|
+
* holds the writer lock, so we cannot open the read-only view in the
|
|
123
|
+
* same process. The store reference IS the active write handle; we
|
|
124
|
+
* just call `loadEvents` and run the masks.
|
|
125
|
+
*
|
|
126
|
+
* Same shape as `loadSessionForReplay` minus the read-only-view setup.
|
|
127
|
+
*/
|
|
128
|
+
export async function loadFromStore(store, sessionId) {
|
|
129
|
+
const row = await store.getSession(sessionId);
|
|
130
|
+
if (!row)
|
|
131
|
+
return null;
|
|
132
|
+
const rawEvents = await store.loadEvents(sessionId);
|
|
133
|
+
const visibleEvents = applyAllMasks(rawEvents);
|
|
134
|
+
const latest = findLatestActiveRewind(rawEvents);
|
|
135
|
+
return {
|
|
136
|
+
row,
|
|
137
|
+
rawEvents,
|
|
138
|
+
visibleEvents,
|
|
139
|
+
hasActiveRewind: latest !== null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function clampLimit(raw, min, max) {
|
|
143
|
+
if (!Number.isFinite(raw) || raw < min)
|
|
144
|
+
return min;
|
|
145
|
+
if (raw > max)
|
|
146
|
+
return max;
|
|
147
|
+
return Math.floor(raw);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=resumer.js.map
|