@skrr-ai/cli 0.1.44 → 0.1.46

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 (73) hide show
  1. package/dist/base-command.d.ts +13 -1
  2. package/dist/base-command.js +37 -3
  3. package/dist/commands/agents/actions/create.js +11 -4
  4. package/dist/commands/agents/chat.js +3 -4
  5. package/dist/commands/browser/cloud-agents.d.ts +20 -0
  6. package/dist/commands/browser/cloud-agents.js +43 -0
  7. package/dist/commands/code/handover.d.ts +11 -0
  8. package/dist/commands/code/handover.js +105 -1
  9. package/dist/commands/commitments/handover.d.ts +29 -0
  10. package/dist/commands/commitments/handover.js +99 -0
  11. package/dist/commands/followups/list.js +1 -1
  12. package/dist/commands/followups/watch.js +40 -5
  13. package/dist/commands/initiatives/list.d.ts +1 -0
  14. package/dist/commands/initiatives/list.js +11 -2
  15. package/dist/commands/labels/list.d.ts +28 -0
  16. package/dist/commands/labels/list.js +50 -32
  17. package/dist/commands/tasks/fork.d.ts +32 -0
  18. package/dist/commands/tasks/fork.js +171 -0
  19. package/dist/commands/tasks/handover.d.ts +23 -0
  20. package/dist/commands/tasks/handover.js +205 -0
  21. package/dist/commands/tasks/promote.d.ts +23 -0
  22. package/dist/commands/tasks/promote.js +82 -0
  23. package/dist/commands/tasks/runs.d.ts +16 -0
  24. package/dist/commands/tasks/runs.js +46 -1
  25. package/dist/commands/tasks/show.d.ts +23 -0
  26. package/dist/commands/tasks/show.js +59 -0
  27. package/dist/lib/agentic-stream.js +41 -2
  28. package/dist/lib/api-fetch.d.ts +7 -1
  29. package/dist/lib/api-fetch.js +9 -2
  30. package/dist/lib/auth-core-init.js +11 -4
  31. package/dist/lib/code-handover.d.ts +60 -0
  32. package/dist/lib/code-handover.js +120 -0
  33. package/dist/lib/commitments.d.ts +22 -0
  34. package/dist/lib/commitments.js +42 -0
  35. package/dist/lib/dedicated-wait.js +22 -1
  36. package/dist/lib/followups.d.ts +18 -0
  37. package/dist/lib/followups.js +44 -0
  38. package/dist/lib/initiatives.d.ts +15 -3
  39. package/dist/lib/label-ref.d.ts +2 -0
  40. package/dist/lib/node-adapter.js +6 -0
  41. package/dist/lib/task-extras.d.ts +44 -0
  42. package/dist/lib/task-extras.js +73 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  70. package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34300 -33843
  73. package/package.json +2 -2
