@reticlehq/server 2.12.0 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +68 -0
- package/dist/setup/dev-server-wait.js +67 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +203 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying out an agent-config plan.
|
|
3
|
+
*
|
|
4
|
+
* The plan decides; this writes. Kept apart because the deciding is where the judgement is — which
|
|
5
|
+
* paths are evidenced, which formats are safe to touch — and it is worth testing against a pretend
|
|
6
|
+
* filesystem rather than a real one.
|
|
7
|
+
*
|
|
8
|
+
* One rule governs everything here: only our key is ever written. The rest of somebody's config is
|
|
9
|
+
* copied through untouched, and a file we cannot parse is left exactly as it is. A config file is
|
|
10
|
+
* not ours, and the cost of being wrong about that is not a failed install but a broken editor.
|
|
11
|
+
*/
|
|
12
|
+
import { AgentAction, type AgentPlanStep, type PlatformPaths } from './agent-configs.js';
|
|
13
|
+
/** The filesystem, injected so the writer is testable without one. */
|
|
14
|
+
export interface AgentWriterIo {
|
|
15
|
+
readonly exists: (path: string) => boolean;
|
|
16
|
+
readonly readFile: (path: string) => string;
|
|
17
|
+
readonly writeFile: (path: string, contents: string) => void;
|
|
18
|
+
readonly mkdirp: (dir: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface AgentWriteResult {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly file: string;
|
|
24
|
+
/** What actually happened, which is not always what was planned. */
|
|
25
|
+
readonly action: AgentAction | 'created' | 'merged';
|
|
26
|
+
readonly why: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Write what the plan says to write, and nothing else.
|
|
30
|
+
*
|
|
31
|
+
* A step that fails becomes a `manual` result rather than an exception: one unwritable config on a
|
|
32
|
+
* machine with a dozen agents is not a reason to abandon the other eleven, and the user needs to be
|
|
33
|
+
* told which one to do by hand.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyAgentPlan(plan: readonly AgentPlanStep[], io: AgentWriterIo): AgentWriteResult[];
|
|
36
|
+
/** The `/reticle` instruction, for agents that load skills from a directory. */
|
|
37
|
+
export declare const RETICLE_SKILL = "---\nname: reticle\ndescription: Verify a change against the running app, from inside it, before calling it done.\n---\n\nReticle exposes the running app to you as `reticle_*` MCP tools. Look, act, observe, assert.\n\nOnly `reticle_act_and_wait` and `reticle_assert` produce a verdict. Everything else moves or\nreads the app and proves nothing, so a drive that ends without one of those has no result however\nmany tools it used.\n\n`verified: \"unknown\"` is not a pass: it means Reticle could not tell what happened.\n`verified: \"no-fault\"` is not a pass either, because nothing was declared to prove. Never weaken a\ncheck to make it pass.\n\nThe cheapest path that answers the question:\n\n- \"did my edit break anything?\" -> `reticle_run({ tool: \"reticle_verify\", args: { action: \"change\", files: [...] } })`\n- \"does this known journey still work?\" -> `reticle_run({ tool: \"reticle_flow_replay\", args: { flowName: \"...\" } })`\n- \"does this new behaviour work?\" -> `reticle_act_sequence` for the setup, then ONE `reticle_act_and_wait`\n";
|
|
38
|
+
export interface SkillWriteResult {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly file: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write the skill for agents that have somewhere to put it, and only where they are installed.
|
|
44
|
+
*
|
|
45
|
+
* Scaffolding a skills tree for software somebody does not have is litter, and unlike a config file
|
|
46
|
+
* it is not even inert: it is a directory that appears in their home for no reason they can trace.
|
|
47
|
+
*/
|
|
48
|
+
export declare function applyAgentSkills(io: AgentWriterIo, where: {
|
|
49
|
+
readonly home: string;
|
|
50
|
+
readonly platform: keyof PlatformPaths;
|
|
51
|
+
}): SkillWriteResult[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying out an agent-config plan.
|
|
3
|
+
*
|
|
4
|
+
* The plan decides; this writes. Kept apart because the deciding is where the judgement is — which
|
|
5
|
+
* paths are evidenced, which formats are safe to touch — and it is worth testing against a pretend
|
|
6
|
+
* filesystem rather than a real one.
|
|
7
|
+
*
|
|
8
|
+
* One rule governs everything here: only our key is ever written. The rest of somebody's config is
|
|
9
|
+
* copied through untouched, and a file we cannot parse is left exactly as it is. A config file is
|
|
10
|
+
* not ours, and the cost of being wrong about that is not a failed install but a broken editor.
|
|
11
|
+
*/
|
|
12
|
+
import { AgentAction, AGENT_CLIENTS, joinFor, } from './agent-configs.js';
|
|
13
|
+
/** Two spaces, because that is what every one of these files already uses. */
|
|
14
|
+
const INDENT = 2;
|
|
15
|
+
const RETICLE_KEY = 'reticle';
|
|
16
|
+
/** A fresh Continue config. YAML is only ever created, never edited. */
|
|
17
|
+
const CONTINUE_YAML = 'mcpServers:\n - name: reticle\n type: stdio\n command: npx\n args: ["@reticlehq/server", "mcp"]\n';
|
|
18
|
+
const parentOf = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
19
|
+
/**
|
|
20
|
+
* Write what the plan says to write, and nothing else.
|
|
21
|
+
*
|
|
22
|
+
* A step that fails becomes a `manual` result rather than an exception: one unwritable config on a
|
|
23
|
+
* machine with a dozen agents is not a reason to abandon the other eleven, and the user needs to be
|
|
24
|
+
* told which one to do by hand.
|
|
25
|
+
*/
|
|
26
|
+
export function applyAgentPlan(plan, io) {
|
|
27
|
+
return plan.map((step) => {
|
|
28
|
+
const base = { id: step.id, name: step.name, file: step.file, why: step.why };
|
|
29
|
+
if (AgentAction.SKIP === step.action ||
|
|
30
|
+
AgentAction.ALREADY === step.action ||
|
|
31
|
+
AgentAction.MANUAL === step.action) {
|
|
32
|
+
return { ...base, action: step.action };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
io.mkdirp(parentOf(step.file));
|
|
36
|
+
if (AgentAction.CREATE_YAML === step.action) {
|
|
37
|
+
io.writeFile(step.file, CONTINUE_YAML);
|
|
38
|
+
return { ...base, action: 'created' };
|
|
39
|
+
}
|
|
40
|
+
const key = step.key;
|
|
41
|
+
if (undefined === key || undefined === step.entry) {
|
|
42
|
+
return {
|
|
43
|
+
...base,
|
|
44
|
+
action: AgentAction.MANUAL,
|
|
45
|
+
why: 'the plan named no key or entry to write',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Only our key. Everything else in the file is read and written back untouched.
|
|
49
|
+
const current = AgentAction.MERGE === step.action
|
|
50
|
+
? JSON.parse(io.readFile(step.file))
|
|
51
|
+
: {};
|
|
52
|
+
const bucket = current[key];
|
|
53
|
+
const servers = 'object' === typeof bucket && null !== bucket ? bucket : {};
|
|
54
|
+
const next = { ...current, [key]: { ...servers, [RETICLE_KEY]: step.entry } };
|
|
55
|
+
io.writeFile(step.file, `${JSON.stringify(next, null, INDENT)}\n`);
|
|
56
|
+
return { ...base, action: AgentAction.MERGE === step.action ? 'merged' : 'created' };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
action: AgentAction.MANUAL,
|
|
62
|
+
why: `could not write it (${String(err?.message ?? err).slice(0, 120)})`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The `/reticle` instruction, for agents that load skills from a directory. */
|
|
68
|
+
export const RETICLE_SKILL = `---
|
|
69
|
+
name: reticle
|
|
70
|
+
description: Verify a change against the running app, from inside it, before calling it done.
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
Reticle exposes the running app to you as \`reticle_*\` MCP tools. Look, act, observe, assert.
|
|
74
|
+
|
|
75
|
+
Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict. Everything else moves or
|
|
76
|
+
reads the app and proves nothing, so a drive that ends without one of those has no result however
|
|
77
|
+
many tools it used.
|
|
78
|
+
|
|
79
|
+
\`verified: "unknown"\` is not a pass: it means Reticle could not tell what happened.
|
|
80
|
+
\`verified: "no-fault"\` is not a pass either, because nothing was declared to prove. Never weaken a
|
|
81
|
+
check to make it pass.
|
|
82
|
+
|
|
83
|
+
The cheapest path that answers the question:
|
|
84
|
+
|
|
85
|
+
- "did my edit break anything?" -> \`reticle_run({ tool: "reticle_verify", args: { action: "change", files: [...] } })\`
|
|
86
|
+
- "does this known journey still work?" -> \`reticle_run({ tool: "reticle_flow_replay", args: { flowName: "..." } })\`
|
|
87
|
+
- "does this new behaviour work?" -> \`reticle_act_sequence\` for the setup, then ONE \`reticle_act_and_wait\`
|
|
88
|
+
`;
|
|
89
|
+
/**
|
|
90
|
+
* Write the skill for agents that have somewhere to put it, and only where they are installed.
|
|
91
|
+
*
|
|
92
|
+
* Scaffolding a skills tree for software somebody does not have is litter, and unlike a config file
|
|
93
|
+
* it is not even inert: it is a directory that appears in their home for no reason they can trace.
|
|
94
|
+
*/
|
|
95
|
+
export function applyAgentSkills(io, where) {
|
|
96
|
+
const written = [];
|
|
97
|
+
const join = joinFor(where.platform);
|
|
98
|
+
for (const client of AGENT_CLIENTS) {
|
|
99
|
+
const dir = client.skillDir;
|
|
100
|
+
if (undefined === dir)
|
|
101
|
+
continue;
|
|
102
|
+
if (!io.exists(join(where.home, client.marker[where.platform])))
|
|
103
|
+
continue;
|
|
104
|
+
try {
|
|
105
|
+
const target = join(where.home, dir);
|
|
106
|
+
io.mkdirp(target);
|
|
107
|
+
io.writeFile(join(target, 'SKILL.md'), RETICLE_SKILL);
|
|
108
|
+
written.push({ id: client.id, file: join(target, 'SKILL.md') });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* an unwritable home directory is not a reason to fail an install */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return written;
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying the pre-approval rules onto a machine that installed Reticle before they existed.
|
|
3
|
+
*
|
|
4
|
+
* The alternative was a command the user has to know to type. Nothing reaches back into an existing
|
|
5
|
+
* install, so `init --files-only` is a real upgrade path but only for somebody who reads a
|
|
6
|
+
* changelog; everyone else keeps clicking Accept on every call forever, which is precisely the
|
|
7
|
+
* thing this was supposed to end.
|
|
8
|
+
*
|
|
9
|
+
* So it runs from `reticle mcp` — the one process every client starts, every session, on the
|
|
10
|
+
* version they actually have. Four properties make that safe to do unattended:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Once per version.** A stamp in the state dir, so it is not a write on every editor launch.
|
|
13
|
+
* 2. **Only where an agent is installed**, and only ever the `reticle` server.
|
|
14
|
+
* 3. **Never a create that supersedes an allowlist we cannot read.** Cursor's file is deferred to an
|
|
15
|
+
* explicit `init` when it does not already exist: a version bump that silently makes somebody's
|
|
16
|
+
* OTHER MCP servers start prompting again is a regression we caused and they cannot trace.
|
|
17
|
+
* 4. **Never throws, never writes to stdout.** This runs inside a stdio MCP server, where a stray
|
|
18
|
+
* byte on stdout corrupts the protocol, and where a crash is experienced as "Reticle is down".
|
|
19
|
+
* 5. **Never runs against a sandboxed state directory.** A gate or a fixture that points Reticle at
|
|
20
|
+
* a temp state dir is not asking to have the real user's editor config rewritten, and every
|
|
21
|
+
* battery in this repo starts MCP servers. The one write in this repo that reached a developer's
|
|
22
|
+
* own home did so from a gate run, which is exactly the shape this rules out.
|
|
23
|
+
*/
|
|
24
|
+
import type { AgentWriterIo } from './agent-writer.js';
|
|
25
|
+
import { type PlatformPaths } from './agent-configs.js';
|
|
26
|
+
export declare const APPROVAL_STAMP_FILE = "approvals.json";
|
|
27
|
+
export interface MigrationDeps {
|
|
28
|
+
readonly io: AgentWriterIo;
|
|
29
|
+
readonly home: string;
|
|
30
|
+
readonly platform: keyof PlatformPaths;
|
|
31
|
+
/** The state directory, which is where the once-per-version stamp lives. */
|
|
32
|
+
readonly stateHome: string;
|
|
33
|
+
readonly version: string;
|
|
34
|
+
readonly log: (event: string, data: Record<string, unknown>) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface MigrationResult {
|
|
37
|
+
readonly ran: boolean;
|
|
38
|
+
readonly granted: readonly string[];
|
|
39
|
+
readonly deferred: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Apply the approval rules once for this version, and record that it happened.
|
|
43
|
+
*
|
|
44
|
+
* The stamp is written even when nothing was granted. A machine with no agents installed should not
|
|
45
|
+
* re-scan on every editor launch for the rest of the version's life.
|
|
46
|
+
*/
|
|
47
|
+
export declare function migrateApprovals(deps: MigrationDeps): MigrationResult;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying the pre-approval rules onto a machine that installed Reticle before they existed.
|
|
3
|
+
*
|
|
4
|
+
* The alternative was a command the user has to know to type. Nothing reaches back into an existing
|
|
5
|
+
* install, so `init --files-only` is a real upgrade path but only for somebody who reads a
|
|
6
|
+
* changelog; everyone else keeps clicking Accept on every call forever, which is precisely the
|
|
7
|
+
* thing this was supposed to end.
|
|
8
|
+
*
|
|
9
|
+
* So it runs from `reticle mcp` — the one process every client starts, every session, on the
|
|
10
|
+
* version they actually have. Four properties make that safe to do unattended:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Once per version.** A stamp in the state dir, so it is not a write on every editor launch.
|
|
13
|
+
* 2. **Only where an agent is installed**, and only ever the `reticle` server.
|
|
14
|
+
* 3. **Never a create that supersedes an allowlist we cannot read.** Cursor's file is deferred to an
|
|
15
|
+
* explicit `init` when it does not already exist: a version bump that silently makes somebody's
|
|
16
|
+
* OTHER MCP servers start prompting again is a regression we caused and they cannot trace.
|
|
17
|
+
* 4. **Never throws, never writes to stdout.** This runs inside a stdio MCP server, where a stray
|
|
18
|
+
* byte on stdout corrupts the protocol, and where a crash is experienced as "Reticle is down".
|
|
19
|
+
* 5. **Never runs against a sandboxed state directory.** A gate or a fixture that points Reticle at
|
|
20
|
+
* a temp state dir is not asking to have the real user's editor config rewritten, and every
|
|
21
|
+
* battery in this repo starts MCP servers. The one write in this repo that reached a developer's
|
|
22
|
+
* own home did so from a gate run, which is exactly the shape this rules out.
|
|
23
|
+
*/
|
|
24
|
+
import { grantAutoApproval, ApprovalOutcome } from './auto-approve.js';
|
|
25
|
+
import { joinFor } from './agent-configs.js';
|
|
26
|
+
export const APPROVAL_STAMP_FILE = 'approvals.json';
|
|
27
|
+
const NOTHING = { ran: false, granted: [], deferred: [] };
|
|
28
|
+
/**
|
|
29
|
+
* Apply the approval rules once for this version, and record that it happened.
|
|
30
|
+
*
|
|
31
|
+
* The stamp is written even when nothing was granted. A machine with no agents installed should not
|
|
32
|
+
* re-scan on every editor launch for the rest of the version's life.
|
|
33
|
+
*/
|
|
34
|
+
export function migrateApprovals(deps) {
|
|
35
|
+
const { io, home, platform, stateHome, version, log } = deps;
|
|
36
|
+
const stamp = joinFor(platform)(stateHome, APPROVAL_STAMP_FILE);
|
|
37
|
+
try {
|
|
38
|
+
if (io.exists(stamp)) {
|
|
39
|
+
const seen = JSON.parse(io.readFile(stamp));
|
|
40
|
+
if (version === seen.version)
|
|
41
|
+
return NOTHING;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// An unreadable stamp means we do not know what has run, and the grants below are idempotent,
|
|
46
|
+
// so the safe reading is "not yet" rather than skipping the migration forever.
|
|
47
|
+
}
|
|
48
|
+
let results = [];
|
|
49
|
+
try {
|
|
50
|
+
results = grantAutoApproval(io, { home, platform }, undefined, { onlyIfNoSupersede: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return NOTHING;
|
|
54
|
+
}
|
|
55
|
+
const granted = results.filter((r) => ApprovalOutcome.GRANTED === r.outcome).map((r) => r.name);
|
|
56
|
+
const deferred = results.filter((r) => ApprovalOutcome.DEFERRED === r.outcome).map((r) => r.name);
|
|
57
|
+
try {
|
|
58
|
+
io.mkdirp(stateHome);
|
|
59
|
+
io.writeFile(stamp, `${JSON.stringify({ version }, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A stamp we could not write means this runs again next launch. Wasteful, not harmful.
|
|
63
|
+
}
|
|
64
|
+
if (0 < granted.length || 0 < deferred.length) {
|
|
65
|
+
log('reticle_approvals_migrated', { version, granted, deferred });
|
|
66
|
+
}
|
|
67
|
+
return { ran: true, granted, deferred };
|
|
68
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing the Accept button, for the tools Reticle owns and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A drive that stops on a per-call approval dialog is not automated, and the dialog is per CALL:
|
|
5
|
+
* a single verification run makes dozens, so a human sits there clicking through a loop that was
|
|
6
|
+
* supposed to run without them. Every client below documents a way to pre-approve a named server,
|
|
7
|
+
* and this writes exactly that rule, at install time, in the client's own spelling.
|
|
8
|
+
*
|
|
9
|
+
* Two limits, both deliberate:
|
|
10
|
+
*
|
|
11
|
+
* 1. ONLY `reticle` is allowed. Not `mcp(*)`, not a global auto-run switch. Reticle's tools drive
|
|
12
|
+
* the user's own dev server on their own machine, which is what makes pre-approving them
|
|
13
|
+
* defensible; the same edit made globally would hand the same pass to every other MCP server
|
|
14
|
+
* they ever install, including ones that reach the network. Their approval gate is not ours to
|
|
15
|
+
* dismantle, only ours to step out of.
|
|
16
|
+
* 2. Nothing is written for an agent that is not installed. An absent agent has no Accept button to
|
|
17
|
+
* remove, and the file would be litter — Cursor's especially, since that one TAKES OVER from an
|
|
18
|
+
* allowlist we cannot read.
|
|
19
|
+
*
|
|
20
|
+
* Codex is absent by design: its approval policy is global (`approval_policy`), so there is no
|
|
21
|
+
* reticle-shaped rule to write, and its config is TOML, which this repo never rewrites. Its
|
|
22
|
+
* headless form, `codex exec`, does not prompt at all — which is the form the drive uses.
|
|
23
|
+
*/
|
|
24
|
+
import { type PlatformPaths } from './agent-configs.js';
|
|
25
|
+
import type { AgentWriterIo } from './agent-writer.js';
|
|
26
|
+
/** A documented way to pre-approve one MCP server, in one client's own spelling. */
|
|
27
|
+
export interface ApprovalGrant {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** Home-relative, per platform. */
|
|
31
|
+
readonly paths: PlatformPaths;
|
|
32
|
+
/** Present if ANY of these exist. An agent that is not installed is not configured. */
|
|
33
|
+
readonly markers: readonly PlatformPaths[];
|
|
34
|
+
/** The rule, in the client's words, for the plan output. */
|
|
35
|
+
readonly rule: string;
|
|
36
|
+
/** Adds our rule to a parsed config, leaving everything else exactly as it was. */
|
|
37
|
+
readonly grant: (current: Record<string, unknown>) => Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* True where creating this file supersedes an in-app allowlist we cannot read, so the user has to
|
|
40
|
+
* be told rather than have their other servers quietly start prompting again.
|
|
41
|
+
*/
|
|
42
|
+
readonly supersedesInApp?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare const APPROVAL_GRANTS: readonly ApprovalGrant[];
|
|
45
|
+
export declare const ApprovalOutcome: {
|
|
46
|
+
readonly GRANTED: "granted";
|
|
47
|
+
readonly ALREADY: "already";
|
|
48
|
+
readonly ABSENT: "absent";
|
|
49
|
+
/** Deliberately left for an explicit run, because doing it unattended costs the user something. */
|
|
50
|
+
readonly DEFERRED: "deferred";
|
|
51
|
+
readonly FAILED: "failed";
|
|
52
|
+
};
|
|
53
|
+
export type ApprovalOutcome = (typeof ApprovalOutcome)[keyof typeof ApprovalOutcome];
|
|
54
|
+
export interface ApprovalResult {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly file: string;
|
|
58
|
+
readonly outcome: ApprovalOutcome;
|
|
59
|
+
readonly rule: string;
|
|
60
|
+
/** Set where the user has to be told something the write alone does not say. */
|
|
61
|
+
readonly warn?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ApprovalWhere {
|
|
64
|
+
readonly home: string;
|
|
65
|
+
readonly platform: keyof PlatformPaths;
|
|
66
|
+
}
|
|
67
|
+
export interface ApprovalOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Refuse any grant that would CREATE a file superseding an allowlist we cannot read.
|
|
70
|
+
*
|
|
71
|
+
* Set on the unattended path. Writing Cursor's permissions.json for the first time takes over
|
|
72
|
+
* from what the user approved inside the app, which is a fair trade when they just ran a command
|
|
73
|
+
* and can read the line saying so, and not a fair trade at all when a version bump did it behind
|
|
74
|
+
* them: their OTHER MCP servers would start prompting again and nothing would connect that to us.
|
|
75
|
+
* Merging into a file they already own stays safe either way.
|
|
76
|
+
*/
|
|
77
|
+
readonly onlyIfNoSupersede?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Pre-approve Reticle's tools everywhere the machine has an agent that would otherwise ask.
|
|
81
|
+
*
|
|
82
|
+
* A client that cannot be written is reported, never thrown: one unwritable settings file is not a
|
|
83
|
+
* reason to leave the other three prompting.
|
|
84
|
+
*/
|
|
85
|
+
export declare function grantAutoApproval(io: AgentWriterIo, where: ApprovalWhere, grants?: readonly ApprovalGrant[], options?: ApprovalOptions): ApprovalResult[];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing the Accept button, for the tools Reticle owns and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A drive that stops on a per-call approval dialog is not automated, and the dialog is per CALL:
|
|
5
|
+
* a single verification run makes dozens, so a human sits there clicking through a loop that was
|
|
6
|
+
* supposed to run without them. Every client below documents a way to pre-approve a named server,
|
|
7
|
+
* and this writes exactly that rule, at install time, in the client's own spelling.
|
|
8
|
+
*
|
|
9
|
+
* Two limits, both deliberate:
|
|
10
|
+
*
|
|
11
|
+
* 1. ONLY `reticle` is allowed. Not `mcp(*)`, not a global auto-run switch. Reticle's tools drive
|
|
12
|
+
* the user's own dev server on their own machine, which is what makes pre-approving them
|
|
13
|
+
* defensible; the same edit made globally would hand the same pass to every other MCP server
|
|
14
|
+
* they ever install, including ones that reach the network. Their approval gate is not ours to
|
|
15
|
+
* dismantle, only ours to step out of.
|
|
16
|
+
* 2. Nothing is written for an agent that is not installed. An absent agent has no Accept button to
|
|
17
|
+
* remove, and the file would be litter — Cursor's especially, since that one TAKES OVER from an
|
|
18
|
+
* allowlist we cannot read.
|
|
19
|
+
*
|
|
20
|
+
* Codex is absent by design: its approval policy is global (`approval_policy`), so there is no
|
|
21
|
+
* reticle-shaped rule to write, and its config is TOML, which this repo never rewrites. Its
|
|
22
|
+
* headless form, `codex exec`, does not prompt at all — which is the form the drive uses.
|
|
23
|
+
*/
|
|
24
|
+
import { joinFor } from './agent-configs.js';
|
|
25
|
+
const RETICLE_KEY = 'reticle';
|
|
26
|
+
const INDENT = 2;
|
|
27
|
+
const home = (path) => ({ darwin: path, linux: path, win32: path });
|
|
28
|
+
/** Append to a nested array, without duplicating and without disturbing what is already in it. */
|
|
29
|
+
function addToList(current, outer, key, value) {
|
|
30
|
+
const scope = null === outer
|
|
31
|
+
? current
|
|
32
|
+
: 'object' === typeof current[outer] && null !== current[outer]
|
|
33
|
+
? { ...current[outer] }
|
|
34
|
+
: {};
|
|
35
|
+
const list = Array.isArray(scope[key]) ? scope[key] : [];
|
|
36
|
+
if (list.includes(value))
|
|
37
|
+
return current;
|
|
38
|
+
const next = { ...scope, [key]: [...list, value] };
|
|
39
|
+
return null === outer ? next : { ...current, [outer]: next };
|
|
40
|
+
}
|
|
41
|
+
export const APPROVAL_GRANTS = [
|
|
42
|
+
{
|
|
43
|
+
id: 'claude-code',
|
|
44
|
+
name: 'Claude Code',
|
|
45
|
+
paths: home('.claude/settings.json'),
|
|
46
|
+
markers: [home('.claude')],
|
|
47
|
+
rule: 'permissions.allow += "mcp__reticle"',
|
|
48
|
+
grant: (c) => addToList(c, 'permissions', 'allow', 'mcp__reticle'),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'cursor',
|
|
52
|
+
name: 'Cursor',
|
|
53
|
+
paths: home('.cursor/permissions.json'),
|
|
54
|
+
markers: [home('.cursor')],
|
|
55
|
+
rule: 'mcpAllowlist += "reticle:*"',
|
|
56
|
+
// Documented: "when a key appears in permissions.json, it fully replaces the in-app allowlist
|
|
57
|
+
// for that type". Merging keeps everything already in the FILE; it cannot restore what was only
|
|
58
|
+
// ever clicked in the UI, so the user is told.
|
|
59
|
+
supersedesInApp: true,
|
|
60
|
+
grant: (c) => addToList(c, null, 'mcpAllowlist', `${RETICLE_KEY}:*`),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'antigravity',
|
|
64
|
+
name: 'Antigravity',
|
|
65
|
+
paths: home('.gemini/antigravity-cli/settings.json'),
|
|
66
|
+
markers: [
|
|
67
|
+
{
|
|
68
|
+
darwin: 'Library/Application Support/Antigravity',
|
|
69
|
+
linux: '.config/Antigravity',
|
|
70
|
+
win32: 'AppData/Roaming/Antigravity',
|
|
71
|
+
},
|
|
72
|
+
home('.gemini/antigravity-cli'),
|
|
73
|
+
],
|
|
74
|
+
rule: 'permissions.allow += "mcp(reticle/*)"',
|
|
75
|
+
grant: (c) => addToList(c, 'permissions', 'allow', `mcp(${RETICLE_KEY}/*)`),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'gemini-cli',
|
|
79
|
+
name: 'Gemini CLI',
|
|
80
|
+
paths: home('.gemini/settings.json'),
|
|
81
|
+
markers: [home('.gemini')],
|
|
82
|
+
rule: 'mcpServers.reticle.trust = true',
|
|
83
|
+
// Gemini has no allowlist: trust is a property of the server entry itself, so this reaches into
|
|
84
|
+
// the entry init already wrote rather than adding a rule beside it.
|
|
85
|
+
grant: (c) => {
|
|
86
|
+
const servers = 'object' === typeof c['mcpServers'] && null !== c['mcpServers']
|
|
87
|
+
? { ...c['mcpServers'] }
|
|
88
|
+
: {};
|
|
89
|
+
const entry = 'object' === typeof servers[RETICLE_KEY] && null !== servers[RETICLE_KEY]
|
|
90
|
+
? servers[RETICLE_KEY]
|
|
91
|
+
: {};
|
|
92
|
+
return { ...c, mcpServers: { ...servers, [RETICLE_KEY]: { ...entry, trust: true } } };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
export const ApprovalOutcome = {
|
|
97
|
+
GRANTED: 'granted',
|
|
98
|
+
ALREADY: 'already',
|
|
99
|
+
ABSENT: 'absent',
|
|
100
|
+
/** Deliberately left for an explicit run, because doing it unattended costs the user something. */
|
|
101
|
+
DEFERRED: 'deferred',
|
|
102
|
+
FAILED: 'failed',
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Pre-approve Reticle's tools everywhere the machine has an agent that would otherwise ask.
|
|
106
|
+
*
|
|
107
|
+
* A client that cannot be written is reported, never thrown: one unwritable settings file is not a
|
|
108
|
+
* reason to leave the other three prompting.
|
|
109
|
+
*/
|
|
110
|
+
export function grantAutoApproval(io, where, grants = APPROVAL_GRANTS, options = {}) {
|
|
111
|
+
const join = joinFor(where.platform);
|
|
112
|
+
return grants.map((grant) => {
|
|
113
|
+
const file = join(where.home, grant.paths[where.platform]);
|
|
114
|
+
const base = { id: grant.id, name: grant.name, file, rule: grant.rule };
|
|
115
|
+
const installed = grant.markers.some((m) => io.exists(join(where.home, m[where.platform])));
|
|
116
|
+
if (!installed)
|
|
117
|
+
return { ...base, outcome: ApprovalOutcome.ABSENT };
|
|
118
|
+
try {
|
|
119
|
+
const existed = io.exists(file);
|
|
120
|
+
if (true === options.onlyIfNoSupersede && true === grant.supersedesInApp && !existed) {
|
|
121
|
+
return {
|
|
122
|
+
...base,
|
|
123
|
+
outcome: ApprovalOutcome.DEFERRED,
|
|
124
|
+
warn: `creating ${file} would supersede what you approved inside ${grant.name}, which is not something to do unannounced. Run: npx @reticlehq/server init --files-only`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// A settings file we cannot parse is left exactly as it is. Reformatting somebody's config to
|
|
128
|
+
// add one line is a worse outcome than one dialog they have to click.
|
|
129
|
+
const current = existed
|
|
130
|
+
? JSON.parse(io.readFile(file))
|
|
131
|
+
: {};
|
|
132
|
+
const next = grant.grant(current);
|
|
133
|
+
if (next === current)
|
|
134
|
+
return { ...base, outcome: ApprovalOutcome.ALREADY };
|
|
135
|
+
io.mkdirp(file.slice(0, Math.max(0, file.lastIndexOf('/'))));
|
|
136
|
+
io.writeFile(file, `${JSON.stringify(next, null, INDENT)}\n`);
|
|
137
|
+
const warn = true === grant.supersedesInApp && !existed
|
|
138
|
+
? `${file} now governs which MCP tools run without asking; anything you had approved inside ${grant.name} itself will ask again until it is listed here too`
|
|
139
|
+
: undefined;
|
|
140
|
+
return { ...base, outcome: ApprovalOutcome.GRANTED, ...(undefined === warn ? {} : { warn }) };
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
...base,
|
|
145
|
+
outcome: ApprovalOutcome.FAILED,
|
|
146
|
+
warn: String(err?.message ?? err).slice(0, 120),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge port, checked before the connect wait.
|
|
3
|
+
*
|
|
4
|
+
* The bridge is the channel between the daemon and the SDK in the page. While something else holds
|
|
5
|
+
* that port no session can ever appear, however correct the instrumentation is — so a run that goes
|
|
6
|
+
* ahead spends its entire connect budget and then reports what looks like a wiring problem, sending
|
|
7
|
+
* the reader to the one place that is fine.
|
|
8
|
+
*
|
|
9
|
+
* `serve` refuses a held port too, in prose written for somebody starting a daemon. This is the same
|
|
10
|
+
* fact told to somebody installing, and it has to say what the bridge IS: that sentence is what
|
|
11
|
+
* makes "no session appeared" mean something.
|
|
12
|
+
*/
|
|
13
|
+
import { PortPresence } from '../daemon/port-presence.js';
|
|
14
|
+
/** The refusal to print, or undefined when the port is usable. */
|
|
15
|
+
export declare function bridgeOccupied(presence: PortPresence, port: number): string | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge port, checked before the connect wait.
|
|
3
|
+
*
|
|
4
|
+
* The bridge is the channel between the daemon and the SDK in the page. While something else holds
|
|
5
|
+
* that port no session can ever appear, however correct the instrumentation is — so a run that goes
|
|
6
|
+
* ahead spends its entire connect budget and then reports what looks like a wiring problem, sending
|
|
7
|
+
* the reader to the one place that is fine.
|
|
8
|
+
*
|
|
9
|
+
* `serve` refuses a held port too, in prose written for somebody starting a daemon. This is the same
|
|
10
|
+
* fact told to somebody installing, and it has to say what the bridge IS: that sentence is what
|
|
11
|
+
* makes "no session appeared" mean something.
|
|
12
|
+
*/
|
|
13
|
+
import { PortPresence } from '../daemon/port-presence.js';
|
|
14
|
+
/** The refusal to print, or undefined when the port is usable. */
|
|
15
|
+
export function bridgeOccupied(presence, port) {
|
|
16
|
+
// DAEMON is the ordinary case — that is the daemon init is about to use. FREE is fine too: one
|
|
17
|
+
// gets started. Only a stranger is fatal.
|
|
18
|
+
if (PortPresence.FOREIGN !== presence)
|
|
19
|
+
return undefined;
|
|
20
|
+
return (`port ${String(port)} is held by something that is not a Reticle daemon. That is the bridge — ` +
|
|
21
|
+
'the channel between the daemon and the SDK in your page — so no session can ever appear while ' +
|
|
22
|
+
'it is occupied, however correct the wiring is. Free it, or re-run with --port <n> and set the ' +
|
|
23
|
+
'same port in .reticle.json.');
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a desktop app differs from a web app, for the phases that run after the files are written.
|
|
3
|
+
*
|
|
4
|
+
* `init` already understands Electron and Tauri when it writes files: desktop-doctor.ts checks the
|
|
5
|
+
* Tauri CSP for a blocked bridge or IPC, the Electron preload, the capture helper. The runtime
|
|
6
|
+
* phases did not, and they made three web-shaped assumptions that are wrong here — one of them
|
|
7
|
+
* actively harmful.
|
|
8
|
+
*
|
|
9
|
+
* 1. **Do not open a browser.** The app's own window IS the client. Opening a tab creates a SECOND
|
|
10
|
+
* session that is not the app, and the session picker would then have to choose between them —
|
|
11
|
+
* which is exactly the stale-tab false green, arranged deliberately.
|
|
12
|
+
* 2. **Do not wait for an HTTP port.** Tauri serves `tauri://localhost` (`http://tauri.localhost`
|
|
13
|
+
* on Windows) from inside the webview; there is nothing for `fetch` to probe. Electron under
|
|
14
|
+
* electron-vite does serve a renderer over HTTP, but the window is what matters, not the port.
|
|
15
|
+
* 3. **Wait for the app to launch instead.** `tauri dev` builds Rust before a window appears, which
|
|
16
|
+
* is minutes on a cold cache, so the budget has to be the app's, not a web app's.
|
|
17
|
+
*/
|
|
18
|
+
/** What kind of thing setup is about to run. */
|
|
19
|
+
export declare const AppShape: {
|
|
20
|
+
readonly WEB: "web";
|
|
21
|
+
readonly ELECTRON: "electron";
|
|
22
|
+
readonly TAURI: "tauri";
|
|
23
|
+
};
|
|
24
|
+
export type AppShape = (typeof AppShape)[keyof typeof AppShape];
|
|
25
|
+
export declare const isDesktop: (shape: AppShape) => boolean;
|
|
26
|
+
/** Files and dependencies that name the shell, read through an injected reader. */
|
|
27
|
+
export interface ShapeEvidence {
|
|
28
|
+
/** `src-tauri/tauri.conf.json` exists. */
|
|
29
|
+
readonly hasTauriConf: boolean;
|
|
30
|
+
/** `electron` is a dependency or devDependency. */
|
|
31
|
+
readonly hasElectronDep: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Which shell this is.
|
|
35
|
+
*
|
|
36
|
+
* Tauri wins when both are present: a project with a Tauri config is a Tauri app whatever else it
|
|
37
|
+
* has installed, and electron sometimes appears in a monorepo's root devDependencies.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readShape(evidence: ShapeEvidence): AppShape;
|
|
40
|
+
export declare function isDesktopOrigin(url: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How the phases should behave for this shape.
|
|
43
|
+
*
|
|
44
|
+
* Returned as data rather than branched at each call site, so the differences are visible in one
|
|
45
|
+
* place and testable without a desktop runtime.
|
|
46
|
+
*/
|
|
47
|
+
export interface ShapePolicy {
|
|
48
|
+
/** Open a browser at the url. False for desktop: the app window is already the client. */
|
|
49
|
+
readonly openBrowser: boolean;
|
|
50
|
+
/** Require an HTTP response before looking for a session. */
|
|
51
|
+
readonly requireHttpReady: boolean;
|
|
52
|
+
/** How long the app gets to produce a session, which for Tauri includes a Rust build. */
|
|
53
|
+
readonly connectBudgetMs: number;
|
|
54
|
+
/** Said out loud, because a desktop run looks stuck if nobody explains the wait. */
|
|
55
|
+
readonly note: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
export declare function policyFor(shape: AppShape): ShapePolicy;
|