@pugi/cli 0.1.0-beta.25 → 0.1.0-beta.27

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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * `pugi sessions` extensions — leak L9 (2026-05-27).
3
+ *
4
+ * The legacy `pugi sessions` handler lives inline in `runtime/cli.ts`
5
+ * (it predates the per-command runner pattern). This module exposes the
6
+ * new sub-commands the L9 sprint adds:
7
+ *
8
+ * - `pugi sessions undo-rewind [<session-id>]`
9
+ * Append an inverse marker that nullifies the latest 'rewind'
10
+ * marker on the target session. Operator escape hatch: nothing
11
+ * is destroyed, the masked range simply becomes visible again.
12
+ *
13
+ * Wire pattern matches `runCompactCommand` / `runRewindCommand`: one
14
+ * structured payload + one human line per invocation, returned to the
15
+ * caller for the JSON path.
16
+ */
17
+ import { homedir } from 'node:os';
18
+ import { slugForCwd } from '../../core/repl/history.js';
19
+ import { appendRewindMarker, findLatestActiveRewind, } from '../../core/checkpoint/rewinder.js';
20
+ import { loadFromStore } from '../../core/checkpoint/resumer.js';
21
+ import { SqliteSessionStore, resolveProjectStoreDir, } from '../../core/repl/store/session-store.js';
22
+ /**
23
+ * Sub-command router. The caller passes the full positional args; we
24
+ * peel `args[0]` as the sub-command name and forward the tail. Unknown
25
+ * sub-commands surface a usage hint at exit code 2.
26
+ */
27
+ export async function runSessionsCommand(args, ctx) {
28
+ const sub = args[0];
29
+ if (sub === 'undo-rewind') {
30
+ return runUndoRewind(args.slice(1), ctx);
31
+ }
32
+ // Other sub-commands (the legacy `pugi sessions` list path) are
33
+ // owned by runtime/cli.ts — return null so the caller knows to fall
34
+ // through to that handler.
35
+ return null;
36
+ }
37
+ async function runUndoRewind(args, ctx) {
38
+ const explicitId = args[0] && !args[0].startsWith('--') ? args[0] : undefined;
39
+ const slug = slugForCwd(ctx.workspaceRoot);
40
+ let store = ctx.store ?? null;
41
+ let sessionId = ctx.sessionId ?? explicitId ?? null;
42
+ let storeOpenedHere = false;
43
+ if (store === null) {
44
+ sessionId = sessionId ?? (await pickMostRecentSessionIdReadOnly(slug));
45
+ if (!sessionId) {
46
+ return emit(ctx, {
47
+ command: 'sessions',
48
+ sub: 'undo-rewind',
49
+ status: 'failed_no_session',
50
+ reason: 'No session to undo-rewind. Start a REPL with `pugi`.',
51
+ });
52
+ }
53
+ const opened = await openLiveStore(slug, sessionId);
54
+ if (!opened) {
55
+ return emit(ctx, {
56
+ command: 'sessions',
57
+ sub: 'undo-rewind',
58
+ status: 'failed_store',
59
+ sessionId,
60
+ reason: 'Could not open local session store (lock held by another REPL?).',
61
+ });
62
+ }
63
+ store = opened;
64
+ storeOpenedHere = true;
65
+ }
66
+ else if (sessionId === null) {
67
+ const rows = await store.listSessions({ project: slug, limit: 1, status: 'active' });
68
+ if (rows.length === 0) {
69
+ return emit(ctx, {
70
+ command: 'sessions',
71
+ sub: 'undo-rewind',
72
+ status: 'failed_no_session',
73
+ reason: 'No active session to undo-rewind.',
74
+ });
75
+ }
76
+ sessionId = rows[0].id;
77
+ }
78
+ try {
79
+ const loaded = await loadFromStore(store, sessionId);
80
+ if (!loaded) {
81
+ return emit(ctx, {
82
+ command: 'sessions',
83
+ sub: 'undo-rewind',
84
+ status: 'failed_no_session',
85
+ sessionId,
86
+ reason: `Session '${sessionId}' not found.`,
87
+ });
88
+ }
89
+ const latest = findLatestActiveRewind(loaded.rawEvents);
90
+ if (latest === null) {
91
+ return emit(ctx, {
92
+ command: 'sessions',
93
+ sub: 'undo-rewind',
94
+ status: 'noop_no_rewind',
95
+ sessionId,
96
+ reason: 'No active rewind to undo on this session.',
97
+ }, 'No active rewind to undo.');
98
+ }
99
+ const fromEventIndex = loaded.rawEvents.length;
100
+ await appendRewindMarker({
101
+ store,
102
+ mode: 'undo-rewind',
103
+ toEventIndex: latest.payload.toEventIndex,
104
+ fromEventIndex,
105
+ turnsRewound: latest.payload.turnsRewound,
106
+ reason: 'undo',
107
+ ...(ctx.now !== undefined ? { now: ctx.now } : {}),
108
+ });
109
+ return emit(ctx, {
110
+ command: 'sessions',
111
+ sub: 'undo-rewind',
112
+ status: 'undone',
113
+ sessionId,
114
+ undoneTurns: latest.payload.turnsRewound,
115
+ }, `Undid the latest rewind (${latest.payload.turnsRewound} turn${latest.payload.turnsRewound === 1 ? '' : 's'} restored).`);
116
+ }
117
+ finally {
118
+ if (storeOpenedHere && store) {
119
+ try {
120
+ await store.close();
121
+ }
122
+ catch {
123
+ /* idempotent */
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function emit(ctx, payload, text) {
129
+ const human = text ?? payload.reason ?? `sessions ${payload.sub}: ${payload.status}`;
130
+ ctx.writeOutput(payload, human);
131
+ return payload;
132
+ }
133
+ async function openLiveStore(projectSlug, sessionId) {
134
+ try {
135
+ const store = new SqliteSessionStore({ projectSlug, home: homedir() });
136
+ await store.open({
137
+ id: sessionId,
138
+ workspaceRoot: process.cwd(),
139
+ projectSlug,
140
+ });
141
+ return store;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ async function pickMostRecentSessionIdReadOnly(projectSlug) {
148
+ try {
149
+ const dir = resolveProjectStoreDir(projectSlug, homedir());
150
+ const view = await SqliteSessionStore.openReadOnly(dir);
151
+ try {
152
+ const rows = await view.list({ project: projectSlug, limit: 1, status: 'active' });
153
+ return rows.length > 0 ? rows[0].id : null;
154
+ }
155
+ finally {
156
+ await view.close();
157
+ }
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ //# sourceMappingURL=sessions.js.map
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.25');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.27');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.
@@ -50,6 +50,7 @@ import { z } from 'zod';
50
50
  import { randomUUID } from 'node:crypto';
51
51
  import { relative as relativePath } from 'node:path';
52
52
  import { spawnSubagentWithOutcome } from '../core/subagents/spawn.js';
53
+ import { inheritCacheContext } from '../core/dispatch/cache-handoff.js';
53
54
  /**
54
55
  * Argument schema. `isolation: 'auto'` defers to the role-default
55
56
  * isolation tier (set by `isolationForRole` in dispatcher.ts). The
@@ -157,6 +158,28 @@ export async function agentTool(args, ctx) {
157
158
  // restriction layer regardless of permissionMode.
158
159
  permissionMode: 'auto',
159
160
  };
161
+ // L10 (2026-05-27): synthesize a prompt-cache inheritance handle for
162
+ // the child before we dispatch. The handle is persisted under
163
+ // `.pugi/cache-refs/<childAgentId>.json` so:
164
+ // - The child engine loop's first turn (dispatcher-real.ts) can
165
+ // forward parent_cache_id onto Anvil — provider-dependent honour
166
+ // (Anthropic cache_control breakpoints today; OpenAI/Gemini
167
+ // silently ignore until Anvil grows per-provider adapters).
168
+ // - Operators can introspect via `pugi dispatch list-cache-refs`.
169
+ // - `pugi dispatch clear-cache-refs --older-than 1h` can GC after
170
+ // a long session.
171
+ // Failure to persist must NOT block the dispatch — the handle is a
172
+ // best-effort optimisation; if disk is full or the workspace root is
173
+ // read-only, we degrade silently to a cache-miss dispatch.
174
+ try {
175
+ inheritCacheContext(ctx.session.id, task.id, {
176
+ workspaceRoot: ctx.session.root,
177
+ ...(ctx.now ? { now: ctx.now } : {}),
178
+ });
179
+ }
180
+ catch {
181
+ // Silent degrade: cache inheritance is forward-compat, not load-bearing.
182
+ }
160
183
  const useWorktree = validated.isolation === 'worktree'
161
184
  ? true
162
185
  : validated.isolation === 'shared_fs'
@@ -89,33 +89,21 @@ export function loadPugMascotAnsi() {
89
89
  // icon, clipboard, hyperlinks, color-palette change). Drop them
90
90
  // so a corrupted asset cannot rename the operator's terminal tab
91
91
  // or smuggle a hyperlink into the splash region.
92
- // 2. Drop ALL CSI ? <numbers and semicolons> [lh] (DEC private-mode
93
- // set / reset). The legitimate chafa output for a splash is
94
- // truecolor SGR (`CSI 38;2;R;G;B m`) plus cursor-positioning
95
- // no private-mode toggle ever appears там legitimately. A
96
- // permissive deny-all pattern covers every disruptive private
97
- // mode in one regex:
98
- // - cursor visibility (25)
99
- // - alt-screen buffer (47, 1047, 1048, 1049)
100
- // - mouse tracking (1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015)
101
- // - bracketed paste (2004)
102
- // - focus reporting (1004)
103
- // - multi-mode forms (e.g. `CSI ? 47 ; 1049 h` — legal per
104
- // xterm ctlseqs but missed by the previous single-mode regex)
105
- // - any future private mode a corrupt asset might emit
106
- // Allowlisting the modes the splash needs is impossible because
107
- // the splash needs ZERO of them — chafa renders glyph-by-glyph,
108
- // not via private-mode toggles. A pure deny-all is strictly
109
- // safer than enumerating known-bad modes one-by-one.
92
+ // 2. Drop CSI ? <mode> [hl] for mouse-tracking and screen-buffer
93
+ // switch modes (1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015,
94
+ // 1049, 47, 1047, 1048). These would either start swallowing
95
+ // mouse input or flip the terminal into the alternate screen.
110
96
  // 3. Drop CSI 6 n (cursor-position report). Would inject a fake
111
97
  // CPR into the operator's stdin stream.
112
98
  // 4. Drop CSI [23]J / CSI [23]K (full screen / line clear). A
113
99
  // chafa render uses cursor-positioning per row, not bulk
114
100
  // erases; bulk clears would wipe whatever the operator already
115
101
  // had on screen above the splash.
102
+ // The cursor-hide/show wrappers (CSI ? 25 [lh]) are handled by
103
+ // the same CSI-?-mode pattern as the mouse / alt-screen modes.
116
104
  const stripped = raw
117
105
  .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
118
- .replace(/\x1b\[\?[0-9;]+[lh]/g, '')
106
+ .replace(/\x1b\[\?(?:25|47|1000|1001|1002|1003|1004|1005|1006|1015|1047|1048|1049)[lh]/g, '')
119
107
  .replace(/\x1b\[6n/g, '')
120
108
  .replace(/\x1b\[[23]?[JK]/g, '');
121
109
  if (stripped.trim().length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.25",
3
+ "version": "0.1.0-beta.27",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -53,8 +53,8 @@
53
53
  "turndown": "^7.2.4",
54
54
  "undici": "^8.3.0",
55
55
  "zod": "^3.23.0",
56
- "@pugi/sdk": "0.1.0-beta.25",
57
- "@pugi/personas": "0.1.2"
56
+ "@pugi/personas": "0.1.2",
57
+ "@pugi/sdk": "0.1.0-beta.27"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.0.0",