@delegance/claude-autopilot 6.2.2 → 7.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +841 -0
  2. package/README.md +10 -1
  3. package/bin/_launcher.js +38 -23
  4. package/dist/src/cli/autopilot.d.ts +4 -0
  5. package/dist/src/cli/autopilot.js +15 -0
  6. package/dist/src/cli/dashboard/index.d.ts +5 -0
  7. package/dist/src/cli/dashboard/index.js +49 -0
  8. package/dist/src/cli/dashboard/login.d.ts +22 -0
  9. package/dist/src/cli/dashboard/login.js +260 -0
  10. package/dist/src/cli/dashboard/logout.d.ts +12 -0
  11. package/dist/src/cli/dashboard/logout.js +45 -0
  12. package/dist/src/cli/dashboard/status.d.ts +30 -0
  13. package/dist/src/cli/dashboard/status.js +65 -0
  14. package/dist/src/cli/dashboard/upload.d.ts +16 -0
  15. package/dist/src/cli/dashboard/upload.js +48 -0
  16. package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
  17. package/dist/src/cli/engine-flag-deprecation.js +20 -0
  18. package/dist/src/cli/help-text.d.ts +1 -1
  19. package/dist/src/cli/help-text.js +44 -28
  20. package/dist/src/cli/index.d.ts +2 -1
  21. package/dist/src/cli/index.js +72 -17
  22. package/dist/src/cli/scaffold.d.ts +39 -0
  23. package/dist/src/cli/scaffold.js +287 -0
  24. package/dist/src/cli/setup.d.ts +30 -0
  25. package/dist/src/cli/setup.js +137 -0
  26. package/dist/src/core/run-state/events.js +10 -2
  27. package/dist/src/core/run-state/resolve-engine.d.ts +26 -81
  28. package/dist/src/core/run-state/resolve-engine.js +39 -155
  29. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +5 -9
  30. package/dist/src/core/run-state/run-phase-with-lifecycle.js +26 -19
  31. package/dist/src/core/run-state/state.d.ts +1 -1
  32. package/dist/src/core/run-state/types.d.ts +8 -2
  33. package/dist/src/core/run-state/types.js +8 -2
  34. package/dist/src/dashboard/auto-upload.d.ts +26 -0
  35. package/dist/src/dashboard/auto-upload.js +107 -0
  36. package/dist/src/dashboard/config.d.ts +22 -0
  37. package/dist/src/dashboard/config.js +109 -0
  38. package/dist/src/dashboard/upload/canonical.d.ts +3 -0
  39. package/dist/src/dashboard/upload/canonical.js +16 -0
  40. package/dist/src/dashboard/upload/chain.d.ts +9 -0
  41. package/dist/src/dashboard/upload/chain.js +27 -0
  42. package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
  43. package/dist/src/dashboard/upload/snapshot.js +66 -0
  44. package/dist/src/dashboard/upload/uploader.d.ts +54 -0
  45. package/dist/src/dashboard/upload/uploader.js +330 -0
  46. package/package.json +18 -3
  47. package/scripts/test-runner.mjs +4 -0
@@ -1,50 +1,30 @@
1
1
  // src/core/run-state/resolve-engine.ts
2
2
  //
3
- // v6.0.1pure precedence resolver for whether the Run State Engine should
4
- // run for a given CLI invocation. Spec: docs/specs/v6-run-state-engine.md
5
- // "Migration path (v5.6 v6) + precedence matrix" + docs/v6/migration-guide.md
6
- // "How to opt in".
3
+ // v7.0 — engine-off path retired. The function is preserved for source
4
+ // compatibility with callers that pass `cliEngine` / `envValue` /
5
+ // `configEnabled`, but it now returns `enabled: true` unconditionally.
7
6
  //
8
- // v6.1 update — built-in default flipped from `false` → `true` per
9
- // docs/specs/v6.1-default-flip.md. Users who opt out explicitly via
10
- // `--no-engine`, `CLAUDE_AUTOPILOT_ENGINE=off|false|0|no`, or
11
- // `engine.enabled: false` in `guardrail.config.yaml` keep the legacy
12
- // (engine-off) behavior, but they now receive a deprecation warning
13
- // the escape hatch goes away in v7. See `emitEngineOffDeprecationWarning`
14
- // below.
7
+ // What changed in v7.0 vs v6.x:
8
+ // - `ENGINE_DEFAULT_V6_0` and `ENGINE_DEFAULT_V6_1` exports REMOVED.
9
+ // Direct importers must replace with literal `true` (see
10
+ // docs/v7/breaking-changes.md).
11
+ // - The deprecation warning helpers (`emitEngineOffDeprecationWarning`
12
+ // / `shouldWarnEngineOffDeprecation` / `ENGINE_OFF_DEPRECATION_MESSAGE`)
13
+ // are RETAINED as no-op stubs so call sites don't have to change in
14
+ // the same PR — they always return false / never fire.
15
+ // - `parseEngineEnvValue()` is RETAINED for back-compat with any
16
+ // out-of-tree callers; `resolveEngineEnabled()` ignores the env
17
+ // value entirely (the engine-off env path is gone).
15
18
  //
