@skrr-ai/cli 0.1.51 → 0.1.52
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 +7 -0
- package/dist/base-command.js +38 -8
- package/dist/commands/agents/create.js +8 -4
- package/dist/commands/agents/update.d.ts +23 -0
- package/dist/commands/agents/update.js +54 -9
- package/dist/commands/commitments/next-tick.js +9 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.js +16 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/tasks/create.d.ts +5 -0
- package/dist/commands/tasks/create.js +26 -6
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.js +8 -1
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +14 -10
- package/dist/lib/agentic-stream.d.ts +20 -0
- package/dist/lib/agentic-stream.js +39 -5
- package/dist/lib/auth-failure.js +4 -1
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +342 -11
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +4 -0
- package/dist/lib/commitments.js +12 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/daemonBroker.d.ts +26 -4
- package/dist/lib/daemonBroker.js +82 -10
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +33 -2
- package/dist/lib/dedicated-machines.js +50 -1
- package/dist/lib/delegated-cli.js +16 -0
- package/dist/lib/first-party-harness-broker.js +6 -4
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +228 -39
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +68 -1
- package/dist/lib/login.js +24 -12
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.js +33 -8
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3138 -3100
- package/oclif.manifest.json +36364 -36355
- package/package.json +1 -1
package/dist/lib/label-ref.js
CHANGED
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
* their path.
|
|
34
34
|
*/
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.LabelRefError = void 0;
|
|
36
|
+
exports.TAG_FLAG_HELP = exports.LabelRefError = void 0;
|
|
37
37
|
exports.looksLikeLabelId = looksLikeLabelId;
|
|
38
38
|
exports.closestNames = closestNames;
|
|
39
39
|
exports.collectLabelVocabulary = collectLabelVocabulary;
|
|
40
40
|
exports.resolveLabelRefs = resolveLabelRefs;
|
|
41
|
+
exports.tagsNamingLabels = tagsNamingLabels;
|
|
42
|
+
exports.describeTagsNamingLabels = describeTagsNamingLabels;
|
|
43
|
+
exports.tagLabelWarning = tagLabelWarning;
|
|
41
44
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
42
45
|
const workspaces_1 = require("./workspaces");
|
|
43
46
|
/** A canonical v4 uuid, which is what every label id is. */
|
|
@@ -171,3 +174,67 @@ async function resolveLabelRefs(refs, context) {
|
|
|
171
174
|
}
|
|
172
175
|
return resolved;
|
|
173
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Help for a `--tag` flag that sits beside `--label` (OSK-12053).
|
|
179
|
+
*
|
|
180
|
+
* Tags and labels are two DIFFERENT vocabularies: a tag is any free string,
|
|
181
|
+
* stored as typed on `task.tags`; a label is declared (`skrr labels`) and is
|
|
182
|
+
* what `--label` filters, the board and the label guidance read. The flags used
|
|
183
|
+
* to be described as "Tag (repeatable)" and "Attach a task label", so a person
|
|
184
|
+
* wanting the `dogfood` label typed `--tag dogfood` on every task of a program
|
|
185
|
+
* and none of them was labelled.
|
|
186
|
+
*/
|
|
187
|
+
exports.TAG_FLAG_HELP = 'Free-form tag (repeatable), stored exactly as typed and never checked against anything. ' +
|
|
188
|
+
'NOT a label: to classify the task under a declared label (`labels list`), use --label.';
|
|
189
|
+
/**
|
|
190
|
+
* The `--tag` values that equal a declared label's name, each paired with that
|
|
191
|
+
* label — the one case where the mistake is certain enough to say so.
|
|
192
|
+
*
|
|
193
|
+
* Best-effort by construction: it only ever feeds a WARNING, so a vocabulary
|
|
194
|
+
* that cannot be read yields no matches rather than an error, and the tags are
|
|
195
|
+
* stored as typed either way. Nothing is converted into a label implicitly —
|
|
196
|
+
* which one a person meant is theirs to say, and a warning that names the exact
|
|
197
|
+
* flag costs one retry.
|
|
198
|
+
*/
|
|
199
|
+
async function tagsNamingLabels(tags, context) {
|
|
200
|
+
const input = (tags ?? []).map((tag) => tag.trim()).filter(Boolean);
|
|
201
|
+
if (input.length === 0)
|
|
202
|
+
return [];
|
|
203
|
+
let vocabulary;
|
|
204
|
+
try {
|
|
205
|
+
vocabulary = await collectLabelVocabulary(context);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const hits = [];
|
|
211
|
+
for (const tag of input) {
|
|
212
|
+
const needle = tag.toLowerCase();
|
|
213
|
+
const label = vocabulary.find((row) => (row.name ?? '').toLowerCase() === needle);
|
|
214
|
+
if (label)
|
|
215
|
+
hits.push({ tag, label });
|
|
216
|
+
}
|
|
217
|
+
return hits;
|
|
218
|
+
}
|
|
219
|
+
/** The stderr line for `tagsNamingLabels` hits; an empty string when there are none. */
|
|
220
|
+
function describeTagsNamingLabels(hits) {
|
|
221
|
+
if (hits.length === 0)
|
|
222
|
+
return '';
|
|
223
|
+
const names = hits.map((hit) => `"${hit.tag}"`).join(', ');
|
|
224
|
+
const flags = hits.map((hit) => `--label ${hit.label.name}`).join(' ');
|
|
225
|
+
return (`--tag ${names} ${hits.length === 1 ? 'was' : 'were'} stored as free-form ` +
|
|
226
|
+
`${hits.length === 1 ? 'tag' : 'tags'}, NOT as the declared label of the same name, ` +
|
|
227
|
+
`so a label filter will not find this task. To attach the label, pass ${flags}.`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The whole `--tag` check a write command runs: the warning for tags that equal
|
|
231
|
+
* a declared label AND were not also attached through `--label` (passing both
|
|
232
|
+
* is someone doing it right). An empty string means say nothing.
|
|
233
|
+
*/
|
|
234
|
+
async function tagLabelWarning(tags, attachedLabelIds, context) {
|
|
235
|
+
if (!tags || tags.length === 0)
|
|
236
|
+
return '';
|
|
237
|
+
const attached = new Set(attachedLabelIds ?? []);
|
|
238
|
+
const hits = await tagsNamingLabels(tags, context);
|
|
239
|
+
return describeTagsNamingLabels(hits.filter((hit) => !attached.has(hit.label.id)));
|
|
240
|
+
}
|
package/dist/lib/login.js
CHANGED
|
@@ -31,12 +31,13 @@ const node_child_process_1 = require("node:child_process");
|
|
|
31
31
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
32
32
|
const auth_storage_1 = require("./auth-storage");
|
|
33
33
|
const config_1 = require("./config");
|
|
34
|
-
const
|
|
34
|
+
const cli_identity_1 = require("./cli-identity");
|
|
35
35
|
const ssh_detect_1 = require("./ssh-detect");
|
|
36
36
|
const device_code_1 = require("./device-code");
|
|
37
37
|
const device_identity_1 = require("./device-identity");
|
|
38
38
|
const oauthLogin_1 = require("./oauthLogin");
|
|
39
39
|
const daemonBroker_1 = require("./daemonBroker");
|
|
40
|
+
const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
|
|
40
41
|
/**
|
|
41
42
|
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
42
43
|
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
@@ -73,16 +74,16 @@ async function cliLogin(opts = {}) {
|
|
|
73
74
|
const serverUrl = (opts.serverUrl ?? cfg.baseURL).replace(/\/+$/, '');
|
|
74
75
|
const scope = opts.scope ?? 'cli';
|
|
75
76
|
// Persist the cliId on first login so future refreshes present a
|
|
76
|
-
// stable device identity.
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
81
|
-
}
|
|
77
|
+
// stable device identity. A named profile signing in with no credential is
|
|
78
|
+
// issued its OWN id here, so the hand-off confirm below cannot retire another
|
|
79
|
+
// profile's session (OSK-12156); see `cli-identity.ts`.
|
|
80
|
+
const cliId = (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
82
81
|
const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
|
|
83
82
|
const ssh = (0, ssh_detect_1.detectSshSession)();
|
|
84
83
|
/** Set when the broker was refused for a reason that WAITING fixes. */
|
|
85
84
|
let brokerRateLimited = false;
|
|
85
|
+
/** Set when a live daemon answered the hand-off and refused it (OSK-12149). */
|
|
86
|
+
let brokerRefusal = null;
|
|
86
87
|
// --- Daemon-as-broker pre-flight ---------------------------------------
|
|
87
88
|
// When the local `oversky` daemon is already authenticated for this user
|
|
88
89
|
// on this machine, the cleanest UX is no second browser flow at all:
|
|
@@ -129,7 +130,8 @@ async function cliLogin(opts = {}) {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
console.log('');
|
|
132
|
-
console.log(' Authorized via the skrr background service on this computer (no browser needed).'
|
|
133
|
+
console.log(' Authorized via the skrr background service on this computer (no browser needed). ' +
|
|
134
|
+
'Stored a durable session for this profile.');
|
|
133
135
|
return {
|
|
134
136
|
token: brokerResult.accessToken,
|
|
135
137
|
refreshToken: brokerResult.refreshToken,
|
|
@@ -179,6 +181,15 @@ async function cliLogin(opts = {}) {
|
|
|
179
181
|
console.log('');
|
|
180
182
|
console.log(describeBrokerBaseUrlMismatch(brokerResult, serverUrl));
|
|
181
183
|
}
|
|
184
|
+
// The daemon answered and refused. Say so before whatever comes next —
|
|
185
|
+
// a browser that opens with no stated cause, or a refusal about terminals
|
|
186
|
+
// that says nothing about the daemon, both hid the real reason.
|
|
187
|
+
brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(brokerResult);
|
|
188
|
+
if (brokerRefusal && !(0, ssh_detect_1.isNonInteractive)()) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(` Asked the local skrr service for a session first. ${brokerRefusal.headline}`);
|
|
191
|
+
console.log(' Continuing with a sign-in of this CLI’s own.');
|
|
192
|
+
}
|
|
182
193
|
}
|
|
183
194
|
// --- Decision tree (first match wins) ---
|
|
184
195
|
//
|
|
@@ -199,7 +210,10 @@ async function cliLogin(opts = {}) {
|
|
|
199
210
|
? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
|
|
200
211
|
'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
|
|
201
212
|
`retry shortly, or sign in now with ${exports.HEADLESS_LOGIN_HINT}`
|
|
202
|
-
:
|
|
213
|
+
: brokerRefusal
|
|
214
|
+
? `${brokerRefusal.headline} ${brokerRefusal.remedy}\n` +
|
|
215
|
+
`skrr login would otherwise open a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`
|
|
216
|
+
: `skrr login opens a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`);
|
|
203
217
|
}
|
|
204
218
|
if (ssh.detected) {
|
|
205
219
|
// Device-code's "open this URL on your laptop" UX is the correct
|
|
@@ -282,9 +296,7 @@ async function cliLoginWithRefreshToken(opts) {
|
|
|
282
296
|
// the operator retries. The refresh exchange itself is idempotent on
|
|
283
297
|
// the server side (rotation is the unit of retry); writing the cliId
|
|
284
298
|
// up-front never causes a server-state mismatch.
|
|
285
|
-
|
|
286
|
-
(0, config_1.saveConfig)({ ...cfg, cliId: opts.cliId });
|
|
287
|
-
}
|
|
299
|
+
(0, cli_identity_1.persistPresentedCliId)(cfg, opts.cliId);
|
|
288
300
|
let pair;
|
|
289
301
|
try {
|
|
290
302
|
pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, opts.refreshToken, opts.cliId, {
|
package/dist/lib/oauthLogin.js
CHANGED
|
@@ -44,7 +44,7 @@ const node_http_1 = __importDefault(require("node:http"));
|
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const auth_storage_1 = require("./auth-storage");
|
|
46
46
|
const config_1 = require("./config");
|
|
47
|
-
const
|
|
47
|
+
const cli_identity_1 = require("./cli-identity");
|
|
48
48
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// PKCE helpers
|
|
@@ -272,13 +272,7 @@ async function runOAuthLogin(opts) {
|
|
|
272
272
|
});
|
|
273
273
|
// Resolve cliId — explicit override wins; otherwise persist on disk
|
|
274
274
|
// so subsequent refreshes present the same device identity.
|
|
275
|
-
|
|
276
|
-
if (!cliId) {
|
|
277
|
-
cliId = cfg.cliId ?? (0, device_id_1.generateCliId)();
|
|
278
|
-
if (!cfg.cliId) {
|
|
279
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
275
|
+
const cliId = opts?.cliId ?? (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
282
276
|
const { codeVerifier, codeChallenge } = generatePkce();
|
|
283
277
|
const state = generateState();
|
|
284
278
|
return new Promise((resolve) => {
|
package/dist/lib/refresh.js
CHANGED
|
@@ -27,6 +27,7 @@ Object.defineProperty(exports, "PermanentAuthFailure", { enumerable: true, get:
|
|
|
27
27
|
Object.defineProperty(exports, "TransientAuthFailure", { enumerable: true, get: function () { return auth_core_1.TransientAuthFailure; } });
|
|
28
28
|
const auth_storage_1 = require("./auth-storage");
|
|
29
29
|
const config_1 = require("./config");
|
|
30
|
+
const cli_id_1 = require("./cli-id");
|
|
30
31
|
const credential_resolver_1 = require("./credential-resolver");
|
|
31
32
|
const device_identity_1 = require("./device-identity");
|
|
32
33
|
exports.CLI_REFRESH_EARLY_MS = 5 * 60 * 1000;
|
|
@@ -182,7 +183,10 @@ async function ensureFreshCliCredential(opts = {}) {
|
|
|
182
183
|
*/
|
|
183
184
|
async function refreshCliToken() {
|
|
184
185
|
const cfg = (0, config_1.loadConfig)();
|
|
185
|
-
|
|
186
|
+
// The id the stored credential was minted under — the machine id, or this
|
|
187
|
+
// profile's own (OSK-12156). One resolver, so refresh cannot disagree with login.
|
|
188
|
+
const cliId = (0, cli_id_1.resolveCliId)(cfg);
|
|
189
|
+
if (!cliId) {
|
|
186
190
|
throw new CliRefreshUnavailableError('No CLI identity persisted (cliId). Run `skrr login` to initialize.');
|
|
187
191
|
}
|
|
188
192
|
const origin = getCliOrigin();
|
|
@@ -195,7 +199,7 @@ async function refreshCliToken() {
|
|
|
195
199
|
if (!bundle?.refreshToken) {
|
|
196
200
|
throw new CliRefreshUnavailableError('No refresh token available — sign in with `skrr login`.');
|
|
197
201
|
}
|
|
198
|
-
const pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, bundle.refreshToken,
|
|
202
|
+
const pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, bundle.refreshToken, cliId,
|
|
199
203
|
// L11 Phase 3 — attach a device-identity proof to every refresh.
|
|
200
204
|
// The signer returns null when device identity isn't initialized
|
|
201
205
|
// (Phase 1 binary, KEK rotation hot path), and the server's lenient
|
package/dist/lib/tasks.js
CHANGED
|
@@ -587,14 +587,24 @@ async function resolveTaskCreateScope(payload, opts = {}) {
|
|
|
587
587
|
payload.spaceId = parentSpaceId;
|
|
588
588
|
}
|
|
589
589
|
const projectId = stringValue(payload.projectId);
|
|
590
|
-
|
|
590
|
+
// A Mongo `_id` is resolved even beside --space: the server addresses a
|
|
591
|
+
// project by its `id` or its ref only, so passing the `_id` through would
|
|
592
|
+
// fail there with less to say about it (OSK-12054).
|
|
593
|
+
if (projectId && (!stringValue(payload.spaceId) || looksLikeStorageId(projectId))) {
|
|
591
594
|
const project = await getCachedProject(projectId, opts.projectCache);
|
|
592
595
|
resolution.project = project;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
+
// Accepting a second spelling obliges returning the canonical one: the
|
|
597
|
+
// write carries `id`, never whatever the caller happened to paste.
|
|
598
|
+
const canonicalId = stringValue(project.id);
|
|
599
|
+
if (canonicalId && canonicalId !== projectId)
|
|
600
|
+
payload.projectId = canonicalId;
|
|
601
|
+
if (!stringValue(payload.spaceId)) {
|
|
602
|
+
const projectSpaceId = stringValue(project.spaceId);
|
|
603
|
+
if (!projectSpaceId) {
|
|
604
|
+
throw new Error(`Project ${projectId} did not return an owning space`);
|
|
605
|
+
}
|
|
606
|
+
payload.spaceId = projectSpaceId;
|
|
596
607
|
}
|
|
597
|
-
payload.spaceId = projectSpaceId;
|
|
598
608
|
}
|
|
599
609
|
return resolution;
|
|
600
610
|
}
|
|
@@ -894,21 +904,36 @@ async function getCachedTask(taskIdValue, cache) {
|
|
|
894
904
|
}
|
|
895
905
|
return cache.get(taskIdValue);
|
|
896
906
|
}
|
|
907
|
+
/** A 24-hex Mongo ObjectId — the storage `_id` entity JSON prints beside `id`. */
|
|
908
|
+
const STORAGE_ID_RE = /^[0-9a-f]{24}$/i;
|
|
909
|
+
function looksLikeStorageId(value) {
|
|
910
|
+
return STORAGE_ID_RE.test(value);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Entity JSON prints both `id` (canonical) and `_id` (storage) with nothing
|
|
914
|
+
* saying which one commands take, so a pasted `_id` is accepted as the same
|
|
915
|
+
* project rather than refused as unknown (OSK-12054). The caller then writes
|
|
916
|
+
* the canonical `id` — see `resolveTaskCreateScope`.
|
|
917
|
+
*/
|
|
918
|
+
function projectMatchesId(project, ref) {
|
|
919
|
+
return String(project.id ?? '') === ref || String(project._id ?? '') === ref;
|
|
920
|
+
}
|
|
897
921
|
async function getCachedProject(projectId, cache) {
|
|
898
922
|
const load = async () => {
|
|
899
923
|
const filtered = await projects_1.projectApi.search({ q: projectId, limit: 20 });
|
|
900
|
-
let exact = (0, projects_1.projectsFrom)(filtered).filter((project) =>
|
|
924
|
+
let exact = (0, projects_1.projectsFrom)(filtered).filter((project) => projectMatchesId(project, projectId));
|
|
901
925
|
// The cross-space endpoint's `q` contract covers title/description/ref,
|
|
902
926
|
// not UUID. Fall back to the bounded visible project catalog so canonical
|
|
903
927
|
// IDs still resolve without inventing a second project read endpoint.
|
|
904
928
|
if (exact.length === 0) {
|
|
905
929
|
const catalog = await projects_1.projectApi.search({ limit: 200 });
|
|
906
|
-
exact = (0, projects_1.projectsFrom)(catalog).filter((project) =>
|
|
930
|
+
exact = (0, projects_1.projectsFrom)(catalog).filter((project) => projectMatchesId(project, projectId));
|
|
907
931
|
}
|
|
908
932
|
if (exact.length !== 1) {
|
|
909
933
|
throw new Error(exact.length > 1
|
|
910
934
|
? `Project id ${projectId} matched multiple projects`
|
|
911
|
-
: `Project
|
|
935
|
+
: `Project ${projectId} was not found among the projects you can see. Pass the ` +
|
|
936
|
+
"project's `id` (the uuid `projects list` prints), or its ref (OSK-P-5) together with --space");
|
|
912
937
|
}
|
|
913
938
|
return exact[0];
|
|
914
939
|
};
|