@reticlehq/server 3.4.0 → 3.6.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 +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +13 -11
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +110 -12
- package/dist/command/cli/cloud-login.d.ts +9 -1
- package/dist/command/cli/cloud-login.js +43 -8
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +15 -5
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +45 -23
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
- package/dist/command/setup/bringup/dev-server-wait.js +20 -1
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +71 -9
- package/dist/command/setup/node-effects.js +8 -2
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +7 -0
- package/dist/command/setup/run-setup.js +38 -25
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -69
- package/dist/features/harness/fill-values.js +3 -133
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -89
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +21 -3
- package/dist/memory/cloud/sync-cycle.js +45 -5
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +88 -5
- package/dist/memory/journal/envelope.d.ts +16 -16
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
- package/dist/memory/project/artifact-root-resolver.js +134 -26
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +7 -4
- package/dist/portal/bridge/bridge.js +25 -3
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +19 -9
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +12 -10
- package/dist/portal/session/session.js +30 -31
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.d.ts +1 -1
- package/dist/surface/http-server.js +15 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +28 -14
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +25 -9
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +38 -6
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -202
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +64 -9
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +25 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/actions.mdx +1 -2
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/cloud.mdx +6 -4
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +14 -8
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +14 -2
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +24 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +23 -11
- package/docs/telemetry.md +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -3
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -46
- package/dist/features/harness/openai-driver.js +0 -194
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import * as http from 'node:http';
|
|
32
32
|
import { DRIVE_PATH, LOOPBACK_HOST } from '@reticlehq/core';
|
|
33
33
|
import { PortPresence } from '../../daemon/binding/port-presence.js';
|
|
34
|
+
import { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
34
35
|
/** What `drive` should do about the port, given what is on it. */
|
|
35
36
|
export const DriveMode = {
|
|
36
37
|
/** Nothing is listening: bind it and run the foreground engine. */
|
|
@@ -134,18 +135,84 @@ export async function requestDriveSession(port, url, post = postJson) {
|
|
|
134
135
|
}
|
|
135
136
|
return readAttachResponse(port, url, res);
|
|
136
137
|
}
|
|
138
|
+
/** Nothing known about the app, as for a page that never connected. */
|
|
139
|
+
export const NO_ATTACHED_APP = { fields: {}, mismatch: false };
|
|
140
|
+
/**
|
|
141
|
+
* Control characters, and the 0x80 to 0x9f range some terminals also obey. Built with
|
|
142
|
+
* `fromCharCode` to keep them out of a regex literal (no-control-regex).
|
|
143
|
+
*/
|
|
144
|
+
const TERMINAL_CONTROLS = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(0x1f)}${String.fromCharCode(0x7f)}-${String.fromCharCode(0x9f)}]`, 'g');
|
|
145
|
+
/**
|
|
146
|
+
* A value safe to print. The project id and title come from the page, and the expected project id
|
|
147
|
+
* from a `.reticle.json` anyone can commit. All three are written straight to the terminal (and into
|
|
148
|
+
* the JSON line beside it), so they could otherwise move the cursor or retitle the window. Each
|
|
149
|
+
* control character becomes U+FFFD, so the reader can still see that something was there.
|
|
150
|
+
*/
|
|
151
|
+
function printable(value) {
|
|
152
|
+
return value.replace(TERMINAL_CONTROLS, '\uFFFD');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Pick the attached session out of a `/status` payload. Pure.
|
|
156
|
+
*
|
|
157
|
+
* The drive answer is a lease result, which carries no project: the page reports that itself when
|
|
158
|
+
* it connects, and `/status` is where it is already listed.
|
|
159
|
+
*/
|
|
160
|
+
export function attachedApp(status, sessionId, expectedProjectId) {
|
|
161
|
+
const row = summarizeStatus(status).sessions.find((s) => s.sessionId === sessionId);
|
|
162
|
+
return {
|
|
163
|
+
fields: {
|
|
164
|
+
...(row?.projectId === undefined ? {} : { projectId: printable(row.projectId) }),
|
|
165
|
+
...(row?.title === undefined ? {} : { title: printable(row.title) }),
|
|
166
|
+
...(expectedProjectId === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { expectedProjectId: printable(expectedProjectId) }),
|
|
169
|
+
},
|
|
170
|
+
// Only warned when both sides are known: a page with no project id proves nothing, and neither
|
|
171
|
+
// does a directory without a `.reticle.json`.
|
|
172
|
+
mismatch: row?.projectId !== undefined &&
|
|
173
|
+
expectedProjectId !== undefined &&
|
|
174
|
+
row.projectId !== expectedProjectId,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** The fallback when the daemon sent no hint for a page that did not connect. */
|
|
178
|
+
const MSG_NOT_READY_HINT = 'Check that the app embeds @reticlehq/browser.';
|
|
179
|
+
/** How the attach line names the page's project. */
|
|
180
|
+
const PROJECT_LABEL = (projectId) => `project ${JSON.stringify(projectId)}`;
|
|
181
|
+
/** The page is a different project from the one this directory's `.reticle.json` names. */
|
|
182
|
+
const MSG_PROJECT_MISMATCH = (url, expected, actual) => `Warning: this directory's .reticle.json is ${PROJECT_LABEL(expected)}, but the page at ${url} ` +
|
|
183
|
+
`is ${PROJECT_LABEL(actual)}, so another app is probably serving that port. Check which dev ` +
|
|
184
|
+
'server is on it before trusting anything this session reports.';
|
|
185
|
+
/**
|
|
186
|
+
* The project and title, and a warning when this directory names a different project.
|
|
187
|
+
*
|
|
188
|
+
* In a monorepo the port you meant for one app is easily held by another, and the port, url and
|
|
189
|
+
* session id read the same either way.
|
|
190
|
+
*/
|
|
191
|
+
function describeApp(url, app) {
|
|
192
|
+
const { projectId, title, expectedProjectId } = app.fields;
|
|
193
|
+
const named = [
|
|
194
|
+
...(projectId === undefined ? [] : [PROJECT_LABEL(projectId)]),
|
|
195
|
+
...(title === undefined ? [] : [JSON.stringify(title)]),
|
|
196
|
+
];
|
|
197
|
+
const label = 0 === named.length ? '' : ` (${named.join(', ')})`;
|
|
198
|
+
const warning = app.mismatch && projectId !== undefined && expectedProjectId !== undefined
|
|
199
|
+
? ` ${MSG_PROJECT_MISMATCH(url, expectedProjectId, projectId)}`
|
|
200
|
+
: '';
|
|
201
|
+
return { label, warning };
|
|
202
|
+
}
|
|
137
203
|
/** The success line. Names the session, because that is the handle everything downstream needs. */
|
|
138
|
-
export function describeAttached(port, url, session) {
|
|
204
|
+
export function describeAttached(port, url, session, app = NO_ATTACHED_APP) {
|
|
139
205
|
const life = session.expiresInMs === undefined
|
|
140
206
|
? ''
|
|
141
207
|
: ` It is released after ${String(Math.round(session.expiresInMs / 1000))}s untouched; any ` +
|
|
142
208
|
'tool call against it resets that.';
|
|
143
209
|
if (!session.ready) {
|
|
144
210
|
return (`the Reticle daemon on :${String(port)} opened ${url} as session ${session.sessionId}, but ` +
|
|
145
|
-
`the page did not connect to the bridge. ${session.hint ??
|
|
211
|
+
`the page did not connect to the bridge. ${session.hint ?? MSG_NOT_READY_HINT}`);
|
|
146
212
|
}
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
const { label, warning } = describeApp(url, app);
|
|
214
|
+
return (`attached to the Reticle daemon on :${String(port)} — it is driving ${url}${label} as session ` +
|
|
215
|
+
`${session.sessionId}, which every Reticle tool can address.${life}${warning}`);
|
|
149
216
|
}
|
|
150
217
|
/**
|
|
151
218
|
* What an attach cannot honour: the window.
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* place. The pure parts (which mode, and every sentence a user reads) live next door in
|
|
7
7
|
* drive-attach.ts, which is where the reasoning for attaching is written down.
|
|
8
8
|
*/
|
|
9
|
+
/** `projectId` is the one in this directory's `.reticle.json`, read by the caller. */
|
|
9
10
|
export declare function handleDrive(parsed: {
|
|
10
11
|
port: number;
|
|
11
12
|
driveUrl: string;
|
|
12
13
|
headless: boolean;
|
|
13
|
-
}): void;
|
|
14
|
+
}, projectId?: string): void;
|
|
@@ -13,9 +13,10 @@ import { probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
|
|
|
13
13
|
import { readPid } from '../../daemon/daemon.js';
|
|
14
14
|
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
15
15
|
import { captureLookup, findPortHolder } from '../ports/port-holder.js';
|
|
16
|
-
import { DriveMode, decideDriveMode, describeAttached, driveForeignHolder, driveHeadlessOnAttach, driveRaceLost, requestDriveSession, } from './drive-attach.js';
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
import { DriveMode, NO_ATTACHED_APP, attachedApp, decideDriveMode, describeAttached, driveForeignHolder, driveHeadlessOnAttach, driveRaceLost, requestDriveSession, } from './drive-attach.js';
|
|
17
|
+
/** `projectId` is the one in this directory's `.reticle.json`, read by the caller. */
|
|
18
|
+
export function handleDrive(parsed, projectId) {
|
|
19
|
+
void driveWithHonestConflict(parsed, projectId);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Decide who gets the port BEFORE binding it — and when the answer is "the daemon", ask it instead.
|
|
@@ -29,7 +30,7 @@ export function handleDrive(parsed) {
|
|
|
29
30
|
* because the MCP proxy respawns a daemon into the gap. So a healthy daemon is now ATTACHED to —
|
|
30
31
|
* see cli/drive/drive-attach.ts.
|
|
31
32
|
*/
|
|
32
|
-
async function driveWithHonestConflict(parsed) {
|
|
33
|
+
async function driveWithHonestConflict(parsed, projectId) {
|
|
33
34
|
const presence = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
34
35
|
const mode = decideDriveMode(presence);
|
|
35
36
|
if (DriveMode.REFUSE === mode) {
|
|
@@ -43,7 +44,7 @@ async function driveWithHonestConflict(parsed) {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
if (DriveMode.ATTACH === mode) {
|
|
46
|
-
await attachToRunningDaemon(parsed.port, parsed.driveUrl, parsed.headless);
|
|
47
|
+
await attachToRunningDaemon(parsed.port, parsed.driveUrl, parsed.headless, projectId);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
// The probe cannot close the race: something can take the port between here and the bind, and the
|
|
@@ -81,7 +82,7 @@ async function driveWithHonestConflict(parsed) {
|
|
|
81
82
|
* against it renews its lease), and a foreground hold here could only keep it alive by faking tool
|
|
82
83
|
* calls at it.
|
|
83
84
|
*/
|
|
84
|
-
async function attachToRunningDaemon(port, url, headless) {
|
|
85
|
+
async function attachToRunningDaemon(port, url, headless, projectId) {
|
|
85
86
|
const result = await requestDriveSession(port, url);
|
|
86
87
|
if (!result.ok) {
|
|
87
88
|
log('reticle_drive_attach_failed', { port, url, reason: result.reason });
|
|
@@ -89,13 +90,18 @@ async function attachToRunningDaemon(port, url, headless) {
|
|
|
89
90
|
process.exit(1);
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
93
|
+
// Only a page that connected has reported its project; one that never dialled in is not listed.
|
|
94
|
+
const app = result.session.ready
|
|
95
|
+
? attachedApp(await fetchStatus(port), result.session.sessionId, projectId)
|
|
96
|
+
: NO_ATTACHED_APP;
|
|
92
97
|
log('reticle_drive_attached', {
|
|
93
98
|
port,
|
|
94
99
|
url,
|
|
95
100
|
sessionId: result.session.sessionId,
|
|
96
101
|
ready: result.session.ready,
|
|
102
|
+
...app.fields,
|
|
97
103
|
});
|
|
98
|
-
process.stderr.write(`${describeAttached(port, url, result.session)}\n`);
|
|
104
|
+
process.stderr.write(`${describeAttached(port, url, result.session, app)}\n`);
|
|
99
105
|
// The command's own default is headed, and this path cannot deliver that. Said out loud rather
|
|
100
106
|
// than left for the user to discover by watching a window that never opens.
|
|
101
107
|
if (!headless)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { log } from '../../../log.js';
|
|
18
18
|
import { RETICLE_VERIFY_DEFAULT_PORT } from '../../../index.js';
|
|
19
|
-
import { verifyEndpointMismatch } from '../../../status-payload.js';
|
|
19
|
+
import { strandedClientsNote, verifyEndpointMismatch } from '../../../status-payload.js';
|
|
20
20
|
import { probeDaemon, waitForDaemonBind } from '../../../surface/mcp/mcp-proxy.js';
|
|
21
21
|
import { stateDirProblem } from '../../daemon/state-dir.js';
|
|
22
22
|
import { readPid, isAlive, removePid, spawnDaemon, logPath, reticleStateHome, } from '../../daemon/daemon.js';
|
|
@@ -168,6 +168,9 @@ export function handleStop(port, quiet) {
|
|
|
168
168
|
* anything at all, and a single `ok: false` cannot tell them which happened.
|
|
169
169
|
*/
|
|
170
170
|
export async function handleRestart(port, force) {
|
|
171
|
+
const stranded = strandedClientsNote(await fetchStatus(port));
|
|
172
|
+
if (stranded !== undefined)
|
|
173
|
+
process.stderr.write(`${stranded}\n`);
|
|
171
174
|
const freed = await runKill(port, force);
|
|
172
175
|
if (!freed) {
|
|
173
176
|
log('reticle_restart_aborted', {
|
|
@@ -51,3 +51,13 @@ export declare function describeForeignHolder(port: number, holder: PortHolder |
|
|
|
51
51
|
ourPid?: number | null): string;
|
|
52
52
|
/** The lookup itself. `exec` is injected so the shelling out never runs in a unit test. */
|
|
53
53
|
export declare function findPortHolder(port: number, exec: (command: string, args: readonly string[]) => string | null): PortHolder | null;
|
|
54
|
+
/** The path in `lsof -a -p <pid> -d cwd -Fn` output, or null when it printed none. */
|
|
55
|
+
export declare function parseProcessCwd(stdout: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The working directory of whatever LISTENS on `port`, or null.
|
|
58
|
+
*
|
|
59
|
+
* How the daemon tells which checkout served a page when every git worktree declares the same
|
|
60
|
+
* projectId: the dev server runs inside exactly one of them. Null wherever `lsof` cannot answer —
|
|
61
|
+
* Windows, a slim container — and the caller then keeps the answer it had without this.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findListenerCwd(port: number, exec: (command: string, args: readonly string[]) => string | null): string | null;
|
|
@@ -121,3 +121,27 @@ export function findPortHolder(port, exec) {
|
|
|
121
121
|
const out = exec('lsof', ['-nP', `-iTCP:${String(port)}`, '-sTCP:LISTEN', '-F', 'pc']);
|
|
122
122
|
return null === out ? null : parsePortHolder(out);
|
|
123
123
|
}
|
|
124
|
+
/** `lsof -Fn` emits the file name on its own line, prefixed `n`. */
|
|
125
|
+
const NAME_FIELD = 'n';
|
|
126
|
+
/** The path in `lsof -a -p <pid> -d cwd -Fn` output, or null when it printed none. */
|
|
127
|
+
export function parseProcessCwd(stdout) {
|
|
128
|
+
for (const raw of stdout.split('\n')) {
|
|
129
|
+
if (raw.startsWith(NAME_FIELD) && raw.length > NAME_FIELD.length)
|
|
130
|
+
return raw.slice(1);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The working directory of whatever LISTENS on `port`, or null.
|
|
136
|
+
*
|
|
137
|
+
* How the daemon tells which checkout served a page when every git worktree declares the same
|
|
138
|
+
* projectId: the dev server runs inside exactly one of them. Null wherever `lsof` cannot answer —
|
|
139
|
+
* Windows, a slim container — and the caller then keeps the answer it had without this.
|
|
140
|
+
*/
|
|
141
|
+
export function findListenerCwd(port, exec) {
|
|
142
|
+
const holder = findPortHolder(port, exec);
|
|
143
|
+
if (null === holder)
|
|
144
|
+
return null;
|
|
145
|
+
const out = exec('lsof', ['-a', '-p', String(holder.pid), '-d', 'cwd', '-Fn']);
|
|
146
|
+
return null === out ? null : parseProcessCwd(out);
|
|
147
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { probeCli, terminalWidth, wrapForTerminal } from '@reticlehq/init';
|
|
4
|
+
import { claudeAddCommand, McpClient, probeCli, terminalWidth, wrapForTerminal, } from '@reticlehq/init';
|
|
5
5
|
import { installClosing, tutorialShownSteps } from './tutorial.js';
|
|
6
6
|
import { setupMcp, knownClientLabels } from '../setup/setup-mcp.js';
|
|
7
7
|
import { reportInstallSteps } from '../setup/setup-install.js';
|
|
@@ -35,20 +35,30 @@ export function handleSetupMcp(reportStep, standalone = true) {
|
|
|
35
35
|
const result = setupMcp(io);
|
|
36
36
|
if (0 === result.detected.length) {
|
|
37
37
|
// Even with no file-backed client, the broader set may still have somewhere to write.
|
|
38
|
-
registerOtherAgents((line) => {
|
|
38
|
+
const preRegistered = registerOtherAgents((line) => {
|
|
39
39
|
io.print(line);
|
|
40
40
|
});
|
|
41
41
|
// Named rather than a bare "none found". A person whose agent IS installed needs to know
|
|
42
42
|
// whether we looked for it at all, and the list is the difference between "we did not find
|
|
43
|
-
// yours" and "we do not support yours".
|
|
44
|
-
|
|
45
|
-
io.print(
|
|
43
|
+
// yours" and "we do not support yours". When the broader set was written, saying "none found"
|
|
44
|
+
// right under its tick read as a contradiction: those agents are pre-registered, not found.
|
|
45
|
+
io.print(0 < preRegistered
|
|
46
|
+
? `None of ${knownClientLabels().join(', ')} is installed here yet. The agents above are pre-registered and have Reticle's tools once installed; install one of the others, then run \`reticle setup mcp\` again.`
|
|
47
|
+
: `No coding agent config found. Looked for: ${knownClientLabels().join(', ')}. Install one, then run \`reticle setup mcp\` again.`);
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
for (const id of result.registered)
|
|
49
51
|
io.print(` ✓ ${id}`);
|
|
50
52
|
for (const id of result.alreadyThere)
|
|
51
53
|
io.print(` ✓ ${id} (already set up)`);
|
|
54
|
+
for (const id of result.failed) {
|
|
55
|
+
io.print(` ⚠ ${id}: Reticle registration failed; this agent may not have the tools.`);
|
|
56
|
+
io.print(id === McpClient.CLAUDE_CODE
|
|
57
|
+
? ` Run this command to see the error and retry: ${claudeAddCommand().display}`
|
|
58
|
+
: ' Check your agent config file permissions, then retry: reticle setup mcp');
|
|
59
|
+
}
|
|
60
|
+
if (result.failed.length > 0)
|
|
61
|
+
process.exitCode = 1;
|
|
52
62
|
// Said, not skipped. These are the clients whose format we will not rewrite, and they used to be
|
|
53
63
|
// counted as registered: the line below is the difference between a user who adds four lines to
|
|
54
64
|
// one file and a user who believes they are set up and has no tools.
|
|
@@ -84,8 +84,8 @@ export declare function tutorialNextSteps(audience: TutorialAudience, restartAge
|
|
|
84
84
|
* on `process.stdout.isTTY` does not work: that is `undefined` through every pipe, so the tour never
|
|
85
85
|
* prints. A heuristic that silently answers "nobody is watching" is worse than no heuristic.
|
|
86
86
|
*
|
|
87
|
-
* It ends with the tour's OWN closing
|
|
88
|
-
*
|
|
87
|
+
* It ends with the tour's OWN closing. Installing changes no project: the app is wired the first time
|
|
88
|
+
* an agent uses Reticle in it (see first-run-wiring.ts).
|
|
89
89
|
*/
|
|
90
90
|
export declare function installClosing(registered?: boolean): string;
|
|
91
91
|
export declare function renderTutorial(audience: TutorialAudience): string;
|
|
@@ -95,8 +95,8 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
95
95
|
const lines = TutorialAudience.AGENT === audience
|
|
96
96
|
? [
|
|
97
97
|
'Next:',
|
|
98
|
-
'
|
|
99
|
-
'
|
|
98
|
+
' reticle_session list # the first call wires this project: Reticle runs init itself',
|
|
99
|
+
' # and names every file it changed — tell the user which',
|
|
100
100
|
' then run the four steps above against it, and report the verdict',
|
|
101
101
|
]
|
|
102
102
|
: [
|
|
@@ -104,12 +104,9 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
104
104
|
...(restartAgent
|
|
105
105
|
? [' restart your agent # it loads new tools only when it starts']
|
|
106
106
|
: []),
|
|
107
|
-
' cd <your project> && reticle init # wire Reticle into the app',
|
|
108
|
-
' npm run dev # then load it in a browser',
|
|
109
|
-
' reticle status # confirms the app connected, or says why not',
|
|
110
107
|
'',
|
|
111
108
|
// One line: the terminal wraps it at the reader's width, not ours.
|
|
112
|
-
'
|
|
109
|
+
'Ask your agent to verify one real flow in your project. Nothing in your app changed at install: the first time the agent uses Reticle there, Reticle wires the app, starts it, and tells you which files it changed.',
|
|
113
110
|
];
|
|
114
111
|
return lines.join('\n');
|
|
115
112
|
}
|
|
@@ -120,8 +117,8 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
120
117
|
* on `process.stdout.isTTY` does not work: that is `undefined` through every pipe, so the tour never
|
|
121
118
|
* prints. A heuristic that silently answers "nobody is watching" is worse than no heuristic.
|
|
122
119
|
*
|
|
123
|
-
* It ends with the tour's OWN closing
|
|
124
|
-
*
|
|
120
|
+
* It ends with the tour's OWN closing. Installing changes no project: the app is wired the first time
|
|
121
|
+
* an agent uses Reticle in it (see first-run-wiring.ts).
|
|
125
122
|
*/
|
|
126
123
|
export function installClosing(registered = true) {
|
|
127
124
|
// One line a step. The reasons are for `reticle tutorial`; right after an install they turned a
|
|
@@ -140,7 +137,7 @@ export function installClosing(registered = true) {
|
|
|
140
137
|
* Reticle" points at a panel on screen; straight after an install there is none to point at.
|
|
141
138
|
*/
|
|
142
139
|
const INSTALL_SAYS = {
|
|
143
|
-
connect: '
|
|
140
|
+
connect: 'The first time your agent uses Reticle in a project, it wires the app and a Reticle panel appears in it: your proof the SDK is connected.',
|
|
144
141
|
};
|
|
145
142
|
export function renderTutorial(audience) {
|
|
146
143
|
const steps = tutorialScript(audience)
|
package/dist/command/cli.d.ts
CHANGED
|
@@ -2,5 +2,11 @@
|
|
|
2
2
|
import { parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
|
|
3
3
|
export { parseCliArgs, CLI_USAGE };
|
|
4
4
|
export type { CliResult } from './cli/cli-parse.js';
|
|
5
|
+
/**
|
|
6
|
+
* Report status to the terminal. `--json` writes one structured line to STDOUT so it can be piped
|
|
7
|
+
* into `jq`; the human-readable block goes to stdout too. `log()` stays on stderr (the MCP
|
|
8
|
+
* transport lives on stdout), so `--json` must not go through it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function reportStatus(fields: Record<string, unknown>, json: boolean): void;
|
|
5
11
|
export declare function handleStatus(port: number, json?: boolean): Promise<void>;
|
|
6
12
|
export declare function main(): void;
|
package/dist/command/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { hudSignIn } from './cli/cloud-login.js';
|
|
2
3
|
import { handleSetupMcp, handleSetupInstall } from './cli/setup-mcp-cli.js';
|
|
3
4
|
import { reportStepFromCli, reportTutorialShown } from './cli-onboarding.js';
|
|
4
5
|
import { runDemoTour } from './cli/demo/demo-run.js';
|
|
5
6
|
import { renderTutorial } from './cli/tutorial.js';
|
|
6
7
|
import { pathToFileURL } from 'node:url';
|
|
7
|
-
import { openFailureNote } from './cli/answers/open-note.js';
|
|
8
|
+
import { openFailureNote, openLaunchFailureNote } from './cli/answers/open-note.js';
|
|
8
9
|
import { realpathSync } from 'node:fs';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { installCrashGuard } from './setup/setup-command.js';
|
|
@@ -15,9 +16,9 @@ import { splitBrainFields, withNextAction } from './cli/status-fields.js';
|
|
|
15
16
|
import { reticleStateHome } from './daemon/daemon.js';
|
|
16
17
|
import { handleMcp } from './cli/mcp-command.js';
|
|
17
18
|
import { handleReport } from './cli/report-command.js';
|
|
18
|
-
import { daemonProjectAt, resolveDaemonForProject } from './daemon/daemon-resolve.js';
|
|
19
|
+
import { daemonProjectAt, readDaemonRegistry, resolveDaemonForProject, } from './daemon/daemon-resolve.js';
|
|
19
20
|
import { pickDaemonPortToBind } from './daemon/binding/free-port.js';
|
|
20
|
-
import { portForInit, portFromEnv } from './setup/init/init-port.js';
|
|
21
|
+
import { isSameOrAbove, portForInit, portFromEnv } from './setup/init/init-port.js';
|
|
21
22
|
import { daemonStartOptions } from './cli/daemon-start-options.js';
|
|
22
23
|
import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
|
|
23
24
|
import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
|
|
@@ -78,8 +79,12 @@ async function handleInit(parsed) {
|
|
|
78
79
|
},
|
|
79
80
|
daemonPresent: async (p) => presenceIsUsable(await probePresence(p, { tcpOpen: probeDaemon, status: fetchStatus })),
|
|
80
81
|
pickPort: (p) => pickDaemonPortToBind(p),
|
|
82
|
+
daemonStartedHere: (p) => {
|
|
83
|
+
const started = readDaemonRegistry(reticleStateHome()).find((e) => e.port === p)?.cwd;
|
|
84
|
+
return started !== undefined && isSameOrAbove(started, cwd);
|
|
85
|
+
},
|
|
81
86
|
});
|
|
82
|
-
const io = buildNodeIo(cwd, serverInitHost());
|
|
87
|
+
const io = buildNodeIo(cwd, serverInitHost(), { stderr: true === parsed.json });
|
|
83
88
|
const result = runInit({
|
|
84
89
|
cwd,
|
|
85
90
|
port,
|
|
@@ -104,15 +109,18 @@ async function handleInit(parsed) {
|
|
|
104
109
|
import { handleServe, handleStop, handleRestart } from './cli/lifecycle/daemon-lifecycle.js';
|
|
105
110
|
import { statusLines } from './cli/status/status-lines.js';
|
|
106
111
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* whoever is reading the terminal — which, once an agent is running the command, is somebody who
|
|
111
|
-
* did not type it and cannot be assumed to parse it.
|
|
112
|
+
* Report status to the terminal. `--json` writes one structured line to STDOUT so it can be piped
|
|
113
|
+
* into `jq`; the human-readable block goes to stdout too. `log()` stays on stderr (the MCP
|
|
114
|
+
* transport lives on stdout), so `--json` must not go through it.
|
|
112
115
|
*/
|
|
113
|
-
function reportStatus(fields, json) {
|
|
116
|
+
export function reportStatus(fields, json) {
|
|
114
117
|
if (json) {
|
|
115
|
-
|
|
118
|
+
const line = JSON.stringify({
|
|
119
|
+
t: new Date().toISOString(),
|
|
120
|
+
event: 'reticle_status',
|
|
121
|
+
...fields,
|
|
122
|
+
});
|
|
123
|
+
process.stdout.write(`${line}\n`);
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
126
|
process.stdout.write(`${statusLines(fields).join('\n')}\n`);
|
|
@@ -223,14 +231,13 @@ export async function handleStatus(port, json = false) {
|
|
|
223
231
|
...nudge,
|
|
224
232
|
});
|
|
225
233
|
}
|
|
226
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Print the running package version (resolved once in server-version.ts), and nothing else.
|
|
236
|
+
*
|
|
237
|
+
* Bare on stdout so `V=$(reticle version)` works. It also wrote a JSON event to stderr, which a
|
|
238
|
+
* terminal shows: the first thing a new user typed after installing answered with a log line.
|
|
239
|
+
*/
|
|
227
240
|
function handleVersion() {
|
|
228
|
-
log('reticle_version', { version: SERVER_VERSION });
|
|
229
|
-
// Also on stdout, bare, because every diagnostic here starts by asking which build is running and
|
|
230
|
-
// that answer has to be copyable. The event alone looked fine in a terminal -- stderr is on the
|
|
231
|
-
// screen too -- but `V=$(reticle version)` came back empty, which is exactly the shape somebody
|
|
232
|
-
// reaches for when writing an issue template or a CI check. The event stays; machine-readable
|
|
233
|
-
// events all go to stderr and that consistency is worth keeping.
|
|
234
241
|
process.stdout.write(`${SERVER_VERSION}\n`);
|
|
235
242
|
}
|
|
236
243
|
/** `reticle license` — show enterprise activation resolved from the environment (offline; nothing leaves). */
|
|
@@ -395,9 +402,7 @@ function handleOpen(requestedPort, url) {
|
|
|
395
402
|
// Points at the OS handler, not at `reticle doctor`. Doctor's browser check is about
|
|
396
403
|
// Reticle's own Chromium, which this command never touches — the same misdirect the
|
|
397
404
|
// no-session note below spells out at length, left standing here for one more release.
|
|
398
|
-
recovery:
|
|
399
|
-
`(\`${openCommand(decision.url, process.platform).cmd}\` on this platform) and that ` +
|
|
400
|
-
'failed, so the fix is on the OS side: open the url yourself, or set a default browser.',
|
|
405
|
+
recovery: openLaunchFailureNote(decision.url, openCommand(decision.url, process.platform).cmd),
|
|
401
406
|
});
|
|
402
407
|
process.exit(1);
|
|
403
408
|
return;
|
|
@@ -431,7 +436,8 @@ function handleOpen(requestedPort, url) {
|
|
|
431
436
|
});
|
|
432
437
|
}
|
|
433
438
|
function handleDaemonInner(parsed) {
|
|
434
|
-
|
|
439
|
+
// The daemon the CLI starts is the one that can sign in for the panel: it owns ~/.reticle.
|
|
440
|
+
const options = { ...daemonStartOptions(parsed), hudSignIn };
|
|
435
441
|
startDaemon(options)
|
|
436
442
|
.then((server) => {
|
|
437
443
|
log('reticle_daemon_ready', { port: parsed.port, pid: process.pid });
|
|
@@ -587,6 +593,22 @@ export function main() {
|
|
|
587
593
|
process.stdout.write(`${CLI_USAGE}\n`);
|
|
588
594
|
return;
|
|
589
595
|
}
|
|
596
|
+
if ('connect' === argv[0]) {
|
|
597
|
+
void (async () => {
|
|
598
|
+
// A fresh project gets the same install, dev-server handover and browser proof as `init`.
|
|
599
|
+
// Its failure exits non-zero before any cloud binding can claim this repo is ready.
|
|
600
|
+
if (readProjectId(process.cwd()) === undefined) {
|
|
601
|
+
await handleInit({ port: undefined, mcp: true, dryRun: false, install: true });
|
|
602
|
+
}
|
|
603
|
+
return runCloudCommand(argv);
|
|
604
|
+
})()
|
|
605
|
+
.then((code) => process.exit(code))
|
|
606
|
+
.catch((cause) => {
|
|
607
|
+
process.stderr.write(`reticle connect: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
590
612
|
void runCloudCommand(argv).then((code) => process.exit(code));
|
|
591
613
|
return;
|
|
592
614
|
}
|
|
@@ -706,7 +728,7 @@ export function main() {
|
|
|
706
728
|
handleOpen(parsed.port, parsed.url);
|
|
707
729
|
break;
|
|
708
730
|
case 'drive':
|
|
709
|
-
handleDrive(parsed);
|
|
731
|
+
handleDrive(parsed, readProjectId(process.cwd()));
|
|
710
732
|
break;
|
|
711
733
|
case 'verify':
|
|
712
734
|
handleVerify(parsed);
|
|
@@ -15,6 +15,8 @@ interface StatusSession {
|
|
|
15
15
|
sessionId: string;
|
|
16
16
|
url: string;
|
|
17
17
|
projectId?: string;
|
|
18
|
+
/** The page's own title. Absent when the page has none, or on a daemon too old to send it. */
|
|
19
|
+
title?: string;
|
|
18
20
|
throttled: boolean;
|
|
19
21
|
/**
|
|
20
22
|
* The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
|
|
@@ -102,6 +102,7 @@ export function summarizeStatus(payload) {
|
|
|
102
102
|
...('string' === typeof r['projectId'] && 0 < r['projectId'].length
|
|
103
103
|
? { projectId: r['projectId'] }
|
|
104
104
|
: {}),
|
|
105
|
+
...('string' === typeof r['title'] && 0 < r['title'].length ? { title: r['title'] } : {}),
|
|
105
106
|
throttled: true === r['throttled'],
|
|
106
107
|
hidden: true === r['hidden'],
|
|
107
108
|
stale: true === r['stale'],
|
|
@@ -76,6 +76,20 @@ export declare function daemonProjectAt(port: number, home: string): string | un
|
|
|
76
76
|
* Only LIVE daemons are named: a port a session used yesterday is history, not a split.
|
|
77
77
|
*/
|
|
78
78
|
export declare function daemonsServingProjectElsewhere(projectId: string | undefined, myPort: number, home: string, alive: (pid: number) => boolean, connectedOn: (port: number) => boolean): readonly number[];
|
|
79
|
+
/** Live daemons that serve a named project, on any port but `myPort`. */
|
|
80
|
+
export declare function projectDaemonsElsewhere(myPort: number, home: string, alive: (pid: number) => boolean): {
|
|
81
|
+
port: number;
|
|
82
|
+
projectId: string;
|
|
83
|
+
}[];
|
|
84
|
+
/**
|
|
85
|
+
* The sentence for a daemon started outside any project while projects run their own daemons, or
|
|
86
|
+
* `undefined` when there are none. Its sessions are empty because the apps dial their own ports,
|
|
87
|
+
* and "check your scope" is not something an agent inside an editor can act on (#1240).
|
|
88
|
+
*/
|
|
89
|
+
export declare function projectlessNote(myPort: number, others: readonly {
|
|
90
|
+
port: number;
|
|
91
|
+
projectId: string;
|
|
92
|
+
}[]): string | undefined;
|
|
79
93
|
/**
|
|
80
94
|
* The sentence for a split brain, or `undefined` when there is none.
|
|
81
95
|
*
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { DaemonRegistryEntrySchema, daemonRegistryPort, pickDaemonPort, } from '@reticlehq/core/artifacts';
|
|
22
|
+
import { ReticleEnv } from '@reticlehq/core';
|
|
23
|
+
const RETICLE_PORT_ENV = ReticleEnv.PORT;
|
|
22
24
|
/**
|
|
23
25
|
* Every well-formed registry entry in `home`.
|
|
24
26
|
*
|
|
@@ -117,6 +119,30 @@ export function daemonsServingProjectElsewhere(projectId, myPort, home, alive, c
|
|
|
117
119
|
.map((entry) => entry.port)
|
|
118
120
|
.sort((a, b) => a - b);
|
|
119
121
|
}
|
|
122
|
+
/** Live daemons that serve a named project, on any port but `myPort`. */
|
|
123
|
+
export function projectDaemonsElsewhere(myPort, home, alive) {
|
|
124
|
+
return readDaemonRegistry(home)
|
|
125
|
+
.filter((entry) => entry.port !== myPort && alive(entry.pid))
|
|
126
|
+
.flatMap((entry) => entry.projectId === undefined || 0 === entry.projectId.length
|
|
127
|
+
? []
|
|
128
|
+
: [{ port: entry.port, projectId: entry.projectId }])
|
|
129
|
+
.sort((a, b) => a.port - b.port);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The sentence for a daemon started outside any project while projects run their own daemons, or
|
|
133
|
+
* `undefined` when there are none. Its sessions are empty because the apps dial their own ports,
|
|
134
|
+
* and "check your scope" is not something an agent inside an editor can act on (#1240).
|
|
135
|
+
*/
|
|
136
|
+
export function projectlessNote(myPort, others) {
|
|
137
|
+
const first = others[0];
|
|
138
|
+
if (first === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
const listed = others.map((o) => `${o.projectId} on :${String(o.port)}`).join(', ');
|
|
141
|
+
return (`this daemon on :${String(myPort)} was started outside any project, and the projects on this ` +
|
|
142
|
+
`machine run their own daemons: ${listed}. Their apps connect there, not here. Point the MCP ` +
|
|
143
|
+
`server at one — set ${RETICLE_PORT_ENV}=${String(first.port)} in its config, or launch it from ` +
|
|
144
|
+
'that project directory.');
|
|
145
|
+
}
|
|
120
146
|
/**
|
|
121
147
|
* The sentence for a split brain, or `undefined` when there is none.
|
|
122
148
|
*
|
|
@@ -179,8 +205,13 @@ export async function resolveMcpPort(preferred, projectId, home, deps) {
|
|
|
179
205
|
const mine = resolveDaemonForProject(projectId, home, deps.alive);
|
|
180
206
|
if (mine !== undefined)
|
|
181
207
|
return mine;
|
|
182
|
-
if (!(await deps.daemonPresent(preferred)))
|
|
183
|
-
|
|
208
|
+
if (!(await deps.daemonPresent(preferred))) {
|
|
209
|
+
// A caller outside any project (an editor's user-scope MCP, launched from $HOME) used to dial the
|
|
210
|
+
// default and never see the one project that had moved off it (#1240). With exactly one project
|
|
211
|
+
// daemon live, that is the one; with several, the no-session answer names them instead.
|
|
212
|
+
const projects = projectId === undefined ? projectDaemonsElsewhere(preferred, home, deps.alive) : [];
|
|
213
|
+
return 1 === projects.length ? (projects[0]?.port ?? preferred) : preferred;
|
|
214
|
+
}
|
|
184
215
|
return adoptable(daemonProjectAt(preferred, home), projectId)
|
|
185
216
|
? preferred
|
|
186
217
|
: deps.pickPort(preferred);
|
|
@@ -34,7 +34,7 @@ interface AgentWriteResult {
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function applyAgentPlan(plan: readonly AgentPlanStep[], io: AgentWriterIo): AgentWriteResult[];
|
|
36
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_verify { action: \"change\", files: [...] }`\n- \"does this known journey still work?\" -> `reticle_verify { action: \"flows\", names: [\"...\"] }`\n- \"does this new behaviour work?\" -> `reticle_act { steps: [...] }` for the setup, then ONE `reticle_act_and_wait`\n- \"nothing is recorded yet\" ->
|
|
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_verify { action: \"change\", files: [...] }`\n- \"does this known journey still work?\" -> `reticle_verify { action: \"flows\", names: [\"...\"] }`\n- \"does this new behaviour work?\" -> `reticle_act { steps: [...] }` for the setup, then ONE `reticle_act_and_wait`\n- \"nothing is recorded yet\" -> drive the journey yourself with `reticle_act_and_wait` and an `until` on its\n last step; it is saved as a flow, so the next run replays with no model in the loop. With a linked\n project, `reticle_verify { action: \"explore\", persona: \"<who does what>\" }` drives it for you\n";
|
|
38
38
|
interface SkillWriteResult {
|
|
39
39
|
readonly id: string;
|
|
40
40
|
readonly file: string;
|
|
@@ -84,8 +84,9 @@ The cheapest path that answers the question:
|
|
|
84
84
|
- "did my edit break anything?" -> \`reticle_verify { action: "change", files: [...] }\`
|
|
85
85
|
- "does this known journey still work?" -> \`reticle_verify { action: "flows", names: ["..."] }\`
|
|
86
86
|
- "does this new behaviour work?" -> \`reticle_act { steps: [...] }\` for the setup, then ONE \`reticle_act_and_wait\`
|
|
87
|
-
- "nothing is recorded yet" ->
|
|
88
|
-
|
|
87
|
+
- "nothing is recorded yet" -> drive the journey yourself with \`reticle_act_and_wait\` and an \`until\` on its
|
|
88
|
+
last step; it is saved as a flow, so the next run replays with no model in the loop. With a linked
|
|
89
|
+
project, \`reticle_verify { action: "explore", persona: "<who does what>" }\` drives it for you
|
|
89
90
|
`;
|
|
90
91
|
/**
|
|
91
92
|
* Write the skill for agents that have somewhere to put it, and only where they are installed.
|