@skrr-ai/cli 0.1.10 → 0.1.12

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 (120) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +15 -0
  10. package/dist/commands/daemon/install.js +39 -2
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +53 -0
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +57 -9
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.js +6 -2
  83. package/dist/lib/daemon-installer.d.ts +62 -0
  84. package/dist/lib/daemon-installer.js +247 -0
  85. package/dist/lib/daemon-setup.d.ts +49 -0
  86. package/dist/lib/daemon-setup.js +103 -0
  87. package/dist/lib/daemonBroker.d.ts +7 -1
  88. package/dist/lib/daemonBroker.js +71 -0
  89. package/dist/lib/daemonHandoff.d.ts +47 -0
  90. package/dist/lib/daemonHandoff.js +304 -0
  91. package/dist/lib/exec-oversky.d.ts +30 -0
  92. package/dist/lib/exec-oversky.js +41 -0
  93. package/dist/lib/format.d.ts +7 -0
  94. package/dist/lib/format.js +27 -5
  95. package/dist/lib/login.js +22 -2
  96. package/dist/lib/sky-code.js +1 -1
  97. package/dist/lib/task-transcript.d.ts +9 -0
  98. package/dist/lib/task-transcript.js +13 -2
  99. package/dist/lib/tasks.d.ts +34 -0
  100. package/dist/lib/tasks.js +70 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +21 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.d.ts +87 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.js +94 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +161 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +235 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.d.ts +87 -0
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.js +91 -0
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +161 -0
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +227 -0
  117. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  119. package/oclif.manifest.json +1831 -1598
  120. package/package.json +5 -3
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * What to say when a command id does not resolve — ONE description, shared by
4
+ * every surface that can produce a miss.
5
+ *
6
+ * There are two such surfaces, and they used to disagree. `skrr daemon reload`
7
+ * goes through the `command_not_found` hook and gets a message naming the real
8
+ * command, the mistake, and the next step. `skrr daemon reload --help` goes
9
+ * through oclif's HELP command, which does its own lookup and never reaches
10
+ * that hook — so it answered `Command daemon:reload not found.`: raw
11
+ * boilerplate naming an internal colon-form id the user never typed and cannot
12
+ * type, with no route forward.
13
+ *
14
+ * The audience is what makes that the wrong way round. Someone exploring a
15
+ * service-manager CLI types `--help` on a guessed subcommand FIRST. So the
16
+ * readers most likely to hit a miss were exactly the readers the good message
17
+ * was written for, and they systematically got the bad one.
18
+ *
19
+ * Keeping the text here rather than in either caller is the point: a fix to one
20
+ * phrasing reaches both, and neither surface can quietly drift from the other.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.editDistance = editDistance;
24
+ exports.nearestCommands = nearestCommands;
25
+ exports.describeCommandMiss = describeCommandMiss;
26
+ exports.lookupFromConfig = lookupFromConfig;
27
+ /** A uuid-ish token, which is what a stray space id looks like. */
28
+ const LOOKS_LIKE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
+ /** Cheap edit distance, bounded — this runs only on a miss. */
30
+ function editDistance(a, b) {
31
+ const rows = a.length + 1;
32
+ const cols = b.length + 1;
33
+ let prev = Array.from({ length: cols }, (_, j) => j);
34
+ for (let i = 1; i < rows; i += 1) {
35
+ const cur = [i, ...Array(cols - 1).fill(0)];
36
+ for (let j = 1; j < cols; j += 1) {
37
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
38
+ }
39
+ prev = cur;
40
+ }
41
+ return prev[cols - 1];
42
+ }
43
+ /**
44
+ * The closest real command ids, so a typo gets a route forward rather than
45
+ * only a verdict.
46
+ */
47
+ function nearestCommands(id, all, limit = 3) {
48
+ // Deduped, because oclif registers a TOPIC for a command path as well as the
49
+ // command itself — `daemon:status` is in `commandIDs` and in `topics`, so the
50
+ // combined candidate list held it twice and a near-miss was answered with
51
+ // "Did you mean: `skrr daemon status`, `skrr daemon status`, …?", spending
52
+ // two of the three suggestion slots saying one thing.
53
+ return ([...new Set(all)]
54
+ .map((candidate) => ({ candidate, d: editDistance(id, candidate) }))
55
+ // A third of the length keeps this to genuine near-misses; without a bound
56
+ // every miss suggests three unrelated commands, which is noise wearing the
57
+ // shape of help.
58
+ .filter(({ d }) => d <= Math.max(2, Math.floor(id.length / 3)))
59
+ .sort((x, y) => x.d - y.d)
60
+ .slice(0, limit)
61
+ .map(({ candidate }) => candidate));
62
+ }
63
+ /**
64
+ * The message for a colon-joined id that did not resolve.
65
+ *
66
+ * Two shapes, in order:
67
+ *
68
+ * 1. A PREFIX of the id is a real command, so the extra tokens are arguments
69
+ * it did not want. Longest prefix first, because `a:b:c` should report
70
+ * `a b`, not `a`.
71
+ * 2. Nothing resolved, so the command genuinely is not there — say so, and
72
+ * suggest the nearest siblings under the same topic before falling back to
73
+ * the whole command list.
74
+ */
75
+ function describeCommandMiss(config, id) {
76
+ const parts = String(id ?? '')
77
+ .split(':')
78
+ .filter(Boolean);
79
+ for (let take = parts.length - 1; take >= 1; take -= 1) {
80
+ const found = config.findCommand(parts.slice(0, take).join(':'));
81
+ if (!found)
82
+ continue;
83
+ const extras = parts.slice(take);
84
+ const spoken = `${config.bin} ${parts.slice(0, take).join(' ')}`;
85
+ // Suggest the flag only when the command actually has it, so the hint can
86
+ // never point at something that does not exist.
87
+ const flags = Object.keys(found.flags ?? {});
88
+ const hint = extras.some((e) => LOOKS_LIKE_ID.test(e)) && flags.includes('space')
89
+ ? ` Pass it as \`--space ${extras[0]}\`.`
90
+ : flags.includes('space')
91
+ ? ' It takes `--space <id>`.'
92
+ : '';
93
+ const plural = extras.length === 1 ? 'argument' : 'arguments';
94
+ return (`\`${spoken}\` exists, but does not take the positional ${plural} ` +
95
+ `${extras.map((e) => `\`${e}\``).join(' ')}.${hint}\n` +
96
+ `See \`${spoken} --help\`.`);
97
+ }
98
+ // Topic names are candidates too, so a near-miss on a bare topic gets routed
99
+ // to the topic rather than nothing (OSK-4869): `skrr harness` (singular) is
100
+ // one edit from the `harnesses` topic, but that topic owns no command whose
101
+ // id is close to `harness`, so a command-id-only search suggests nothing.
102
+ //
103
+ // Siblings first: `commitments:archive:<id>` is a long way from every real
104
+ // command id, so a whole-string comparison finds nothing and the reader is
105
+ // told only that they are wrong — when the useful answer is "that topic
106
+ // exists and here is what it does have".
107
+ const siblings = config.commandIDs.filter((c) => c.startsWith(`${parts[0]}:`));
108
+ const suggestions = siblings.length
109
+ ? nearestCommands(parts.slice(0, 2).join(':'), siblings)
110
+ : nearestCommands(parts.join(':'), [...config.commandIDs, ...config.topicNames]);
111
+ return (`\`${config.bin} ${parts.join(' ')}\` is not a command.` +
112
+ (suggestions.length
113
+ ? `\nDid you mean: ${suggestions
114
+ .map((s) => `\`${config.bin} ${s.split(':').join(' ')}\``)
115
+ .join(', ')}?`
116
+ : `\nRun \`${config.bin} --help\` to see the topics.`));
117
+ }
118
+ /** Adapt an oclif `Config` to the lookup shape above. */
119
+ function lookupFromConfig(config) {
120
+ return {
121
+ bin: config.bin,
122
+ findCommand: (id) => config.findCommand(id),
123
+ commandIDs: config.commandIDs.map((c) => String(c)),
124
+ topicNames: Array.isArray(config.topics)
125
+ ? config.topics.map((t) => String(t?.name ?? '')).filter(Boolean)
126
+ : [],
127
+ };
128
+ }
@@ -22,10 +22,18 @@ export declare const ANALYTICS_REPORTS: {
22
22
  readonly path: "/api/commitment-analytics/distribution";
23
23
  readonly summary: "How commitments are spread across agents, tiers and cadences";
24
24
  };
