@reticlehq/server 2.12.0 → 2.13.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/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 +60 -0
- package/dist/setup/dev-server-wait.js +65 -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 +187 -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
package/dist/cli.js
CHANGED
|
@@ -5,19 +5,24 @@ import { realpathSync } from 'node:fs';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { stateDirProblem } from './daemon/state-dir.js';
|
|
7
7
|
import { statusNextAction } from './cli/status-next-action.js';
|
|
8
|
-
import {
|
|
8
|
+
import { readDevServers } from './daemon/dev-servers.js';
|
|
9
|
+
import { hasConnectedBefore, hasProjectConnectedBefore } from './session/connection-memory.js';
|
|
9
10
|
import { attachStatusFields } from './mcp/attach-memory.js';
|
|
10
11
|
import { reticleStateHome } from './daemon/daemon.js';
|
|
11
12
|
import { handleMcp } from './cli/mcp-command.js';
|
|
12
|
-
import {
|
|
13
|
+
import { daemonSpawnArgs, daemonStartOptions } from './cli/daemon-start-options.js';
|
|
14
|
+
import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from './daemon/daemon-resolve.js';
|
|
13
15
|
import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
|
|
14
|
-
import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
|
|
16
|
+
import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv, devServersForProject, } from '@reticlehq/core';
|
|
15
17
|
import { loadDotEnv } from './telemetry/dev-repo.js';
|
|
18
|
+
import { licenseKeyFromEnvFiles } from './license/license-env.js';
|
|
19
|
+
import { LICENSE_KEY_ENV } from './license/license.js';
|
|
16
20
|
import { createNodeFileSystem } from './project/fs-port.js';
|
|
17
21
|
import { affectedSavedFlows } from './flows/flow-sources.js';
|
|
18
22
|
import { availableUpdate } from './update/update-nudge.js';
|
|
19
23
|
import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
|
|
20
|
-
import { startDaemon } from './index.js';
|
|
24
|
+
import { startDaemon, RETICLE_VERIFY_DEFAULT_PORT } from './index.js';
|
|
25
|
+
import { verifyEndpointMismatch } from './status-payload.js';
|
|
21
26
|
import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
|
|
22
27
|
import { SERVER_VERSION } from './version/server-version.js';
|
|
23
28
|
import { log } from './log.js';
|
|
@@ -34,12 +39,12 @@ import { handleVerify } from './cli/cli-verify.js';
|
|
|
34
39
|
import { runKill } from './cli/cli-kill.js';
|
|
35
40
|
import { summarizeHunt } from './hunt/hunt-report.js';
|
|
36
41
|
import { runInit } from './init/run.js';
|
|
37
|
-
import {
|
|
42
|
+
import { continueAfterInit } from './setup/init-runtime.js';
|
|
38
43
|
import { handleDoctor } from './cli/cli-doctor.js';
|
|
39
44
|
import { buildNodeIo } from './init/node-io.js';
|
|
40
45
|
import { describeLicense } from './license/license.js';
|
|
41
46
|
import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, } from './cli/cli-port.js';
|
|
42
|
-
import { DAEMON_INNER_COMMAND, PORT_FLAG,
|
|
47
|
+
import { DAEMON_INNER_COMMAND, PORT_FLAG, parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
|
|
43
48
|
import { handleFeedback, handleIdentify, handleTelemetry } from './telemetry/feedback-cli.js';
|
|
44
49
|
import { installDaemonTelemetry } from './telemetry/daemon-telemetry.js';
|
|
45
50
|
import { reportCliRun } from './telemetry/cli-telemetry.js';
|
|
@@ -55,14 +60,13 @@ function handleInit(parsed) {
|
|
|
55
60
|
dryRun: parsed.dryRun,
|
|
56
61
|
install: parsed.install,
|
|
57
62
|
...(parsed.app === undefined ? {} : { app: parsed.app }),
|
|
63
|
+
captureBodies: true === parsed.captureBodies,
|
|
58
64
|
// The outcome is reported by confirmInstall instead, once it knows whether an app connected —
|
|
59
65
|
// `init` writing files was never the same thing as `init` working (#269).
|
|
60
66
|
deferOutcome: true,
|
|
67
|
+
continuesToRuntime: true !== parsed.filesOnly && true !== parsed.dryRun,
|
|
61
68
|
}, io);
|
|
62
|
-
void
|
|
63
|
-
if (!result.ok)
|
|
64
|
-
process.exit(1);
|
|
65
|
-
});
|
|
69
|
+
void continueAfterInit(parsed, result, io, cwd);
|
|
66
70
|
}
|
|
67
71
|
function handleServe(parsed) {
|
|
68
72
|
void serveWithHonestExit(parsed);
|
|
@@ -84,6 +88,20 @@ async function serveWithHonestExit(parsed) {
|
|
|
84
88
|
status: fetchStatus,
|
|
85
89
|
});
|
|
86
90
|
if (presence === PortPresence.DAEMON) {
|
|
91
|
+
// A daemon that is already up was started with ITS flags, not these — `serve` only attaches.
|
|
92
|
+
// Exiting 0 here regardless is how `--http-port` came to be accepted and ignored (#687): the
|
|
93
|
+
// running daemon kept serving whatever it was started with, and the flag vanished without a
|
|
94
|
+
// word. Ask the daemon which verify port it actually serves and refuse when it is not the one
|
|
95
|
+
// requested — a flag that cannot be honoured must say so, not report success.
|
|
96
|
+
if (parsed.http) {
|
|
97
|
+
const mismatch = verifyEndpointMismatch(await fetchStatus(parsed.port), parsed.httpPort ?? RETICLE_VERIFY_DEFAULT_PORT);
|
|
98
|
+
if (mismatch !== undefined) {
|
|
99
|
+
log('reticle_daemon_start_refused', { port: parsed.port, reason: mismatch });
|
|
100
|
+
process.stderr.write(`${mismatch}\n`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
87
105
|
log('reticle_daemon_already_running', { port: parsed.port });
|
|
88
106
|
return;
|
|
89
107
|
}
|
|
@@ -103,19 +121,7 @@ async function serveWithHonestExit(parsed) {
|
|
|
103
121
|
process.exit(1);
|
|
104
122
|
return;
|
|
105
123
|
}
|
|
106
|
-
const daemonArgs =
|
|
107
|
-
if (parsed.driveUrl !== undefined) {
|
|
108
|
-
daemonArgs.push(DRIVE_FLAG, parsed.driveUrl);
|
|
109
|
-
if (!parsed.headless)
|
|
110
|
-
daemonArgs.push(HEADED_FLAG);
|
|
111
|
-
}
|
|
112
|
-
if (parsed.http) {
|
|
113
|
-
daemonArgs.push(HTTP_FLAG);
|
|
114
|
-
if (parsed.httpPort !== undefined)
|
|
115
|
-
daemonArgs.push(HTTP_PORT_FLAG, String(parsed.httpPort));
|
|
116
|
-
if (parsed.httpToken !== undefined)
|
|
117
|
-
daemonArgs.push(HTTP_TOKEN_FLAG, parsed.httpToken);
|
|
118
|
-
}
|
|
124
|
+
const daemonArgs = daemonSpawnArgs(parsed);
|
|
119
125
|
spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
|
|
120
126
|
// The child binds asynchronously and, when it cannot, exits 1 long after this process would have
|
|
121
127
|
// reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
|
|
@@ -232,7 +238,20 @@ function withNextAction(facts) {
|
|
|
232
238
|
const next = statusNextAction(facts);
|
|
233
239
|
return next === undefined ? {} : { nextAction: next };
|
|
234
240
|
}
|
|
235
|
-
|
|
241
|
+
/**
|
|
242
|
+
* What `status` says about a project whose daemons have split in two.
|
|
243
|
+
*
|
|
244
|
+
* Both halves in one place because they are one condition asked from two positions, and a command
|
|
245
|
+
* can be standing on either. Silent — no key at all — when there is nothing to report, so a healthy
|
|
246
|
+
* run reads exactly as it did.
|
|
247
|
+
*/
|
|
248
|
+
function splitBrainFields(port, projectId) {
|
|
249
|
+
const home = reticleStateHome();
|
|
250
|
+
const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)));
|
|
251
|
+
const note = elsewhere ?? wrongDaemonNote(port, resolveDaemonForProject(projectId, home, isAlive));
|
|
252
|
+
return note === undefined ? {} : { splitBrain: note };
|
|
253
|
+
}
|
|
254
|
+
export async function handleStatus(port) {
|
|
236
255
|
const pid = readPid(port);
|
|
237
256
|
// Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
|
|
238
257
|
const projectId = readProjectId(process.cwd());
|
|
@@ -245,51 +264,90 @@ function handleStatus(port) {
|
|
|
245
264
|
// Reticle and never lists its tools looks exactly like an abandoned install from here — same idle
|
|
246
265
|
// daemon, same zero sessions — and the user sees a tool that does nothing. See attach-memory.ts.
|
|
247
266
|
const client = attachStatusFields(reticleStateHome(), port);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
// What the dev servers themselves said. The reason this command could previously only guess at
|
|
268
|
+
// whether the app was running is that it had no way to see one; now the ones carrying Reticle
|
|
269
|
+
// announce, and the advice below stops contradicting the terminal the reader is looking at.
|
|
270
|
+
// Scoped to THIS project: the registry is machine-wide, and an unrelated app's dev server must
|
|
271
|
+
// not make this project look like it is running.
|
|
272
|
+
const devServerPorts = devServersForProject(readDevServers(reticleStateHome()), {
|
|
273
|
+
projectId,
|
|
274
|
+
root: process.cwd(),
|
|
275
|
+
}).map((d) => d.port);
|
|
276
|
+
// The failure where every individual check is green and the chain is broken: the agent's proxy and
|
|
277
|
+
// the app resolved their ports independently and landed on two different daemons. Reported from
|
|
278
|
+
// BOTH stances, because neither one can see it alone.
|
|
279
|
+
const split = splitBrainFields(port, projectId);
|
|
280
|
+
// Doctor and status must answer the same liveness question. A live pid only says that some
|
|
281
|
+
// process exists; a Reticle daemon is running only when the shared port probe reaches /status.
|
|
282
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
283
|
+
if (!presenceIsUsable(presence)) {
|
|
284
|
+
log('reticle_status', {
|
|
285
|
+
port,
|
|
286
|
+
running: false,
|
|
287
|
+
presence,
|
|
288
|
+
...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
|
|
289
|
+
// `init` promises this command says why the app has not connected. Without it the answer was
|
|
290
|
+
// `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
|
|
291
|
+
// just a daemon that has not been asked to do anything yet.
|
|
292
|
+
...withNextAction({
|
|
293
|
+
running: false,
|
|
294
|
+
sessionCount: 0,
|
|
295
|
+
previouslyConnected,
|
|
296
|
+
initialized,
|
|
297
|
+
devServerPorts,
|
|
298
|
+
}),
|
|
299
|
+
...client,
|
|
300
|
+
...split,
|
|
264
301
|
});
|
|
265
302
|
return;
|
|
266
303
|
}
|
|
267
|
-
// The daemon
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
// The daemon answered the same probe doctor trusts — ask it for live sessions + health.
|
|
305
|
+
const payload = await fetchStatus(port);
|
|
306
|
+
// `status` is the second-most-run command and the one a HUMAN types. The update nudge otherwise
|
|
307
|
+
// only rides a tool result, which the majority of daemons never produce — so the people most in
|
|
308
|
+
// need of an upgrade were the ones with no path to hearing about it.
|
|
309
|
+
const update = availableUpdate();
|
|
310
|
+
const nudge = update === undefined ? {} : { updateAvailable: update };
|
|
311
|
+
if (payload === undefined) {
|
|
312
|
+
log('reticle_status', {
|
|
313
|
+
port,
|
|
314
|
+
running: true,
|
|
315
|
+
pid,
|
|
316
|
+
...nudge,
|
|
317
|
+
...withNextAction({
|
|
277
318
|
running: true,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
319
|
+
sessionCount: 0,
|
|
320
|
+
previouslyConnected,
|
|
321
|
+
initialized,
|
|
322
|
+
devServerPorts,
|
|
323
|
+
}),
|
|
324
|
+
...client,
|
|
325
|
+
...split,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const summary = summarizeStatus(payload);
|
|
330
|
+
// Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
|
|
331
|
+
// puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
|
|
332
|
+
// answers pointing different ways, which is worse than one.
|
|
333
|
+
const next = summary.why === undefined
|
|
334
|
+
? withNextAction({
|
|
335
|
+
running: true,
|
|
336
|
+
...summary,
|
|
337
|
+
previouslyConnected,
|
|
338
|
+
initialized,
|
|
339
|
+
devServerPorts,
|
|
340
|
+
})
|
|
341
|
+
: {};
|
|
342
|
+
log('reticle_status', {
|
|
343
|
+
port,
|
|
344
|
+
running: true,
|
|
345
|
+
pid,
|
|
346
|
+
...summary,
|
|
347
|
+
...next,
|
|
348
|
+
...client,
|
|
349
|
+
...split,
|
|
350
|
+
...nudge,
|
|
293
351
|
});
|
|
294
352
|
}
|
|
295
353
|
/** Print the running package version (resolved once in server-version.ts). */
|
|
@@ -492,25 +550,13 @@ function handleOpen(requestedPort, url) {
|
|
|
492
550
|
});
|
|
493
551
|
}
|
|
494
552
|
function handleDaemonInner(parsed) {
|
|
495
|
-
const options =
|
|
496
|
-
port: parsed.port,
|
|
497
|
-
...(parsed.driveUrl !== undefined
|
|
498
|
-
? { driveUrl: parsed.driveUrl, headless: parsed.headless }
|
|
499
|
-
: {}),
|
|
500
|
-
...(parsed.http
|
|
501
|
-
? {
|
|
502
|
-
httpVerify: true,
|
|
503
|
-
...(parsed.httpPort !== undefined ? { httpVerifyPort: parsed.httpPort } : {}),
|
|
504
|
-
...(parsed.httpToken !== undefined ? { httpVerifyToken: parsed.httpToken } : {}),
|
|
505
|
-
}
|
|
506
|
-
: {}),
|
|
507
|
-
};
|
|
553
|
+
const options = daemonStartOptions(parsed);
|
|
508
554
|
startDaemon(options)
|
|
509
555
|
.then((server) => {
|
|
510
556
|
log('reticle_daemon_ready', { port: parsed.port, pid: process.pid });
|
|
511
557
|
// Daemon lifecycle telemetry: started, a one-shot project profile, the periodic counter flush,
|
|
512
558
|
// and the rich summary on shutdown. All of it lives in one module — see daemon-telemetry.ts.
|
|
513
|
-
const daemonTelemetry = installDaemonTelemetry(process.cwd());
|
|
559
|
+
const daemonTelemetry = installDaemonTelemetry(process.cwd(), undefined, parsed.port);
|
|
514
560
|
// Publish to the discovery registry so a build plugin can find this daemon by projectId — no
|
|
515
561
|
// hand-reconciled port. Written from the child (only it knows its cwd); removePid drops it.
|
|
516
562
|
const registryProjectId = readProjectId(process.cwd());
|
|
@@ -607,6 +653,14 @@ function main() {
|
|
|
607
653
|
// Before anything reads process.env — notably the telemetry gate and the bridge's security
|
|
608
654
|
// options — fold in a project-local `.env`. Values already in the environment always win.
|
|
609
655
|
loadDotEnv(process.cwd());
|
|
656
|
+
// The licence key specifically is searched for HARDER than the rest of the environment, because
|
|
657
|
+
// the daemon is spawned without an explicit cwd and inherits the editor's. A key in the app's own
|
|
658
|
+
// `.env` — or in the repo root when the editor started inside the app — was never read, and the
|
|
659
|
+
// customer then produced events that said `missing`, which is indistinguishable from having no
|
|
660
|
+
// licence at all. Only the key is taken; see license-env.ts for why nothing else is.
|
|
661
|
+
const licenseKey = licenseKeyFromEnvFiles(process.cwd());
|
|
662
|
+
if (licenseKey !== undefined)
|
|
663
|
+
process.env[LICENSE_KEY_ENV] = licenseKey;
|
|
610
664
|
const argv = process.argv.slice(2);
|
|
611
665
|
// Every invocation passes through here — the single chokepoint for the "how often is it used / how
|
|
612
666
|
// many distinct machines + projects" metrics. Fire-and-forget: a metric must never delay or fail a run.
|
|
@@ -677,7 +731,7 @@ function main() {
|
|
|
677
731
|
void handleRestart(parsed.port, parsed.force);
|
|
678
732
|
break;
|
|
679
733
|
case 'status':
|
|
680
|
-
handleStatus(parsed.port);
|
|
734
|
+
void handleStatus(parsed.port);
|
|
681
735
|
break;
|
|
682
736
|
case 'license':
|
|
683
737
|
handleLicense();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { FileSystemPort } from '../project/fs-port.js';
|
|
2
2
|
import { type CloudConfig } from './cloud-sync.js';
|
|
3
3
|
/** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
|
|
4
|
-
|
|
4
|
+
/** Re-exported so the existing importers keep working; the string itself lives in core. */
|
|
5
|
+
export declare const CLOUD_LINK_FILE: "cloud.json";
|
|
5
6
|
/** `~/.reticle/credentials.json` — the user's per-cloud-project API keys (the only secret). */
|
|
6
7
|
export declare const CREDENTIALS_FILE = "credentials.json";
|
|
7
8
|
/** What the daemon mirrors to the cloud for a project. Each surface is independently toggleable. */
|
|
@@ -27,6 +28,26 @@ export interface ProjectCloud {
|
|
|
27
28
|
/** The cloud project id this project is linked to (for display/logging); null when env-fallback. */
|
|
28
29
|
projectId: string | null;
|
|
29
30
|
}
|
|
31
|
+
/** Look up the API key for a cloud project id in the user credentials map. */
|
|
32
|
+
/**
|
|
33
|
+
* The API key this machine holds for one project ON ONE CLOUD.
|
|
34
|
+
*
|
|
35
|
+
* The store used to be keyed by cloud project id ALONE, which collides in the case that is not rare
|
|
36
|
+
* at all — it is the default. `reticle link` names every project "default", so a repo linked to a
|
|
37
|
+
* self-hosted install and a repo linked to the hosted service both claim the slot `default`, and
|
|
38
|
+
* whichever was linked last wins. Measured: two repos on one machine, and the production key was
|
|
39
|
+
* being sent to a localhost server, which answered 401.
|
|
40
|
+
*
|
|
41
|
+
* That is the same class of defect as sending a session token to a host that did not issue it, and
|
|
42
|
+
* it deserves the same answer: a credential is scoped to the cloud that minted it, and no credential
|
|
43
|
+
* at all is better than one belonging to somewhere else.
|
|
44
|
+
*
|
|
45
|
+
* Two shapes are accepted. `{ key, url }` is what is written now and is checked against the URL
|
|
46
|
+
* being dialled. A bare string is the legacy shape, has no URL to check, and is still honoured —
|
|
47
|
+
* refusing it would silently unlink every repo that predates this change, which is a worse outage
|
|
48
|
+
* than the collision. Re-linking upgrades a repo to the safe shape.
|
|
49
|
+
*/
|
|
50
|
+
export declare const credentialSlot: (url: string, projectId: string, orgId?: string) => string;
|
|
30
51
|
/**
|
|
31
52
|
* Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
|
|
32
53
|
* user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
|
|
@@ -13,7 +13,8 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { ReticleDir } from '@reticlehq/core';
|
|
14
14
|
import { resolveCloudConfig } from './cloud-sync.js';
|
|
15
15
|
/** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
|
|
16
|
-
|
|
16
|
+
/** Re-exported so the existing importers keep working; the string itself lives in core. */
|
|
17
|
+
export const CLOUD_LINK_FILE = ReticleDir.CLOUD_LINK_FILE;
|
|
17
18
|
/** `~/.reticle/credentials.json` — the user's per-cloud-project API keys (the only secret). */
|
|
18
19
|
export const CREDENTIALS_FILE = 'credentials.json';
|
|
19
20
|
const DEFAULT_SYNC_POLICY = { runs: true, memory: true, flows: true };
|
|
@@ -43,6 +44,7 @@ function parseLink(raw) {
|
|
|
43
44
|
const sync = 'object' === typeof o.sync && o.sync !== null ? o.sync : {};
|
|
44
45
|
return {
|
|
45
46
|
projectId: o.projectId,
|
|
47
|
+
orgId: 'string' === typeof o.orgId && o.orgId.length > 0 ? o.orgId : undefined,
|
|
46
48
|
url: o.url,
|
|
47
49
|
sync: {
|
|
48
50
|
runs: asBool(sync.runs, DEFAULT_SYNC_POLICY.runs),
|
|
@@ -53,12 +55,69 @@ function parseLink(raw) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
/** Look up the API key for a cloud project id in the user credentials map. */
|
|
56
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The API key this machine holds for one project ON ONE CLOUD.
|
|
60
|
+
*
|
|
61
|
+
* The store used to be keyed by cloud project id ALONE, which collides in the case that is not rare
|
|
62
|
+
* at all — it is the default. `reticle link` names every project "default", so a repo linked to a
|
|
63
|
+
* self-hosted install and a repo linked to the hosted service both claim the slot `default`, and
|
|
64
|
+
* whichever was linked last wins. Measured: two repos on one machine, and the production key was
|
|
65
|
+
* being sent to a localhost server, which answered 401.
|
|
66
|
+
*
|
|
67
|
+
* That is the same class of defect as sending a session token to a host that did not issue it, and
|
|
68
|
+
* it deserves the same answer: a credential is scoped to the cloud that minted it, and no credential
|
|
69
|
+
* at all is better than one belonging to somewhere else.
|
|
70
|
+
*
|
|
71
|
+
* Two shapes are accepted. `{ key, url }` is what is written now and is checked against the URL
|
|
72
|
+
* being dialled. A bare string is the legacy shape, has no URL to check, and is still honoured —
|
|
73
|
+
* refusing it would silently unlink every repo that predates this change, which is a worse outage
|
|
74
|
+
* than the collision. Re-linking upgrades a repo to the safe shape.
|
|
75
|
+
*/
|
|
76
|
+
export const credentialSlot = (url, projectId, orgId) => orgId === undefined || 0 === orgId.length
|
|
77
|
+
? `${normalizeCloudUrl(url)}::${projectId}`
|
|
78
|
+
: `${normalizeCloudUrl(url)}::org::${orgId}::${projectId}`;
|
|
79
|
+
function credentialFor(raw, projectId, url, orgId) {
|
|
57
80
|
if (typeof raw !== 'object' || null === raw)
|
|
58
81
|
return null;
|
|
59
|
-
const
|
|
60
|
-
|
|
82
|
+
const store = raw;
|
|
83
|
+
/*
|
|
84
|
+
* The ORG slot first, because cloud + project is still not an identity.
|
|
85
|
+
*
|
|
86
|
+
* `link` names every project "default", so two ACCOUNTS on one cloud both claimed the slot
|
|
87
|
+
* `<url>::default` and the last link won. Measured on a laptop: a brand-new workspace ran
|
|
88
|
+
* `reticle login` and was handed a stored key belonging to a different organisation — valid, so
|
|
89
|
+
* every validation passed, and every run it pushed would have landed in a stranger's dashboard.
|
|
90
|
+
* Keying by cloud fixed two clouds colliding; only keying by ORG fixes two tenants colliding.
|
|
91
|
+
*/
|
|
92
|
+
const orgScoped = orgId === undefined ? undefined : store[credentialSlot(url, projectId, orgId)];
|
|
93
|
+
if ('string' === typeof orgScoped && orgScoped.length > 0)
|
|
94
|
+
return orgScoped;
|
|
95
|
+
/*
|
|
96
|
+
* Then the cloud+project slot, which is what every repo linked before this change still holds. It
|
|
97
|
+
* is ambiguous between tenants by construction, which is why it is consulted second and why the
|
|
98
|
+
* writer above stopped producing it as the only entry.
|
|
99
|
+
*/
|
|
100
|
+
const composite = store[credentialSlot(url, projectId)];
|
|
101
|
+
if ('string' === typeof composite && composite.length > 0)
|
|
102
|
+
return composite;
|
|
103
|
+
const entry = store[projectId];
|
|
104
|
+
if ('string' === typeof entry)
|
|
105
|
+
return entry.length > 0 ? entry : null;
|
|
106
|
+
if (typeof entry !== 'object' || null === entry)
|
|
107
|
+
return null;
|
|
108
|
+
const record = entry;
|
|
109
|
+
const key = record['key'];
|
|
110
|
+
const forUrl = record['url'];
|
|
111
|
+
if ('string' !== typeof key || 0 === key.length)
|
|
112
|
+
return null;
|
|
113
|
+
// A credential stamped with a DIFFERENT cloud is not this project's credential, however much the
|
|
114
|
+
// project ids match. Refuse rather than dial somewhere with somebody else's key.
|
|
115
|
+
if ('string' === typeof forUrl && normalizeCloudUrl(forUrl) !== normalizeCloudUrl(url))
|
|
116
|
+
return null;
|
|
117
|
+
return key;
|
|
61
118
|
}
|
|
119
|
+
/** Trailing slashes are not identity: `https://x/` and `https://x` are one cloud. */
|
|
120
|
+
const normalizeCloudUrl = (url) => url.replace(/\/+$/, '');
|
|
62
121
|
/**
|
|
63
122
|
* Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
|
|
64
123
|
* user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
|
|
@@ -80,7 +139,7 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
|
|
|
80
139
|
memory: link.sync.memory ?? DEFAULT_SYNC_POLICY.memory,
|
|
81
140
|
flows: link.sync.flows ?? DEFAULT_SYNC_POLICY.flows,
|
|
82
141
|
};
|
|
83
|
-
const key = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId);
|
|
142
|
+
const key = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId, link.url, link.orgId);
|
|
84
143
|
const config = key !== null ? { url: link.url.replace(/\/+$/, ''), apiKey: key } : null;
|
|
85
144
|
return { config, policy, verify: link.verify, projectId: link.projectId };
|
|
86
145
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
|
|
3
|
-
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
3
|
+
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
4
4
|
* dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
|
|
5
5
|
* suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
|
|
6
6
|
* everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
|
|
@@ -115,21 +115,21 @@ declare const ServerVerificationSchema: z.ZodObject<{
|
|
|
115
115
|
}>, "many">;
|
|
116
116
|
summary: z.ZodString;
|
|
117
117
|
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
verdict: string | null;
|
|
118
119
|
flows: {
|
|
119
120
|
status: string;
|
|
120
121
|
name: string;
|
|
121
122
|
}[];
|
|
122
|
-
verificationId: string;
|
|
123
|
-
verdict: string | null;
|
|
124
123
|
summary: string;
|
|
124
|
+
verificationId: string;
|
|
125
125
|
}, {
|
|
126
|
+
verdict: string | null;
|
|
126
127
|
flows: {
|
|
127
128
|
status: string;
|
|
128
129
|
name: string;
|
|
129
130
|
}[];
|
|
130
|
-
verificationId: string;
|
|
131
|
-
verdict: string | null;
|
|
132
131
|
summary: string;
|
|
132
|
+
verificationId: string;
|
|
133
133
|
}>;
|
|
134
134
|
export type ServerVerification = z.infer<typeof ServerVerificationSchema>;
|
|
135
135
|
/**
|
package/dist/cloud/cloud-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional cloud sync for saved flows. "Logged in" here means the two cloud env vars are set (written by
|
|
3
|
-
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
3
|
+
* `reticle login` later; settable by hand today): the hosted URL and an API key from the Reticle
|
|
4
4
|
* dashboard. When present, a freshly-saved flow is pushed to `POST /v1/flows` so the team's regression
|
|
5
5
|
* suite lives in the cloud — surviving refactors and runnable in CI. When absent, sync is a no-op and
|
|
6
6
|
* everything stays 100% local (the "no phone-home" default: nothing leaves the machine unless you opt in).
|
|
@@ -44,18 +44,49 @@ const CLOUD_FETCH_TIMEOUT_MS = 30_000;
|
|
|
44
44
|
export const CLOUD_VERIFY_TIMEOUT_MS = 120_000;
|
|
45
45
|
/** Names Node gives an abort: `AbortSignal.timeout` raises TimeoutError, an explicit abort AbortError. */
|
|
46
46
|
const TIMEOUT_ERROR_NAMES = new Set(['TimeoutError', 'AbortError']);
|
|
47
|
+
/**
|
|
48
|
+
* A header value `fetch` can actually send. Anything above U+00FF is not a valid ByteString.
|
|
49
|
+
*
|
|
50
|
+
* The one that happens in practice is an ellipsis. A dashboard masks a key as `rk_live_…` so it is
|
|
51
|
+
* never shown in full, somebody copies what they can SEE into `.env`, and the request dies deep
|
|
52
|
+
* inside undici with "Cannot convert argument to a ByteString because the character at index 15 has
|
|
53
|
+
* a value of 8230" — a byte offset into a header the user never wrote, naming no variable, no file
|
|
54
|
+
* and no fix.
|
|
55
|
+
*/
|
|
56
|
+
const UNSENDABLE_HEADER_CHAR = /[^ -ÿ]/;
|
|
57
|
+
/** Which knob holds the credential behind a header, so the error can name the thing to go and edit. */
|
|
58
|
+
const CREDENTIAL_SOURCE = {
|
|
59
|
+
authorization: `${CloudEnv.KEY} (or the session from \`reticle login\`)`,
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Refuse a request whose headers cannot be encoded, before it is sent.
|
|
63
|
+
*
|
|
64
|
+
* Before dialling on purpose: a credential that cannot be encoded cannot succeed, so the round trip
|
|
65
|
+
* buys only a slower and less specific failure.
|
|
66
|
+
*/
|
|
67
|
+
function assertSendableHeaders(headers) {
|
|
68
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
69
|
+
if (!UNSENDABLE_HEADER_CHAR.test(value))
|
|
70
|
+
continue;
|
|
71
|
+
const source = CREDENTIAL_SOURCE[name];
|
|
72
|
+
throw new Error(`the ${name} header carries a character that cannot be sent, which means the value is truncated — ` +
|
|
73
|
+
`a credential displayed masked as \`rk_live_…\` keeps that ellipsis when it is copied. ` +
|
|
74
|
+
`Paste the full value${source === undefined ? '' : ` into ${source}`}.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
47
77
|
/**
|
|
48
78
|
* The fetch every cloud call goes through. Adds the abort signal, and turns the abort into a message
|
|
49
79
|
* that says what happened and what to do — a bare `AbortError` reaching a user or an agent is a riddle,
|
|
50
80
|
* and the agent-facing half of this product is judged on whether its errors are actionable.
|
|
51
81
|
*/
|
|
52
82
|
export async function cloudFetch(url, init, timeoutMs = CLOUD_FETCH_TIMEOUT_MS) {
|
|
83
|
+
assertSendableHeaders(init.headers);
|
|
53
84
|
try {
|
|
54
85
|
return await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
55
86
|
}
|
|
56
87
|
catch (err) {
|
|
57
88
|
if (err instanceof Error && TIMEOUT_ERROR_NAMES.has(err.name)) {
|
|
58
|
-
throw new Error(`Reticle
|
|
89
|
+
throw new Error(`Reticle request timed out after ${Math.round(timeoutMs / 1000)}s: ${init.method} ${url}. ` +
|
|
59
90
|
`The server accepted the connection but never answered. Check the network and ${CloudEnv.URL}, then retry — ` +
|
|
60
91
|
`verification works locally without cloud.`);
|
|
61
92
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The derived records that ride along with runs, and the file each one lives in.
|
|
3
|
+
*
|
|
4
|
+
* A list rather than three hand-written blocks so adding a fourth is one line and cannot be
|
|
5
|
+
* half-done — the bundle, the hashing and the reporting all walk this.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DERIVED_RECORDS: readonly [{
|
|
8
|
+
readonly kind: "impact";
|
|
9
|
+
readonly file: "impact.json";
|
|
10
|
+
}, {
|
|
11
|
+
readonly kind: "flake";
|
|
12
|
+
readonly file: "flake.json";
|
|
13
|
+
}, {
|
|
14
|
+
readonly kind: "intent";
|
|
15
|
+
readonly file: "intent.json";
|
|
16
|
+
}];
|
|
17
|
+
export type DerivedKind = (typeof DERIVED_RECORDS)[number]['kind'];
|
|
18
|
+
/** What the machine reads from disk. Injected so a cycle is testable with no filesystem at all. */
|
|
19
|
+
export interface SyncSource {
|
|
20
|
+
/** Every run artifact currently on disk. */
|
|
21
|
+
runs: () => ReadonlyArray<{
|
|
22
|
+
runId: string;
|
|
23
|
+
payload: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
/** Every saved flow. Small, upserted by name, so they ride along whenever anything else does. */
|
|
26
|
+
flows: () => readonly unknown[];
|
|
27
|
+
/** One derived record, or undefined when the file is absent. */
|
|
28
|
+
derived: (kind: DerivedKind) => unknown;
|
|
29
|
+
}
|
|
30
|
+
/** What the machine writes back. Separated from reads so a dry run is a source with no sink. */
|
|
31
|
+
export interface SyncSink {
|
|
32
|
+
/** Persist the triage decisions pulled from the dashboard. */
|
|
33
|
+
writeIssues: (issues: PulledIssues) => void;
|
|
34
|
+
/** Persist the cursor and the bookkeeping. */
|
|
35
|
+
writeState: (state: CloudSyncState) => void;
|
|
36
|
+
}
|
|
37
|
+
/** A decision a human made on the dashboard, as the machine stores it. */
|
|
38
|
+
export interface PulledIssue {
|
|
39
|
+
status: string;
|
|
40
|
+
flowName: string | null;
|
|
41
|
+
title: string;
|
|
42
|
+
at: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PulledIssues {
|
|
45
|
+
/** Keyed by the server's fingerprint — stable across runs, which is the point of it. */
|
|
46
|
+
triage: Record<string, PulledIssue>;
|
|
47
|
+
}
|
|
48
|
+
/** `.reticle/cloud-state.json`. This machine's side of the conversation, never git-checked. */
|
|
49
|
+
export interface CloudSyncState {
|
|
50
|
+
/** Opaque. Handed back to the server verbatim; the machine must never parse or invent one. */
|
|
51
|
+
cursor?: string;
|
|
52
|
+
lastPushAt?: number;
|
|
53
|
+
lastPullAt?: number;
|
|
54
|
+
/** The last failure, kept so `reticle sync` can say why it is behind instead of just "0 sent". */
|
|
55
|
+
lastError?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface SyncReport {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
/** Runs the server accepted this cycle. */
|
|
60
|
+
runsSent: number;
|
|
61
|
+
/** Runs it refused, with the reason, so a bad artifact is visible rather than silently stuck. */
|
|
62
|
+
runsRejected: Array<{
|
|
63
|
+
index: number;
|
|
64
|
+
reason: string;
|
|
65
|
+
}>;
|
|
66
|
+
flowsSent: number;
|
|
67
|
+
/** Which derived records had actually moved. Empty on a quiet cycle, which is the normal case. */
|
|
68
|
+
derivedSent: DerivedKind[];
|
|
69
|
+
/** Decisions collected from the dashboard. */
|
|
70
|
+
pulled: number;
|
|
71
|
+
/** True when the pull page was full — call again now rather than waiting for the next tick. */
|
|
72
|
+
morePending: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The repo holds NO artifacts at all — no runs, no flows, no derived records.
|
|
75
|
+
*
|
|
76
|
+
* Distinct from "everything here is already pushed", which is the healthy steady state and looks
|
|
77
|
+
* identical from the outside. An empty repo usually means the app announces no projectId, so its
|
|
78
|
+
* runs are pooling into a different root and this binding will never report anything. Tracked so
|
|
79
|
+
* the summary can tell a user which of the two they are looking at.
|
|
80
|
+
*/
|
|
81
|
+
localIsEmpty?: boolean;
|
|
82
|
+
/** Set when the cycle could not complete. The local record is untouched either way. */
|
|
83
|
+
error?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface SyncDeps {
|
|
86
|
+
config: {
|
|
87
|
+
url: string;
|
|
88
|
+
apiKey: string;
|
|
89
|
+
};
|
|
90
|
+
source: SyncSource;
|
|
91
|
+
sink: SyncSink;
|
|
92
|
+
state: CloudSyncState;
|
|
93
|
+
now: () => number;
|
|
94
|
+
/** Injected so a cycle can be driven against a scripted server with no network. */
|
|
95
|
+
request: (url: string, init: {
|
|
96
|
+
method: string;
|
|
97
|
+
headers: Record<string, string>;
|
|
98
|
+
body?: string;
|
|
99
|
+
}) => Promise<{
|
|
100
|
+
status: number;
|
|
101
|
+
text: string;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run one cycle. Never throws: the local record is already safe on disk, and a sync that can take
|
|
106
|
+
* down the thing it is backing up is worse than no sync.
|
|
107
|
+
*/
|
|
108
|
+
export declare function runSyncCycle(deps: SyncDeps): Promise<SyncReport>;
|
|
109
|
+
/** One line a human can read, for the CLI and the daemon log. */
|
|
110
|
+
export declare function describeSync(report: SyncReport): string;
|