16
- // Precedence (highest wins):
17
- // 1. CLI flag — `--engine` / `--no-engine`
18
- // 2. Env var — `CLAUDE_AUTOPILOT_ENGINE=on|off|true|false|1|0|yes|no`
19
- // 3. Config `engine.enabled: true|false` in guardrail.config.yaml
20
- // 4. Built-in default v6.1+: true (was false in v6.0)
21
- //
22
- // This module is intentionally pure and side-effect-free: it never reads from
23
- // the environment or the config file directly. Callers (the CLI dispatcher)
24
- // gather the inputs and pass them in — that keeps the function trivially
25
- // testable and lets the dispatcher own all I/O.
26
- //
27
- // Invalid env values do NOT throw. The contract from the spec / migration
28
- // guide is "treat as unset and emit a run.warning so observers can attribute
29
- // the fallthrough." This module returns metadata — the resolver caller
30
- // (cli/index.ts) is responsible for emitting the warning event.
31
- /** v6.1+ ships with the engine ON by default — flipped from the v6.0
32
- * default (`false`) per `docs/specs/v6.1-default-flip.md`. Exported so
33
- * tests / future releases can pin a known value. */
34
- export const ENGINE_DEFAULT_V6_1 = true;
35
- /** Historical v6.0 default. Preserved verbatim — its semantic meaning
36
- * ("the v6.0 default was off") doesn't change just because the active
37
- * default flipped. Out-of-tree consumers that pinned this constant get
38
- * the value the name promises. Use `ENGINE_DEFAULT_V6_1` for the active
39
- * default. Removed in v7.
40
- * @deprecated Use `ENGINE_DEFAULT_V6_1` or omit `builtInDefault` to inherit
41
- * the active default. */
42
- export const ENGINE_DEFAULT_V6_0 = false;
19
+ // Why keep the stub function shape: the CLI dispatcher passes
20
+ // `cliEngine` / `envEngine` / config to `runPhaseWithLifecycle`, which
21
+ // in turn calls `resolveEngineEnabled()`. Those parameters become
22
+ // effective no-ops in v7.0the values are observed (so a future PR
23
+ // can re-enable the path or surface a deprecation event) but never
24
+ // override the always-on result.
43
25
  /** Parse a stringly-typed env value into a tri-state boolean.
44
- * Accepts (case-insensitive): on, off, true, false, 1, 0, yes, no.
45
- * Returns undefined for any other input INCLUDING empty / whitespace-only
46
- * strings — that signals the caller to fall through to the next precedence
47
- * layer. */
26
+ * Retained for back-compat with any out-of-tree callers; the v7
27
+ * resolver does not consult env values. */
48
28
  export function parseEngineEnvValue(raw) {
49
29
  if (raw === undefined)
50
30
  return undefined;
@@ -66,125 +46,29 @@ export function parseEngineEnvValue(raw) {
66
46
  return undefined;
67
47
  }
68
48
  }
