gentle-pi 4.2.0 → 4.4.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -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 +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- 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/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -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/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- 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-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -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 +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- 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/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- 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 +485 -7
- 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 +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- 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/destructive-command-guard.test.ts +85 -1
- 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 +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- 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-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- 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 +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- 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 +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -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/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- 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 +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
+
import { chmodSync, closeSync, linkSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { detectJsonFormatting } from "./gentle-shell-launcher.mjs";
|
|
5
|
+
|
|
6
|
+
// `gentle-shell home import-pi`: a one-time, opt-in copy of the user's pi
|
|
7
|
+
// sign-ins and MCP servers into a separate Gentle Shell home, as an
|
|
8
|
+
// alternative to --link. Only the files and settings keys below are read;
|
|
9
|
+
// packages, extensions, caches, AGENTS.md and sessions are never copied, so
|
|
10
|
+
// the target home keeps its own provisioning. Nothing is ever overwritten and
|
|
11
|
+
// the source is only read.
|
|
12
|
+
|
|
13
|
+
// auth.json holds OAuth tokens and API keys: always private in the copy.
|
|
14
|
+
const IMPORTED_FILES = [
|
|
15
|
+
{ name: "auth.json", mode: 0o600 },
|
|
16
|
+
{ name: "mcp.json" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Pi 1.2.0's own settings for the default model (SettingsManager
|
|
20
|
+
// get/setDefaultProvider, DefaultModel, DefaultThinkingLevel).
|
|
21
|
+
export const IMPORTED_SETTINGS_KEYS = ["defaultProvider", "defaultModel", "defaultThinkingLevel"];
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
function tempPathFor(path ) {
|
|
49
|
+
return join(dirname(path), `.${basename(path)}.gentle-shell-${process.pid}.tmp`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeQuietly(path ) {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(path);
|
|
55
|
+
} catch {
|
|
56
|
+
// Nothing left to clean up.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeTemp(path , data , mode ) {
|
|
61
|
+
const temp = tempPathFor(path);
|
|
62
|
+
const fd = openSync(temp, "wx", mode);
|
|
63
|
+
try {
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(fd, data);
|
|
66
|
+
} finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
chmodSync(temp, mode); // the umask must not change the mode
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// A partial copy may hold credentials: never leave it behind.
|
|
72
|
+
removeQuietly(temp);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return temp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const nodeImportPiFs = {
|
|
79
|
+
kind: (path) => {
|
|
80
|
+
try {
|
|
81
|
+
const stat = lstatSync(path);
|
|
82
|
+
if (stat.isSymbolicLink()) return "other";
|
|
83
|
+
return stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error ).code === "ENOENT") return "missing";
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
mode: (path) => lstatSync(path).mode & 0o777,
|
|
90
|
+
readText: (path) => readFileSync(path, "utf8"),
|
|
91
|
+
copyNew: (from, to, mode) => {
|
|
92
|
+
const temp = writeTemp(to, readFileSync(from), mode);
|
|
93
|
+
try {
|
|
94
|
+
linkSync(temp, to); // EEXIST instead of replacing a file that appeared meanwhile
|
|
95
|
+
} finally {
|
|
96
|
+
removeQuietly(temp);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
writeAtomic: (path, text) => {
|
|
100
|
+
const mode = lstatSync(path, { throwIfNoEntry: false })?.mode;
|
|
101
|
+
const temp = writeTemp(path, text, mode === undefined ? 0o600 : mode & 0o777);
|
|
102
|
+
try {
|
|
103
|
+
renameSync(temp, path);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
removeQuietly(temp);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function reason(error ) {
|
|
112
|
+
const code = (error )?.code;
|
|
113
|
+
return typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseObject(text ) {
|
|
117
|
+
try {
|
|
118
|
+
const value = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
119
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value ) : undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function importFile(name , mode , input ) {
|
|
126
|
+
const { fs } = input;
|
|
127
|
+
const from = join(input.source, name);
|
|
128
|
+
const to = join(input.target, name);
|
|
129
|
+
// Classification can throw too (ENOTDIR under a file): one failed item, never an aborted report.
|
|
130
|
+
try {
|
|
131
|
+
const kind = fs.kind(from);
|
|
132
|
+
if (kind === "missing") return { item: name, status: "not found" };
|
|
133
|
+
if (kind !== "file") return { item: name, status: "skipped", detail: "not a regular file" };
|
|
134
|
+
if (fs.kind(to) !== "missing") return { item: name, status: "present" };
|
|
135
|
+
const copyMode = mode ?? fs.mode(from);
|
|
136
|
+
const detail = `${copyMode.toString(8).padStart(4, "0")}`;
|
|
137
|
+
if (input.dryRun) return { item: name, status: "would copy", detail };
|
|
138
|
+
fs.copyNew(from, to, copyMode);
|
|
139
|
+
return { item: name, status: "copied", detail };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { item: name, status: "failed", detail: reason(error) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function importSettings(input ) {
|
|
146
|
+
try {
|
|
147
|
+
return planAndWriteSettings(input);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// An unreadable settings.json stays one failed item, never an aborted report.
|
|
150
|
+
return [{ item: "settings.json", status: "failed", detail: reason(error) }];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function planAndWriteSettings(input ) {
|
|
155
|
+
const { fs } = input;
|
|
156
|
+
const from = join(input.source, "settings.json");
|
|
157
|
+
const to = join(input.target, "settings.json");
|
|
158
|
+
const sourceKind = fs.kind(from);
|
|
159
|
+
if (sourceKind === "missing") return [{ item: "settings.json", status: "not found" }];
|
|
160
|
+
if (sourceKind !== "file") return [{ item: "settings.json", status: "skipped", detail: "not a regular file" }];
|
|
161
|
+
const source = parseObject(fs.readText(from));
|
|
162
|
+
if (source === undefined) return [{ item: "settings.json", status: "skipped", detail: `${from} is not a JSON object` }];
|
|
163
|
+
const targetKind = fs.kind(to);
|
|
164
|
+
if (targetKind !== "missing" && targetKind !== "file") return [{ item: "settings.json", status: "skipped", detail: `${to} is not a regular file` }];
|
|
165
|
+
const targetText = targetKind === "missing" ? undefined : fs.readText(to);
|
|
166
|
+
const target = targetText === undefined ? {} : parseObject(targetText);
|
|
167
|
+
if (target === undefined) return [{ item: "settings.json", status: "skipped", detail: `${to} is not a JSON object` }];
|
|
168
|
+
|
|
169
|
+
const items = [];
|
|
170
|
+
const added = {};
|
|
171
|
+
for (const key of IMPORTED_SETTINGS_KEYS) {
|
|
172
|
+
const item = `settings.json ${key}`;
|
|
173
|
+
if (!Object.hasOwn(source, key)) items.push({ item, status: "not found" });
|
|
174
|
+
else if (Object.hasOwn(target, key)) items.push({ item, status: "present" });
|
|
175
|
+
else if (typeof source[key] !== "string") items.push({ item, status: "skipped", detail: "not a string" });
|
|
176
|
+
else {
|
|
177
|
+
added[key] = source[key];
|
|
178
|
+
items.push({ item, status: input.dryRun ? "would copy" : "copied" });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (input.dryRun || Object.keys(added).length === 0) return items;
|
|
182
|
+
try {
|
|
183
|
+
// Keep the target's own indentation and trailing-newline style.
|
|
184
|
+
const style = targetText === undefined ? { indent: undefined, trailingNewline: true } : detectJsonFormatting(targetText);
|
|
185
|
+
const body = JSON.stringify({ ...target, ...added }, null, style.indent ?? 2);
|
|
186
|
+
fs.writeAtomic(to, style.trailingNewline ? `${body}\n` : body);
|
|
187
|
+
return items;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const detail = reason(error);
|
|
190
|
+
return items.map((entry) => (entry.status === "copied" ? { ...entry, status: "failed", detail } : entry));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Copies what the target lacks. A dry run reads the same files and writes
|
|
195
|
+
// nothing. The caller has already made sure `target` is a home this launcher
|
|
196
|
+
// owns (and exists, unless this is a dry run).
|
|
197
|
+
export function importPi(input ) {
|
|
198
|
+
const options = { ...input, fs: input.fs ?? nodeImportPiFs };
|
|
199
|
+
const items = IMPORTED_FILES.map((file) => importFile(file.name, file.mode, options));
|
|
200
|
+
items.push(...importSettings(options));
|
|
201
|
+
return { items, failed: items.some((item) => item.status === "failed") };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function describe(item ) {
|
|
205
|
+
switch (item.status) {
|
|
206
|
+
case "copied":
|
|
207
|
+
case "would copy":
|
|
208
|
+
return item.detail !== undefined && item.item === "auth.json" ? `${item.status} (${item.detail})` : item.status;
|
|
209
|
+
case "present":
|
|
210
|
+
return "already present, kept";
|
|
211
|
+
case "not found":
|
|
212
|
+
return "not found";
|
|
213
|
+
case "skipped":
|
|
214
|
+
return `skipped: ${item.detail}`;
|
|
215
|
+
case "failed":
|
|
216
|
+
return `failed: ${item.detail}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function formatImportPiReport(result , context ) {
|
|
221
|
+
const lines = [
|
|
222
|
+
context.dryRun
|
|
223
|
+
? `Dry run: would import from ${context.source} into ${context.target} (nothing is written)`
|
|
224
|
+
: `Importing from ${context.source} into ${context.target}`,
|
|
225
|
+
];
|
|
226
|
+
for (const item of result.items) lines.push(` ${item.item}: ${describe(item)}`);
|
|
227
|
+
const count = (...statuses ) => result.items.filter((item) => statuses.includes(item.status)).length;
|
|
228
|
+
const imported = count("copied", "would copy");
|
|
229
|
+
const rest = `${count("present")} already present, ${count("not found")} not found, ${count("skipped")} skipped`;
|
|
230
|
+
if (result.failed) lines.push(`Import failed for ${count("failed")} item(s); ${count("copied")} copied and kept. ${rest}.`);
|
|
231
|
+
else if (imported === 0) lines.push(`Nothing to import: ${rest}.`);
|
|
232
|
+
else lines.push(`${context.dryRun ? "Would import" : "Imported"} ${imported} item(s); ${rest}.`);
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
-
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection, } from "./child-package-injection.mjs";
|
|
4
|
+
import { GENTLE_SHELL_CONFIG_HOME_ENV } from "./gentle-shell-bundled.mjs";
|
|
4
5
|
|
|
5
6
|
// The gentle-shell launcher: pure, side-effect-free functions over injected
|
|
6
7
|
// env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
|
|
@@ -11,7 +12,7 @@ import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection,
|
|
|
11
12
|
// pi's own package-management subcommands (see pi's cli/args.ts printHelp
|
|
12
13
|
// "Commands" list): each is dispatched by pi itself, before pi's own flag
|
|
13
14
|
// parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
|
|
14
|
-
export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] ;
|
|
15
|
+
export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth", "mcp"] ;
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
|
|
@@ -42,7 +43,7 @@ function isPiSubcommand(token ) {
|
|
|
42
43
|
// untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
|
|
43
44
|
// `self-uninstall` is not `uninstall`, which stays pi's own alias for `remove`.
|
|
44
45
|
export function parseLauncherArgs(argv ) {
|
|
45
|
-
if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall") {
|
|
46
|
+
if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall" || argv[0] === "memory") {
|
|
46
47
|
return {
|
|
47
48
|
link: false,
|
|
48
49
|
isolated: false,
|
|
@@ -77,6 +78,9 @@ export function parseLauncherArgs(argv ) {
|
|
|
77
78
|
if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
|
|
78
79
|
piSubcommand = rest[0];
|
|
79
80
|
}
|
|
81
|
+
// After a pi subcommand the separator belongs to it, e.g.
|
|
82
|
+
// `mcp add <server> -- <command>`; only a leading one is the launcher's.
|
|
83
|
+
if (piSubcommand !== undefined && passthrough.length > 0) passthrough.push(arg);
|
|
80
84
|
passthrough.push(...rest);
|
|
81
85
|
break;
|
|
82
86
|
}
|
|
@@ -148,6 +152,14 @@ export function parseLauncherArgs(argv ) {
|
|
|
148
152
|
commandArgs = argv.slice(i + 1);
|
|
149
153
|
break;
|
|
150
154
|
}
|
|
155
|
+
// `home import-pi` likewise takes the home selectors ahead of it, so it
|
|
156
|
+
// imports into the home they resolve to; any other `home` after a
|
|
157
|
+
// selector stays pi's.
|
|
158
|
+
if (arg === "home" && argv[i + 1] === "import-pi" && command === undefined && passthrough.length === 0) {
|
|
159
|
+
command = "home";
|
|
160
|
+
commandArgs = argv.slice(i + 1);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
151
163
|
if (passthrough.length === 0 && isPiSubcommand(arg)) {
|
|
152
164
|
piSubcommand = arg;
|
|
153
165
|
}
|
|
@@ -212,6 +224,77 @@ export function isolatedDir(env , homedir
|
|
|
212
224
|
return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
|
|
213
225
|
}
|
|
214
226
|
|
|
227
|
+
// #2201: the isolated home keeps Gentle Shell's config in a gentle-ai folder
|
|
228
|
+
// next to its agent dir (~/.gentle-shell/gentle-ai by default), given to Pi as
|
|
229
|
+
// GENTLE_SHELL_CONFIG_HOME_ENV. `--link` and a custom `--home` keep the shared
|
|
230
|
+
// ~/.pi/gentle-ai: undefined for them.
|
|
231
|
+
export function standaloneConfigHome(home ) {
|
|
232
|
+
if (home.mode !== "isolated") return undefined;
|
|
233
|
+
const dir = resolvePath(home.dir);
|
|
234
|
+
const parent = dirname(dir);
|
|
235
|
+
// Only inside a .gentle-shell folder is the sibling Gentle Shell's own: next
|
|
236
|
+
// to a custom GENTLE_SHELL_HOME (~/myagent) it would adopt ~/gentle-ai, which
|
|
237
|
+
// may be anyone's, so such homes keep the shared config home.
|
|
238
|
+
return basename(dir) === "agent" && basename(parent) === ".gentle-shell" ? join(parent, "gentle-ai") : undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The filesystem behind migrateStandaloneConfig; bin/gentle-shell.mjs wires the real one.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
// Install-level main-channel state stays in ~/.pi/gentle-ai: the installer and
|
|
254
|
+
// `gentle-shell upgrade` write it outside any session (gentleAiDevBinaryRegistrationPath).
|
|
255
|
+
const UNMIGRATED_CONFIG_FILES = new Set(["dev-binary.json", "channel.json"]);
|
|
256
|
+
|
|
257
|
+
// #2201: the first isolated launch after the move keeps the user's settings.
|
|
258
|
+
// Only when `target` does not exist yet: it is created (0700), and the regular
|
|
259
|
+
// files of `source` are copied into it, never overwriting and never changing
|
|
260
|
+
// `source`, which stays for Pi; symbolic links, subdirectories and the
|
|
261
|
+
// main-channel files are skipped. Once the folder exists this does nothing, so
|
|
262
|
+
// it is a one-time snapshot. A failure is reported in `lines`, never thrown:
|
|
263
|
+
// it never blocks the launch.
|
|
264
|
+
export function migrateStandaloneConfig(input ) {
|
|
265
|
+
const { source, target, fs } = input;
|
|
266
|
+
if (fs.kind(target) !== "missing") return { lines: [] };
|
|
267
|
+
const reason = (error ) => (error instanceof Error ? error.message : String(error));
|
|
268
|
+
try {
|
|
269
|
+
fs.mkdirPrivate(target);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return { lines: [`gentle-shell: could not create ${target} for your Gentle Shell settings (${reason(error)}); continuing`] };
|
|
272
|
+
}
|
|
273
|
+
if (fs.kind(source) === "missing") return { lines: [] };
|
|
274
|
+
let names ;
|
|
275
|
+
try {
|
|
276
|
+
names = fs.list(source).sort();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return { lines: [`gentle-shell: could not copy your settings from ${source} to ${target} (${reason(error)}); continuing`] };
|
|
279
|
+
}
|
|
280
|
+
const copied = [];
|
|
281
|
+
const failed = [];
|
|
282
|
+
for (const name of names) {
|
|
283
|
+
if (UNMIGRATED_CONFIG_FILES.has(name) || fs.kind(join(source, name)) !== "file") continue;
|
|
284
|
+
try {
|
|
285
|
+
fs.copyNew(join(source, name), join(target, name));
|
|
286
|
+
copied.push(name);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
failed.push(`${name}: ${reason(error)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (failed.length > 0) {
|
|
292
|
+
return { lines: [`gentle-shell: could not copy ${failed.join("; ")} from ${source} to ${target}; continuing with ${copied.length} copied file(s)`] };
|
|
293
|
+
}
|
|
294
|
+
if (copied.length === 0) return { lines: [] };
|
|
295
|
+
return { lines: [`gentle-shell: copied ${copied.length} settings file(s) from ${source} to ${target}, Gentle Shell's own config folder from now on (one-time copy; ${source} stays for Pi)`] };
|
|
296
|
+
}
|
|
297
|
+
|
|
215
298
|
export function resolveHome(input ) {
|
|
216
299
|
const { args, env, homedir, config } = input;
|
|
217
300
|
|
|
@@ -242,6 +325,15 @@ export function homeSelectorFlags(home ) {
|
|
|
242
325
|
return [];
|
|
243
326
|
}
|
|
244
327
|
|
|
328
|
+
// The first-run line of a separate home. `home import-pi` is offered only
|
|
329
|
+
// when the user's pi home has sign-ins (auth.json) to copy.
|
|
330
|
+
export function separateHomeHint(home , input ) {
|
|
331
|
+
const base = `gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats`;
|
|
332
|
+
if (!input.piSignIns) return `${base}.`;
|
|
333
|
+
const command = ["gentle-shell", ...homeSelectorFlags(home).map(shellQuote), "home", "import-pi"].join(" ");
|
|
334
|
+
return `${base}, or '${command}' to copy your sign-ins and MCP servers into this home.`;
|
|
335
|
+
}
|
|
336
|
+
|
|
245
337
|
export function launcherConfigPath(homedir ) {
|
|
246
338
|
return join(homedir, ".gentle-shell", "config.json");
|
|
247
339
|
}
|
|
@@ -576,6 +668,20 @@ const POST_INSTALL_REMOVAL_PACKAGES
|
|
|
576
668
|
// bin/gentle-shell.mjs.
|
|
577
669
|
export const POST_INSTALL_REMOVAL_SOURCES = POST_INSTALL_REMOVAL_PACKAGES.map((entry) => entry.source);
|
|
578
670
|
|
|
671
|
+
// #2143: a Gentle AI with package exclusion (gentle-ai#5503) skips these
|
|
672
|
+
// managed Pi packages instead of installing them only for the post-install
|
|
673
|
+
// removal above to delete them. An older Gentle AI ignores the variable (a flag
|
|
674
|
+
// would be rejected), so the removal stays as the fallback. Only sources Gentle
|
|
675
|
+
// AI manages belong here: it rejects any other value.
|
|
676
|
+
export const GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV = "GENTLE_AI_PI_EXCLUDE_PACKAGES";
|
|
677
|
+
const LAUNCHER_EXCLUDED_PI_PACKAGES = ["npm:gentle-pi"];
|
|
678
|
+
|
|
679
|
+
export function withLauncherPackagesExcluded(env ) {
|
|
680
|
+
const own = (env[GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
681
|
+
const merged = [...own, ...LAUNCHER_EXCLUDED_PI_PACKAGES.filter((source) => !own.includes(source))];
|
|
682
|
+
return { ...env, [GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV]: merged.join(",") };
|
|
683
|
+
}
|
|
684
|
+
|
|
579
685
|
// Scans a settings.json `packages` list (same string/object-source parsing
|
|
580
686
|
// as settingsDeclareGentlePi/findGentlePiDeclaration above) for any entry
|
|
581
687
|
// whose npm package name matches POST_INSTALL_REMOVAL_PACKAGES, at any
|
|
@@ -891,13 +997,16 @@ export function discoverLooseExtensionEntries(dir , fs )
|
|
|
891
997
|
|
|
892
998
|
|
|
893
999
|
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
|
|
894
1003
|
|
|
895
1004
|
|
|
896
1005
|
|
|
897
1006
|
|
|
898
1007
|
// Four cases, checked in this order — `piSubcommand` first, then `takeOver`:
|
|
899
1008
|
// - piSubcommand: pi dispatches install/remove/uninstall/update/list/
|
|
900
|
-
// config/auth on argv[0] before it even parses flags, so any injected
|
|
1009
|
+
// config/auth/mcp on argv[0] before it even parses flags, so any injected
|
|
901
1010
|
// -e flag ahead of it stops pi from
|
|
902
1011
|
// recognising its subcommand at all — this is exactly the observed
|
|
903
1012
|
// 2026-09-22 bug where `gentle-shell install npm:x` opened an
|
|
@@ -978,6 +1087,12 @@ export function buildPiInvocation(input ) {
|
|
|
978
1087
|
GENTLE_PI_AGENT_HOME: input.home.dir,
|
|
979
1088
|
[USER_PI_HOME_ENV]: userPiHome(input.baseEnv, input.homedir),
|
|
980
1089
|
};
|
|
1090
|
+
// #2201: only the standalone isolated home gets its own config folder; any
|
|
1091
|
+
// other launch removes an inherited marker, so a nested `--link` launch from
|
|
1092
|
+
// an isolated session reads ~/.pi/gentle-ai again.
|
|
1093
|
+
const configHome = input.bundled === true ? undefined : standaloneConfigHome(input.home);
|
|
1094
|
+
if (configHome === undefined) delete env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
1095
|
+
else env[GENTLE_SHELL_CONFIG_HOME_ENV] = configHome;
|
|
981
1096
|
if (childInjection === undefined) delete env[CHILD_PACKAGE_INJECTION_ENV];
|
|
982
1097
|
else env[CHILD_PACKAGE_INJECTION_ENV] = encodeChildPackageInjection(childInjection);
|
|
983
1098
|
|
|
@@ -1004,12 +1119,17 @@ const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
|
|
|
1004
1119
|
|
|
1005
1120
|
// cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
|
|
1006
1121
|
// token in double quotes when it is empty or contains whitespace or any of
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
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.
|
|
1010
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
|
+
}
|
|
1011
1131
|
if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
|
|
1012
|
-
return `"${token
|
|
1132
|
+
return `"${token}"`;
|
|
1013
1133
|
}
|
|
1014
1134
|
|
|
1015
1135
|
const POSIX_SHELL_SPECIAL_CHARS = /[\s"'`\\$&|;<>(){}*?[\]!#~]/;
|
|
@@ -1053,7 +1173,7 @@ export function planSpawn(input ) {
|
|
|
1053
1173
|
// formatting as practical instead of imposing its own. `indent` is
|
|
1054
1174
|
// `undefined` for compact (no-whitespace) JSON, matching what
|
|
1055
1175
|
// `JSON.stringify(value)` (no third argument) produces.
|
|
1056
|
-
function detectJsonFormatting(text ) {
|
|
1176
|
+
export function detectJsonFormatting(text ) {
|
|
1057
1177
|
const match = text.match(/\{\r?\n([ \t]+)/);
|
|
1058
1178
|
return { indent: match ? match[1] : undefined, trailingNewline: text.endsWith("\n") };
|
|
1059
1179
|
}
|
|
@@ -1175,9 +1295,11 @@ export function helpText() {
|
|
|
1175
1295
|
return [
|
|
1176
1296
|
"Usage: gentle-shell [options] [-- pi-args...]",
|
|
1177
1297
|
" gentle-shell home [link|isolated|<path>]",
|
|
1298
|
+
" gentle-shell [home selectors] home import-pi [--dry-run]",
|
|
1178
1299
|
" gentle-shell [home selectors] setup [--dry-run]",
|
|
1179
1300
|
" gentle-shell upgrade [--channel release|main]",
|
|
1180
1301
|
" gentle-shell upgrade --rollback",
|
|
1302
|
+
" gentle-shell memory migrate --confirm-quiescent",
|
|
1181
1303
|
" gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]",
|
|
1182
1304
|
"",
|
|
1183
1305
|
"Opens pi with the Gentle Shell package loaded, without touching your",
|
|
@@ -1185,7 +1307,8 @@ export function helpText() {
|
|
|
1185
1307
|
"",
|
|
1186
1308
|
"Options:",
|
|
1187
1309
|
" --link Use your existing pi agent home (never edits its settings.json).",
|
|
1188
|
-
" --isolated Use the dedicated ~/.gentle-shell/agent home (default)
|
|
1310
|
+
" --isolated Use the dedicated ~/.gentle-shell/agent home (default), with its own",
|
|
1311
|
+
" settings in ~/.gentle-shell/gentle-ai (copied once from ~/.pi/gentle-ai).",
|
|
1189
1312
|
" --home <path> Use a custom agent home directory.",
|
|
1190
1313
|
" --package-root <dir> Force this directory as the gentle-pi package to load, taking over",
|
|
1191
1314
|
" from any conflicting package the target settings.json already declares.",
|
|
@@ -1194,6 +1317,11 @@ export function helpText() {
|
|
|
1194
1317
|
"",
|
|
1195
1318
|
"Commands:",
|
|
1196
1319
|
" home Print or persist the effective home mode (link, isolated, or a path).",
|
|
1320
|
+
" home import-pi Copy your pi sign-ins (auth.json) and MCP servers (mcp.json) into the",
|
|
1321
|
+
" resolved separate home, plus your default provider, model and thinking",
|
|
1322
|
+
" level when it has none. Never overwrites a file or setting, never copies",
|
|
1323
|
+
" packages, extensions or caches, and never changes your pi home.",
|
|
1324
|
+
" --dry-run only shows the plan. Refuses --link (nothing to import).",
|
|
1197
1325
|
" setup Provision the resolved home with the gentle-ai companion packages",
|
|
1198
1326
|
" (runs the package-local gentle-ai 'install --agent pi --scope global').",
|
|
1199
1327
|
" Accepts --dry-run, forwarded to gentle-ai. Accepts a home selector",
|
|
@@ -1205,9 +1333,15 @@ export function helpText() {
|
|
|
1205
1333
|
" installer's own Node, pnpm and Pi) follows the release channel only:",
|
|
1206
1334
|
" it installs the new version beside the current one, verifies it, then",
|
|
1207
1335
|
" switches; --rollback switches back to the previous kept version.",
|
|
1336
|
+
" memory migrate Explicit bundled-store recovery using the verified core's migrate-copy.",
|
|
1337
|
+
" Requires --confirm-quiescent: stop ALL Engram clients first. Keeps the",
|
|
1338
|
+
" original directory as a backup; never runs automatically on upgrade.",
|
|
1339
|
+
" Requires a bundled core release supporting migrate-copy; older cores",
|
|
1340
|
+
" fail closed without changing the source or its provenance.",
|
|
1208
1341
|
" self-uninstall Remove Gentle Shell: its own data, the isolated home and the gentle-pi",
|
|
1209
|
-
" package
|
|
1210
|
-
" the question). The shared
|
|
1342
|
+
" package, with its settings in ~/.gentle-shell/gentle-ai. Shows the plan",
|
|
1343
|
+
" first and asks before removing (--yes skips the question). The shared",
|
|
1344
|
+
" ~/.pi/gentle-ai configuration is kept unless",
|
|
1211
1345
|
" you accept or pass --include-shared; --dry-run only shows the plan.",
|
|
1212
1346
|
" Never touches your pi home or custom --home homes.",
|
|
1213
1347
|
"",
|
|
@@ -1220,6 +1354,8 @@ export function helpText() {
|
|
|
1220
1354
|
" runs 'gentle-shell upgrade' instead of updating Pi.",
|
|
1221
1355
|
" gentle-shell config Run pi's own 'config' against the resolved home.",
|
|
1222
1356
|
" gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
|
|
1357
|
+
" gentle-shell mcp <command> Run pi's own 'mcp' (add, list, login, logout) against the",
|
|
1358
|
+
" resolved home, e.g. gentle-shell mcp add <name> -- <command>.",
|
|
1223
1359
|
" 'gentle-shell uninstall <source>' is pi's own alias for 'remove'; to remove Gentle",
|
|
1224
1360
|
" Shell itself, use 'gentle-shell self-uninstall'.",
|
|
1225
1361
|
" These run pi's own commands, forwarded verbatim, against the --isolated home",
|
|
@@ -1234,7 +1370,10 @@ export function helpText() {
|
|
|
1234
1370
|
"",
|
|
1235
1371
|
"Environment variables:",
|
|
1236
1372
|
" GENTLE_SHELL_PI Path to the pi executable to run.",
|
|
1237
|
-
" GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent)
|
|
1373
|
+
" GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent);",
|
|
1374
|
+
" inside a .gentle-shell folder, its settings live in the gentle-ai",
|
|
1375
|
+
" folder next to it; elsewhere they stay in ~/.pi/gentle-ai.",
|
|
1376
|
+
" GENTLE_PI_CONFIG_HOME Gentle Shell settings folder for every home, overriding the above.",
|
|
1238
1377
|
" PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
|
|
1239
1378
|
"",
|
|
1240
1379
|
"Every other argument is forwarded to pi unchanged.",
|
|
@@ -21,6 +21,30 @@
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** The launcher config records the last live lookup so the everyday launch stays off the network. */
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/** Reads the config's `startupReleaseCheck` key; anything malformed is no cache. */
|
|
39
|
+
export function parseStartupReleaseCache(value ) {
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
41
|
+
const { checkedAt, latest } = value ;
|
|
42
|
+
const ms = typeof checkedAt === "number" ? checkedAt : typeof checkedAt === "string" ? Date.parse(checkedAt) : Number.NaN;
|
|
43
|
+
if (!Number.isFinite(ms) || typeof latest !== "string" || latest === "") return undefined;
|
|
44
|
+
return { checkedAt: ms, latest };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const STARTUP_RELEASE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
24
48
|
|
|
25
49
|
export function shouldCheckStartupUpdate({ args, env, bundled, inputTTY, outputTTY } ) {
|
|
26
50
|
if (!bundled || !inputTTY || !outputTTY || args.command || args.piSubcommand || args.error || args.help || args.version || args.packageRoot !== undefined) {
|
|
@@ -62,9 +86,30 @@ function isNewer(latest , current ) {
|
|
|
62
86
|
return false;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
|
|
89
|
+
function offerFor(current , latest , skipped ) {
|
|
66
90
|
const currentParts = stableParts(current);
|
|
67
91
|
if (!currentParts) return undefined;
|
|
92
|
+
if (latest === undefined || latest === skipped) return undefined;
|
|
93
|
+
const latestParts = stableParts(latest);
|
|
94
|
+
if (!latestParts || !isNewer(latestParts, currentParts)) return undefined;
|
|
95
|
+
return {
|
|
96
|
+
current,
|
|
97
|
+
latest,
|
|
98
|
+
notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A malformed cached latest is never trusted — it must not suppress the
|
|
103
|
+
// network check for a whole TTL window — so it counts as no cache at all.
|
|
104
|
+
function usableCache(cache , ttlMs , nowMs ) {
|
|
105
|
+
if (cache === undefined) return undefined;
|
|
106
|
+
if (!Number.isFinite(cache.checkedAt) || typeof cache.latest !== "string" || cache.latest === "" || stableParts(cache.latest) === undefined) return undefined;
|
|
107
|
+
return nowMs - cache.checkedAt < ttlMs ? cache : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function findStartupUpdate({ current, skipped, lookup, timeoutMs = 1500, cache, nowMs = Date.now(), ttlMs = STARTUP_RELEASE_TTL_MS, cacheWrite } ) {
|
|
111
|
+
const cached = usableCache(cache, ttlMs, nowMs);
|
|
112
|
+
if (cached !== undefined) return offerFor(current, cached.latest, skipped);
|
|
68
113
|
const controller = new AbortController();
|
|
69
114
|
const deadline = Number.isFinite(timeoutMs) && timeoutMs >= 0 && timeoutMs <= 2_147_483_647 ? timeoutMs : 1500;
|
|
70
115
|
let timer ;
|
|
@@ -77,17 +122,24 @@ export async function findStartupUpdate({ current, skipped, lookup, timeoutMs =
|
|
|
77
122
|
});
|
|
78
123
|
// The race bounds startup even if the injected lookup ignores cancellation.
|
|
79
124
|
const latest = await Promise.race([lookup(controller.signal), timeout]);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
84
|
-
current,
|
|
85
|
-
latest,
|
|
86
|
-
notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
|
|
87
|
-
};
|
|
125
|
+
// Only a real answer is worth a day's trust: a timeout or rejection must
|
|
126
|
+
// not suppress tomorrow's check.
|
|
127
|
+
if (typeof latest === "string") cacheWrite?.(latest);
|
|
128
|
+
return offerFor(current, latest, skipped);
|
|
88
129
|
} catch {
|
|
89
130
|
return undefined;
|
|
90
131
|
} finally {
|
|
91
132
|
clearTimeout(timer);
|
|
92
133
|
}
|
|
93
134
|
}
|
|
135
|
+
|
|
136
|
+
// Mirrors omitStartupVersion: preserves every other config key, fails loudly on
|
|
137
|
+
// an invalid config, and records the lookup's answer with an injected clock.
|
|
138
|
+
export function writeStartupReleaseCache(read , write , version , nowMs = Date.now()) {
|
|
139
|
+
const text = read();
|
|
140
|
+
const config = text === undefined ? {} : JSON.parse(text);
|
|
141
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) throw new Error("Invalid launcher config");
|
|
142
|
+
const cache = { checkedAt: nowMs, latest: version };
|
|
143
|
+
const serialized = JSON.stringify({ ...config, startupReleaseCheck: { checkedAt: new Date(cache.checkedAt).toISOString(), latest: cache.latest } }, null, 2);
|
|
144
|
+
write(`${serialized}\n`);
|
|
145
|
+
}
|