@skrr-ai/cli 0.1.9 → 0.1.11
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/dist/base-command.d.ts +1 -13
- package/dist/base-command.js +78 -1
- package/dist/commands/browser/skill/show.js +7 -1
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +9 -1
- package/dist/commands/commitments/analytics/index.js +2 -0
- package/dist/commands/daemon/byok.d.ts +1 -0
- package/dist/commands/daemon/byok.js +2 -1
- package/dist/commands/daemon/install.d.ts +1 -0
- package/dist/commands/daemon/install.js +2 -1
- package/dist/commands/daemon/login.d.ts +31 -0
- package/dist/commands/daemon/login.js +56 -0
- package/dist/commands/daemon/restart.d.ts +9 -0
- package/dist/commands/daemon/restart.js +36 -0
- package/dist/commands/daemon/start.d.ts +1 -0
- package/dist/commands/daemon/start.js +2 -1
- package/dist/commands/daemon/status.d.ts +1 -0
- package/dist/commands/daemon/status.js +2 -1
- package/dist/commands/daemon/stop.d.ts +1 -0
- package/dist/commands/daemon/stop.js +2 -1
- package/dist/commands/daemon/uninstall.d.ts +1 -0
- package/dist/commands/daemon/uninstall.js +2 -1
- package/dist/commands/daemon/unlock.d.ts +9 -0
- package/dist/commands/daemon/unlock.js +33 -0
- package/dist/commands/goals/key-results/create.js +32 -1
- package/dist/commands/goals/key-results/update.d.ts +11 -0
- package/dist/commands/goals/key-results/update.js +80 -2
- package/dist/commands/goals/plan-now.d.ts +54 -2
- package/dist/commands/goals/plan-now.js +175 -18
- package/dist/commands/goals/planner-config.d.ts +60 -9
- package/dist/commands/goals/planner-config.js +82 -34
- package/dist/commands/goals/revisions.js +17 -0
- package/dist/commands/goals/show.d.ts +17 -0
- package/dist/commands/goals/show.js +90 -3
- package/dist/commands/login.js +36 -3
- package/dist/commands/spaces/create.js +2 -1
- package/dist/commands/spaces/index.js +9 -1
- package/dist/commands/spaces/list.d.ts +18 -0
- package/dist/commands/spaces/list.js +57 -7
- package/dist/commands/spaces/show.js +4 -1
- package/dist/commands/spaces/summary.d.ts +4 -0
- package/dist/commands/spaces/summary.js +77 -1
- package/dist/commands/spaces/update.d.ts +4 -0
- package/dist/commands/spaces/update.js +38 -1
- package/dist/commands/tasks/actionability.js +40 -1
- package/dist/commands/tasks/activity.d.ts +29 -0
- package/dist/commands/tasks/activity.js +47 -0
- package/dist/commands/tasks/complete.d.ts +47 -0
- package/dist/commands/tasks/complete.js +159 -12
- package/dist/commands/tasks/create.d.ts +26 -0
- package/dist/commands/tasks/create.js +60 -1
- package/dist/commands/tasks/events/append.d.ts +2 -0
- package/dist/commands/tasks/events/append.js +41 -10
- package/dist/commands/tasks/events/list.js +60 -9
- package/dist/commands/tasks/output.js +22 -2
- package/dist/commands/tasks/ready.d.ts +38 -0
- package/dist/commands/tasks/ready.js +37 -0
- package/dist/commands/tasks/runs.d.ts +22 -0
- package/dist/commands/tasks/runs.js +110 -2
- package/dist/commands/tasks/show.d.ts +28 -0
- package/dist/commands/tasks/show.js +61 -0
- package/dist/commands/tasks/timeline.d.ts +7 -0
- package/dist/commands/tasks/timeline.js +34 -3
- package/dist/commands/tasks/update.d.ts +20 -0
- package/dist/commands/tasks/update.js +38 -1
- package/dist/commands/whoami.d.ts +40 -0
- package/dist/commands/whoami.js +61 -10
- package/dist/commands/wiki/ls.d.ts +23 -0
- package/dist/commands/wiki/ls.js +63 -10
- package/dist/commands/wiki/mv.d.ts +70 -0
- package/dist/commands/wiki/mv.js +198 -4
- package/dist/commands/wiki/rm.js +12 -1
- package/dist/commands/wiki/write.js +35 -3
- package/dist/help.d.ts +27 -0
- package/dist/help.js +48 -0
- package/dist/hooks/command-not-found.d.ts +31 -0
- package/dist/hooks/command-not-found.js +12 -80
- package/dist/lib/command-miss.d.ts +60 -0
- package/dist/lib/command-miss.js +128 -0
- package/dist/lib/commitment-analytics.d.ts +10 -0
- package/dist/lib/commitment-analytics.js +10 -0
- package/dist/lib/config.d.ts +40 -0
- package/dist/lib/config.js +72 -9
- package/dist/lib/daemonBroker.d.ts +7 -1
- package/dist/lib/daemonBroker.js +71 -0
- package/dist/lib/daemonHandoff.d.ts +38 -0
- package/dist/lib/daemonHandoff.js +297 -0
- package/dist/lib/exec-oversky.d.ts +30 -0
- package/dist/lib/exec-oversky.js +41 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +27 -5
- package/dist/lib/login.js +22 -2
- package/dist/lib/sky-code.js +9 -4
- package/dist/lib/task-transcript.d.ts +9 -0
- package/dist/lib/task-transcript.js +13 -2
- package/dist/lib/tasks.d.ts +34 -0
- package/dist/lib/tasks.js +70 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
- package/oclif.manifest.json +15713 -15480
- 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)),
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -119,6 +119,19 @@ export declare function environmentForBaseURL(value: string): 'dev' | 'prod' | '
|
|
|
119
119
|
* display and not harmless for routing, because two spellings of prod are two
|
|
120
120
|
* different keys to anything comparing base URLs.
|
|
121
121
|
*/
|
|
122
|
+
/**
|
|
123
|
+
* A deployment, written so a human can tell which one it is.
|
|
124
|
+
*
|
|
125
|
+
* A published `npm i -g @skrr-ai/cli` defaults to the DEV deployment, and nothing
|
|
126
|
+
* in the login flow said so — you got "Signed in via browser" and no indication
|
|
127
|
+
* of which backend now holds your account (OSK-301). Naming the environment as
|
|
128
|
+
* well as the URL matters because `oversky.dev` and `skrr.ai` do not look like a
|
|
129
|
+
* dev/prod pair to anyone who has not read this file.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to the bare URL for a deployment we do not recognise — a
|
|
132
|
+
* self-hosted or local server is legitimate, and labelling it would be a guess.
|
|
133
|
+
*/
|
|
134
|
+
export declare function describeDeployment(baseURL: string): string;
|
|
122
135
|
export declare function canonicalizeKnownBaseURL(value: string): string;
|
|
123
136
|
/**
|
|
124
137
|
* True when any auth-carrying env var is set. In this mode credentials are
|
|
@@ -127,6 +140,33 @@ export declare function canonicalizeKnownBaseURL(value: string): string;
|
|
|
127
140
|
* us persist a rotated pair that the next `loadConfig()` would shadow.
|
|
128
141
|
*/
|
|
129
142
|
export declare function isEnvAuthOverride(): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Storage layout (Phase H+).
|
|
145
|
+
*
|
|
146
|
+
* Canonical: ~/.skrr/cli-config.json (metadata only)
|
|
147
|
+
* ~/.skrr/cli-auth.json (file-backend auth storage)
|
|
148
|
+
* Keychain (darwin-preferred auth storage)
|
|
149
|
+
* Legacy: ~/.sky/config.json (read-only fallback, copied forward once)
|
|
150
|
+
*
|
|
151
|
+
* The CLI shares the daemon's config root so `auth.lock` and
|
|
152
|
+
* `needs-reauth.json` are coordinated across binaries.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* The skrr root for this process — the ONE place every root-derived path comes
|
|
156
|
+
* from.
|
|
157
|
+
*
|
|
158
|
+
* It is exported and takes `env` because the alternative is what this replaced:
|
|
159
|
+
* two functions each deriving the root themselves, one honouring the override
|
|
160
|
+
* and one not, disagreeing only when someone relocated their root (OSK-300).
|
|
161
|
+
* Fixing that by teaching the second function the same two variable names would
|
|
162
|
+
* have re-created the split the moment a third name appeared — and did, briefly,
|
|
163
|
+
* when `SKRR_CONFIG_DIR` was added to one side only.
|
|
164
|
+
*
|
|
165
|
+
* `SKRR_CONFIG_DIR` is the spelling for anything new (root CLAUDE.md) and wins
|
|
166
|
+
* when both are set. `OVERSKY_CONFIG_DIR` keeps working: the daemon, the engine
|
|
167
|
+
* resolver and every existing install read it.
|
|
168
|
+
*/
|
|
169
|
+
export declare function configRoot(env?: NodeJS.ProcessEnv): string;
|
|
130
170
|
/**
|
|
131
171
|
* Read the config file. Returns defaults if missing. Throws on corrupt JSON.
|
|
132
172
|
* Performs a one-shot legacy → new copy when only the legacy file exists, so
|
package/dist/lib/config.js
CHANGED
|
@@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ENVIRONMENT_BASE_URLS = exports.LEGACY_PROD_BASE_URL = exports.LEGACY_DEV_BASE_URL = exports.CANONICAL_PROD_BASE_URL = exports.CANONICAL_DEV_BASE_URL = void 0;
|
|
37
37
|
exports.environmentForBaseURL = environmentForBaseURL;
|
|
38
|
+
exports.describeDeployment = describeDeployment;
|
|
38
39
|
exports.canonicalizeKnownBaseURL = canonicalizeKnownBaseURL;
|
|
39
40
|
exports.isEnvAuthOverride = isEnvAuthOverride;
|
|
41
|
+
exports.configRoot = configRoot;
|
|
40
42
|
exports.loadConfig = loadConfig;
|
|
41
43
|
exports.saveConfig = saveConfig;
|
|
42
44
|
exports.getConfigPath = getConfigPath;
|
|
@@ -49,6 +51,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
49
51
|
const os = __importStar(require("node:os"));
|
|
50
52
|
const path = __importStar(require("node:path"));
|
|
51
53
|
const keychain_1 = require("./keychain");
|
|
54
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
52
55
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
53
56
|
// Both from config/public-endpoints.json, the one domain contract, rather than a
|
|
54
57
|
// fourth hand-copied pair. The CLI previously knew only about dev — there was no
|
|
@@ -97,6 +100,22 @@ function environmentForBaseURL(value) {
|
|
|
97
100
|
* display and not harmless for routing, because two spellings of prod are two
|
|
98
101
|
* different keys to anything comparing base URLs.
|
|
99
102
|
*/
|
|
103
|
+
/**
|
|
104
|
+
* A deployment, written so a human can tell which one it is.
|
|
105
|
+
*
|
|
106
|
+
* A published `npm i -g @skrr-ai/cli` defaults to the DEV deployment, and nothing
|
|
107
|
+
* in the login flow said so — you got "Signed in via browser" and no indication
|
|
108
|
+
* of which backend now holds your account (OSK-301). Naming the environment as
|
|
109
|
+
* well as the URL matters because `oversky.dev` and `skrr.ai` do not look like a
|
|
110
|
+
* dev/prod pair to anyone who has not read this file.
|
|
111
|
+
*
|
|
112
|
+
* Falls back to the bare URL for a deployment we do not recognise — a
|
|
113
|
+
* self-hosted or local server is legitimate, and labelling it would be a guess.
|
|
114
|
+
*/
|
|
115
|
+
function describeDeployment(baseURL) {
|
|
116
|
+
const env = environmentForBaseURL(baseURL);
|
|
117
|
+
return env ? `${env} (${baseURL})` : baseURL;
|
|
118
|
+
}
|
|
100
119
|
function canonicalizeKnownBaseURL(value) {
|
|
101
120
|
const trimmed = value.trim().replace(/\/+$/, '');
|
|
102
121
|
const normalized = trimmed.toLowerCase().replace(/^http:\/\//, 'https://');
|
|
@@ -113,10 +132,27 @@ function canonicalizeKnownBaseURL(value) {
|
|
|
113
132
|
return value;
|
|
114
133
|
}
|
|
115
134
|
const DEFAULT_CONFIG = {
|
|
116
|
-
//
|
|
117
|
-
// (daemon/src/config.ts → ENVIRONMENT_URLS.
|
|
118
|
-
// `skrr login --env
|
|
119
|
-
|
|
135
|
+
// PROD for an unconfigured CLI, matching the daemon
|
|
136
|
+
// (daemon/src/config.ts → ENVIRONMENT_URLS.prod). Reach dev with
|
|
137
|
+
// `skrr login --env dev`, `--base-url https://oversky.dev`, or OVERSKY_BASE_URL.
|
|
138
|
+
//
|
|
139
|
+
// This was dev, which was right when the CLI was something the team installed
|
|
140
|
+
// from the repo. `@skrr-ai/cli` is a PUBLIC npm package now, and the default
|
|
141
|
+
// decides where a stranger's first `skrr login` creates their account,
|
|
142
|
+
// workspace and agents. `oversky.dev` is the integration environment: it
|
|
143
|
+
// deploys on every push to `dev` and, by design, without waiting for the api or
|
|
144
|
+
// client suites. A reasonable place for the team to live, a poor place to send
|
|
145
|
+
// someone who typed `npm install` (OSK-301).
|
|
146
|
+
//
|
|
147
|
+
// It moves with the daemon or not at all. The two defaults are ONE decision:
|
|
148
|
+
// changing this alone would put every fresh machine into the CLI-on-prod /
|
|
149
|
+
// daemon-on-dev split that OSK-279 exists to prevent — which is exactly the
|
|
150
|
+
// state this machine was found in. `cli/src/__tests__/default-deployment.spec.ts`
|
|
151
|
+
// reads the daemon's own source and fails if they diverge.
|
|
152
|
+
//
|
|
153
|
+
// Nobody's existing setup moves: a stored `cli-config.json` wins over this, so
|
|
154
|
+
// only an install with no configuration at all is affected.
|
|
155
|
+
baseURL: exports.CANONICAL_PROD_BASE_URL,
|
|
120
156
|
};
|
|
121
157
|
/**
|
|
122
158
|
* Environment-variable overlay for headless use (CI, Secrets Manager, EC2
|
|
@@ -205,8 +241,30 @@ function isEnvAuthOverride() {
|
|
|
205
241
|
* The CLI shares the daemon's config root so `auth.lock` and
|
|
206
242
|
* `needs-reauth.json` are coordinated across binaries.
|
|
207
243
|
*/
|
|
244
|
+
/**
|
|
245
|
+
* The skrr root for this process — the ONE place every root-derived path comes
|
|
246
|
+
* from.
|
|
247
|
+
*
|
|
248
|
+
* It is exported and takes `env` because the alternative is what this replaced:
|
|
249
|
+
* two functions each deriving the root themselves, one honouring the override
|
|
250
|
+
* and one not, disagreeing only when someone relocated their root (OSK-300).
|
|
251
|
+
* Fixing that by teaching the second function the same two variable names would
|
|
252
|
+
* have re-created the split the moment a third name appeared — and did, briefly,
|
|
253
|
+
* when `SKRR_CONFIG_DIR` was added to one side only.
|
|
254
|
+
*
|
|
255
|
+
* `SKRR_CONFIG_DIR` is the spelling for anything new (root CLAUDE.md) and wins
|
|
256
|
+
* when both are set. `OVERSKY_CONFIG_DIR` keeps working: the daemon, the engine
|
|
257
|
+
* resolver and every existing install read it.
|
|
258
|
+
*/
|
|
259
|
+
function configRoot(env = process.env) {
|
|
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);
|
|
265
|
+
}
|
|
208
266
|
function newConfigDir() {
|
|
209
|
-
return
|
|
267
|
+
return configRoot();
|
|
210
268
|
}
|
|
211
269
|
/**
|
|
212
270
|
* Fields that belong to the MACHINE, not to a profile.
|
|
@@ -288,10 +346,15 @@ function readMachineScoped() {
|
|
|
288
346
|
* This was missing from the fallback chain, which went straight from `.skrr` to
|
|
289
347
|
* `.sky` and skipped the directory that every current install actually has.
|
|
290
348
|
* The effect is not a missing preference: with no config found, `loadConfig`
|
|
291
|
-
* returns DEFAULT_CONFIG, whose baseURL is the
|
|
292
|
-
* had been pointed at
|
|
293
|
-
*
|
|
294
|
-
*
|
|
349
|
+
* returns DEFAULT_CONFIG, whose baseURL is the DEV deployment — so a CLI that
|
|
350
|
+
* had been pointed at production silently starts addressing dev, and its
|
|
351
|
+
* `workspaceId` is gone. Nothing errors; commands just answer about a different
|
|
352
|
+
* backend.
|
|
353
|
+
*
|
|
354
|
+
* This read "the production apex", and said the drift ran the other way. It did
|
|
355
|
+
* not: DEFAULT_CONFIG has been dev since it was written, forty lines further up.
|
|
356
|
+
* The direction matters here more than most stale comments, because it is the
|
|
357
|
+
* difference between a lost session and one that silently addresses production.
|
|
295
358
|
*
|
|
296
359
|
* Note the filename differs by generation: `.sky` used `config.json`, `.oversky`
|
|
297
360
|
* and `.skrr` use `cli-config.json`.
|
|
@@ -80,6 +80,12 @@ export interface DaemonBrokerResult {
|
|
|
80
80
|
cliId: string;
|
|
81
81
|
userId: string;
|
|
82
82
|
email: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* The refresh family this credential belongs to, named so the caller can
|
|
85
|
+
* retire its predecessors AFTER persisting it. Absent on servers that predate
|
|
86
|
+
* the mint/confirm split, where the mint still revoked eagerly.
|
|
87
|
+
*/
|
|
88
|
+
familyId?: string;
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
85
91
|
* Explicit reasons the broker path could fail. These exist to keep the
|
|
@@ -87,7 +93,7 @@ export interface DaemonBrokerResult {
|
|
|
87
93
|
* fall through to PKCE; `ci_token_refused` is the one we surface
|
|
88
94
|
* because re-driving it via PKCE would not change the outcome.
|
|
89
95
|
*/
|
|
90
|
-
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
96
|
+
export type DaemonBrokerErrorReason = 'no_bootstrap' | 'stale_bootstrap' | 'bootstrap_parse' | 'base_url_mismatch' | 'daemon_not_authed' | 'ci_token_refused' | 'rate_limited' | 'server_error' | 'network' | 'timeout' | 'unknown';
|
|
91
97
|
export interface DaemonBrokerFailure {
|
|
92
98
|
ok: false;
|
|
93
99
|
reason: DaemonBrokerErrorReason;
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -351,6 +351,22 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
351
351
|
if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
|
|
352
352
|
return { ok: false, reason: 'daemon_not_authed', detail: message, status: 503, code };
|
|
353
353
|
}
|
|
354
|
+
// 429 is TRANSIENT and its remedy is time, which makes it the one failure
|
|
355
|
+
// here that a fall-through cannot substitute for. Left in the `unknown`
|
|
356
|
+
// bucket it fell silently through to PKCE, and in a non-interactive shell
|
|
357
|
+
// that surfaced as "skrr login requires an interactive terminal" — a
|
|
358
|
+
// sentence about the wrong thing entirely. Reachable in the ordinary way: a
|
|
359
|
+
// cold machine running several skrr commands at once has every process
|
|
360
|
+
// brokering, because none of them has a token yet.
|
|
361
|
+
if (response.status === 429) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
reason: 'rate_limited',
|
|
365
|
+
detail: message,
|
|
366
|
+
status: 429,
|
|
367
|
+
...(code ? { code } : {}),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
354
370
|
if (response.status === 400 && code === 'BASE_URL_MISMATCH') {
|
|
355
371
|
// Older daemon binary that lacked the bootstrap.serverUrl field
|
|
356
372
|
// ended up here despite the CLI sending baseURL. Treat as the
|
|
@@ -397,8 +413,52 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
397
413
|
cliId: parsedBody.cliId,
|
|
398
414
|
userId: parsedBody.userId,
|
|
399
415
|
email: parsedBody.email ?? null,
|
|
416
|
+
...(typeof parsedBody.familyId === 'string' ? { familyId: parsedBody.familyId } : {}),
|
|
400
417
|
};
|
|
401
418
|
}
|
|
419
|
+
/** Bound the confirm so a slow server cannot stall a login that already worked. */
|
|
420
|
+
const CONFIRM_TIMEOUT_MS = 10_000;
|
|
421
|
+
/**
|
|
422
|
+
* Retire the cli credentials this one replaces — after it is on disk, never
|
|
423
|
+
* before.
|
|
424
|
+
*
|
|
425
|
+
* The mint deliberately no longer does this. Between the server's response and
|
|
426
|
+
* the credential being usable there are two more hops (the daemon's loopback
|
|
427
|
+
* broker forwarding the body, this process writing it to the keychain), and a
|
|
428
|
+
* failure at either one used to leave the user with no working cli credential at
|
|
429
|
+
* all, recoverable only by a full browser login.
|
|
430
|
+
*
|
|
431
|
+
* Called with the NEW access token explicitly rather than through the shared
|
|
432
|
+
* client. That is the point rather than a detail: the route refuses unless the
|
|
433
|
+
* bearer's own `did` claim is the cliId being swept, so this call is not a claim
|
|
434
|
+
* that the delivery happened — it is a demonstration that it did.
|
|
435
|
+
*
|
|
436
|
+
* Best-effort and silent. The credential is already persisted and working by the
|
|
437
|
+
* time this runs; a failure leaves one extra live family, which the server's
|
|
438
|
+
* session cap bounds and the next confirm sweeps.
|
|
439
|
+
*/
|
|
440
|
+
async function confirmCliHandoff(opts) {
|
|
441
|
+
const controller = new AbortController();
|
|
442
|
+
const timer = setTimeout(() => controller.abort(), CONFIRM_TIMEOUT_MS);
|
|
443
|
+
try {
|
|
444
|
+
await fetch(`${opts.serverOrigin}/api/daemons/cli-handoff/confirm`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
Authorization: `Bearer ${opts.accessToken}`,
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
Accept: 'application/json',
|
|
450
|
+
},
|
|
451
|
+
body: JSON.stringify({ cliId: opts.cliId, familyId: opts.familyId }),
|
|
452
|
+
signal: controller.signal,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
/* bookkeeping — the credential is already delivered */
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
clearTimeout(timer);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
402
462
|
/**
|
|
403
463
|
* Helper: coerce an ISO-string or numeric expiry to epoch ms for
|
|
404
464
|
* `writeToBackend`. Returns undefined on missing / unparseable input.
|
|
@@ -436,6 +496,17 @@ async function attemptDaemonBrokerLoginAndPersist(opts) {
|
|
|
436
496
|
? { refreshExpiresAt: expiryToEpochMs(outcome.refreshExpiresAt) }
|
|
437
497
|
: {}),
|
|
438
498
|
}, { serverOrigin, clearReauth: true });
|
|
499
|
+
// Only now: the credential is on disk, so "the predecessor dies iff the
|
|
500
|
+
// replacement is in place" holds. A server that predates the split ships no
|
|
501
|
+
// `familyId` and has already revoked eagerly — nothing to do there.
|
|
502
|
+
if (outcome.familyId) {
|
|
503
|
+
await confirmCliHandoff({
|
|
504
|
+
serverOrigin,
|
|
505
|
+
accessToken: outcome.accessToken,
|
|
506
|
+
cliId: outcome.cliId,
|
|
507
|
+
familyId: outcome.familyId,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
439
510
|
return outcome;
|
|
440
511
|
}
|
|
441
512
|
// ---------------------------------------------------------------------
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand the local daemon a credential of its own, right after `skrr login`.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of the daemon-as-broker path that already ships. That one lets an
|
|
5
|
+
* authenticated DAEMON mint a cli-scope credential so the CLI can skip the
|
|
6
|
+
* browser; this lets an authenticated HUMAN mint a daemon-scope one so the
|
|
7
|
+
* machine is logged in by the same act. The direction added here is the safer of
|
|
8
|
+
* the two: daemon scope is deliberately lower-privileged than cli scope
|
|
9
|
+
* (`requireHumanScope` refuses it from billing, account management, password
|
|
10
|
+
* change and deletion), so this is a de-escalation, while the shipping direction
|
|
11
|
+
* is an escalation.
|
|
12
|
+
*
|
|
13
|
+
* Design record: docs/architecture/machine-login-broker-inversion-2026-09-03.md
|
|
14
|
+
*
|
|
15
|
+
* Three properties are deliberate:
|
|
16
|
+
*
|
|
17
|
+
* - **Best-effort, never fatal.** `skrr login` has already SUCCEEDED by the time
|
|
18
|
+
* this runs. A failure here must not fail the login or discard the credential
|
|
19
|
+
* the user just earned — it degrades to exactly the old behaviour, with a hint.
|
|
20
|
+
* - **Skipped when no daemon service is installed.** There is nothing to hand a
|
|
21
|
+
* credential to, and minting one anyway leaves an orphaned daemon-scope family
|
|
22
|
+
* on the account. A binary on PATH is not a service, so this is decided by the
|
|
23
|
+
* daemon's own answer about where its id came from — not by whether `skrrd`
|
|
24
|
+
* exists.
|
|
25
|
+
* - **The bundle goes over STDIN**, never a file. It is a live credential; a temp
|
|
26
|
+
* file would put it at rest with a lifetime nobody owns.
|
|
27
|
+
* - **The predecessor is retired only after the new credential is written.** The
|
|
28
|
+
* mint no longer revokes anything; `confirmDaemonHandoff` does, once the daemon
|
|
29
|
+
* has accepted. So a handoff that fails leaves the machine exactly as it was,
|
|
30
|
+
* rather than signed out.
|
|
31
|
+
*/
|
|
32
|
+
export interface DaemonHandoffOutcome {
|
|
33
|
+
status: 'delivered' | 'skipped' | 'failed';
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
|
|
37
|
+
loginFlow?: string;
|
|
38
|
+
}): Promise<DaemonHandoffOutcome>;
|