69
- /** Resolve whether the Run State Engine should run for this invocation.
70
- * Pure function does not touch process.env, fs, or anything I/O. */
71
- export function resolveEngineEnabled(opts = {}) {
72
- const { cliEngine, envValue, configEnabled, builtInDefault } = opts;
73
- const builtIn = builtInDefault ?? ENGINE_DEFAULT_V6_1;
74
- // Layer 1 — CLI flag wins outright.
75
- if (cliEngine === true) {
76
- return { enabled: true, source: 'cli', reason: '--engine flag' };
77
- }
78
- if (cliEngine === false) {
79
- return { enabled: false, source: 'cli', reason: '--no-engine flag' };
80
- }
81
- // Layer 2 — env var.
82
- if (envValue !== undefined && envValue.trim() !== '') {
83
- const parsed = parseEngineEnvValue(envValue);
84
- if (parsed !== undefined) {
85
- return {
86
- enabled: parsed,
87
- source: 'env',
88
- reason: `CLAUDE_AUTOPILOT_ENGINE=${envValue}`,
89
- };
90
- }
91
- // Invalid value — fall through, but record the raw value so the caller
92
- // can emit a run.warning. Continue to config / default below.
93
- // We bind it here so it survives the recursion-style fallthrough.
94
- return resolveWithFallthrough({
95
- configEnabled,
96
- builtIn,
97
- invalidEnvValue: envValue,
98
- });
99
- }
100
- return resolveWithFallthrough({ configEnabled, builtIn });
101
- }
102
- /** Layers 3 + 4 — config, then built-in default. Factored out so the env
103
- * invalid-value path can reach the same logic without recursing into
104
- * resolveEngineEnabled (which would re-evaluate the env var and loop). */
105
- function resolveWithFallthrough(opts) {
106
- const { configEnabled, builtIn, invalidEnvValue } = opts;
107
- const invalidSuffix = invalidEnvValue !== undefined
108
- ? `; invalid CLAUDE_AUTOPILOT_ENGINE=${JSON.stringify(invalidEnvValue)} ignored`
109
- : '';
110
- if (configEnabled === true) {
111
- return {
112
- enabled: true,
113
- source: 'config',
114
- reason: `engine.enabled: true in guardrail.config.yaml${invalidSuffix}`,
115
- ...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
116
- };
117
- }
118
- if (configEnabled === false) {
119
- return {
120
- enabled: false,
121
- source: 'config',
122
- reason: `engine.enabled: false in guardrail.config.yaml${invalidSuffix}`,
123
- ...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
124
- };
125
- }
49
+ /** v7.0+ engine is always on. Pure function; ignores all inputs.
50
+ * Source compatible with v6.x call sites. */
51
+ export function resolveEngineEnabled(_opts = {}) {
126
52
  return {
127
- enabled: builtIn,
53
+ enabled: true,
128
54
  source: 'default',
129
- reason: `built-in default (engine ${builtIn ? 'on' : 'off'} in v6.1+)${invalidSuffix}`,
130
- ...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
55
+ reason: 'v7.0+ engine always on (engine-off path removed)',
131
56
  };
132
57
  }
133
58
  // ---------------------------------------------------------------------------
134
- // v6.1 deprecation warning for explicit engine-off
59
+ // v6.1 deprecation helpers — retained as no-op stubs for source compat.
60
+ // v7.0 removed the engine-off path entirely; no warning ever fires.
135
61
  // ---------------------------------------------------------------------------
136
- /** Stable copy emitted on stderr when a user explicitly opts out of the
137
- * engine via `--no-engine`, `CLAUDE_AUTOPILOT_ENGINE=off`, or
138
- * `engine.enabled: false`. v7 removes the escape hatch entirely.
139
- *
140
- * Exported for tests + downstream consumers (e.g. CI parsers) that want to
141
- * match against the exact string. Kept on a single line so terminals don't
142
- * wrap mid-message. */
143
- export const ENGINE_OFF_DEPRECATION_MESSAGE = '[deprecation] --no-engine / engine.enabled: false will be removed in v7. Migrate to engine-on (default).';
144
- /** Decide whether v6.1's `--no-engine` deprecation warning applies for a
145
- * given resolver result. Returns `true` ONLY when the user explicitly
146
- * opted out (via CLI flag, env var, or config) — never on the v6.1 default
147
- * (which is `enabled: true`, so it can't trigger here anyway) and never
148
- * when the engine is actually on. Pure: takes the resolver result, returns
149
- * a boolean.
150
- *
151
- * Why this is a separate predicate (not collapsed into the warner): the
152
- * CLI dispatcher wants to ALSO emit a typed `run.warning` event into a
153
- * ledger when the engine ends up on but the resolver came from a layer
154
- * that's about to be removed — except today, on v6.1, the only path that
155
- * warns IS the "engine off, explicit opt-out" path. So the predicate
156
- * collapses cleanly to that single condition. v7 removes both. */
157
- export function shouldWarnEngineOffDeprecation(resolved) {
158
- if (resolved.enabled)
159
- return false;
160
- return (resolved.source === 'cli' ||
161
- resolved.source === 'env' ||
162
- resolved.source === 'config');
62
+ /** v6.1-era stable deprecation banner. v7.0+ never emits this string
63
+ * the path is gone. Kept exported so out-of-tree consumers that imported
64
+ * it still type-check. */
65
+ export const ENGINE_OFF_DEPRECATION_MESSAGE = '[deprecation] --no-engine / engine.enabled: false were removed in v7.0. Migration: drop the flag/env/config.';
66
+ /** v7.0+ no-op. Always returns false. */
67
+ export function shouldWarnEngineOffDeprecation(_resolved) {
68
+ return false;
163
69
  }
164
- /** Emit the v6.1 `--no-engine` deprecation warning to stderr (or the
165
- * supplied `warn` callback) when the resolver result indicates the user
166
- * explicitly opted out of the engine. No-op when:
167
- * - the engine is on (no opt-out happened);
168
- * - the source is `'default'` (v6.1's flipped default = on, so a default
169
- * result with `enabled: false` is impossible without a custom
170
- * `builtInDefault` override — and even that path doesn't warn since
171
- * it's not a user-driven opt-out).
172
- *
173
- * Pure-ish: side-effect is captured behind the optional `warn` callback so
174
- * tests can assert on the message without spawning a subprocess. The
175
- * default warner writes to `process.stderr` with a trailing newline.
176
- *
177
- * Returns `true` when the warning fired, `false` when it was a no-op. The
178
- * return value is purely informational — callers can use it to decide
179
- * whether to also append a `run.warning` event into a run ledger (only
180
- * meaningful on the engine-on path; the v6.1 deprecation only fires on
181
- * engine-off, where there's no run dir to write into). */
182
- export function emitEngineOffDeprecationWarning(resolved, warn = (msg) => {
183
- process.stderr.write(`${msg}\n`);
184
- }) {
185
- if (!shouldWarnEngineOffDeprecation(resolved))
186
- return false;
187
- warn(ENGINE_OFF_DEPRECATION_MESSAGE);
188
- return true;
70
+ /** v7.0+ no-op. Always returns false. */
71
+ export function emitEngineOffDeprecationWarning(_resolved, _warn = () => { }) {
72
+ return false;
189
73
  }
190
74
  //# sourceMappingURL=resolve-engine.js.map
@@ -28,15 +28,11 @@ export interface RunPhaseWithLifecycleOpts<I, O> {
28
28
  * unset. The helper passes this through to `resolveEngineEnabled` —
29
29
  * invalid values fall through with a `run.warning` recorded automatically. */
30
30
  envEngine: string | undefined;
31
- /** Engine-off escape hatch — what to return when `resolveEngineEnabled`
32
- * decides the engine should NOT run. Most callers pass an async function
33
- * that runs the legacy code path (typically the same `phase.run` body
34
- * invoked without the lifecycle wrapper). The helper does not invoke
35
- * `phase.run` for engine-off so the caller has full control over the
36
- * legacy path's behavior — keeps engine-off byte-for-byte identical to
37
- * pre-v6 behavior even when the phase body's signature would otherwise
38
- * pin the call shape. */
39
- runEngineOff: () => Promise<O>;
31
+ /** v6.x escape hatch — invoked when the engine was disabled. Retained
32
+ * in the v7.0 type for source compatibility with existing callers,
33
+ * but the helper NEVER calls it in v7.0+ (engine-off path was
34
+ * removed). Optional in v7.0; can be omitted from new call sites. */
35
+ runEngineOff?: () => Promise<O>;
40
36
  }
41
37
  /** What the helper hands back. `runId` and `runDir` are null on the
42
38
  * engine-off path so callers can branch on whether engine artifacts exist
@@ -39,7 +39,7 @@ import { createRun } from "./runs.js";
39
39
  import { runPhase } from "./phase-runner.js";
40
40
  import { appendEvent, replayState } from "./events.js";
41
41
  import { writeStateSnapshot } from "./state.js";
42
- import { resolveEngineEnabled, emitEngineOffDeprecationWarning, } from "./resolve-engine.js";
42
+ import { resolveEngineEnabled, } from "./resolve-engine.js";
43
43
  // Inline ANSI codes — same shape every wrapped verb uses. Kept here so the
44
44
  // helper doesn't depend on a verb-local `fmt`. The error message format
45
45
  // (`[<phase>] engine: phase failed — <msg>` + dim inspect hint) is
@@ -69,12 +69,12 @@ const ANSI_RED = '\x1b[31m';
69
69
  * catch block does not need to release the lock itself — `finally` covers
70
70
  * both the success and failure exit paths. */
71
71
  export async function runPhaseWithLifecycle(opts) {
72
- const { cwd, phase, input, config, cliEngine, envEngine, runEngineOff } = opts;
73
- // Resolve engine via the canonical precedence (CLI > env > config >
74
- // built-in default). The resolver is pure same inputs always produce
75
- // the same decision. We DO consult the loaded config's `engine.enabled`
76
- // here so the helper's caller doesn't have to repeat the conditional
77
- // spread that every wrapped verb wrote inline.
72
+ const { cwd, phase, input, config, cliEngine, envEngine } = opts;
73
+ // v7.0 engine is always on. resolveEngineEnabled() returns
74
+ // { enabled: true, source: 'default' } unconditionally. We still
75
+ // pass the v6-era inputs through so any future re-introduction of
76
+ // observability (a run.warning when a user passes the removed flags
77
+ // via env vars in CI) is a one-line change.
78
78
  const engineResolved = resolveEngineEnabled({
79
79
  ...(cliEngine !== undefined ? { cliEngine } : {}),
80
80
  ...(envEngine !== undefined ? { envValue: envEngine } : {}),
@@ -82,18 +82,6 @@ export async function runPhaseWithLifecycle(opts) {
82
82
  ? { configEnabled: config.engine.enabled }
83
83
  : {}),
84
84
  });
85
- if (!engineResolved.enabled) {
86
- // Engine off — call the caller's legacy path. No run dir, no events,
87
- // no lifecycle work. Behavior is byte-for-byte identical to pre-engine
88
- // versions of the verb. v6.1+ emits a one-line stderr deprecation
89
- // notice when the user explicitly opted out (CLI / env / config); the
90
- // v6.1 default is `enabled: true`, so a `'default'` source can't reach
91
- // this branch and the deprecation helper no-ops on the `enabled: true`
92
- // path. v7 removes the opt-out entirely.
93
- emitEngineOffDeprecationWarning(engineResolved);
94
- const output = await runEngineOff();
95
- return { output, runId: null, runDir: null };
96
- }
97
85
  // Engine on — full lifecycle. Mirrors the pre-v6.0.6 inline shape that
98
86
  // every wrapped verb duplicated.
99
87
  const created = await createRun({
@@ -115,6 +103,25 @@ export async function runPhaseWithLifecycle(opts) {
115
103
  details: { resolution: engineResolved },
116
104
  }, { writerId: created.lock.writerId, runId: created.runId });
117
105
  }
106
+ // v7.0 — emit `engine_off_removed` warning when CLAUDE_AUTOPILOT_ENGINE
107
+ // is set to an off-style value. The env value is otherwise ignored
108
+ // (engine remains on). Softer than --no-engine (which exits 1) because
109
+ // env vars in CI are sticky and silently breaking every v6.x → v7
110
+ // upgrade in CI on day one would burn user trust. See spec test #1(c).
111
+ if (envEngine !== undefined) {
112
+ const normalized = envEngine.trim().toLowerCase();
113
+ if (normalized === 'off' || normalized === 'false' || normalized === '0' || normalized === 'no') {
114
+ appendEvent(created.runDir, {
115
+ event: 'run.warning',
116
+ message: 'engine_off_removed',
117
+ details: {
118
+ code: 'engine_off_removed',
119
+ envValue: envEngine,
120
+ note: 'CLAUDE_AUTOPILOT_ENGINE=off has no effect in v7.0+; engine remains on.',
121
+ },
122
+ }, { writerId: created.lock.writerId, runId: created.runId });
123
+ }
124
+ }
118
125
  const runStartedAt = Date.now();
119
126
  try {
120
127
  const output = await runPhase(phase, input, {
@@ -5,7 +5,7 @@ export declare const RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION: 1;
5
5
  /** Highest `schema_version` value this binary can replay. Always equal to
6
6
  * the writer's `RUN_STATE_SCHEMA_VERSION` — the writer never produces a
7
7
  * newer shape than the reader on the same binary. */
8
- export declare const RUN_STATE_MAX_SUPPORTED_SCHEMA_VERSION: 1;
8
+ export declare const RUN_STATE_MAX_SUPPORTED_SCHEMA_VERSION: 2;
9
9
  export declare function statePath(runDir: string): string;
10
10
  /** Write the snapshot atomically. Sequence:
11
11
  * open(tmp, 'w') → write → fsync(fd) → close → rename → fsync(dirfd).
@@ -1,6 +1,12 @@
1
1
  /** Schema version for everything written by this engine. Bump on breaking
2
- * changes to RunState / RunEvent / PhaseSnapshot shape. */
3
- export declare const RUN_STATE_SCHEMA_VERSION: 1;
2
+ * changes to RunState / RunEvent / PhaseSnapshot shape.
3
+ *
4
+ * v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
5
+ * state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
6
+ * because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
7
+ * cannot read v7-written runs; the corrupted_state error includes a
8
+ * "downgrade resume is not supported" hint so operators know why. */
9
+ export declare const RUN_STATE_SCHEMA_VERSION: 2;
4
10
  export type SchemaVersion = typeof RUN_STATE_SCHEMA_VERSION;
5
11
  /** Identifies a single OS-level writer. PID + a hash of the hostname (we
6
12
  * don't persist the raw hostname to the lock metadata so co-tenant signal
@@ -8,6 +8,12 @@
8
8
  // Spec: docs/specs/v6-run-state-engine.md ("State on disk", "Run lifecycle",
9
9
  // "Idempotency rules + external operation ledger", "Persistence protocol").
10
10
  /** Schema version for everything written by this engine. Bump on breaking
11
- * changes to RunState / RunEvent / PhaseSnapshot shape. */
12
- export const RUN_STATE_SCHEMA_VERSION = 1;
11
+ * changes to RunState / RunEvent / PhaseSnapshot shape.
12
+ *
13
+ * v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
14
+ * state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
15
+ * because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
16
+ * cannot read v7-written runs; the corrupted_state error includes a
17
+ * "downgrade resume is not supported" hint so operators know why. */
18
+ export const RUN_STATE_SCHEMA_VERSION = 2;
13
19
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,26 @@
1
+ export interface AutoUploadOptions {
2
+ /** Caller's explicit opt-out (e.g. CLI --no-upload). */
3
+ disabled?: boolean;
4
+ /** Test seam — substitute fetch impl. */
5
+ fetchImpl?: typeof fetch;
6
+ /** Test seam — silence stdout. */
7
+ silent?: boolean;
8
+ }
9
+ export interface AutoUploadResult {
10
+ attempted: boolean;
11
+ ok: boolean;
12
+ url: string | null;
13
+ skipped: boolean;
14
+ reason?: 'opt-out-flag' | 'env-off' | 'not-logged-in' | 'no-events' | 'aborted' | 'error' | 'limit-reached';
15
+ }
16
+ export declare function shouldAutoUpload(options?: AutoUploadOptions): {
17
+ ok: boolean;
18
+ reason?: AutoUploadResult['reason'];
19
+ };
20
+ /**
21
+ * Run an auto-upload for the given runId. Wraps the foreground uploader
22
+ * in a SIGINT/SIGTERM handler so Ctrl-C is clean. Always returns a result
23
+ * — never throws. Caller preserves the run's original exit code.
24
+ */
25
+ export declare function autoUploadAtComplete(runId: string, runDir: string, options?: AutoUploadOptions): Promise<AutoUploadResult>;
26
+ //# sourceMappingURL=auto-upload.d.ts.map
@@ -0,0 +1,107 @@
1
+ // Auto-upload at run.complete — non-fatal hosted-product hook.
2
+ //
3
+ // Contract (per spec): never fails the run. Always preserves the original
4
+ // exit code. Failure prints a resume command. Empty events.ndjson skips
5
+ // upload cleanly (Phase 2.2's POST /api/upload-session 422s expectedChunkCount=0).
6
+ //
7
+ // Opt-outs:
8
+ // - explicit `--no-upload` flag → caller passes options.disabled=true
9
+ // - env CLAUDE_AUTOPILOT_UPLOAD=off
10
+ // - not logged in (no config)
11
+ // - events.ndjson missing or 0 bytes
12
+ import { promises as fs } from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { readConfig } from "./config.js";
15
+ import { uploadRun, UploadLimitError } from "./upload/uploader.js";
16
+ export function shouldAutoUpload(options = {}) {
17
+ if (options.disabled)
18
+ return { ok: false, reason: 'opt-out-flag' };
19
+ const env = process.env.CLAUDE_AUTOPILOT_UPLOAD;
20
+ if (env && /^(off|false|0|no)$/i.test(env))
21
+ return { ok: false, reason: 'env-off' };
22
+ return { ok: true };
23
+ }
24
+ async function fileExistsNonEmpty(p) {
25
+ try {
26
+ const stat = await fs.stat(p);
27
+ return stat.isFile() && stat.size > 0;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ /**
34
+ * Run an auto-upload for the given runId. Wraps the foreground uploader
35
+ * in a SIGINT/SIGTERM handler so Ctrl-C is clean. Always returns a result
36
+ * — never throws. Caller preserves the run's original exit code.
37
+ */
38
+ export async function autoUploadAtComplete(runId, runDir, options = {}) {
39
+ const gate = shouldAutoUpload(options);
40
+ if (!gate.ok) {
41
+ return { attempted: false, ok: true, url: null, skipped: true, reason: gate.reason ?? 'opt-out-flag' };
42
+ }
43
+ const cfg = await readConfig();
44
+ if (!cfg) {
45
+ return { attempted: false, ok: true, url: null, skipped: true, reason: 'not-logged-in' };
46
+ }
47
+ const eventsPath = path.join(runDir, 'events.ndjson');
48
+ const hasEvents = await fileExistsNonEmpty(eventsPath);
49
+ if (!hasEvents) {
50
+ return { attempted: false, ok: true, url: null, skipped: true, reason: 'no-events' };
51
+ }
52
+ const ac = new AbortController();
53
+ const sigintHandler = () => ac.abort();
54
+ process.on('SIGINT', sigintHandler);
55
+ process.on('SIGTERM', sigintHandler);
56
+ try {
57
+ const result = await uploadRun(runId, runDir, {
58
+ apiKey: cfg.apiKey,
59
+ signal: ac.signal,
60
+ ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
61
+ });
62
+ if (result.ok && result.url) {
63
+ if (!options.silent)
64
+ process.stdout.write(`[autopilot] uploaded to ${result.url}\n`);
65
+ return { attempted: true, ok: true, url: result.url, skipped: false };
66
+ }
67
+ if (result.ok && result.skipped) {
68
+ if (!options.silent)
69
+ process.stdout.write(`[autopilot] skipping upload — events.ndjson is empty\n`);
70
+ return { attempted: true, ok: true, url: null, skipped: true, reason: 'no-events' };
71
+ }
72
+ if (!options.silent) {
73
+ process.stderr.write(`[autopilot] upload failed: ${result.error}\n`);
74
+ process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
75
+ }
76
+ return { attempted: true, ok: false, url: null, skipped: false, reason: 'error' };
77
+ }
78
+ catch (err) {
79
+ if (ac.signal.aborted) {
80
+ if (!options.silent) {
81
+ process.stderr.write(`\n[autopilot] upload interrupted. Run is saved locally.\n`);
82
+ process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
83
+ }
84
+ return { attempted: true, ok: false, url: null, skipped: false, reason: 'aborted' };
85
+ }
86
+ // Phase 3 — runs/storage cap reached. Print a friendly message but
87
+ // do NOT print the resume hint (resume would just hit 402 again until
88
+ // the user upgrades) and do NOT bubble the error so the run's exit
89
+ // code is preserved.
90
+ if (err instanceof UploadLimitError) {
91
+ if (!options.silent) {
92
+ process.stderr.write(`[autopilot] ${err.message}\n`);
93
+ }
94
+ return { attempted: true, ok: false, url: null, skipped: false, reason: 'limit-reached' };
95
+ }
96
+ if (!options.silent) {
97
+ process.stderr.write(`[autopilot] upload error: ${err.message}\n`);
98
+ process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
99
+ }
100
+ return { attempted: true, ok: false, url: null, skipped: false, reason: 'error' };
101
+ }
102
+ finally {
103
+ process.off('SIGINT', sigintHandler);
104
+ process.off('SIGTERM', sigintHandler);
105
+ }
106
+ }
107
+ //# sourceMappingURL=auto-upload.js.map
@@ -0,0 +1,22 @@
1
+ export interface DashboardConfig {
2
+ schemaVersion: 1;
3
+ apiKey: string;
4
+ fingerprint: string;
5
+ accountEmail: string;
6
+ loggedInAt: string;
7
+ lastUploadAt: string | null;
8
+ }
9
+ export declare function getConfigDir(): string;
10
+ export declare function getConfigPath(): string;
11
+ export declare function readConfig(): Promise<DashboardConfig | null>;
12
+ export declare function writeConfig(config: DashboardConfig): Promise<void>;
13
+ export declare function deleteConfig(): Promise<void>;
14
+ export declare function getAutopilotBaseUrl(): string;
15
+ export declare function _resetAutopilotBaseUrlWarning(): void;
16
+ /**
17
+ * Returns a warning string if the config file is group/world-readable on
18
+ * a POSIX filesystem; null otherwise (or on Windows, where mode bits
19
+ * don't apply meaningfully).
20
+ */
21
+ export declare function warnIfPermissive(): Promise<string | null>;
22
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,109 @@
1
+ // CLI dashboard config — atomic read/write of ~/.claude-autopilot/dashboard.json.
2
+ //
3
+ // Codex plan-pass WARNING: respect CLAUDE_AUTOPILOT_HOME env override so
4
+ // tests + experimentation never touch the developer's real home dir.
5
+ import { promises as fs } from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ const KEY_RE = /^clp_[0-9a-f]{64}$/;
9
+ function resolveHome() {
10
+ return process.env.CLAUDE_AUTOPILOT_HOME ?? path.join(os.homedir(), '.claude-autopilot');
11
+ }
12
+ export function getConfigDir() {
13
+ return resolveHome();
14
+ }
15
+ export function getConfigPath() {
16
+ return path.join(resolveHome(), 'dashboard.json');
17
+ }
18
+ export async function readConfig() {
19
+ try {
20
+ const raw = await fs.readFile(getConfigPath(), 'utf-8');
21
+ const parsed = JSON.parse(raw);
22
+ if (parsed.schemaVersion !== 1)
23
+ return null;
24
+ if (!KEY_RE.test(parsed.apiKey))
25
+ return null;
26
+ return parsed;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export async function writeConfig(config) {
33
+ if (!KEY_RE.test(config.apiKey)) {
34
+ throw new Error('writeConfig: invalid apiKey shape');
35
+ }
36
+ const dir = getConfigDir();
37
+ const file = getConfigPath();
38
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
39
+ // Try to tighten dir mode even if it already existed.
40
+ try {
41
+ await fs.chmod(dir, 0o700);
42
+ }
43
+ catch { /* best effort */ }
44
+ // Atomic write: temp-file + rename.
45
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
46
+ const payload = JSON.stringify(config, null, 2);
47
+ await fs.writeFile(tmp, payload, { mode: 0o600 });
48
+ await fs.chmod(tmp, 0o600);
49
+ await fs.rename(tmp, file);
50
+ }
51
+ export async function deleteConfig() {
52
+ try {
53
+ await fs.unlink(getConfigPath());
54
+ }
55
+ catch {
56
+ /* idempotent */
57
+ }
58
+ }
59
+ /**
60
+ * Phase 4 — resolve the dashboard / public base URL from env, with
61
+ * AUTOPILOT_PUBLIC_BASE_URL preferred and AUTOPILOT_DASHBOARD_BASE_URL
62
+ * accepted as a deprecated alias. Logs a one-time deprecation warning
63
+ * when only the older variable is set.
64
+ *
65
+ * Defaults to https://autopilot.dev when neither is present.
66
+ */
67
+ let _deprecationWarned = false;
68
+ export function getAutopilotBaseUrl() {
69
+ const canonical = process.env.AUTOPILOT_PUBLIC_BASE_URL;
70
+ const legacy = process.env.AUTOPILOT_DASHBOARD_BASE_URL;
71
+ if (canonical)
72
+ return canonical;
73
+ if (legacy) {
74
+ if (!_deprecationWarned) {
75
+ _deprecationWarned = true;
76
+ // eslint-disable-next-line no-console
77
+ console.warn('[autopilot] AUTOPILOT_DASHBOARD_BASE_URL is deprecated; ' +
78
+ 'use AUTOPILOT_PUBLIC_BASE_URL instead. Both are accepted for now.');
79
+ }
80
+ return legacy;
81
+ }
82
+ return 'https://autopilot.dev';
83
+ }
84
+ // Test seam — reset the one-shot warning flag.
85
+ export function _resetAutopilotBaseUrlWarning() {
86
+ _deprecationWarned = false;
87
+ }
88
+ /**
89
+ * Returns a warning string if the config file is group/world-readable on
90
+ * a POSIX filesystem; null otherwise (or on Windows, where mode bits
91
+ * don't apply meaningfully).
92
+ */
93
+ export async function warnIfPermissive() {
94
+ if (process.platform === 'win32')
95
+ return null;
96
+ const file = getConfigPath();
97
+ try {
98
+ const stat = await fs.stat(file);
99
+ const mode = stat.mode & 0o777;
100
+ if ((mode & 0o077) !== 0) {
101
+ return `Warning: ${file} mode is ${mode.toString(8)} (group/world readable). Run: chmod 600 ${file}`;
102
+ }
103
+ }
104
+ catch {
105
+ /* file doesn't exist, no warning */
106
+ }
107
+ return null;
108
+ }
109
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ export declare function canonicalJsonBytes(value: unknown): Buffer;
2
+ export declare function sha256OfCanonical(value: unknown): string;
3
+ //# sourceMappingURL=canonical.d.ts.map
@@ -0,0 +1,16 @@
1
+ // Parity copy of apps/web/lib/upload/canonical.ts (RFC 8785 / JCS).
2
+ // CLI ↔ web byte-equality asserted in tests/dashboard/parity.test.ts.
3
+ import canonicalize from 'canonicalize';
4
+ import { createHash } from 'node:crypto';
5
+ export function canonicalJsonBytes(value) {
6
+ // canonicalize implements RFC 8785 (JCS). Returns undefined only for
7
+ // inputs JSON cannot represent at the root; coerce to '' so callers
8
+ // always get a Buffer.
9
+ const str = canonicalize(value) ?? '';
10
+ return Buffer.from(str, 'utf-8');
11
+ }
12
+ export function sha256OfCanonical(value) {
13
+ const bytes = canonicalJsonBytes(value);
14
+ return createHash('sha256').update(bytes).digest('hex');
15
+ }
16
+ //# sourceMappingURL=canonical.js.map