@@ -0,0 +1,333 @@
1
+ /**
2
+ * profileStateDir.ts — the ONE definition of where a profile's auth state lives
3
+ * on disk, and the migration for a machine that ended up with two.
4
+ *
5
+ * A profile is this product's unit of credential isolation, and its state has
6
+ * exactly one home:
7
+ *
8
+ * <machine root>/profiles/<profile>/cred-dek-wrapped
9
+ * <machine root>/profiles/<profile>/cred-envelope.lock
10
+ * <machine root>/profiles/<profile>/device-private-key
11
+ * …
12
+ *
13
+ * It lives here, next to `configRoot.ts`, for the same reason that file exists:
14
+ * both binaries need the same answer and neither owns it. `configRoot.ts` made
15
+ * "where is the skrr root" one function; this makes "where is a profile's auth
16
+ * state" one function, because the layer above it had the identical defect one
17
+ * level down.
18
+ *
19
+ * WHAT WENT WRONG (OSK-10314). `credentialEnvelopeBridge` and
20
+ * `deviceIdentityBridge` each wrote `path.join(getAuthConfigDir(), 'profiles',
21
+ * profile)` — two independent spellings of one rule — and `configDir` means
22
+ * different things to the two consumers:
23
+ *
24
+ * - the CLI passes the MACHINE ROOT, so it landed on the layout above;
25
+ * - the daemon passes `getConfigDir()`, which is ALREADY `<root>/profiles/<p>`,
26
+ * so it landed on `<root>/profiles/<p>/profiles/<p>/…`.
27
+ *
28
+ * One profile therefore had two wrapped DEKs, two device keypairs and two
29
+ * lockfiles, and the lockfile the shared module exists to share served neither
30
+ * of the two processes it was meant to serialize. Nothing cross-read, so
31
+ * nothing broke — it was a split-brain waiting for the first cross-read.
32
+ *
33
+ * The fix is to derive the directory from `getAuthMachineConfigDir()`, which
34
+ * BOTH consumers already wire explicitly (daemon: `getMachineConfigDir()`; CLI:
35
+ * the skrr root). Note what is deliberately NOT done: the daemon is not asked to
36
+ * pass a different `configDir`. It has one knob and auth-core needs two answers
37
+ * from it — `refresh.ts` reads `getAuthConfigDir()` raw for `auth.lock` and
38
+ * `needs-reauth.json`, so repointing the daemon's `configDir` at the machine
39
+ * root would quietly move its reauth latch from per-profile to machine-wide and
40
+ * let one profile latch every profile. Profile-scoped and root-scoped artifacts
41
+ * are only distinguishable INSIDE auth-core, so that is where the line is drawn.
42
+ *
43
+ * WHY A BARE PATH SWAP IS NOT THE FIX. A daemon with a nested wrapped DEK has
44
+ * sealed real credentials under it. Pointing it at the canonical path hands it a
45
+ * DIFFERENT DEK and every one of those credentials stops decrypting — a silent,
46
+ * fleet-wide forced re-login, to repair a bug that had so far harmed nobody. So
47
+ * the resolver PREFERS THE FILE A PROCESS IS ALREADY USING until a migration has
48
+ * proved the canonical one is equivalent, and the migration COPIES.
49
+ *
50
+ * Resolution order for `<filename>` under `<profile>` (stateless — no ordering
51
+ * requirement between this and any init):
52
+ *
53
+ * 1. legacy dir IS the canonical dir (the CLI) → canonical
54
+ * 2. no legacy copy of the file → canonical
55
+ * 3. a `<filename>.superseded.json` marker beside it → canonical (consolidated)
56
+ * 4. no canonical copy of the file → legacy (not yet consolidated)
57
+ * 5. both copies, no marker → legacy (unresolved: stay put)
58
+ *
59
+ * The migration writes the marker, never deletes, and refuses case 5 when both
60
+ * sides hold a usable and DIFFERENT key — because merging those would require
61
+ * re-sealing content that lives in the OS keychain and is not auth-core's to
62
+ * re-seal. A human resolves that one, and the loud signal is the deliverable.
63
+ *
64
+ * ROLLBACK. The legacy file is left exactly where it was, under its own name.
65
+ * Older code looks there, finds it, and works — no operator step. The marker is
66
+ * a sidecar older code never reads.
67
+ */
68
+ import { createHash } from 'node:crypto';
69
+ import fs from 'node:fs';
70
+ import path from 'node:path';
71
+ import { getAuthConfigDir, getAuthMachineConfigDir } from './runtime.js';
72
+ /** Directory segment that separates per-profile state from machine-scoped state. */
73
+ export const PROFILES_SEGMENT = 'profiles';
74
+ /**
75
+ * Sidecar written beside a legacy file once it has been consolidated. Its
76
+ * PRESENCE is what flips resolution to the canonical copy; its contents are for
77
+ * a human reading the directory afterwards.
78
+ */
79
+ export const SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
80
+ /**
81
+ * Suffix for a canonical file moved aside to make room. Only used when the
82
+ * canonical copy existed and was NOT usable — a usable one is never replaced.
83
+ */
84
+ export const SUPERSEDED_COPY_SUFFIX = '.superseded';
85
+ /** The canonical directory for a profile's auth state. */
86
+ export function profileStateDir(profile) {
87
+ assertProfile(profile);
88
+ return path.join(getAuthMachineConfigDir(), PROFILES_SEGMENT, profile);
89
+ }
90
+ /**
91
+ * Where a consumer whose `configDir` is already profile-scoped used to write.
92
+ *
93
+ * `null` when it resolves to the canonical directory — i.e. the consumer passes
94
+ * the machine root and never had a second location. Callers branch on `null`
95
+ * rather than comparing strings so the "one root" case costs one check.
96
+ */
97
+ export function legacyProfileStateDir(profile) {
98
+ assertProfile(profile);
99
+ const legacy = path.join(getAuthConfigDir(), PROFILES_SEGMENT, profile);
100
+ const canonical = profileStateDir(profile);
101
+ return path.resolve(legacy) === path.resolve(canonical) ? null : legacy;
102
+ }
103
+ /**
104
+ * Resolve the directory holding `primaryFilename` for `profile`.
105
+ *
106
+ * `primaryFilename` is the file whose presence decides where a group of related
107
+ * files lives. Device identity has four files that must be read from ONE
108
+ * directory — a public JWK from one copy and a private key from the other is a
109
+ * keypair mismatch — so the group anchors on the private key rather than each
110
+ * file answering for itself.
111
+ */
112
+ export function resolveProfileStateDir(profile, primaryFilename) {
113
+ const canonicalDir = profileStateDir(profile);
114
+ const legacyDir = legacyProfileStateDir(profile);
115
+ if (legacyDir === null) {
116
+ return { dir: canonicalDir, canonicalDir, legacyDir: null, usingLegacy: false };
117
+ }
118
+ const legacyFile = path.join(legacyDir, primaryFilename);
119
+ const stayOnLegacy = existsFile(legacyFile) && !existsFile(`${legacyFile}${SUPERSEDED_MARKER_SUFFIX}`);
120
+ return {
121
+ dir: stayOnLegacy ? legacyDir : canonicalDir,
122
+ canonicalDir,
123
+ legacyDir,
124
+ usingLegacy: stayOnLegacy,
125
+ };
126
+ }
127
+ /** `resolveProfileStateDir` for a single self-anchoring file. */
128
+ export function resolveProfileStateFile(profile, filename) {
129
+ const resolved = resolveProfileStateDir(profile, filename);
130
+ return {
131
+ ...resolved,
132
+ path: path.join(resolved.dir, filename),
133
+ canonicalPath: path.join(resolved.canonicalDir, filename),
134
+ legacyPath: resolved.legacyDir === null ? null : path.join(resolved.legacyDir, filename),
135
+ };
136
+ }
137
+ /** Every location `filename` may occupy for `profile`, canonical first. */
138
+ export function profileStateFileLocations(profile, filename) {
139
+ const canonicalDir = profileStateDir(profile);
140
+ const legacyDir = legacyProfileStateDir(profile);
141
+ const out = [path.join(canonicalDir, filename)];
142
+ if (legacyDir !== null)
143
+ out.push(path.join(legacyDir, filename));
144
+ return out;
145
+ }
146
+ /**
147
+ * Consolidate a profile's state onto the canonical path. Idempotent, additive,
148
+ * and refuses rather than guesses.
149
+ *
150
+ * The one case it will not decide is two usable, different keys for one profile:
151
+ * each side has sealed content under its own, that content lives in the OS
152
+ * keychain, and nothing here can re-seal it. Picking a winner there would
153
+ * destroy the loser's credentials silently, which is the exact failure this
154
+ * whole module exists to avoid.
155
+ */
156
+ export async function consolidateProfileState(profile, spec) {
157
+ assertProfile(profile);
158
+ const dryRun = spec.dryRun === true;
159
+ const canonicalDir = profileStateDir(profile);
160
+ const legacyDir = legacyProfileStateDir(profile);
161
+ const base = { profile, canonicalDir, legacyDir, written: [], supersededAside: [], dryRun };
162
+ if (legacyDir === null)
163
+ return { ...base, status: 'not_applicable' };
164
+ const legacyPrimary = path.join(legacyDir, spec.primary);
165
+ if (!existsFile(legacyPrimary))
166
+ return { ...base, status: 'nothing_to_migrate' };
167
+ const markerPath = `${legacyPrimary}${SUPERSEDED_MARKER_SUFFIX}`;
168
+ if (existsFile(markerPath))
169
+ return { ...base, status: 'already_consolidated' };
170
+ const canonicalPrimary = path.join(canonicalDir, spec.primary);
171
+ if (!existsFile(canonicalPrimary)) {
172
+ const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
173
+ if (!dryRun)
174
+ writeMarker(markerPath, canonicalPrimary, 'adopted');
175
+ emitOutcome(spec, 'adopted', { profile, from: legacyDir, to: canonicalDir });
176
+ return { ...base, status: 'adopted', ...moved };
177
+ }
178
+ const identify = spec.identify ?? ((dir) => sha256OfFile(path.join(dir, spec.primary)));
179
+ const legacyIdentity = await identify(legacyDir);
180
+ const canonicalIdentity = await identify(canonicalDir);
181
+ const identities = { canonical: canonicalIdentity, legacy: legacyIdentity };
182
+ // Same state in both places, or the legacy copy is unreadable while the
183
+ // canonical one is fine. Either way the canonical copy is authoritative and
184
+ // the legacy one has nothing to contribute — record that and move on.
185
+ const sameState = legacyIdentity !== null && canonicalIdentity !== null && legacyIdentity === canonicalIdentity;
186
+ const legacyHasNothingToOffer = legacyIdentity === null && canonicalIdentity !== null;
187
+ const bothUnreadableButIdentical = legacyIdentity === null &&
188
+ canonicalIdentity === null &&
189
+ sha256OfFile(legacyPrimary) !== null &&
190
+ sha256OfFile(legacyPrimary) === sha256OfFile(canonicalPrimary);
191
+ if (sameState || legacyHasNothingToOffer || bothUnreadableButIdentical) {
192
+ if (!dryRun)
193
+ writeMarker(markerPath, canonicalPrimary, 'already_consolidated');
194
+ emitOutcome(spec, 'already_consolidated', { profile, identities });
195
+ return { ...base, status: 'already_consolidated', identities };
196
+ }
197
+ // The legacy copy is usable and the canonical one is not. Nothing is sealed
198
+ // under an unreadable key that was not already lost, so replacing it costs
199
+ // nothing — but keep it, because "unreadable here, today" is not "worthless".
200
+ if (legacyIdentity !== null && canonicalIdentity === null) {
201
+ const aside = dryRun
202
+ ? plannedAside(canonicalDir, spec.files)
203
+ : moveAside(canonicalDir, spec.files);
204
+ const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
205
+ if (!dryRun)
206
+ writeMarker(markerPath, canonicalPrimary, 'superseded');
207
+ emitOutcome(spec, 'superseded', { profile, identities, supersededAside: aside });
208
+ return {
209
+ ...base,
210
+ status: 'superseded',
211
+ written: moved.written,
212
+ supersededAside: aside,
213
+ identities,
214
+ };
215
+ }
216
+ emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
217
+ return { ...base, status: 'conflict', identities };
218
+ }
219
+ /**
220
+ * Every path a profile's copy of `filename` can occupy, plus its marker —
221
+ * what a genuine logout must remove. Deliberately separate from the migration:
222
+ * this module never deletes, and logout is the one caller that must.
223
+ */
224
+ export function profileStateFilePathsToClear(profile, filename) {
225
+ const out = [];
226
+ for (const location of profileStateFileLocations(profile, filename)) {
227
+ out.push(location);
228
+ out.push(`${location}${SUPERSEDED_MARKER_SUFFIX}`);
229
+ }
230
+ return out;
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // internals
234
+ // ---------------------------------------------------------------------------
235
+ function assertProfile(profile) {
236
+ if (typeof profile !== 'string' || profile.length === 0) {
237
+ throw new Error('profileStateDir: profile must be a non-empty string');
238
+ }
239
+ }
240
+ function existsFile(target) {
241
+ try {
242
+ return fs.statSync(target).isFile();
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
248
+ function sha256OfFile(target) {
249
+ try {
250
+ return createHash('sha256').update(fs.readFileSync(target)).digest('hex');
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ }
256
+ /**
257
+ * tmp + fsync + rename + dir-fsync, 0600. The same shape the two bridges
258
+ * already use, so a crash mid-migration leaves either the old file or the new
259
+ * one and never a half-written key.
260
+ */
261
+ function atomicWrite0600(target, value) {
262
+ const dir = path.dirname(target);
263
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
264
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
265
+ const fd = fs.openSync(tmp, 'w', 0o600);
266
+ try {
267
+ fs.writeSync(fd, value);
268
+ fs.fsyncSync(fd);
269
+ }
270
+ finally {
271
+ fs.closeSync(fd);
272
+ }
273
+ fs.renameSync(tmp, target);
274
+ try {
275
+ const dirFd = fs.openSync(dir, 'r');
276
+ try {
277
+ fs.fsyncSync(dirFd);
278
+ }
279
+ finally {
280
+ fs.closeSync(dirFd);
281
+ }
282
+ }
283
+ catch {
284
+ /* best-effort — Windows / some BSDs reject dir fsync */
285
+ }
286
+ }
287
+ function adopt(fromDir, toDir, files, dryRun) {
288
+ const written = [];
289
+ for (const name of files) {
290
+ const source = path.join(fromDir, name);
291
+ if (!existsFile(source))
292
+ continue;
293
+ const target = path.join(toDir, name);
294
+ if (!dryRun)
295
+ atomicWrite0600(target, fs.readFileSync(source));
296
+ written.push(target);
297
+ }
298
+ return { written, supersededAside: [] };
299
+ }
300
+ function moveAside(dir, files) {
301
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
302
+ const aside = [];
303
+ for (const name of files) {
304
+ const target = path.join(dir, name);
305
+ if (!existsFile(target))
306
+ continue;
307
+ fs.renameSync(target, `${target}${SUPERSEDED_COPY_SUFFIX}.${stamp}`);
308
+ aside.push(target);
309
+ }
310
+ return aside;
311
+ }
312
+ function plannedAside(dir, files) {
313
+ return files.map((name) => path.join(dir, name)).filter((target) => existsFile(target));
314
+ }
315
+ function writeMarker(markerPath, supersededBy, reason) {
316
+ const payload = {
317
+ supersededBy,
318
+ reason,
319
+ sha256: sha256OfFile(supersededBy),
320
+ at: new Date().toISOString(),
321
+ note: 'Kept deliberately. This file is the pre-consolidation copy (OSK-10314); ' +
322
+ 'older binaries read it and it is the only rollback that needs no operator step. ' +
323
+ 'Remove it only through logout.',
324
+ };
325
+ atomicWrite0600(markerPath, Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf-8'));
326
+ }
327
+ function emitOutcome(spec, status, meta) {
328
+ spec.onEvent?.(`path_consolidation.${status}`, status !== 'conflict', {
329
+ ...meta,
330
+ primary: spec.primary,
331
+ ...(spec.dryRun === true ? { dryRun: true } : {}),
332
+ });
333
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/auth-core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "main": "dist/cjs/index.js",
5
5
  "types": "dist/esm/index.d.ts",
6
6
  "exports": {
@@ -33,6 +33,16 @@
33
33
  "types": "./dist/cjs/firstPartyHarness.d.ts",
34
34
  "default": "./dist/cjs/firstPartyHarness.js"
35
35
  }
36
+ },
37
+ "./harness-skill-layout": {
38
+ "import": {
39
+ "types": "./dist/esm/harnessSkillLayout.d.ts",
40
+ "default": "./dist/esm/harnessSkillLayout.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/cjs/harnessSkillLayout.d.ts",
44
+ "default": "./dist/cjs/harnessSkillLayout.js"
45
+ }
36
46
  }
37
47
  },
38
48
  "dependencies": {