25
+ readonly 'distribution-tripwire': {
26
+ readonly path: "/api/commitment-analytics/distribution/tripwire";
27
+ readonly summary: "Persistent weekly confirmation of portfolio overload risk";
28
+ };
25
29
  readonly 'attention-budget': {
26
30
  readonly path: "/api/commitment-analytics/attention-budget";
27
31
  readonly summary: "How much user attention the fleet is actually spending";
28
32
  };
33
+ readonly 'decision-inbox': {
34
+ readonly path: "/api/commitment-analytics/decision-inbox";
35
+ readonly summary: "One ranked owner inbox across approvals, blockers, attention and held surfaces";
36
+ };
29
37
  readonly 'action-adapters-audit': {
30
38
  readonly path: "/api/commitment-analytics/action-adapters/audit";
31
39
  readonly summary: "Which action adapters are installed and what they are permitted to do";
@@ -54,7 +62,9 @@ export declare const commitmentAnalyticsApi: {
54
62
  operationsQueue: (query?: Query) => Promise<Record<string, unknown>>;
55
63
  governanceExceptions: (query?: Query) => Promise<Record<string, unknown>>;
56
64
  distribution: (query?: Query) => Promise<Record<string, unknown>>;
65
+ distributionTripwire: (query?: Query) => Promise<Record<string, unknown>>;
57
66
  attentionBudget: (query?: Query) => Promise<Record<string, unknown>>;
67
+ decisionInbox: () => Promise<Record<string, unknown>>;
58
68
  actionAdapterAudit: () => Promise<Record<string, unknown>>;
59
69
  recoveryMetrics: (query?: Query) => Promise<Record<string, unknown>>;
60
70
  recovery: (query?: Query) => Promise<Record<string, unknown>>;
@@ -41,10 +41,18 @@ exports.ANALYTICS_REPORTS = {
41
41
  path: `${MOUNT}/distribution`,
42
42
  summary: 'How commitments are spread across agents, tiers and cadences',
43
43
  },
44
+ 'distribution-tripwire': {
45
+ path: `${MOUNT}/distribution/tripwire`,
46
+ summary: 'Persistent weekly confirmation of portfolio overload risk',
47
+ },
44
48
  'attention-budget': {
45
49
  path: `${MOUNT}/attention-budget`,
46
50
  summary: 'How much user attention the fleet is actually spending',
47
51
  },
52
+ 'decision-inbox': {
53
+ path: `${MOUNT}/decision-inbox`,
54
+ summary: 'One ranked owner inbox across approvals, blockers, attention and held surfaces',
55
+ },
48
56
  'action-adapters-audit': {
49
57
  path: `${MOUNT}/action-adapters/audit`,
50
58
  summary: 'Which action adapters are installed and what they are permitted to do',
@@ -72,7 +80,9 @@ exports.commitmentAnalyticsApi = {
72
80
  operationsQueue: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/operations/queue`, query)),
73
81
  governanceExceptions: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/governance/exceptions`, query)),
74
82
  distribution: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/distribution`, query)),
83
+ distributionTripwire: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/distribution/tripwire`, query)),
75
84
  attentionBudget: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/attention-budget`, query)),
85
+ decisionInbox: () => data_provider_1.request.get(`${MOUNT}/decision-inbox`),
76
86
  actionAdapterAudit: () => data_provider_1.request.get(`${MOUNT}/action-adapters/audit`),
77
87
  recoveryMetrics: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/recovery-metrics`, query)),
