@skrr-ai/cli 0.1.50 → 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 (131) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +95 -8
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +8 -4
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +32 -0
  9. package/dist/commands/agents/update.js +81 -9
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/commitments/doctor.d.ts +1 -0
  13. package/dist/commands/commitments/doctor.js +19 -4
  14. package/dist/commands/commitments/effective-policy.js +15 -1
  15. package/dist/commands/commitments/next-tick.js +12 -2
  16. package/dist/commands/commitments/preflight.js +10 -0
  17. package/dist/commands/commitments/show.js +11 -1
  18. package/dist/commands/context.js +2 -1
  19. package/dist/commands/daemon/config.d.ts +15 -0
  20. package/dist/commands/daemon/config.js +53 -0
  21. package/dist/commands/daemon/index.js +1 -0
  22. package/dist/commands/daemon/status.js +15 -1
  23. package/dist/commands/doctor.js +8 -1
  24. package/dist/commands/harnesses/models.d.ts +54 -0
  25. package/dist/commands/harnesses/models.js +100 -7
  26. package/dist/commands/harnesses/show.d.ts +11 -0
  27. package/dist/commands/harnesses/show.js +35 -0
  28. package/dist/commands/harnesses/update.d.ts +28 -1
  29. package/dist/commands/harnesses/update.js +73 -10
  30. package/dist/commands/login.d.ts +2 -1
  31. package/dist/commands/login.js +26 -7
  32. package/dist/commands/logout.js +3 -0
  33. package/dist/commands/machines/dedicated/show.js +25 -2
  34. package/dist/commands/outbox/list.d.ts +12 -1
  35. package/dist/commands/outbox/list.js +31 -2
  36. package/dist/commands/revoke-daemon.js +9 -0
  37. package/dist/commands/spaces/create.d.ts +1 -0
  38. package/dist/commands/spaces/create.js +17 -0
  39. package/dist/commands/spaces/show.js +5 -0
  40. package/dist/commands/spaces/update.d.ts +1 -0
  41. package/dist/commands/spaces/update.js +16 -0
  42. package/dist/commands/tasks/create.d.ts +6 -0
  43. package/dist/commands/tasks/create.js +31 -6
  44. package/dist/commands/tasks/show.js +45 -0
  45. package/dist/commands/tasks/start.d.ts +14 -0
  46. package/dist/commands/tasks/start.js +31 -1
  47. package/dist/commands/tasks/update.d.ts +1 -0
  48. package/dist/commands/tasks/update.js +29 -1
  49. package/dist/commands/tasks/upsert.d.ts +1 -0
  50. package/dist/commands/tasks/upsert.js +16 -0
  51. package/dist/commands/whoami.js +5 -3
  52. package/dist/lib/agent-config.d.ts +2 -0
  53. package/dist/lib/agent-config.js +14 -10
  54. package/dist/lib/agentic-stream.d.ts +88 -1
  55. package/dist/lib/agentic-stream.js +330 -9
  56. package/dist/lib/auth-core-init.d.ts +2 -10
  57. package/dist/lib/auth-core-init.js +20 -67
  58. package/dist/lib/auth-failure.d.ts +33 -0
  59. package/dist/lib/auth-failure.js +68 -0
  60. package/dist/lib/auth-storage.d.ts +24 -0
  61. package/dist/lib/auth-storage.js +342 -11
  62. package/dist/lib/cli-id.d.ts +22 -0
  63. package/dist/lib/cli-id.js +56 -0
  64. package/dist/lib/cli-identity.d.ts +49 -0
  65. package/dist/lib/cli-identity.js +159 -0
  66. package/dist/lib/commitments.d.ts +25 -1
  67. package/dist/lib/commitments.js +46 -1
  68. package/dist/lib/config.d.ts +17 -0
  69. package/dist/lib/config.js +15 -3
  70. package/dist/lib/daemonBroker.d.ts +37 -1
  71. package/dist/lib/daemonBroker.js +146 -22
  72. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  73. package/dist/lib/daemonBrokerRefusal.js +127 -0
  74. package/dist/lib/dedicated-machines.d.ts +33 -2
  75. package/dist/lib/dedicated-machines.js +50 -1
  76. package/dist/lib/delegated-cli.js +20 -1
  77. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  78. package/dist/lib/exec-runtime-binary.js +4 -2
  79. package/dist/lib/first-party-harness-broker.js +10 -5
  80. package/dist/lib/harnesses.d.ts +10 -0
  81. package/dist/lib/keychain.d.ts +39 -0
  82. package/dist/lib/keychain.js +228 -39
  83. package/dist/lib/label-ref.d.ts +34 -0
  84. package/dist/lib/label-ref.js +68 -1
  85. package/dist/lib/live-runtime-binary.d.ts +40 -0
  86. package/dist/lib/live-runtime-binary.js +193 -0
  87. package/dist/lib/login.d.ts +23 -0
  88. package/dist/lib/login.js +67 -21
  89. package/dist/lib/message-intent.js +3 -1
  90. package/dist/lib/oauthLogin.js +2 -8
  91. package/dist/lib/refresh.js +6 -2
  92. package/dist/lib/tasks.d.ts +20 -0
  93. package/dist/lib/tasks.js +87 -8
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  117. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  118. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  119. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  120. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  128. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  129. package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
  130. package/oclif.manifest.json +28591 -28438
  131. package/package.json +1 -1
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.profileFromArgs = profileFromArgs;
7
+ exports.decideStatusBinary = decideStatusBinary;
8
+ exports.describeStatusBinary = describeStatusBinary;
9
+ exports.scriptFromArgs = scriptFromArgs;
10
+ exports.defaultLiveRuntimeDeps = defaultLiveRuntimeDeps;
11
+ /**
12
+ * Which runtime binary is the LIVE daemon running, and is it the one
13
+ * `skrr daemon status` is about to forward to? (OSK-12102)
14
+ *
15
+ * `execRuntimeBinary` forwards to the first `skrrd` on PATH. That is right for
16
+ * lifecycle verbs, where PATH order is the person's choice of install. It is
17
+ * wrong for `status`, whose whole subject is the daemon that is running: with
18
+ * an old dev build early on PATH and the release daemon live, the header named
19
+ * `.../releases/0.8.62/skrrd` while the body was rendered by the dev build —
20
+ * its old layout, no per-category permission table — and nothing on screen
21
+ * said two binaries were involved.
22
+ *
23
+ * The live daemon's pid comes from its island bootstrap file (the same
24
+ * discovery the CLI's login broker uses); the program from the OS. When the
25
+ * live program is itself a runtime executable, status is forwarded to it.
26
+ * When it cannot be exec'd directly (a `node .../dist/index.js` dev daemon),
27
+ * status stays on the PATH binary and says so.
28
+ */
29
+ const node_child_process_1 = require("node:child_process");
30
+ const node_fs_1 = require("node:fs");
31
+ const node_path_1 = __importDefault(require("node:path"));
32
+ const daemonBroker_1 = require("./daemonBroker");
33
+ const exec_runtime_binary_1 = require("./exec-runtime-binary");
34
+ function safeRealpath(deps, p) {
35
+ try {
36
+ return deps.realpath(p);
37
+ }
38
+ catch {
39
+ return p;
40
+ }
41
+ }
42
+ function isRuntimeName(p) {
43
+ const base = node_path_1.default
44
+ .basename(p)
45
+ .toLowerCase()
46
+ .replace(/\.(exe|cmd)$/, '');
47
+ return exec_runtime_binary_1.RUNTIME_BINARY_NAMES.some((name) => base === name || base.startsWith(`${name}-`));
48
+ }
49
+ /** The profile a forwarded argv names, `default` otherwise. */
50
+ function profileFromArgs(args) {
51
+ for (let i = 0; i < args.length; i += 1) {
52
+ const arg = args[i];
53
+ if (arg === '--')
54
+ break;
55
+ if (arg === '--profile' && args[i + 1] && !args[i + 1].startsWith('-'))
56
+ return args[i + 1];
57
+ if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
58
+ return arg.slice('--profile='.length);
59
+ }
60
+ }
61
+ return 'default';
62
+ }
63
+ /**
64
+ * Decide which binary renders `status`. Never throws: a probe that cannot
65
+ * answer leaves the PATH binary in place (`unknown`).
66
+ */
67
+ function decideStatusBinary(pathBinary, profile, deps) {
68
+ let program = null;
69
+ try {
70
+ const pid = deps.livePid(profile);
71
+ program = pid ? deps.programOf(pid) : null;
72
+ }
73
+ catch {
74
+ program = null;
75
+ }
76
+ if (!program)
77
+ return { kind: 'unknown', binary: pathBinary };
78
+ const pathReal = safeRealpath(deps, pathBinary);
79
+ const execReal = safeRealpath(deps, program.exec);
80
+ const scriptReal = program.script ? safeRealpath(deps, program.script) : null;
81
+ if (pathReal === execReal || (scriptReal !== null && pathReal === scriptReal)) {
82
+ return { kind: 'same', binary: pathBinary };
83
+ }
84
+ // Only a runtime executable is forwarded to. A `node`/Electron-hosted daemon
85
+ // has an interpreter as its exec, and running the interpreter with status
86
+ // argv would be nonsense.
87
+ const directlyRunnable = program.script === null &&
88
+ (isRuntimeName(program.exec) || isRuntimeName(execReal)) &&
89
+ deps.isExecutable(program.exec);
90
+ if (directlyRunnable) {
91
+ return { kind: 'forward-live', binary: program.exec, pathBinary };
92
+ }
93
+ return { kind: 'mismatch', binary: pathBinary, live: program.script ?? program.exec };
94
+ }
95
+ /** One stderr line naming the binary that rendered status, when it is not obvious. */
96
+ function describeStatusBinary(decision) {
97
+ switch (decision.kind) {
98
+ case 'forward-live':
99
+ return (`Rendered by the live daemon's binary ${decision.binary} ` +
100
+ `(the first skrrd on PATH is ${decision.pathBinary}, a different build).`);
101
+ case 'mismatch':
102
+ return (`Rendered by ${decision.binary}, the first skrrd on PATH — NOT the live daemon's ` +
103
+ `program (${decision.live}). This layout and these checks are that build's, not the live one's.`);
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+ function darwinProgramOf(pid) {
109
+ const out = (args) => (0, node_child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', ...args], {
110
+ encoding: 'utf8',
111
+ timeout: 2_000,
112
+ stdio: ['ignore', 'pipe', 'ignore'],
113
+ }).trim();
114
+ const exec = out(['comm=']);
115
+ if (!exec || !node_path_1.default.isAbsolute(exec))
116
+ return null;
117
+ return { exec, script: scriptFromArgs(exec, out(['args='])) };
118
+ }
119
+ function linuxProgramOf(pid) {
120
+ const exec = (0, node_fs_1.readlinkSync)(`/proc/${pid}/exe`);
121
+ let script = null;
122
+ try {
123
+ const cmdline = (0, node_fs_1.readFileSync)(`/proc/${pid}/cmdline`, 'utf8');
124
+ script = scriptFromArgs(exec, cmdline.split('\0').filter(Boolean).join(' '));
125
+ }
126
+ catch {
127
+ script = null;
128
+ }
129
+ return { exec, script };
130
+ }
131
+ /**
132
+ * For an interpreter-hosted daemon (`node --import x.mjs /x/dist/index.js
133
+ * start …`), the script: the first argument after the exec that is neither an
134
+ * option nor the value of a module-loading option. `ps -o args=` is
135
+ * space-joined, so a path containing a space is not recovered — the result is
136
+ * only ever used for display and an equality check, where a miss degrades to
137
+ * the "mismatch" note rather than to a wrong forward.
138
+ */
139
+ const INTERPRETER_VALUE_OPTIONS = new Set([
140
+ '-r',
141
+ '--require',
142
+ '--import',
143
+ '--loader',
144
+ '--experimental-loader',
145
+ '-e',
146
+ '--eval',
147
+ ]);
148
+ function scriptFromArgs(exec, args) {
149
+ if (isRuntimeName(exec))
150
+ return null;
151
+ const rest = args.startsWith(exec) ? args.slice(exec.length) : args.replace(/^\S+/, '');
152
+ const tokens = rest.split(/\s+/).filter(Boolean);
153
+ for (let i = 0; i < tokens.length; i += 1) {
154
+ const token = tokens[i];
155
+ if (INTERPRETER_VALUE_OPTIONS.has(token)) {
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (token.startsWith('-'))
160
+ continue;
161
+ return token;
162
+ }
163
+ return null;
164
+ }
165
+ function defaultLiveRuntimeDeps() {
166
+ return {
167
+ livePid(profile) {
168
+ for (const candidate of (0, daemonBroker_1.resolveExactProfileBootstrapPaths)(profile)) {
169
+ const bootstrap = (0, daemonBroker_1.readBootstrap)(candidate);
170
+ if (typeof bootstrap?.pid === 'number' && bootstrap.pid > 0)
171
+ return bootstrap.pid;
172
+ }
173
+ return null;
174
+ },
175
+ programOf(pid) {
176
+ if (process.platform === 'darwin')
177
+ return darwinProgramOf(pid);
178
+ if (process.platform === 'linux')
179
+ return linuxProgramOf(pid);
180
+ return null;
181
+ },
182
+ realpath: (p) => (0, node_fs_1.realpathSync)(p),
183
+ isExecutable(p) {
184
+ try {
185
+ (0, node_fs_1.accessSync)(p, node_fs_1.constants.X_OK);
186
+ return true;
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ },
192
+ };
193
+ }
@@ -38,6 +38,29 @@ export interface CliLoginWithRefreshTokenOptions {
38
38
  */
39
39
  cliId: string;
40
40
  }
41
+ /**
42
+ * What a shell with no terminal can do instead (OSK-12117). The refusal used to
43
+ * name only CI tokens, which is not a sign-in: an agent session asking for one
44
+ * could only hand the job to a person typing `! skrr login`.
45
+ */
46
+ export declare const HEADLESS_LOGIN_HINT: string;
47
+ /**
48
+ * The line printed when the daemon descriptor this CLI found is for another
49
+ * server (OSK-12117).
50
+ *
51
+ * It used to assert "the skrr background service on this computer is logged in
52
+ * to <url>" and recommend repointing the CLI there. What the CLI actually knows
53
+ * is narrower: ONE descriptor file it read names that server. That file can
54
+ * belong to another profile's daemon (observed: a localhost:3080 descriptor
55
+ * while this machine's default daemon was connected to skrr.ai), and in that
56
+ * case both the claim and the advice were wrong — the advice by moving a
57
+ * production CLI onto a development server. So say what was read, from where,
58
+ * and how to check the daemon itself.
59
+ */
60
+ export declare function describeBrokerBaseUrlMismatch(failure: {
61
+ daemonServerUrl?: string;
62
+ descriptorPath?: string;
63
+ }, serverUrl: string): string;
41
64
  /**
42
65
  * Top-level dispatcher. Picks PKCE localhost vs device-code per the
43
66
  * decision tree in the roadmap and persists the result.
package/dist/lib/login.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEADLESS_LOGIN_HINT = void 0;
4
+ exports.describeBrokerBaseUrlMismatch = describeBrokerBaseUrlMismatch;
3
5
  exports.cliLogin = cliLogin;
4
6
  exports.cliLoginWithRefreshToken = cliLoginWithRefreshToken;
5
7
  /**
@@ -29,12 +31,40 @@ const node_child_process_1 = require("node:child_process");
29
31
  const auth_core_1 = require("@skrr-ai/auth-core");
30
32
  const auth_storage_1 = require("./auth-storage");
31
33
  const config_1 = require("./config");
32
- const device_id_1 = require("./device-id");
34
+ const cli_identity_1 = require("./cli-identity");
33
35
  const ssh_detect_1 = require("./ssh-detect");
34
36
  const device_code_1 = require("./device-code");
35
37
  const device_identity_1 = require("./device-identity");
36
38
  const oauthLogin_1 = require("./oauthLogin");
37
39
  const daemonBroker_1 = require("./daemonBroker");
40
+ const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
41
+ /**
42
+ * What a shell with no terminal can do instead (OSK-12117). The refusal used to
43
+ * name only CI tokens, which is not a sign-in: an agent session asking for one
44
+ * could only hand the job to a person typing `! skrr login`.
45
+ */
46
+ exports.HEADLESS_LOGIN_HINT = '`skrr login --device-code` works without a terminal: it prints a URL and a code for a ' +
47
+ 'person to approve in any browser, and finishes when they do. For CI, use ' +
48
+ '`skrr create-token` and set OVERSKY_TOKEN=osk_ci_...';
49
+ /**
50
+ * The line printed when the daemon descriptor this CLI found is for another
51
+ * server (OSK-12117).
52
+ *
53
+ * It used to assert "the skrr background service on this computer is logged in
54
+ * to <url>" and recommend repointing the CLI there. What the CLI actually knows
55
+ * is narrower: ONE descriptor file it read names that server. That file can
56
+ * belong to another profile's daemon (observed: a localhost:3080 descriptor
57
+ * while this machine's default daemon was connected to skrr.ai), and in that
58
+ * case both the claim and the advice were wrong — the advice by moving a
59
+ * production CLI onto a development server. So say what was read, from where,
60
+ * and how to check the daemon itself.
61
+ */
62
+ function describeBrokerBaseUrlMismatch(failure, serverUrl) {
63
+ const where = failure.descriptorPath ? ` (${failure.descriptorPath})` : '';
64
+ return (` Skipped the daemon hand-off: the daemon descriptor found${where} is for ` +
65
+ `${failure.daemonServerUrl}, not ${serverUrl}. It may belong to a different profile's ` +
66
+ 'daemon; `skrr daemon status` shows which server this profile’s daemon is on.');
67
+ }
38
68
  /**
39
69
  * Top-level dispatcher. Picks PKCE localhost vs device-code per the
40
70
  * decision tree in the roadmap and persists the result.
@@ -44,16 +74,16 @@ async function cliLogin(opts = {}) {
44
74
  const serverUrl = (opts.serverUrl ?? cfg.baseURL).replace(/\/+$/, '');
45
75
  const scope = opts.scope ?? 'cli';
46
76
  // Persist the cliId on first login so future refreshes present a
47
- // stable device identity. Same invariant as the device-code-only
48
- // path this supersedes.
49
- const cliId = cfg.cliId ?? (0, device_id_1.generateCliId)();
50
- if (!cfg.cliId) {
51
- (0, config_1.saveConfig)({ ...cfg, cliId });
52
- }
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);
53
81
  const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
54
82
  const ssh = (0, ssh_detect_1.detectSshSession)();
55
83
  /** Set when the broker was refused for a reason that WAITING fixes. */
56
84
  let brokerRateLimited = false;
85
+ /** Set when a live daemon answered the hand-off and refused it (OSK-12149). */
86
+ let brokerRefusal = null;
57
87
  // --- Daemon-as-broker pre-flight ---------------------------------------
58
88
  // When the local `oversky` daemon is already authenticated for this user
59
89
  // on this machine, the cleanest UX is no second browser flow at all:
@@ -100,7 +130,8 @@ async function cliLogin(opts = {}) {
100
130
  };
101
131
  }
102
132
  console.log('');
103
- 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.');
104
135
  return {
105
136
  token: brokerResult.accessToken,
106
137
  refreshToken: brokerResult.refreshToken,
@@ -148,25 +179,42 @@ async function cliLogin(opts = {}) {
148
179
  }
149
180
  if (brokerResult.reason === 'base_url_mismatch' && brokerResult.daemonServerUrl) {
150
181
  console.log('');
151
- console.log(` The skrr background service on this computer is logged in to ${brokerResult.daemonServerUrl}, but skrr is configured for ${serverUrl}.`);
152
- console.log(` Skipping the daemon-broker handoff. Run \`skrr login --base-url ${brokerResult.daemonServerUrl}\` to use the daemon instead.`);
182
+ console.log(describeBrokerBaseUrlMismatch(brokerResult, serverUrl));
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.');
153
192
  }
154
- }
155
- if ((0, ssh_detect_1.isNonInteractive)()) {
156
- throw new Error(brokerRateLimited
157
- ? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
158
- 'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
159
- 'retry shortly. For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...'
160
- : 'skrr login requires an interactive terminal or an SSH session. ' +
161
- 'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
162
193
  }
163
194
  // --- Decision tree (first match wins) ---
195
+ //
196
+ // Device code BEFORE the terminal check (OSK-12117). It needs no terminal of
197
+ // its own: it prints a URL and a code, a person approves them in any browser,
198
+ // and this process polls until they do. That is the one way an agent session
199
+ // or any other shell without a TTY can finish a login, and the refusal below
200
+ // used to fire first — so `--device-code` was refused for lacking the very
201
+ // thing it does not use.
164
202
  if (opts.forceDeviceCode) {
165
203
  return runDeviceCode(serverUrl, cliId, 'user_requested', ssh);
166
204
  }
167
205
  if (envForce) {
168
206
  return runDeviceCode(serverUrl, cliId, 'env_force', ssh);
169
207
  }
208
+ if ((0, ssh_detect_1.isNonInteractive)()) {
209
+ throw new Error(brokerRateLimited
210
+ ? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
211
+ 'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
212
+ `retry shortly, or sign in now with ${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}`);
217
+ }
170
218
  if (ssh.detected) {
171
219
  // Device-code's "open this URL on your laptop" UX is the correct
172
220
  // move under SSH — the localhost callback would bind on the remote
@@ -248,9 +296,7 @@ async function cliLoginWithRefreshToken(opts) {
248
296
  // the operator retries. The refresh exchange itself is idempotent on
249
297
  // the server side (rotation is the unit of retry); writing the cliId
250
298
  // up-front never causes a server-state mismatch.
251
- if (cfg.cliId !== opts.cliId) {
252
- (0, config_1.saveConfig)({ ...cfg, cliId: opts.cliId });
253
- }
299
+ (0, cli_identity_1.persistPresentedCliId)(cfg, opts.cliId);
254
300
  let pair;
255
301
  try {
256
302
  pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, opts.refreshToken, opts.cliId, {
@@ -11,6 +11,7 @@ exports.canonicalSlackChannelRequest = canonicalSlackChannelRequest;
11
11
  exports.canonicalSlackOwnerDmRequest = canonicalSlackOwnerDmRequest;
12
12
  exports.rejectAutonomousSlackTarget = rejectAutonomousSlackTarget;
13
13
  const auth_core_1 = require("@skrr-ai/auth-core");
14
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
14
15
  const api_fetch_1 = require("./api-fetch");
15
16
  const daemonBroker_1 = require("./daemonBroker");
16
17
  const runtime_context_1 = require("./runtime-context");
@@ -152,7 +153,8 @@ async function submitDaemonLoopbackMessageIntent({ context, request, env, bootst
152
153
  });
153
154
  try {
154
155
  const response = await Promise.race([
155
- (fetchImpl || globalThis.fetch)(`http://${bootstrap.host}:${bootstrap.port}/v1/agent/message-intents`, {
156
+ // The daemon secret and session token stay on loopback (OSK-12134).
157
+ (fetchImpl || loopback_http_1.loopbackFetch)(`http://${bootstrap.host}:${bootstrap.port}/v1/agent/message-intents`, {
156
158
  method: 'POST',
157
159
  headers: {
158
160
  Authorization: `Bearer ${bootstrap.secret}`,
@@ -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
@@ -8,6 +8,25 @@ import type { ParsedDeliverableCoordinate, ParsedExpectationSpec } from '@skrr-a
8
8
  */
9
9
  export declare function resolveTaskGoalFlag(value: string | undefined): string | null | undefined;
10
10
  export declare const TASK_GOAL_NOT_FOUND = "TASK_GOAL_NOT_FOUND";
11
+ /**
12
+ * `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
13
+ * `null` and `clear` mean "no per-task override": on update the PATCH sends
14
+ * `workingDirectory: null` ($unset → inherit the agent default), on create it
15
+ * stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
16
+ * spelling a user who knows `--goal none` reaches for.
17
+ * Undefined means the flag was not passed.
18
+ */
19
+ export declare function workingDirectoryFromFlag(value: string | undefined): string | null | undefined;
20
+ /**
21
+ * Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
22
+ * and `--from-json` inputs are checked identically). `null`/`undefined` are
23
+ * always fine — they mean "no override". A string must be absolute: the path
24
+ * is resolved on the daemon's filesystem, so a relative value can neither be
25
+ * verified here nor trusted there — it would fall back, silently, at run
26
+ * time. Failing here is the same contract as every other bad spec in this
27
+ * file: refuse before the write, never store a value that cannot take effect.
28
+ */
29
+ export declare function workingDirectoryValidationError(value: unknown): string | null;
11
30
  /**
12
31
  * The refusal for a goal id that names no goal, in CLI terms.
13
32
  *
@@ -91,6 +110,7 @@ export type TaskCreateFlagValues = {
91
110
  estimate?: number;
92
111
  tag?: string[];
93
112
  'label-id'?: string[];
113
+ 'working-directory'?: string;
94
114
  worktree?: boolean;
95
115
  'lifecycle-definition'?: string;
96
116
  'lifecycle-version'?: string;
package/dist/lib/tasks.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RECOGNIZED_TASK_VERDICTS = exports.DELIVERABLE_LIFECYCLE_VALUES = exports.EXPECT_FLAG_HELP = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = exports.TASK_GOAL_NOT_FOUND = void 0;
4
4
  exports.resolveTaskGoalFlag = resolveTaskGoalFlag;
5
+ exports.workingDirectoryFromFlag = workingDirectoryFromFlag;
6
+ exports.workingDirectoryValidationError = workingDirectoryValidationError;
5
7
  exports.describeTaskGoalNotFound = describeTaskGoalNotFound;
6
8
  exports.releasePinRepairGuard = releasePinRepairGuard;
7
9
  exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
@@ -38,6 +40,7 @@ exports.verdictIsRecognized = verdictIsRecognized;
38
40
  const data_provider_1 = require("@skrr-ai/data-provider");
39
41
  const prompt_1 = require("./prompt");
40
42
  const projects_1 = require("./projects");
43
+ const node_path_1 = require("node:path");
41
44
  /**
42
45
  * `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
43
46
  * sends `goalId: null`. Anything else names a goal. `clear` is accepted because
@@ -54,6 +57,47 @@ function resolveTaskGoalFlag(value) {
54
57
  return v;
55
58
  }
56
59
  exports.TASK_GOAL_NOT_FOUND = 'TASK_GOAL_NOT_FOUND';
60
+ /**
61
+ * `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
62
+ * `null` and `clear` mean "no per-task override": on update the PATCH sends
63
+ * `workingDirectory: null` ($unset → inherit the agent default), on create it
64
+ * stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
65
+ * spelling a user who knows `--goal none` reaches for.
66
+ * Undefined means the flag was not passed.
67
+ */
68
+ function workingDirectoryFromFlag(value) {
69
+ if (value === undefined)
70
+ return undefined;
71
+ const v = value.trim();
72
+ if (!v || ['none', 'null', 'clear'].includes(v.toLowerCase()))
73
+ return null;
74
+ return v;
75
+ }
76
+ /**
77
+ * Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
78
+ * and `--from-json` inputs are checked identically). `null`/`undefined` are
79
+ * always fine — they mean "no override". A string must be absolute: the path
80
+ * is resolved on the daemon's filesystem, so a relative value can neither be
81
+ * verified here nor trusted there — it would fall back, silently, at run
82
+ * time. Failing here is the same contract as every other bad spec in this
83
+ * file: refuse before the write, never store a value that cannot take effect.
84
+ */
85
+ function workingDirectoryValidationError(value) {
86
+ if (value === undefined || value === null)
87
+ return null;
88
+ if (typeof value !== 'string' || !value.trim()) {
89
+ return 'workingDirectory must be an absolute path string (or null for no override)';
90
+ }
91
+ if (value.length > 1024) {
92
+ return 'workingDirectory must be at most 1024 characters';
93
+ }
94
+ if (!(0, node_path_1.isAbsolute)(value.trim())) {
95
+ return (`workingDirectory must be an absolute path on the daemon's filesystem ` +
96
+ `(got '${value}'). ` +
97
+ 'A relative path cannot be resolved there — pass the full path, or "none" to clear.');
98
+ }
99
+ return null;
100
+ }
57
101
  /**
58
102
  * The refusal for a goal id that names no goal, in CLI terms.
59
103
  *
@@ -331,6 +375,8 @@ function buildTaskCreatePayload(body, flags = {}) {
331
375
  // is a Task→Task edge rather than a pointer at a chat message.
332
376
  spawnedByTaskId: flags['spawned-by'],
333
377
  spawnReason: flags['spawn-reason'],
378
+ // Per-task working-directory override; 'none' ⇒ null (no override).
379
+ workingDirectory: workingDirectoryFromFlag(flags['working-directory']),
334
380
  // Worktree isolation opt-in. --worktree ⇒ 'ephemeral', --no-worktree ⇒
335
381
  // 'shared', omitted ⇒ undefined (inherit space policy / global default).
336
382
  executionIsolation: executionIsolationFromWorktreeFlag(flags.worktree),
@@ -409,6 +455,9 @@ function validateTaskCreatePayload(payload) {
409
455
  if (payload.templateId !== undefined && !isBoundedString(payload.templateId, 1, 100)) {
410
456
  errors.push('templateId must be a non-empty string at most 100 characters');
411
457
  }
458
+ const workingDirectoryError = workingDirectoryValidationError(payload.workingDirectory);
459
+ if (workingDirectoryError)
460
+ errors.push(workingDirectoryError);
412
461
  if (payload.lifecycle !== undefined) {
413
462
  const binding = payload.lifecycle;
414
463
  if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
@@ -538,14 +587,24 @@ async function resolveTaskCreateScope(payload, opts = {}) {
538
587
  payload.spaceId = parentSpaceId;
539
588
  }
540
589
  const projectId = stringValue(payload.projectId);
541
- 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))) {
542
594
  const project = await getCachedProject(projectId, opts.projectCache);
543
595
  resolution.project = project;
544
- const projectSpaceId = stringValue(project.spaceId);
545
- if (!projectSpaceId) {
546
- 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;
547
607
  }
548
- payload.spaceId = projectSpaceId;
549
608
  }
550
609
  return resolution;
551
610
  }
@@ -756,6 +815,11 @@ function createPatchFromCreatePayload(payload) {
756
815
  'executionMode',
757
816
  'tags',
758
817
  'labelIds',
818
+ // Per-task working-directory override — part of the same execution shape
819
+ // the create payload carries, and equally meaningful on the update leg of
820
+ // an upsert. Absent here, `upsert --working-directory` silently no-ops on
821
+ // an existing task.
822
+ 'workingDirectory',
759
823
  ];
760
824
  for (const field of copyFields) {
761
825
  if (payload[field] !== undefined)
@@ -840,21 +904,36 @@ async function getCachedTask(taskIdValue, cache) {
840
904
  }
841
905
  return cache.get(taskIdValue);
842
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
+ }
843
921
  async function getCachedProject(projectId, cache) {
844
922
  const load = async () => {
845
923
  const filtered = await projects_1.projectApi.search({ q: projectId, limit: 20 });
846
- 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));
847
925
  // The cross-space endpoint's `q` contract covers title/description/ref,
848
926
  // not UUID. Fall back to the bounded visible project catalog so canonical
849
927
  // IDs still resolve without inventing a second project read endpoint.
850
928
  if (exact.length === 0) {
851
929
  const catalog = await projects_1.projectApi.search({ limit: 200 });
852
- 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));
853
931
  }
854
932
  if (exact.length !== 1) {
855
933
  throw new Error(exact.length > 1
856
934
  ? `Project id ${projectId} matched multiple projects`
857
- : `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");
858
937
  }
859
938
  return exact[0];
860
939
  };
@@ -21,6 +21,8 @@ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported'
21
21
  export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
22
22
  /** @internal Test seam — read state for assertions. */
23
23
  export declare function __getStateForTest(): CredEnvelopeState;
24
+ /** @internal Test seam — each spec owns its warn-dedupe horizon. */
25
+ export declare function __clearWarnDedupeForTest(): void;
24
26
  /**
25
27
  * Initialize the envelope path. Idempotent — second call when state is
26
28
  * non-uninit returns immediately.