gentle-pi 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +138 -155
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +82 -0
- package/docs/gentle-shell.md +80 -10
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +72 -55
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +187 -40
- package/extensions/gentle-ai.ts +1765 -1185
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +354 -0
- package/extensions/gentle-shell.ts +418 -79
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +141 -57
- package/lib/gentle-shell-launcher.ts +11 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/jobs-sidebar-state.ts +22 -3
- package/lib/jobs-widget.ts +112 -0
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +2 -2
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +6 -7
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +27 -7
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +14 -10
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +140 -56
- package/runtime/gentle-shell-launcher.mjs +11 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/bundled-install.mjs +209 -13
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +31 -7
- package/scripts/installer-preflight.mjs +6 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +37 -7
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +276 -5
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +4 -4
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +369 -24
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +928 -448
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +452 -0
- package/tests/gentle-shell-bundled.test.ts +321 -248
- package/tests/gentle-shell-launcher.test.ts +29 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +227 -27
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-downloads.test.ts +32 -0
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/jobs-widget.test.ts +138 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/model-profiles-preservation.test.ts +2 -2
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +104 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/runtime-harness.mjs +188 -211
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +6 -4
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +18 -6
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +67 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/vim-editor-adapter.test.ts +2 -2
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
|
@@ -10,6 +10,12 @@ export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate
|
|
|
10
10
|
|
|
11
11
|
export const YOLO_STATUS_KEY = "gentle:yolo";
|
|
12
12
|
export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
|
|
13
|
+
export const SUPER_YOLO_STATUS_TEXT = "💥 SUPER YOLO ON — no permission prompts; private paths stay blocked";
|
|
14
|
+
export const SUPER_YOLO_DIRECTIVE = `<gentle-super-yolo-session>
|
|
15
|
+
SUPER YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace. No permission questions are asked in this mode: proceed through ordinary and destructive shell work (including rm, resets, force operations and configured-guard confirmations) without confirmation dialogs, and make implementation and delivery choices directly.
|
|
16
|
+
The ONLY hard line is private material: never read, print, copy, transmit or commit secrets or private credentials — .env files, .ssh, keychains, *.pem/*.key/*.p12/*.pfx, secrets/ directories, cloud credential files. If a task seems to need them, stop and ask the human.
|
|
17
|
+
This does NOT override explicit human restrictions in the live conversation, and never auto-answers ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Children remain bounded by their delegated task; they receive no independent delivery grant or Super Yolo inheritance.
|
|
18
|
+
</gentle-super-yolo-session>`;
|
|
13
19
|
export const YOLO_DIRECTIVE = `<gentle-yolo-session>
|
|
14
20
|
YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
|
|
15
21
|
Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
|
|
@@ -40,12 +46,14 @@ export class YoloSessionPolicy {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
/** Remove our exact
|
|
44
|
-
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
|
|
49
|
+
/** Remove our exact blocks as well as adding them: bridge options can be reused. */
|
|
50
|
+
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean, superActive = false): void {
|
|
45
51
|
if (!options) return;
|
|
46
52
|
options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
|
|
47
|
-
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
|
|
48
|
-
|
|
53
|
+
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
|
|
54
|
+
.replace(`\n\n${SUPER_YOLO_DIRECTIVE}`, "").replace(SUPER_YOLO_DIRECTIVE, "");
|
|
55
|
+
if (superActive) appendSystemPromptOnce(options, SUPER_YOLO_DIRECTIVE);
|
|
56
|
+
else if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
|
|
49
57
|
}
|
|
50
58
|
const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
|
|
51
59
|
type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
|
|
@@ -86,7 +94,8 @@ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContex
|
|
|
86
94
|
}
|
|
87
95
|
export interface YoloSessionController {
|
|
88
96
|
active(context: ExtensionContext): Promise<boolean>;
|
|
89
|
-
|
|
97
|
+
superActive(context: ExtensionContext): Promise<boolean>;
|
|
98
|
+
mode(context: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode>;
|
|
90
99
|
setMode(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
91
100
|
defaultMode(): PermissionMode;
|
|
92
101
|
saveDefault(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
@@ -101,7 +110,7 @@ interface ModeIdentity {
|
|
|
101
110
|
}
|
|
102
111
|
/** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
|
|
103
112
|
export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
|
|
104
|
-
options: { onChange?: () => void } = {}): YoloSessionController {
|
|
113
|
+
options: { onChange?: () => void; resolveIdentity?: typeof resolveCanonicalGitRepositoryIdentity } = {}): YoloSessionController {
|
|
105
114
|
const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
|
|
106
115
|
let generation = 0;
|
|
107
116
|
let ownerGeneration = 0;
|
|
@@ -112,6 +121,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
112
121
|
const seen = new WeakMap<object, Set<string>>();
|
|
113
122
|
const observers = new Set<() => void>();
|
|
114
123
|
const pendingCaptures = new Set<{ manager: ModeIdentity["manager"]; id: string }>();
|
|
124
|
+
// Identity is re-captured for every tool admission: freshness is a security
|
|
125
|
+
// invariant, so cross-admission reuse is never allowed (a mid-lookup mode
|
|
126
|
+
// reselection must invalidate). Within one admission the redundant mode()
|
|
127
|
+
// lookups of the same tool call share at most ONE capture, keyed by that
|
|
128
|
+
// admission's tool call id; a different or absent id re-captures, and a
|
|
129
|
+
// generation bump or cwd change still invalidates a shared slot.
|
|
130
|
+
let capturedMode: (ModeIdentity & { cwd: string; generation: number; admission: string }) | undefined;
|
|
131
|
+
// Rendered strings last published for each context; identical repaints are skipped.
|
|
132
|
+
const published = new WeakMap<ExtensionContext, { yolo: string | undefined; modeText: string }>();
|
|
115
133
|
const primary = () => env.GENTLE_PI_AGENTS_CHILD !== "1";
|
|
116
134
|
const interactive = (ctx: ExtensionContext): boolean => {
|
|
117
135
|
if (!primary() || !ctx.hasUI) return false;
|
|
@@ -129,10 +147,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
129
147
|
const ids = seen.get(scope.manager) ?? new Set<string>(); ids.add(scope.id); seen.set(scope.manager, ids);
|
|
130
148
|
};
|
|
131
149
|
const publish = (ctx: ExtensionContext): void => {
|
|
132
|
-
const yolo = liveMode === "yolo";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
const yolo = liveMode === "yolo" ? YOLO_STATUS_TEXT : liveMode === "super-yolo" ? SUPER_YOLO_STATUS_TEXT : undefined;
|
|
151
|
+
const modeText = `Permissions: ${liveMode.toUpperCase()}`;
|
|
152
|
+
const previous = published.get(ctx);
|
|
153
|
+
if (previous && previous.yolo === yolo && previous.modeText === modeText) return;
|
|
154
|
+
let painted = true;
|
|
155
|
+
try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo); } catch { painted = false; /* display only */ }
|
|
156
|
+
try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [yolo] : undefined); } catch { painted = false; /* display only */ }
|
|
157
|
+
try { ctx.ui.setStatus(MODE_STATUS_KEY, modeText); } catch { painted = false; /* display only */ }
|
|
158
|
+
if (painted) published.set(ctx, { yolo, modeText });
|
|
136
159
|
};
|
|
137
160
|
const changed = (ctx: ExtensionContext): void => {
|
|
138
161
|
generation += 1; publish(ctx);
|
|
@@ -162,17 +185,25 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
162
185
|
try {
|
|
163
186
|
const canonical = await realpath(cwd);
|
|
164
187
|
if (!current()) return undefined;
|
|
165
|
-
const clone = await resolveCanonicalGitRepositoryIdentity(canonical);
|
|
188
|
+
const clone = await (options.resolveIdentity ?? resolveCanonicalGitRepositoryIdentity)(canonical);
|
|
166
189
|
if (!current()) return undefined;
|
|
167
190
|
return { ...scope, workspace: clone ?? `cwd:${canonical}` };
|
|
168
191
|
} catch { return undefined; }
|
|
169
192
|
finally { pendingCaptures.delete(scope); }
|
|
170
193
|
};
|
|
171
194
|
const defaultMode = (): PermissionMode => resolvePermissionModePolicy(store).mode;
|
|
172
|
-
const mode = async (ctx: ExtensionContext): Promise<PermissionMode> => {
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
195
|
+
const mode = async (ctx: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode> => {
|
|
196
|
+
const scope = session(ctx);
|
|
197
|
+
const admissionId = admission?.toolCallId;
|
|
198
|
+
let identity: ModeIdentity | undefined;
|
|
199
|
+
if (admissionId !== undefined && scope && capturedMode &&
|
|
200
|
+
capturedMode.admission === admissionId && capturedMode.generation === generation &&
|
|
201
|
+
capturedMode.manager === scope.manager && capturedMode.id === scope.id && capturedMode.cwd === ctx.cwd) identity = capturedMode;
|
|
202
|
+
if (!identity) {
|
|
203
|
+
const epoch = generation;
|
|
204
|
+
identity = await capture(ctx);
|
|
205
|
+
if (epoch !== generation) return "normal";
|
|
206
|
+
}
|
|
176
207
|
if (binding && (!identity || binding.manager !== identity.manager || binding.id !== identity.id ||
|
|
177
208
|
binding.workspace !== identity.workspace || (interactiveGrant && !interactive(ctx)))) {
|
|
178
209
|
binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
|
|
@@ -182,12 +213,17 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
182
213
|
remember(identity); binding = identity;
|
|
183
214
|
if (fresh) { liveMode = defaultMode(); interactiveGrant = false; changed(ctx); }
|
|
184
215
|
}
|
|
216
|
+
// Record after the binding pass: internal changed() bumps above must not strand
|
|
217
|
+
// a cache entry stamped with a superseded generation. Only admission-keyed
|
|
218
|
+
// lookups record; the slot is overwritten when a different admission id arrives.
|
|
219
|
+
if (identity && scope && admissionId !== undefined) capturedMode = { ...identity, cwd: ctx.cwd, generation, admission: admissionId };
|
|
185
220
|
publish(ctx);
|
|
186
221
|
return identity && binding ? liveMode : "normal";
|
|
187
222
|
};
|
|
188
223
|
const active = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "yolo";
|
|
224
|
+
const superActive = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "super-yolo";
|
|
189
225
|
const reset = (ctx: ExtensionContext): void => {
|
|
190
|
-
ownerGeneration += 1;
|
|
226
|
+
ownerGeneration += 1; capturedMode = undefined;
|
|
191
227
|
// Initial session_start remains fresh; shutdown/reload of an observed session revokes.
|
|
192
228
|
if (binding) remember(binding);
|
|
193
229
|
const scope = session(ctx);
|
|
@@ -228,7 +264,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
228
264
|
};
|
|
229
265
|
const humanAction = async (action: HumanAction, ctx: ExtensionContext, current = () => true): Promise<boolean> => {
|
|
230
266
|
if (!current()) return false;
|
|
231
|
-
const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo");
|
|
267
|
+
const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo" && liveMode !== "super-yolo");
|
|
232
268
|
const pending = setMode(enable ? "yolo" : "normal", ctx, current);
|
|
233
269
|
const epoch = generation; // setMode revokes synchronously, then publishes once on success.
|
|
234
270
|
const result = await pending;
|
|
@@ -253,7 +289,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
253
289
|
const epoch = generation, value = await mode(ctx);
|
|
254
290
|
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
255
291
|
if (epoch !== generation) return read();
|
|
256
|
-
return binding ? value === "yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
292
|
+
return binding ? value === "yolo" || value === "super-yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
257
293
|
};
|
|
258
294
|
request.respond({
|
|
259
295
|
read,
|
|
@@ -276,7 +312,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
276
312
|
} catch { /* obsolete trusted context: discovery expires closed */ }
|
|
277
313
|
});
|
|
278
314
|
pi.registerCommand("gentle:mode", {
|
|
279
|
-
description: "Permissions: normal|auto|yolo|status; default normal|auto|yolo saves for new primary sessions.",
|
|
315
|
+
description: "Permissions: normal|auto|yolo|super-yolo|status; default normal|auto|yolo|super-yolo saves for new primary sessions.",
|
|
280
316
|
handler: async (args, ctx) => {
|
|
281
317
|
const words = args.trim().split(/\s+/);
|
|
282
318
|
if (!args.trim() || args.trim() === "status") {
|
|
@@ -290,7 +326,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
290
326
|
else notify(ctx, "Mode unchanged — interactive primary session required or the session changed.", true);
|
|
291
327
|
return;
|
|
292
328
|
}
|
|
293
|
-
|
|
329
|
+
notify(ctx, "Use /gentle:mode normal|auto|yolo|super-yolo|status or /gentle:mode default normal|auto|yolo|super-yolo. State unchanged.", true);
|
|
294
330
|
},
|
|
295
331
|
});
|
|
296
332
|
pi.registerCommand("gentle:yolo", {
|
|
@@ -301,7 +337,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
301
337
|
if (!args.trim() && ctx.hasUI && typeof ctx.ui.select === "function") {
|
|
302
338
|
current = bindCurrent(ctx);
|
|
303
339
|
const state = await active(ctx) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
|
|
304
|
-
const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 —
|
|
340
|
+
const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — standing permission (current: ${state})`, ["enable", "disable", "status"]);
|
|
305
341
|
if (selected === undefined) return;
|
|
306
342
|
action = selected;
|
|
307
343
|
}
|
|
@@ -313,5 +349,5 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
|
|
|
313
349
|
notify(ctx, "YOLO unchanged — the session changed while the menu was open.", true);
|
|
314
350
|
},
|
|
315
351
|
});
|
|
316
|
-
return { active, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
|
|
352
|
+
return { active, superActive, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
|
|
317
353
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
3
|
import { request as httpRequest } from "node:http";
|
|
4
4
|
import { createConnection } from "node:net";
|
|
5
|
-
import { dirname, posix, win32 } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, posix, win32 } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// The launcher in a bundled install (S5, S8): pure decisions over an injected
|
|
@@ -28,6 +28,46 @@ import { fileURLToPath } from "node:url";
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Builds the detached shared server's environment without retaining session scratch.
|
|
33
|
+
* Windows Node can reintroduce an omitted TEMP from its parent, so use the
|
|
34
|
+
* original stable ancestor there; POSIX omits scratch temp settings instead.
|
|
35
|
+
*/
|
|
36
|
+
export function sharedEngramEnv(base , platform = process.platform) {
|
|
37
|
+
const path = pathFor(platform);
|
|
38
|
+
const env = {};
|
|
39
|
+
const scratch = (value ) => /^(gp|ga)[A-Za-z0-9]{6}$/.test(path.basename(path.normalize(value)));
|
|
40
|
+
let stableTemp ;
|
|
41
|
+
let tempKey ;
|
|
42
|
+
for (const [key, value] of Object.entries(base)) {
|
|
43
|
+
const name = platform === "win32" ? key.toUpperCase() : key;
|
|
44
|
+
if (name.startsWith("ENGRAM_") || name === "GH_TOKEN" || name === "GITHUB_TOKEN") continue;
|
|
45
|
+
if (["TMPDIR", "TMP", "TEMP"].includes(name) && value !== undefined && scratch(value)) {
|
|
46
|
+
if (platform !== "win32") continue;
|
|
47
|
+
// A drive-relative or root-relative path depends on ambient state.
|
|
48
|
+
const root = win32.parse(value).root;
|
|
49
|
+
if (!/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+[\\/]?)$/.test(root) || value.split(/[\\/]/).includes("..")) {
|
|
50
|
+
throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
|
|
51
|
+
}
|
|
52
|
+
let stable = win32.normalize(value);
|
|
53
|
+
while (scratch(stable)) {
|
|
54
|
+
const parent = win32.dirname(stable);
|
|
55
|
+
if (parent === stable) throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
|
|
56
|
+
stable = parent;
|
|
57
|
+
}
|
|
58
|
+
env[key] = stable;
|
|
59
|
+
stableTemp ??= stable;
|
|
60
|
+
} else {
|
|
61
|
+
env[key] = value;
|
|
62
|
+
}
|
|
63
|
+
if (name === "TEMP") tempKey = key;
|
|
64
|
+
}
|
|
65
|
+
// Keep an existing custom TEMP, even if TMP/TMPDIR chose another root.
|
|
66
|
+
if (stableTemp !== undefined && (tempKey === undefined || env[tempKey] === undefined)) env[tempKey ?? "TEMP"] = stableTemp;
|
|
67
|
+
env.ENGRAM_NO_UPDATE_CHECK = "1";
|
|
68
|
+
return env;
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
// The same rules scripts/bundled-install.mjs writes versions and markers with.
|
|
32
72
|
const VERSION_ID = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
33
73
|
const VERSION_MARKER = ".gentle-shell-version";
|
|
@@ -233,15 +273,14 @@ export function bundledPi(install , input
|
|
|
233
273
|
// Gentle AI's variables, never set here because the agent's shell would pass
|
|
234
274
|
// them to a gentle-ai the user runs), and the published Engram as
|
|
235
275
|
// gentle-engram's ENGRAM_BIN unless the user chose one. With a memory decision
|
|
236
|
-
// (S2, S6): the startup policy, and ENGRAM_BIN is the
|
|
237
|
-
//
|
|
238
|
-
//
|
|
276
|
+
// (S2, S6): the startup policy, and ENGRAM_BIN is the Shell-owned guard, so no
|
|
277
|
+
// provider, whatever loaded it, runs an Engram binary against a store; allowed
|
|
278
|
+
// memory reaches the shared server through ENGRAM_URL only.
|
|
239
279
|
export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
|
|
240
|
-
export const
|
|
241
|
-
export const
|
|
242
|
-
export const
|
|
280
|
+
export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe server reuse. Matching versions or updating alone do not prove compatibility. Reuse requires a server on 127.0.0.1:7437 with verified /compatibility evidence; Shell never stops or replaces a server already listening there. Back up existing Engram data before manual changes; do not delete it or bypass the safety check. Shell rechecks on every launch and remains usable without memory.";
|
|
281
|
+
export const ENGRAM_START_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not start the shared Engram server, or it did not report ready in time. Restart Shell to retry; if another program holds 127.0.0.1:7437, stop it first. Shell remains usable without memory.";
|
|
282
|
+
export const ENGRAM_NEWER_SCHEMA_NOTICE = "Engram memory disabled: ~/.engram was migrated by a newer Engram than Shell's bundled core, so Shell will not start a server on it. Start that newer Engram's server yourself (engram serve); Shell reuses a running server with verified /compatibility evidence. Back up the store; do not delete or downgrade it. Shell remains usable without memory.";
|
|
243
283
|
// `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
|
|
244
|
-
export const ENGRAM_PROVISION_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not provision a new Engram store identity, so it cannot vouch for that store. Restart Shell to retry. If Shell keeps reporting unknown compatibility and ~/.engram holds only .instance-id files, back it up and remove it, then restart Shell. Shell remains usable without memory.";
|
|
245
284
|
export const ENGRAM_CORE_MISSING_NOTICE = "Engram memory disabled: the bundled Engram core is not installed yet. Run 'gentle-shell setup' to install the verified core, then restart Shell. Shell remains usable without memory.";
|
|
246
285
|
// The first Engram release whose server answers GET /compatibility (PR #1792).
|
|
247
286
|
export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
|
|
@@ -262,12 +301,13 @@ function outdatedServerVersion(version )
|
|
|
262
301
|
// The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
|
|
263
302
|
export const ENGRAM_DEFAULT_PORT = 7437;
|
|
264
303
|
export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
|
|
265
|
-
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL"];
|
|
304
|
+
const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL", "ENGRAM_SERVER_MODE"];
|
|
266
305
|
const HEX32 = /^[a-f0-9]{32}$/;
|
|
267
306
|
|
|
268
307
|
|
|
269
|
-
// `
|
|
270
|
-
// `
|
|
308
|
+
// `url`: the shared server, reached explicitly once /compatibility verified it.
|
|
309
|
+
// `spawn`: nothing listens yet; Shell starts the bundled core's server and must
|
|
310
|
+
// see it verified before Pi starts, or memory is disabled for that launch.
|
|
271
311
|
|
|
272
312
|
|
|
273
313
|
// Without an invalid policy or an explicit Engram selection, the default
|
|
@@ -278,26 +318,24 @@ export function engramProbeAllowed(env ) {
|
|
|
278
318
|
}
|
|
279
319
|
|
|
280
320
|
// Evidence only: never run an executable, open SQLite or infer compatibility
|
|
281
|
-
// from versions.
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
// from versions. Like other agents' hooks, Shell shares the global server: a
|
|
322
|
+
// verified listener is reused, and with nothing listening the verified bundled
|
|
323
|
+
// core starts one on Engram's defaults, unless the default store's header
|
|
324
|
+
// (`userVersion`) shows a newer schema than that core's. A listener that does
|
|
325
|
+
// not verify is never replaced. A server version only selects the wording.
|
|
326
|
+
export function classifyEngramStartup(input ) {
|
|
286
327
|
if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
|
|
287
328
|
const endpoint = input.endpoint;
|
|
288
|
-
const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
|
|
289
|
-
const unknown = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
|
|
290
329
|
if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
291
|
-
if (endpoint.kind === "refused"
|
|
292
|
-
|
|
330
|
+
if (endpoint.kind === "refused") {
|
|
331
|
+
if (!input.core) return { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
|
|
332
|
+
if (input.userVersion !== undefined && !engramHeaderAllows(input.userVersion, bundledEngramSchema(input.coreVersion))) {
|
|
333
|
+
return { policy: "disabled", notice: ENGRAM_NEWER_SCHEMA_NOTICE };
|
|
334
|
+
}
|
|
335
|
+
return { policy: "allow", url: ENGRAM_DEFAULT_URL, spawn: true };
|
|
293
336
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
// Shell's own store, created by this exact core: offline, the provider starts
|
|
297
|
-
// that core; a running server must be that store and that exact core.
|
|
298
|
-
if (endpoint.kind === "refused") return { policy: "allow" };
|
|
299
|
-
if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
|
|
300
|
-
return unknown;
|
|
337
|
+
const outdated = endpoint.kind === "listening" ? outdatedServerVersion(endpoint.healthVersion) : undefined;
|
|
338
|
+
return { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
|
|
301
339
|
}
|
|
302
340
|
|
|
303
341
|
// The exact contract a startup-verified store reports (Engram GET /compatibility).
|
|
@@ -357,8 +395,8 @@ export async function probeEngramEndpoint(input
|
|
|
357
395
|
return { kind: "listening", compatible: false, healthInstanceId: identified ? body.instance_id : undefined, healthVersion: identified ? body.version : undefined };
|
|
358
396
|
}
|
|
359
397
|
|
|
360
|
-
// Shell's
|
|
361
|
-
//
|
|
398
|
+
// Shell's record of a store an earlier Shell provisioned with its bundled core,
|
|
399
|
+
// read only by the explicit `gentle-shell memory migrate` recovery.
|
|
362
400
|
|
|
363
401
|
|
|
364
402
|
export function engramProvenancePath(prefix , platform = process.platform) {
|
|
@@ -372,14 +410,10 @@ export function parseEngramProvenance(text )
|
|
|
372
410
|
return { schema: 1, dataDir: record.dataDir, instanceId: record.instanceId, coreSha256: record.coreSha256 };
|
|
373
411
|
}
|
|
374
412
|
|
|
375
|
-
export function engramProvenanceMatches(record , observed ) {
|
|
376
|
-
return record !== undefined && record.dataDir === observed.dataDir && record.instanceId === observed.instanceId && record.coreSha256 === observed.coreSha256;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
413
|
// The bundled core's own schema (SQLite user_version), verified from the
|
|
380
|
-
// tagged sources v3.3.2 and v3.
|
|
414
|
+
// tagged sources v3.3.2, v3.4.0 and v3.5.2: internal/store/store.go `const schemaVersion = 1`.
|
|
381
415
|
// A pin without a verified entry leaves the header check unavailable.
|
|
382
|
-
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1 };
|
|
416
|
+
const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1, "3.5.2": 1 };
|
|
383
417
|
|
|
384
418
|
export function bundledEngramSchema(coreVersion ) {
|
|
385
419
|
return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
|
|
@@ -427,11 +461,6 @@ export function supportsEngramStartupPolicy(metadata )
|
|
|
427
461
|
return pkg?.name === "gentle-engram" && pkg.engram?.startupPolicy === 1;
|
|
428
462
|
}
|
|
429
463
|
|
|
430
|
-
export function supportsEngramSessionOwned(metadata ) {
|
|
431
|
-
const pkg = parseJson(metadata);
|
|
432
|
-
return supportsEngramStartupPolicy(metadata) && pkg?.engram?.sessionOwned === 1;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
464
|
export function bundledChildEnv(base , input ) {
|
|
436
465
|
const env = { ...base, PI_SKIP_VERSION_CHECK: "1", [GENTLE_SHELL_PREFIX_ENV]: input.prefix };
|
|
437
466
|
delete env.GENTLE_SHELL_PI;
|
|
@@ -454,14 +483,7 @@ export function bundledChildEnv(base , input
|
|
|
454
483
|
return env;
|
|
455
484
|
}
|
|
456
485
|
if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
|
|
457
|
-
|
|
458
|
-
env.ENGRAM_BIN = local ?? input.guard;
|
|
459
|
-
if (local && memory.owned) {
|
|
460
|
-
env.ENGRAM_SERVER_MODE = "session-owned";
|
|
461
|
-
delete env.ENGRAM_URL;
|
|
462
|
-
delete env.ENGRAM_PORT;
|
|
463
|
-
delete env.ENGRAM_SOCKET;
|
|
464
|
-
}
|
|
486
|
+
env.ENGRAM_BIN = input.guard;
|
|
465
487
|
return env;
|
|
466
488
|
}
|
|
467
489
|
|
|
@@ -494,6 +516,61 @@ export function withBundledNpmCommand(settingsText , command
|
|
|
494
516
|
|
|
495
517
|
|
|
496
518
|
|
|
519
|
+
// settings.json without `npmCommand`: every other key kept in order and the
|
|
520
|
+
// file's indentation and final newline kept.
|
|
521
|
+
function settingsWithoutNpmCommand(settingsText , settings ) {
|
|
522
|
+
const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1] ?? " ";
|
|
523
|
+
const { npmCommand: _removed, ...rest } = settings;
|
|
524
|
+
const serialized = JSON.stringify(rest, null, indent);
|
|
525
|
+
return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// The agent home's settings.json without a `npmCommand` naming the prefix a
|
|
529
|
+
// bundled self-uninstall removed (gentle-shell-uninstall.ts): one absolute
|
|
530
|
+
// entry inside `withinRemoved` drops the whole key, every other key kept in
|
|
531
|
+
// order and the file's indentation and final newline kept. A file that is not
|
|
532
|
+
// a JSON object is never rewritten; a command naming nothing inside it stays.
|
|
533
|
+
export function withoutBundledNpmCommand(settingsText , withinRemoved ) {
|
|
534
|
+
if (settingsText === undefined) return { status: "unchanged" };
|
|
535
|
+
const settings = parseJson(settingsText);
|
|
536
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
|
|
537
|
+
const current = settings.npmCommand;
|
|
538
|
+
if (!Array.isArray(current) || !current.some((part) => typeof part === "string" && isAbsolute(part) && withinRemoved(part))) return { status: "unchanged" };
|
|
539
|
+
return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// An `npmCommand` a bundled install wrote (scripts/bundled-install.mjs
|
|
543
|
+
// bundledNpmCommand, with npm-cli.js and --prefix in earlier releases): our
|
|
544
|
+
// node in runtime/node-<v>, then npm's settings and cache flags, and
|
|
545
|
+
// --update-notifier=false last. A command the user chose never matches, even
|
|
546
|
+
// when its program is missing.
|
|
547
|
+
export function isBundledNpmCommand(command ) {
|
|
548
|
+
const [node] = command;
|
|
549
|
+
return typeof node === "string" && /[\\/]runtime[\\/]node-[^\\/]+[\\/](node\.exe|bin[\\/]node)$/.test(node)
|
|
550
|
+
&& ["--userconfig=", "--globalconfig=", "--cache="].every((flag) => command.some((part) => typeof part === "string" && part.startsWith(flag)))
|
|
551
|
+
&& command[command.length - 1] === "--update-notifier=false";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// The agent home's settings.json without an `npmCommand` a bundled install
|
|
555
|
+
// wrote whose executable (its first entry) no longer exists, as after a
|
|
556
|
+
// bundled self-uninstall that kept the home (#2469): Pi runs that command for
|
|
557
|
+
// every package install, so a key naming a deleted node.exe fails them all
|
|
558
|
+
// with ENOENT. A command whose executable exists, that names no absolute
|
|
559
|
+
// path, or that the user set themselves is never touched.
|
|
560
|
+
export function withoutMissingBundledNpmCommand(settingsText , exists ) {
|
|
561
|
+
if (settingsText === undefined) return { status: "unchanged" };
|
|
562
|
+
const settings = parseJson(settingsText);
|
|
563
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
|
|
564
|
+
const executable = Array.isArray(settings.npmCommand) ? settings.npmCommand[0] : undefined;
|
|
565
|
+
if (typeof executable !== "string" || !isAbsolute(executable) || exists(executable) || !isBundledNpmCommand(settings.npmCommand )) return { status: "unchanged" };
|
|
566
|
+
return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
497
574
|
|
|
498
575
|
|
|
499
576
|
|
|
@@ -505,7 +582,9 @@ export function withBundledNpmCommand(settingsText , command
|
|
|
505
582
|
// `update`, `--self`, or a `self`/`pi` positional, and "all" for `--all`,
|
|
506
583
|
// `--self --extensions` or `self`/`pi` with `--extensions`. Only Pi's own part
|
|
507
584
|
// is redirected; "all" still sends `update --extensions` (with the other
|
|
508
|
-
// flags) to Pi.
|
|
585
|
+
// flags) to Pi. `--latest` marks Pi's own part as the newest published Pi;
|
|
586
|
+
// with any form that is not Pi's own update it is unknown to Pi, so it passes
|
|
587
|
+
// through. Packages, models, help, and forms Pi refuses (conflicting or
|
|
509
588
|
// unknown options) reach Pi unchanged.
|
|
510
589
|
export function bundledUpdatePlan(passthrough ) {
|
|
511
590
|
const passThrough = { self: false, piArgs: passthrough };
|
|
@@ -514,8 +593,10 @@ export function bundledUpdatePlan(passthrough ) {
|
|
|
514
593
|
let self = false;
|
|
515
594
|
let extensions = false;
|
|
516
595
|
let all = false;
|
|
596
|
+
let latest = false;
|
|
517
597
|
for (const token of passthrough.slice(1)) {
|
|
518
598
|
if (token === "--self") self = true;
|
|
599
|
+
else if (token === "--latest") latest = true;
|
|
519
600
|
else if (token === "--extensions") extensions = true;
|
|
520
601
|
else if (token === "--all") all = true;
|
|
521
602
|
else if (["--approve", "-a", "--no-approve", "-na", "--force"].includes(token)) flags.push(token);
|
|
@@ -527,19 +608,21 @@ export function bundledUpdatePlan(passthrough ) {
|
|
|
527
608
|
if ((positional[0] !== "self" && positional[0] !== "pi") || self) return passThrough;
|
|
528
609
|
self = true;
|
|
529
610
|
}
|
|
530
|
-
|
|
611
|
+
const marked = latest ? { latest: true } : {};
|
|
612
|
+
if (all || (self && extensions)) return { self: true, ...marked, piArgs: ["update", "--extensions", ...flags] };
|
|
531
613
|
if (extensions) return passThrough;
|
|
532
|
-
return { self: true };
|
|
614
|
+
return { self: true, ...marked };
|
|
533
615
|
}
|
|
534
616
|
|
|
535
|
-
// What `gentle-shell update` does after the redirected upgrade
|
|
536
|
-
// is undefined when it did not run): with
|
|
537
|
-
//
|
|
538
|
-
// extensions even when
|
|
617
|
+
// What `gentle-shell update` does after the redirected upgrade or the
|
|
618
|
+
// `--latest` Pi update (`upgradeCode` is undefined when it did not run): with
|
|
619
|
+
// nothing left for Pi it exits with that code; otherwise Pi still runs, so
|
|
620
|
+
// `update --all` updates the extensions even when that step could not complete.
|
|
539
621
|
export function bundledUpdateNext(plan , upgradeCode ) {
|
|
540
622
|
if (plan.piArgs === undefined) return { exitCode: upgradeCode ?? 0 };
|
|
541
623
|
if (plan.self && upgradeCode !== undefined && upgradeCode !== 0) {
|
|
542
|
-
|
|
624
|
+
const step = plan.latest ? "latest Pi update" : "Gentle Shell upgrade";
|
|
625
|
+
return { piArgs: plan.piArgs, note: `gentle-shell: the ${step} did not complete; updating the extensions anyway.` };
|
|
543
626
|
}
|
|
544
627
|
return { piArgs: plan.piArgs };
|
|
545
628
|
}
|
|
@@ -550,6 +633,7 @@ export function bundledUpdateNotice(input
|
|
|
550
633
|
input.interactive
|
|
551
634
|
? "Running 'gentle-shell upgrade'..."
|
|
552
635
|
: "Run 'gentle-shell upgrade' to install the latest Gentle Shell release and its Pi.",
|
|
636
|
+
"Run 'gentle-shell update --latest' to install the newest published Pi now, without the release's verified lockfile.",
|
|
553
637
|
];
|
|
554
638
|
}
|
|
555
639
|
|
|
@@ -1119,12 +1119,17 @@ const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
|
|
|
1119
1119
|
|
|
1120
1120
|
// cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
|
|
1121
1121
|
// token in double quotes when it is empty or contains whitespace or any of
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1122
|
+
// `&|<>^%()`. A token with an inner `"` cannot be represented: cmd.exe has no
|
|
1123
|
+
// backslash escape, so `\"` still ends the quoted section and the rest of the
|
|
1124
|
+
// token is parsed as command syntax (`x" & whoami & rem "` would run
|
|
1125
|
+
// `whoami`). Doubling the quote is not reliable either, so such a token is
|
|
1126
|
+
// refused instead of producing a command line it cannot protect.
|
|
1125
1127
|
export function quoteForCmdExe(token ) {
|
|
1128
|
+
if (token.includes('"')) {
|
|
1129
|
+
throw new Error("cannot pass an argument containing a double quote through cmd.exe: it would end the quoted argument and let the rest run as a command");
|
|
1130
|
+
}
|
|
1126
1131
|
if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
|
|
1127
|
-
return `"${token
|
|
1132
|
+
return `"${token}"`;
|
|
1128
1133
|
}
|
|
1129
1134
|
|
|
1130
1135
|
const POSIX_SHELL_SPECIAL_CHARS = /[\s"'`\\$&|;<>(){}*?[\]!#~]/;
|
|
@@ -1347,6 +1352,8 @@ export function helpText() {
|
|
|
1347
1352
|
" gentle-shell update [target] Run pi's own 'update' against the resolved home.",
|
|
1348
1353
|
" In a bundled install Pi ships pinned: 'update' without a package",
|
|
1349
1354
|
" runs 'gentle-shell upgrade' instead of updating Pi.",
|
|
1355
|
+
" 'update --latest' installs the newest published Pi instead,",
|
|
1356
|
+
" without the release's verified lockfile.",
|
|
1350
1357
|
" gentle-shell config Run pi's own 'config' against the resolved home.",
|
|
1351
1358
|
" gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
|
|
1352
1359
|
" gentle-shell mcp <command> Run pi's own 'mcp' (add, list, login, logout) against the",
|