78
88
  recovery: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/recovery`, query)),
@@ -51,6 +51,7 @@ const fs = __importStar(require("node:fs"));
51
51
  const os = __importStar(require("node:os"));
52
52
  const path = __importStar(require("node:path"));
53
53
  const keychain_1 = require("./keychain");
54
+ const auth_core_1 = require("@skrr-ai/auth-core");
54
55
  const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
55
56
  // Both from config/public-endpoints.json, the one domain contract, rather than a
56
57
  // fourth hand-copied pair. The CLI previously knew only about dev — there was no
@@ -256,8 +257,11 @@ function isEnvAuthOverride() {
256
257
  * resolver and every existing install read it.
257
258
  */
258
259
  function configRoot(env = process.env) {
259
- const root = (env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim();
260
- return root || path.join(os.homedir(), '.skrr');
260
+ // Delegates. The variable list lives in ONE place (`@skrr-ai/auth-core`)
261
+ // because every copy of it became a place `skrr` and `skrrd` could disagree —
262
+ // twice in one day, each time as a correct local fix that created the next
263
+ // defect. `scripts/config-root-single-source.spec` fails if this is re-derived.
264
+ return (0, auth_core_1.resolveConfigRoot)(env);
261
265
  }
262
266
  function newConfigDir() {
263
267
  return configRoot();
@@ -0,0 +1,62 @@
1
+ import { type DaemonVersionManifest, type ReleaseKey } from '@skrr-ai/auth-core';
2
+ /** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
3
+ export declare const DEFAULT_DAEMON_FEED_BASE = "https://updates.oversky.ai/daemon";
4
+ /** Same override the daemon honours, so one env var steers both. */
5
+ export declare const DAEMON_FEED_BASE_ENV = "OVERSKY_DAEMON_UPDATE_FEED_BASE";
6
+ export type DaemonInstallFailureCode = 'unsupported_platform' | 'manifest_unreachable' | 'manifest_malformed' | 'manifest_unsigned' | 'platform_missing' | 'untrusted_artifact_origin' | 'download_failed' | 'checksum_mismatch' | 'artifact_unsigned' | 'write_failed';
7
+ export type DaemonInstallResult = {
8
+ ok: true;
9
+ version: string;
10
+ platformKey: string;
11
+ binaryPath: string;
12
+ } | {
13
+ ok: false;
14
+ code: DaemonInstallFailureCode;
15
+ error: string;
16
+ };
17
+ export interface DaemonInstallOptions {
18
+ env?: NodeJS.ProcessEnv;
19
+ /** Override the feed root. Tests point this at a local fixture server. */
20
+ feedBase?: string;
21
+ /** Injected for tests; defaults to global fetch. */
22
+ fetchImpl?: typeof fetch;
23
+ /**
24
+ * Trust anchors. Defaults to the pinned production set; tests substitute a
25
+ * generated keypair, exactly as the sky-code installer does. Passing an EMPTY
26
+ * set does not disable verification here — every non-`ok` verdict is a
27
+ * refusal, so an unconfigured trust root turns installs off rather than
28
+ * turning checking off.
29
+ */
30
+ keys?: readonly ReleaseKey[];
31
+ }
32
+ export declare function daemonFeedBase(env?: NodeJS.ProcessEnv): string;
33
+ /**
34
+ * Where the fetched binary lands. `exec-oversky.ts` already looks here, so a
35
+ * daemon installed by this path is found by every `skrr daemon *` command
36
+ * without the user touching PATH.
37
+ */
38
+ export declare function daemonBinaryPath(env?: NodeJS.ProcessEnv): string;
39
+ /**
40
+ * `<platform>-<arch>`, the manifest's `platforms` key. Returns null for a
41
+ * combination the release does not build, which is a clearer refusal than a
42
+ * missing-key lookup one step later.
43
+ */
44
+ export declare function daemonPlatformKey(platform?: string, arch?: string): string | null;
45
+ /**
46
+ * True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
47
+ * Same rule as the daemon's `isTrustedArtifactOrigin`: a signed manifest still
48
+ * does not get to point the download at another origin.
49
+ */
50
+ export declare function isTrustedArtifactOrigin(candidate: string, feedBase: string): boolean;
51
+ /** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
52
+ export declare function fetchVerifiedDaemonManifest(opts?: DaemonInstallOptions): Promise<{
53
+ ok: true;
54
+ manifest: DaemonVersionManifest;
55
+ } | (DaemonInstallResult & {
56
+ ok: false;
57
+ })>;
58
+ /**
59
+ * Download, verify and install the daemon binary. Returns a tagged result; it
60
+ * never throws, so the caller can render one refusal for every failure mode.
61
+ */
62
+ export declare function installDaemon(opts?: DaemonInstallOptions): Promise<DaemonInstallResult>;
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DAEMON_FEED_BASE_ENV = exports.DEFAULT_DAEMON_FEED_BASE = void 0;
7
+ exports.daemonFeedBase = daemonFeedBase;
8
+ exports.daemonBinaryPath = daemonBinaryPath;
9
+ exports.daemonPlatformKey = daemonPlatformKey;
10
+ exports.isTrustedArtifactOrigin = isTrustedArtifactOrigin;
11
+ exports.fetchVerifiedDaemonManifest = fetchVerifiedDaemonManifest;
12
+ exports.installDaemon = installDaemon;
13
+ /**
14
+ * daemon-installer.ts — fetch, verify and place the signed `skrrd` binary.
15
+ *
16
+ * ## Why this exists
17
+ *
18
+ * `skrr daemon install` forwarded to `skrrd install`, and `findOverskyBinary()`
19
+ * refused with "install it with brew" when there was no `skrrd` to forward to.
20
+ * So the one command whose job is to set the machine up required the machine to
21
+ * already be set up, and onboarding's first step was a second install from a
22
+ * second distribution channel — before the user had signed in or seen anything
23
+ * work.
24
+ *
25
+ * npm is the front door now. `npm i -g @skrr-ai/cli` gives you `skrr`, and
26
+ * `skrr daemon install` gets the daemon itself.
27
+ *
28
+ * ## Why it downloads a binary instead of shipping one
29
+ *
30
+ * The daemon is a compiled per-platform executable that is codesigned,
31
+ * notarized, and auto-updates itself in the field from a signed CloudFront feed
32
+ * with rollout cohorts. Vendoring five platform binaries into an npm tarball
33
+ * would bloat every install to serve one platform, and — worse — would freeze a
34
+ * copy that the daemon's own updater cannot replace, because that updater swaps
35
+ * a binary in its release slot, not a file inside a node_modules tree. Fetching
36
+ * from the same feed the updater uses keeps ONE distribution channel for the
37
+ * daemon and leaves its update story intact.
38
+ *
39
+ * ## Trust
40
+ *
41
+ * This is the CLI downloading an executable and putting it on disk, so every
42
+ * gate the daemon's own self-updater applies is applied here too, against the
43
+ * SAME pinned keys (`@skrr-ai/auth-core`, moved there for this reason — a
44
+ * mirrored trust root is a second array that can be emptied):
45
+ *
46
+ * 1. the manifest's ed25519 signature, fail-closed;
47
+ * 2. the artifact URL must share the feed's origin. The signature already
48
+ * covers the URL, so this is not about forgery — it bounds where a
49
+ * LEGITIMATELY signed manifest can send us, which is what protects against
50
+ * operator error and against a signer coerced into signing a redirect;
51
+ * 3. sha256 of the received bytes;
52
+ * 4. the artifact's own ed25519 signature over (platform, version, sha256, url).
53
+ *
54
+ * TLS alone is explicitly not enough: the sha256 travels in the same document
55
+ * over the same channel, so it defends against corruption in transit, not
56
+ * against a spoofed feed. (3) without (4) would be the same mistake.
57
+ *
58
+ * The write is staged then renamed, so an interrupted download can never leave
59
+ * a half-written executable at the path the CLI will later exec.
60
+ */
61
+ const node_crypto_1 = require("node:crypto");
62
+ const node_fs_1 = require("node:fs");
63
+ const node_os_1 = __importDefault(require("node:os"));
64
+ const node_path_1 = __importDefault(require("node:path"));
65
+ const auth_core_1 = require("@skrr-ai/auth-core");
66
+ /** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
67
+ exports.DEFAULT_DAEMON_FEED_BASE = 'https://updates.oversky.ai/daemon';
68
+ /** Same override the daemon honours, so one env var steers both. */
69
+ exports.DAEMON_FEED_BASE_ENV = 'OVERSKY_DAEMON_UPDATE_FEED_BASE';
70
+ /** Refuse a manifest larger than this. It is a small JSON document. */
71
+ const MAX_MANIFEST_BYTES = 1_024 * 1_024;
72
+ /** Refuse an artifact larger than this regardless of what the manifest claims. */
73
+ const MAX_ARTIFACT_BYTES = 256 * 1_024 * 1_024;
74
+ const MANIFEST_TIMEOUT_MS = 30_000;
75
+ const ARTIFACT_TIMEOUT_MS = 10 * 60_000;
76
+ function daemonFeedBase(env = process.env) {
77
+ return (env[exports.DAEMON_FEED_BASE_ENV]?.trim() || exports.DEFAULT_DAEMON_FEED_BASE).replace(/\/+$/, '');
78
+ }
79
+ /**
80
+ * Where the fetched binary lands. `exec-oversky.ts` already looks here, so a
81
+ * daemon installed by this path is found by every `skrr daemon *` command
82
+ * without the user touching PATH.
83
+ */
84
+ function daemonBinaryPath(env = process.env) {
85
+ const home = env.HOME || env.USERPROFILE || node_os_1.default.homedir();
86
+ const exe = process.platform === 'win32' ? 'skrrd.exe' : 'skrrd';
87
+ return node_path_1.default.join(home, '.skrr', 'bin', exe);
88
+ }
89
+ /**
90
+ * `<platform>-<arch>`, the manifest's `platforms` key. Returns null for a
91
+ * combination the release does not build, which is a clearer refusal than a
92
+ * missing-key lookup one step later.
93
+ */
94
+ function daemonPlatformKey(platform = process.platform, arch = process.arch) {
95
+ const supported = new Set([
96
+ 'darwin-arm64',
97
+ 'darwin-x64',
98
+ 'linux-arm64',
99
+ 'linux-x64',
100
+ 'win32-x64',
101
+ ]);
102
+ const key = `${platform}-${arch}`;
103
+ return supported.has(key) ? key : null;
104
+ }
105
+ /**
106
+ * True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
107
+ * Same rule as the daemon's `isTrustedArtifactOrigin`: a signed manifest still
108
+ * does not get to point the download at another origin.
109
+ */
110
+ function isTrustedArtifactOrigin(candidate, feedBase) {
111
+ try {
112
+ return new URL(candidate).origin === new URL(feedBase).origin;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ function fail(code, error) {
119
+ return { ok: false, code, error };
120
+ }
121
+ async function getWithTimeout(url, timeoutMs, fetchImpl) {
122
+ const controller = new AbortController();
123
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
124
+ try {
125
+ return await fetchImpl(url, {
126
+ signal: controller.signal,
127
+ headers: { 'User-Agent': 'skrr-cli (daemon install)' },
128
+ });
129
+ }
130
+ finally {
131
+ clearTimeout(timer);
132
+ }
133
+ }
134
+ /** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
135
+ async function fetchVerifiedDaemonManifest(opts = {}) {
136
+ const env = opts.env ?? process.env;
137
+ const feedBase = opts.feedBase ?? daemonFeedBase(env);
138
+ const fetchImpl = opts.fetchImpl ?? fetch;
139
+ const keys = opts.keys ?? auth_core_1.OVERSKY_RELEASE_PUBLIC_KEYS;
140
+ const url = `${feedBase}/latest/daemon-version.json`;
141
+ let res;
142
+ try {
143
+ res = await getWithTimeout(url, MANIFEST_TIMEOUT_MS, fetchImpl);
144
+ }
145
+ catch (err) {
146
+ return fail('manifest_unreachable', `${url}: ${err.message}`);
147
+ }
148
+ if (!res.ok) {
149
+ return fail('manifest_unreachable', `${url} returned HTTP ${res.status}`);
150
+ }
151
+ const text = await res.text();
152
+ if (text.length > MAX_MANIFEST_BYTES) {
153
+ return fail('manifest_malformed', 'release manifest is implausibly large');
154
+ }
155
+ let manifest;
156
+ try {
157
+ manifest = JSON.parse(text);
158
+ }
159
+ catch {
160
+ return fail('manifest_malformed', `${url} did not return JSON`);
161
+ }
162
+ if (!manifest || typeof manifest.version !== 'string' || !manifest.platforms) {
163
+ return fail('manifest_malformed', 'release manifest is missing version/platforms');
164
+ }
165
+ // Fail-closed by construction: the pinned key set is non-empty, so `disabled`
166
+ // cannot be reached here. Treating it as a refusal anyway means emptying that
167
+ // array turns installs OFF rather than turning verification off silently.
168
+ const verdict = (0, auth_core_1.verifyManifest)(manifest, keys);
169
+ if (!('ok' in verdict) || verdict.ok !== true) {
170
+ const reason = 'reason' in verdict ? verdict.reason : 'no release signing key is pinned';
171
+ return fail('manifest_unsigned', `release manifest failed verification: ${reason}`);
172
+ }
173
+ return { ok: true, manifest };
174
+ }
175
+ /**
176
+ * Download, verify and install the daemon binary. Returns a tagged result; it
177
+ * never throws, so the caller can render one refusal for every failure mode.
178
+ */
179
+ async function installDaemon(opts = {}) {
180
+ const env = opts.env ?? process.env;
181
+ const feedBase = opts.feedBase ?? daemonFeedBase(env);
182
+ const fetchImpl = opts.fetchImpl ?? fetch;
183
+ const keys = opts.keys ?? auth_core_1.OVERSKY_RELEASE_PUBLIC_KEYS;
184
+ const platformKey = daemonPlatformKey();
185
+ if (!platformKey) {
186
+ return fail('unsupported_platform', `no daemon build for ${process.platform}-${process.arch}`);
187
+ }
188
+ const fetched = await fetchVerifiedDaemonManifest({ env, feedBase, fetchImpl, keys });
189
+ if (!('manifest' in fetched))
190
+ return fetched;
191
+ const { manifest } = fetched;
192
+ const artifact = manifest.platforms[platformKey];
193
+ if (!artifact?.url || !artifact.sha256) {
194
+ return fail('platform_missing', `release ${manifest.version} publishes no artifact for ${platformKey}`);
195
+ }
196
+ if (!isTrustedArtifactOrigin(artifact.url, feedBase)) {
197
+ return fail('untrusted_artifact_origin', `manifest points ${platformKey} at ${artifact.url}, which is not on the feed's origin`);
198
+ }
199
+ const artifactVerdict = (0, auth_core_1.verifyArtifact)(platformKey, manifest.version, artifact, keys);
200
+ if (!('ok' in artifactVerdict) || artifactVerdict.ok !== true) {
201
+ const reason = 'reason' in artifactVerdict ? artifactVerdict.reason : 'no key pinned';
202
+ return fail('artifact_unsigned', `${platformKey} artifact failed verification: ${reason}`);
203
+ }
204
+ if (typeof artifact.size === 'number' && artifact.size > MAX_ARTIFACT_BYTES) {
205
+ return fail('download_failed', `manifest declares an implausible size (${artifact.size} bytes)`);
206
+ }
207
+ let bytes;
208
+ try {
209
+ const res = await getWithTimeout(artifact.url, ARTIFACT_TIMEOUT_MS, fetchImpl);
210
+ if (!res.ok) {
211
+ return fail('download_failed', `${artifact.url} returned HTTP ${res.status}`);
212
+ }
213
+ const buf = Buffer.from(await res.arrayBuffer());
214
+ if (buf.byteLength > MAX_ARTIFACT_BYTES) {
215
+ return fail('download_failed', 'downloaded artifact exceeds the size ceiling');
216
+ }
217
+ bytes = buf;
218
+ }
219
+ catch (err) {
220
+ return fail('download_failed', `${artifact.url}: ${err.message}`);
221
+ }
222
+ const actual = (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
223
+ if (actual !== artifact.sha256.toLowerCase()) {
224
+ return fail('checksum_mismatch', `sha256 mismatch for ${platformKey}: expected ${artifact.sha256}, received ${actual}`);
225
+ }
226
+ // Staged then renamed. A rename within one directory is atomic, so the path
227
+ // the CLI later execs is either the previous binary or the fully-written new
228
+ // one — never a truncated download wearing the executable bit.
229
+ const binaryPath = daemonBinaryPath(env);
230
+ const stagingPath = `${binaryPath}.staging-${process.pid}`;
231
+ try {
232
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(binaryPath), { recursive: true });
233
+ (0, node_fs_1.writeFileSync)(stagingPath, bytes, { mode: 0o755 });
234
+ (0, node_fs_1.chmodSync)(stagingPath, 0o755);
235
+ (0, node_fs_1.renameSync)(stagingPath, binaryPath);
236
+ }
237
+ catch (err) {
238
+ try {
239
+ (0, node_fs_1.rmSync)(stagingPath, { force: true });
240
+ }
241
+ catch {
242
+ /* best-effort staging cleanup */
243
+ }
244
+ return fail('write_failed', `could not install to ${binaryPath}: ${err.message}`);
245
+ }
246
+ return { ok: true, version: manifest.version, platformKey, binaryPath };
247
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * daemon-setup.ts — offer to set this machine up, at the moment we can.
3
+ *
4
+ * ## Why login and not `npm install`
5
+ *
6
+ * The obvious way to remove a step is a `postinstall` that installs the daemon
7
+ * when the CLI is installed. It cannot work, and the reason is specific rather
8
+ * than aesthetic: installing the daemon BINDS it to a deployment and profile,
9
+ * taken from the CLI's session (`bindToCliSession`, OSK-279 — a daemon on a
10
+ * different deployment than its CLI is the failure that binding exists to
11
+ * prevent). At `npm install` time there is no session.
12
+ *
13
+ * It would not even fail loudly. With no config on disk `loadConfig()` returns
14
+ * DEFAULT_CONFIG, whose baseURL is the DEV deployment — so a postinstall would
15
+ * quietly bind the machine to dev, and the first `skrr login` against
16
+ * production would produce exactly the split. That is the default path on a
17
+ * clean machine, not an edge case.
18
+ *
19
+ * Login is the first moment the answer exists. It is also where the credential
20
+ * handoff already runs, so this is not a new path through the program; it is
21
+ * the branch where that path used to give up.
22
+ *
23
+ * ## Why it asks
24
+ *
25
+ * Signing in and "run a process on this machine that can execute bash, read and
26
+ * write files, and drive a browser" are not the same size of decision. Doing
27
+ * the second as a silent consequence of the first is the class of surprise this
28
+ * whole area has been removing. So: asked when there is someone to ask, skipped
29
+ * with an instruction when there is not, and never on the opt-out.
30
+ */
31
+ import { confirm } from './prompt';
32
+ export interface DaemonSetupOutcome {
33
+ status: 'installed' | 'declined' | 'skipped' | 'failed';
34
+ detail: string;
35
+ }
36
+ export interface DaemonSetupOptions {
37
+ env?: NodeJS.ProcessEnv;
38
+ /** True when only the OS service is missing and the binary is already here. */
39
+ binaryPresent?: boolean;
40
+ /** Injected by tests. */
41
+ confirmImpl?: typeof confirm;
42
+ interactive?: boolean;
43
+ log?: (line: string) => void;
44
+ }
45
+ /**
46
+ * Ask, then install. Returns without touching anything when there is nobody to
47
+ * ask or the operator has opted out.
48
+ */
49
+ export declare function offerDaemonSetup(opts?: DaemonSetupOptions): Promise<DaemonSetupOutcome>;