@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.
Files changed (53) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +38 -8
  3. package/dist/commands/agents/create.js +8 -4
  4. package/dist/commands/agents/update.d.ts +23 -0
  5. package/dist/commands/agents/update.js +54 -9
  6. package/dist/commands/commitments/next-tick.js +9 -1
  7. package/dist/commands/context.js +2 -1
  8. package/dist/commands/daemon/config.js +16 -1
  9. package/dist/commands/doctor.js +8 -1
  10. package/dist/commands/login.d.ts +2 -1
  11. package/dist/commands/login.js +26 -7
  12. package/dist/commands/logout.js +3 -0
  13. package/dist/commands/machines/dedicated/show.js +25 -2
  14. package/dist/commands/tasks/create.d.ts +5 -0
  15. package/dist/commands/tasks/create.js +26 -6
  16. package/dist/commands/tasks/start.d.ts +14 -0
  17. package/dist/commands/tasks/start.js +31 -1
  18. package/dist/commands/tasks/update.js +8 -1
  19. package/dist/commands/whoami.js +5 -3
  20. package/dist/lib/agent-config.d.ts +2 -0
  21. package/dist/lib/agent-config.js +14 -10
  22. package/dist/lib/agentic-stream.d.ts +20 -0
  23. package/dist/lib/agentic-stream.js +39 -5
  24. package/dist/lib/auth-failure.js +4 -1
  25. package/dist/lib/auth-storage.d.ts +24 -0
  26. package/dist/lib/auth-storage.js +342 -11
  27. package/dist/lib/cli-id.d.ts +22 -0
  28. package/dist/lib/cli-id.js +56 -0
  29. package/dist/lib/cli-identity.d.ts +49 -0
  30. package/dist/lib/cli-identity.js +159 -0
  31. package/dist/lib/commitments.d.ts +4 -0
  32. package/dist/lib/commitments.js +12 -1
  33. package/dist/lib/config.d.ts +17 -0
  34. package/dist/lib/config.js +15 -3
  35. package/dist/lib/daemonBroker.d.ts +26 -4
  36. package/dist/lib/daemonBroker.js +82 -10
  37. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  38. package/dist/lib/daemonBrokerRefusal.js +127 -0
  39. package/dist/lib/dedicated-machines.d.ts +33 -2
  40. package/dist/lib/dedicated-machines.js +50 -1
  41. package/dist/lib/delegated-cli.js +16 -0
  42. package/dist/lib/first-party-harness-broker.js +6 -4
  43. package/dist/lib/keychain.d.ts +39 -0
  44. package/dist/lib/keychain.js +228 -39
  45. package/dist/lib/label-ref.d.ts +34 -0
  46. package/dist/lib/label-ref.js +68 -1
  47. package/dist/lib/login.js +24 -12
  48. package/dist/lib/oauthLogin.js +2 -8
  49. package/dist/lib/refresh.js +6 -2
  50. package/dist/lib/tasks.js +33 -8
  51. package/dist/node_modules/@skrr-ai/data-provider/index.js +3138 -3100
  52. package/oclif.manifest.json +36364 -36355
  53. package/package.json +1 -1
@@ -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 device_id_1 = require("./device-id");
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. Same invariant as the device-code-only
77
- // path this supersedes.
78
- const cliId = cfg.cliId ?? (0, device_id_1.generateCliId)();
79
- if (!cfg.cliId) {
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
- : `skrr login opens a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`);
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
- if (cfg.cliId !== opts.cliId) {
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, {
@@ -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 device_id_1 = require("./device-id");
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
- let cliId = opts?.cliId;
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) => {
@@ -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
- if (!cfg.cliId) {
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, cfg.cliId,
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
- if (projectId && !stringValue(payload.spaceId)) {
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
- const projectSpaceId = stringValue(project.spaceId);
594
- if (!projectSpaceId) {
595
- throw new Error(`Project ${projectId} did not return an owning space`);
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) => String(project.id ?? project._id ?? '') === projectId);
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) => String(project.id ?? project._id ?? '') === projectId);
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 id ${projectId} was not found; pass a canonical project id or add --space`);
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
  };