@skrr-ai/cli 0.1.40 → 0.1.42

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 (120) hide show
  1. package/dist/base-command.d.ts +9 -0
  2. package/dist/base-command.js +18 -3
  3. package/dist/commands/agent-shares/list-for.js +4 -11
  4. package/dist/commands/agent-shares/received.js +4 -11
  5. package/dist/commands/agent-shares/sent.js +4 -11
  6. package/dist/commands/agent-shares/with.js +3 -10
  7. package/dist/commands/agents/activity.js +1 -1
  8. package/dist/commands/agents/api-actions/add.js +6 -0
  9. package/dist/commands/agents/chat.js +6 -1
  10. package/dist/commands/agents/create.js +6 -1
  11. package/dist/commands/agents/manager/ensure.d.ts +18 -0
  12. package/dist/commands/agents/manager/ensure.js +56 -0
  13. package/dist/commands/agents/show.js +4 -0
  14. package/dist/commands/agents/tools/auth.d.ts +6 -0
  15. package/dist/commands/agents/tools/auth.js +27 -1
  16. package/dist/commands/agents/tools/index.js +11 -1
  17. package/dist/commands/agents/update.js +6 -1
  18. package/dist/commands/code/install.d.ts +4 -2
  19. package/dist/commands/code/install.js +15 -14
  20. package/dist/commands/convos/list.js +1 -3
  21. package/dist/commands/convos/show.js +4 -11
  22. package/dist/commands/daemon/index.js +3 -3
  23. package/dist/commands/daemon/install.js +4 -4
  24. package/dist/commands/daemon/login.js +1 -1
  25. package/dist/commands/daemon/logs.d.ts +3 -1
  26. package/dist/commands/daemon/logs.js +16 -1
  27. package/dist/commands/daemon/start.js +3 -3
  28. package/dist/commands/daemon/status.d.ts +9 -0
  29. package/dist/commands/daemon/status.js +22 -0
  30. package/dist/commands/dm/messages.js +1 -11
  31. package/dist/commands/followups/cancel.js +1 -1
  32. package/dist/commands/followups/remind.js +4 -0
  33. package/dist/commands/followups/resolve.js +1 -1
  34. package/dist/commands/followups/show.js +16 -0
  35. package/dist/commands/harnesses/forget.js +20 -3
  36. package/dist/commands/harnesses/install.d.ts +18 -0
  37. package/dist/commands/harnesses/install.js +146 -25
  38. package/dist/commands/harnesses/installers.d.ts +1 -1
  39. package/dist/commands/harnesses/installers.js +36 -12
  40. package/dist/commands/harnesses/list.d.ts +74 -2
  41. package/dist/commands/harnesses/list.js +231 -15
  42. package/dist/commands/harnesses/ping.d.ts +2 -0
  43. package/dist/commands/harnesses/ping.js +21 -0
  44. package/dist/commands/harnesses/show.d.ts +22 -0
  45. package/dist/commands/harnesses/show.js +116 -17
  46. package/dist/commands/harnesses/update.d.ts +8 -0
  47. package/dist/commands/harnesses/update.js +25 -1
  48. package/dist/commands/harnesses/usage.d.ts +2 -0
  49. package/dist/commands/harnesses/usage.js +7 -2
  50. package/dist/commands/instructions/status.js +13 -0
  51. package/dist/commands/instructions/update-target.d.ts +1 -0
  52. package/dist/commands/instructions/update-target.js +11 -3
  53. package/dist/commands/login.js +5 -5
  54. package/dist/commands/machines/list.js +8 -9
  55. package/dist/commands/machines/show.js +46 -17
  56. package/dist/commands/messages/show.js +3 -10
  57. package/dist/commands/shares/get.js +2 -9
  58. package/dist/commands/shares/list.js +1 -9
  59. package/dist/commands/shortcuts/show.js +4 -11
  60. package/dist/commands/skills/import-as-actions.js +6 -15
  61. package/dist/commands/skills/local.js +6 -0
  62. package/dist/commands/spaces/actions/run.js +3 -4
  63. package/dist/commands/spaces/briefs/get.js +2 -9
  64. package/dist/commands/spaces/briefs/list.js +1 -9
  65. package/dist/commands/spaces/conversations.js +1 -9
  66. package/dist/commands/spaces/heartbeats/list.js +1 -9
  67. package/dist/commands/spaces/heartbeats/status.js +2 -9
  68. package/dist/commands/tasks/activity.js +1 -10
  69. package/dist/commands/tasks/approvals/list.js +1 -9
  70. package/dist/commands/tasks/attachments/list.js +1 -9
  71. package/dist/commands/tasks/comments/list.js +1 -9
  72. package/dist/commands/tasks/complete.js +13 -0
  73. package/dist/commands/tasks/dri-status.js +2 -9
  74. package/dist/commands/tasks/events/append.js +9 -7
  75. package/dist/commands/tasks/execution-timeline.js +1 -9
  76. package/dist/commands/tasks/monitor.js +1 -9
  77. package/dist/commands/tasks/result/submit.js +14 -0
  78. package/dist/commands/tasks/runs.js +1 -9
  79. package/dist/commands/tasks/show.js +5 -12
  80. package/dist/commands/token/list.d.ts +8 -0
  81. package/dist/commands/token/list.js +51 -6
  82. package/dist/help.js +8 -12
  83. package/dist/lib/agent-home-workspace.d.ts +19 -0
  84. package/dist/lib/agent-home-workspace.js +39 -0
  85. package/dist/lib/agent-resolver.js +2 -2
  86. package/dist/lib/agentic-stream.d.ts +22 -1
  87. package/dist/lib/agentic-stream.js +48 -7
  88. package/dist/lib/cli-installers.d.ts +124 -2
  89. package/dist/lib/cli-installers.js +196 -13
  90. package/dist/lib/command-miss.d.ts +22 -0
  91. package/dist/lib/command-miss.js +78 -0
  92. package/dist/lib/daemon-setup.js +2 -2
  93. package/dist/lib/daemon-target.d.ts +14 -0
  94. package/dist/lib/daemon-target.js +32 -0
  95. package/dist/lib/daemonBroker.js +1 -1
  96. package/dist/lib/daemonHandoff.js +1 -1
  97. package/dist/lib/exec-runtime-binary.js +2 -2
  98. package/dist/lib/followups.d.ts +12 -0
  99. package/dist/lib/followups.js +14 -1
  100. package/dist/lib/format.d.ts +11 -0
  101. package/dist/lib/format.js +21 -0
  102. package/dist/lib/harness-labels.d.ts +39 -0
  103. package/dist/lib/harness-labels.js +60 -0
  104. package/dist/lib/harnesses.d.ts +6 -0
  105. package/dist/lib/instruction-input.d.ts +13 -0
  106. package/dist/lib/instruction-input.js +20 -1
  107. package/dist/lib/local-skills.d.ts +44 -1
  108. package/dist/lib/local-skills.js +58 -2
  109. package/dist/lib/login.js +5 -5
  110. package/dist/lib/machines.d.ts +6 -0
  111. package/dist/lib/machines.js +68 -2
  112. package/dist/lib/runtime-tool-support.d.ts +34 -0
  113. package/dist/lib/runtime-tool-support.js +77 -0
  114. package/dist/lib/share-rows.d.ts +17 -0
  115. package/dist/lib/share-rows.js +35 -0
  116. package/dist/lib/task-closure.d.ts +13 -0
  117. package/dist/lib/task-closure.js +17 -0
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +5066 -5033
  119. package/oclif.manifest.json +33472 -33360
  120. package/package.json +6 -3
@@ -11,11 +11,19 @@ import { BaseCommand } from '../../base-command';
11
11
  *
12
12
  * `GET /api/auth/sessions/refresh` has served exactly this list all along; it
13
13
  * was simply not reachable from the CLI.
14
+ *
15
+ * The server returns at most `--limit` rows and says whether it left any out.
16
+ * It used to stop at 100 without saying so, and the session somebody went
17
+ * looking for — an old daemon's, still holding the login a purge refusal named
18
+ * — was exactly the one past the cut (OSK-9980). So a partial list says it is
19
+ * partial, and `--kind daemon` is the short way to the row that matters.
14
20
  */
15
21
  export default class TokenList extends BaseCommand {
16
22
  static description: string;
17
23
  static examples: string[];
18
24
  static flags: {
25
+ kind: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ limit: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
27
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
20
28
  };
21
29
  run(): Promise<void>;
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../base-command");
5
5
  const api_fetch_1 = require("../../lib/api-fetch");
6
6
  const format_1 = require("../../lib/format");
7
+ const SESSION_KINDS = ['web', 'daemon', 'cli', 'ci'];
7
8
  /**
8
9
  * `skrr token list`
9
10
  *
@@ -16,20 +17,44 @@ const format_1 = require("../../lib/format");
16
17
  *
17
18
  * `GET /api/auth/sessions/refresh` has served exactly this list all along; it
18
19
  * was simply not reachable from the CLI.
20
+ *
21
+ * The server returns at most `--limit` rows and says whether it left any out.
22
+ * It used to stop at 100 without saying so, and the session somebody went
23
+ * looking for — an old daemon's, still holding the login a purge refusal named
24
+ * — was exactly the one past the cut (OSK-9980). So a partial list says it is
25
+ * partial, and `--kind daemon` is the short way to the row that matters.
19
26
  */
20
27
  class TokenList extends base_command_1.BaseCommand {
21
28
  static description = 'List the active sessions a minted token belongs to — the other half of `token mint`';
22
29
  static examples = [
23
30
  '<%= config.bin %> token list',
24
- '<%= config.bin %> token list --json',
31
+ '<%= config.bin %> token list --kind daemon',
32
+ '<%= config.bin %> token list --kind cli --limit 500 --json',
25
33
  ];
26
- static flags = { json: core_1.Flags.boolean({ description: 'Output JSON' }) };
34
+ static flags = {
35
+ kind: core_1.Flags.string({
36
+ description: 'Only sessions of this kind',
37
+ options: [...SESSION_KINDS],
38
+ }),
39
+ limit: core_1.Flags.integer({
40
+ description: 'Most sessions to list (server maximum 500)',
41
+ min: 1,
42
+ max: 500,
43
+ }),
44
+ json: core_1.Flags.boolean({ description: 'Output JSON' }),
45
+ };
27
46
  async run() {
28
47
  this.requireAuth();
29
48
  const { flags } = await this.parse(TokenList);
49
+ const query = new URLSearchParams();
50
+ if (flags.kind)
51
+ query.set('kind', flags.kind);
52
+ if (flags.limit)
53
+ query.set('limit', String(flags.limit));
54
+ const suffix = query.toString() ? `?${query.toString()}` : '';
30
55
  let data = null;
31
56
  try {
32
- data = await (0, api_fetch_1.apiFetch)('/api/auth/sessions/refresh', {
57
+ data = await (0, api_fetch_1.apiFetch)(`/api/auth/sessions/refresh${suffix}`, {
33
58
  method: 'GET',
34
59
  credential: this.resolvedCredential,
35
60
  });
@@ -37,9 +62,17 @@ class TokenList extends base_command_1.BaseCommand {
37
62
  catch (err) {
38
63
  this.handleApiError(err);
39
64
  }
40
- const sessions = data?.sessions || [];
41
- if (flags.json)
42
- return this.log(JSON.stringify({ sessions, count: sessions.length }, null, 2));
65
+ // Filtered here as well: a server from before `kind` existed ignores the
66
+ // parameter and returns every kind, and `--kind daemon` must never list a
67
+ // web session as if it were one.
68
+ const sessions = (data?.sessions || []).filter((session) => !flags.kind || session.kind === flags.kind);
69
+ // A server from before `hasMore` stopped at 100 and said nothing about it,
70
+ // and ignores `--limit` too — so it gets its own, weaker statement.
71
+ const olderServer = data?.hasMore === undefined;
72
+ const hasMore = olderServer ? (data?.sessions?.length ?? 0) >= 100 : data?.hasMore === true;
73
+ if (flags.json) {
74
+ return this.log(JSON.stringify({ sessions, count: sessions.length, hasMore }, null, 2));
75
+ }
43
76
  if (!sessions.length)
44
77
  return this.log('No active sessions.');
45
78
  (0, format_1.renderTable)(sessions.map((session) => ({
@@ -48,6 +81,8 @@ class TokenList extends base_command_1.BaseCommand {
48
81
  // The operator's own name for the credential wins over a derived
49
82
  // device string — it is what they would search by.
50
83
  device: session.label || session.device || '-',
84
+ // What `instructions`, `machines` and a purge refusal call it.
85
+ daemon: session.daemonId || '-',
51
86
  ip: session.ip || '-',
52
87
  lastActive: session.lastActiveAt || session.createdAt || '-',
53
88
  // The session you are typing in. Revoking it signs THIS machine out,
@@ -57,11 +92,21 @@ class TokenList extends base_command_1.BaseCommand {
57
92
  { key: 'id', header: 'ID' },
58
93
  { key: 'kind', header: 'KIND' },
59
94
  { key: 'device', header: 'DEVICE', maxWidth: 34 },
95
+ { key: 'daemon', header: 'DAEMON', maxWidth: 40 },
60
96
  { key: 'ip', header: 'IP' },
61
97
  { key: 'lastActive', header: 'LAST ACTIVE' },
62
98
  { key: 'current', header: 'CURRENT' },
63
99
  ], (line) => this.log(line));
64
100
  this.log('');
101
+ if (hasMore && olderServer) {
102
+ this.log('This server lists at most 100 sessions and does not say whether it left any out, so older ones may be missing.');
103
+ }
104
+ else if (hasMore) {
105
+ const remedy = flags.kind
106
+ ? 'Raise --limit (up to 500) to see them.'
107
+ : 'Narrow the list with --kind, or raise --limit (up to 500).';
108
+ this.log(`Showing the ${sessions.length} most recently active sessions; older ones were left out. ${remedy}`);
109
+ }
65
110
  this.log('Retire one with: skrr token revoke <id>');
66
111
  }
67
112
  }
package/dist/help.js CHANGED
@@ -46,26 +46,22 @@ class SkrrHelp extends core_1.Help {
46
46
  */
47
47
  async showRootHelp() {
48
48
  this.log('GETTING STARTED');
49
- this.log(' skrr login Sign in. Offers to set this machine up if it has no runtime.');
50
- this.log(' skrr daemon install Install the runtime by hand (fetches the signed release).');
49
+ this.log(' skrr login Sign in. Offers to install the background service if this computer has none.');
50
+ this.log(' skrr daemon install Install the background service by hand (fetches the signed release).');
51
51
  this.log('');
52
52
  return super.showRootHelp();
53
53
  }
54
54
  async showHelp(argv) {
55
- // Mirror oclif's own id resolution: the topic separator is a space here, so
56
- // the id is the leading non-flag tokens joined with `:`.
57
- const tokens = [];
58
- for (const token of argv) {
59
- if (token.startsWith('-'))
60
- break;
61
- tokens.push(token);
62
- }
63
- const id = tokens.join(':');
55
+ // Resolve the id exactly as running the same words would — a word after a
56
+ // command that takes arguments is an argument, not part of the name
57
+ // (OSK-10099: `harnesses install devin --help` was refused).
58
+ const lookup = (0, command_miss_1.lookupFromConfig)(this.config);
59
+ const id = (0, command_miss_1.helpSubjectId)(argv, lookup);
64
60
  // A bare topic (`skrr daemon`) has no command but DOES have help to show,
65
61
  // so it must fall through — only an id that is neither a command nor a
66
62
  // topic is a miss.
67
63
  if (id && !this.config.findCommand(id) && !this.config.findTopic(id)) {
68
- throw new Error((0, command_miss_1.describeCommandMiss)((0, command_miss_1.lookupFromConfig)(this.config), id));
64
+ throw new Error((0, command_miss_1.describeCommandMiss)(lookup, id));
69
65
  }
70
66
  return super.showHelp(argv);
71
67
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The workspace an `agents chat` turn runs in when the caller named none.
3
+ *
4
+ * A turn with no workspace context reaches the server without `X-Workspace-Id`.
5
+ * The server lets it through after checking membership in the agent's own
6
+ * workspace, but the agent runtime then has no workspace, so the shared
7
+ * workspace memory tier refuses with "no workspace context" (OSK-9937). By
8
+ * owner decision the CLI now uses the agent's home workspace in that case: the
9
+ * same thing the web app sends, and the only workspace the server would accept
10
+ * for that agent anyway.
11
+ *
12
+ * Never applied when anything already chose: `--workspace`, an exported
13
+ * `OVERSKY_WORKSPACE_ID`, or `skrr workspaces use` (all arrive as that env var),
14
+ * and never inside an autonomous Agent process, whose workspace belongs to its
15
+ * daemon session.
16
+ */
17
+ export declare function applyAgentHomeWorkspaceDefault(agentId: string, lookup: (agentId: string) => Promise<{
18
+ workspaceId?: string | null;
19
+ } | null | undefined>, env?: NodeJS.ProcessEnv): Promise<string | null>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAgentHomeWorkspaceDefault = applyAgentHomeWorkspaceDefault;
4
+ /**
5
+ * The workspace an `agents chat` turn runs in when the caller named none.
6
+ *
7
+ * A turn with no workspace context reaches the server without `X-Workspace-Id`.
8
+ * The server lets it through after checking membership in the agent's own
9
+ * workspace, but the agent runtime then has no workspace, so the shared
10
+ * workspace memory tier refuses with "no workspace context" (OSK-9937). By
11
+ * owner decision the CLI now uses the agent's home workspace in that case: the
12
+ * same thing the web app sends, and the only workspace the server would accept
13
+ * for that agent anyway.
14
+ *
15
+ * Never applied when anything already chose: `--workspace`, an exported
16
+ * `OVERSKY_WORKSPACE_ID`, or `skrr workspaces use` (all arrive as that env var),
17
+ * and never inside an autonomous Agent process, whose workspace belongs to its
18
+ * daemon session.
19
+ */
20
+ async function applyAgentHomeWorkspaceDefault(agentId, lookup, env = process.env) {
21
+ if (env.OVERSKY_AUTONOMOUS_SESSION === '1')
22
+ return null;
23
+ if (env.OVERSKY_WORKSPACE_ID?.trim())
24
+ return null;
25
+ let agent;
26
+ try {
27
+ agent = await lookup(agentId);
28
+ }
29
+ catch {
30
+ // Unreadable agent: keep the headerless request. The server still decides
31
+ // access, and the chat call itself reports a missing or forbidden agent.
32
+ return null;
33
+ }
34
+ const workspaceId = typeof agent?.workspaceId === 'string' ? agent.workspaceId.trim() : '';
35
+ if (!workspaceId)
36
+ return null;
37
+ env.OVERSKY_WORKSPACE_ID = workspaceId;
38
+ return workspaceId;
39
+ }
@@ -38,7 +38,7 @@ async function resolveAgentIdentity(explicitAgentId) {
38
38
  if (explicitAgentId && explicitAgentId !== runtime.agent.id) {
39
39
  throw new AgentResolutionError({
40
40
  code: 'AGENT_CONTEXT_CONFLICT',
41
- message: 'This local runtime is bound to OVERSKY_SESSION_AGENT_ID. Do not override it with another agent ID.',
41
+ message: 'This session is bound to the agent in OVERSKY_SESSION_AGENT_ID. Do not override it with another agent ID.',
42
42
  suggestion: 'Use the bound session agent, or start a new daemon session for a different agent.',
43
43
  details: {
44
44
  explicitAgentId,
@@ -129,7 +129,7 @@ function resolveSessionAgentIdentity(explicitAgentId, env = process.env) {
129
129
  if (explicitAgentId && explicitAgentId !== runtime.agent.id) {
130
130
  throw new AgentResolutionError({
131
131
  code: 'AGENT_CONTEXT_CONFLICT',
132
- message: 'This local runtime is bound to OVERSKY_SESSION_AGENT_ID. Do not override it with another agent ID.',
132
+ message: 'This session is bound to the agent in OVERSKY_SESSION_AGENT_ID. Do not override it with another agent ID.',
133
133
  suggestion: 'Use the bound session agent, or start a new daemon session for a different agent.',
134
134
  details: {
135
135
  explicitAgentId,
@@ -85,6 +85,8 @@ export type AgenticStreamOptions = {
85
85
  reconcileTerminal?: () => Promise<ReconciledTerminal | null>;
86
86
  /** Silence before the first durable-record probe. Default 20s. */
87
87
  idleTerminalProbeMs?: number;
88
+ /** How long to keep trying to attach before reporting a detach. Default 30s. */
89
+ attachTimeoutMs?: number;
88
90
  };
89
91
  /**
90
92
  * Small Node-side adapter for the existing Agentic Socket.IO contract.
@@ -95,7 +97,12 @@ export type AgenticStreamOptions = {
95
97
  * The assistant text inside a `session:output` frame's envelopes.
96
98
  *
97
99
  * Envelope shape: `{ ev: { t: 'text', text: '…' } }`. Frames whose `ev.t` is
98
- * anything else (tool calls, status) are not the answer and are skipped.
100
+ * anything else (tool calls, status) are not the answer and are skipped, and so
101
+ * is text marked `thinking: true` — the model's reasoning, which the session
102
+ * protocol carries in the same `text` event. Folded in, a Devin turn asked for
103
+ * one word printed "I need to output only the word SECOND, … single
104
+ * word.SECOND" as its answer, and `run.result.text` kept it, because the
105
+ * stream then held the final result as a suffix and the longer text won.
99
106
  */
100
107
  export declare function envelopesText(event: AgenticStreamEvent): string;
101
108
  /**
@@ -281,6 +288,8 @@ export declare class AgenticStreamClient {
281
288
  /** Text already written to the terminal, for suffix-only rendering. */
282
289
  private renderedText;
283
290
  private idleTimer;
291
+ /** Releases a connect wait still in flight; `close()` calls it. */
292
+ private abandonConnectWait;
284
293
  private reconciling;
285
294
  private responseMessageId;
286
295
  private lastEvent;
@@ -307,6 +316,14 @@ export declare class AgenticStreamClient {
307
316
  * rather than giving up — a 200-second tool call is silence, not a failure —
308
317
  * so the cost of being wrong is one cheap read, while the cost of not asking
309
318
  * was an interactive session that never returned.
319
+ *
320
+ * The timer is deliberately NOT unref'd. It exists for the moment the socket
321
+ * stops holding the process open — a server-side disconnect, or a reconnect
322
+ * the server refuses, after which socket.io does not retry. Unref'd, it was
323
+ * the last thing pending at exactly that moment, so Node's event loop emptied
324
+ * and the CLI exited 0 with the run unsettled: `--no-stream --json` printed
325
+ * nothing at all for a turn the record said had failed (OSK-10061). A run
326
+ * that has not settled is a reason to stay alive; `close()` clears the timer.
310
327
  */
311
328
  private armIdleProbe;
312
329
  /**
@@ -364,6 +381,10 @@ export declare class AgenticStreamClient {
364
381
  * Now errors are surfaced as events and retried until a bounded deadline, so
365
382
  * only a sustained inability to attach gives up — and the caller turns that
366
383
  * into "accepted but detached", never "failed".
384
+ *
385
+ * The deadline and retry timers hold the process open (see `armDeadline`), so
386
+ * `close()` is what releases them — and it ends the wait too, whether the
387
+ * caller gave up or the durable record settled the run first (OSK-10061).
367
388
  */
368
389
  private waitForConnect;
369
390
  }
@@ -51,7 +51,12 @@ function writeJsonLine(output, value) {
51
51
  * The assistant text inside a `session:output` frame's envelopes.
52
52
  *
53
53
  * Envelope shape: `{ ev: { t: 'text', text: '…' } }`. Frames whose `ev.t` is
54
- * anything else (tool calls, status) are not the answer and are skipped.
54
+ * anything else (tool calls, status) are not the answer and are skipped, and so
55
+ * is text marked `thinking: true` — the model's reasoning, which the session
56
+ * protocol carries in the same `text` event. Folded in, a Devin turn asked for
57
+ * one word printed "I need to output only the word SECOND, … single
58
+ * word.SECOND" as its answer, and `run.result.text` kept it, because the
59
+ * stream then held the final result as a suffix and the longer text won.
55
60
  */
56
61
  function envelopesText(event) {
57
62
  const envelopes = event.envelopes;
@@ -59,9 +64,11 @@ function envelopesText(event) {
59
64
  return '';
60
65
  let out = '';
61
66
  for (const envelope of envelopes) {
62
- const ev = envelope?.ev;
63
- if (ev && ev.t === 'text' && typeof ev.text === 'string')
67
+ const ev = envelope
68
+ ?.ev;
69
+ if (ev && ev.t === 'text' && ev.thinking !== true && typeof ev.text === 'string') {
64
70
  out += ev.text;
71
+ }
65
72
  }
66
73
  return out;
67
74
  }
@@ -306,6 +313,8 @@ class AgenticStreamClient {
306
313
  /** Text already written to the terminal, for suffix-only rendering. */
307
314
  renderedText = '';
308
315
  idleTimer = null;
316
+ /** Releases a connect wait still in flight; `close()` calls it. */
317
+ abandonConnectWait = null;
309
318
  reconciling = false;
310
319
  responseMessageId;
311
320
  lastEvent;
@@ -351,7 +360,13 @@ class AgenticStreamClient {
351
360
  reconnectionDelayMax: 10_000,
352
361
  });
353
362
  this.installHandlers();
354
- await this.waitForConnect();
363
+ await this.waitForConnect(this.options.attachTimeoutMs);
364
+ // A refused handshake asks the durable record, and the record can settle
365
+ // the run before any attach succeeds. `close()` then ends the wait, and
366
+ // there is nothing left to join: returning here is what lets the caller see
367
+ // the result instead of awaiting an attach nobody is still attempting.
368
+ if (this.settled || this.closed || !this.socket)
369
+ return result;
355
370
  this.socket.emit('session:join', { sessionId: this.options.sessionId });
356
371
  this.emit({ type: 'session.joined', sessionId: this.options.sessionId });
357
372
  this.armIdleProbe();
@@ -365,6 +380,14 @@ class AgenticStreamClient {
365
380
  * rather than giving up — a 200-second tool call is silence, not a failure —
366
381
  * so the cost of being wrong is one cheap read, while the cost of not asking
367
382
  * was an interactive session that never returned.
383
+ *
384
+ * The timer is deliberately NOT unref'd. It exists for the moment the socket
385
+ * stops holding the process open — a server-side disconnect, or a reconnect
386
+ * the server refuses, after which socket.io does not retry. Unref'd, it was
387
+ * the last thing pending at exactly that moment, so Node's event loop emptied
388
+ * and the CLI exited 0 with the run unsettled: `--no-stream --json` printed
389
+ * nothing at all for a turn the record said had failed (OSK-10061). A run
390
+ * that has not settled is a reason to stay alive; `close()` clears the timer.
368
391
  */
369
392
  armIdleProbe() {
370
393
  if (!this.options.reconcileTerminal)
@@ -374,7 +397,6 @@ class AgenticStreamClient {
374
397
  if (this.idleTimer)
375
398
  clearTimeout(this.idleTimer);
376
399
  this.idleTimer = setTimeout(() => void this.probeTerminal('idle'), this.options.idleTerminalProbeMs ?? 20_000);
377
- this.idleTimer.unref?.();
378
400
  }
379
401
  /**
380
402
  * Settle from the durable record, if it has reached a terminal state.
@@ -438,6 +460,7 @@ class AgenticStreamClient {
438
460
  clearTimeout(this.idleTimer);
439
461
  this.idleTimer = null;
440
462
  }
463
+ this.abandonConnectWait?.();
441
464
  const socket = this.socket;
442
465
  this.socket = null;
443
466
  // Stop dispatch BEFORE tearing anything down. socket.io can deliver a
@@ -838,6 +861,11 @@ class AgenticStreamClient {
838
861
  if (finalText && finalText !== this.accumulated && !accumulationIsSuperset) {
839
862
  this.accumulated = finalText;
840
863
  }
864
+ // The finished answer, as every other surface shows it: the web applies
865
+ // this same sanitizer, so a scheduling JSON block or leaked function-call
866
+ // XML the model wrote into its reply is removed here too instead of being
867
+ // printed as part of the agent's answer.
868
+ this.accumulated = (0, data_provider_1.sanitizeFinalText)(this.accumulated);
841
869
  if (!this.options.quiet && !this.options.jsonl && this.accumulated) {
842
870
  const output = this.options.output ?? process.stdout;
843
871
  const { append, shown } = renderableDelta(this.renderedText, this.accumulated);
@@ -872,6 +900,10 @@ class AgenticStreamClient {
872
900
  * Now errors are surfaced as events and retried until a bounded deadline, so
873
901
  * only a sustained inability to attach gives up — and the caller turns that
874
902
  * into "accepted but detached", never "failed".
903
+ *
904
+ * The deadline and retry timers hold the process open (see `armDeadline`), so
905
+ * `close()` is what releases them — and it ends the wait too, whether the
906
+ * caller gave up or the durable record settled the run first (OSK-10061).
875
907
  */
876
908
  waitForConnect(timeoutMs = 30_000) {
877
909
  if (this.socket?.connected)
@@ -889,7 +921,12 @@ class AgenticStreamClient {
889
921
  reject(Object.assign(new Error(`Could not attach to the run stream within ${Math.round(ms / 1000)}s` +
890
922
  `${lastError ? `: ${lastError.message}` : ''}`), { code: 'STREAM_ATTACH_TIMEOUT', lastError }));
891
923
  }, ms);
892
- handle.unref?.();
924
+ // Deliberately NOT unref'd, nor is the retry timer below. A connect
925
+ // refused by middleware leaves no live socket, so these timers are the
926
+ // only thing keeping the process alive while it waits. Unref'd, Node's
927
+ // event loop drained and `agents chat` exited 0 right after printing
928
+ // "Retrying in 30s" — before the retry, without the answer the run had
929
+ // produced, and without the detach notice a script needs.
893
930
  return handle;
894
931
  }
895
932
  const onConnect = () => {
@@ -933,7 +970,6 @@ class AgenticStreamClient {
933
970
  // must not reject here and turn a pause into a failure.
934
971
  }
935
972
  }, jitteredRetryDelay(refusal.retryAfterMs));
936
- retryTimer.unref?.();
937
973
  };
938
974
  const cleanup = () => {
939
975
  clearTimeout(timer);
@@ -941,6 +977,11 @@ class AgenticStreamClient {
941
977
  clearTimeout(retryTimer);
942
978
  socket.off('connect', onConnect);
943
979
  socket.off('connect_error', onError);
980
+ this.abandonConnectWait = null;
981
+ };
982
+ this.abandonConnectWait = () => {
983
+ cleanup();
984
+ resolve();
944
985
  };
945
986
  socket.once('connect', onConnect);
946
987
  socket.on('connect_error', onError);
@@ -25,6 +25,13 @@ export interface ManagedCliProviderState {
25
25
  source?: string;
26
26
  installable?: boolean;
27
27
  installDisabledReason?: string | null;
28
+ /**
29
+ * Whether the daemon can update this installed CLI in place, and why not.
30
+ * ABSENT from a daemon older than managed updates (OSK-10096) — read that as
31
+ * "this daemon cannot say", never as `false`.
32
+ */
33
+ updatable?: boolean;
34
+ updateDisabledReason?: string | null;
28
35
  platformSupported?: boolean;
29
36
  authentication?: ManagedCliAuthentication | null;
30
37
  job?: {
@@ -44,7 +51,7 @@ export interface CliInstallersResponse {
44
51
  * Providers the platform knows about that this daemon did not offer.
45
52
  *
46
53
  * The inventory is whatever the CONNECTED daemon supports, and an older daemon
47
- * legitimately supports fewer: `devin` arrived in daemon 0.8.43, so a machine
54
+ * legitimately supports fewer: `devin` arrived in the 0.8.45 daemon release, so a machine
48
55
  * running 0.8.35 answers with six providers instead of seven. Observed here —
49
56
  * the desktop app restarted the daemon from its bundled 0.8.35 copy and Devin
50
57
  * disappeared from the install list between two page loads, with nothing on
@@ -56,8 +63,28 @@ export interface CliInstallersResponse {
56
63
  * is already on this side of the wire. Naming the gap is what turns "Devin is
57
64
  * gone" into "this daemon is older than Devin".
58
65
  */
66
+ /**
67
+ * The first STABLE daemon release (`daemon-v*` tag) whose
68
+ * MANAGED_CLI_PROVIDER_IDS included each provider. Release tags, not
69
+ * package.json at the commit that added it: the version is bumped before a
70
+ * release is cut, so the commit that added devin already read 0.8.43 while the
71
+ * 0.8.43 release did not contain it — this table said "added in 0.8.43" to a
72
+ * 0.8.43 user (OSK-9537 verifier). Derived with, per provider:
73
+ * for t in $(git tag -l 'daemon-v*' | sort -V); do
74
+ * git show "$t:daemon/src/cli-installers.ts" | grep -q "'devin'" && echo "$t" && break
75
+ * done
76
+ * Providers present since managed installers first shipped (0.8.1) are omitted.
77
+ */
78
+ export declare const MANAGED_CLI_PROVIDER_SINCE: Readonly<Record<string, string>>;
79
+ /**
80
+ * "Not offered" with the release that added each provider, so a reader learns
81
+ * WHICH update fixes it rather than only that one might (OSK-9537).
82
+ */
83
+ export declare function describeProvidersNotOffered(missing: readonly string[]): string;
59
84
  export declare function providersNotOffered(offered: readonly ManagedCliProviderState[] | undefined, known: readonly string[]): string[];
60
- export declare function listCliInstallers(daemonId: string, credential?: unknown): Promise<CliInstallersResponse>;
85
+ export declare function listCliInstallers(daemonId: string, credential?: unknown, opts?: {
86
+ signal?: AbortSignal;
87
+ }): Promise<CliInstallersResponse>;
61
88
  /**
62
89
  * The one-word readiness verdict for a provider, and whether it is good news.
63
90
  *
@@ -74,14 +101,82 @@ export declare function readinessOf(provider: ManagedCliProviderState): {
74
101
  ok: boolean;
75
102
  };
76
103
  /** A job the daemon runs to install one managed provider. */
104
+ /**
105
+ * Readiness per provider for one daemon, keyed by CANONICAL provider — or null
106
+ * when the daemon could not be asked (disconnected, older, refused).
107
+ *
108
+ * The harness row's `authStatus` is the DAEMON's standing with skrr, written
109
+ * across every row of a daemon at once, so `harnesses show`, `machines show`
110
+ * and `ping` answered "ok" per provider for a Devin its own CLI called "Not
111
+ * logged in" (OSK-9263, OSK-9265). This is the per-provider answer those
112
+ * surfaces print instead. Best-effort by design: a failure here must never
113
+ * fail the command that asked.
114
+ */
115
+ export declare function readinessByProvider(daemonId: string, credential?: unknown, opts?: {
116
+ signal?: AbortSignal;
117
+ }): Promise<Map<string, ReturnType<typeof readinessOf>> | null>;
118
+ /** The canonical provider a harness row names, for looking it up in that map. */
119
+ export declare function readinessKey(provider: string | null | undefined): string;
120
+ export type ProviderReadiness = ReturnType<typeof readinessOf>;
121
+ /** The fields of a harness row that decide whether, and whom, to ask. */
122
+ export interface ReadinessRow {
123
+ daemonId?: string | null;
124
+ provider?: string | null;
125
+ status?: string | null;
126
+ superseded?: boolean;
127
+ }
128
+ /** How long one daemon may take to answer before a list prints `-` for it. */
129
+ export declare const ROW_READINESS_TIMEOUT_MS = 5000;
130
+ /**
131
+ * Readiness for many harness rows at once: ONE installer read per connected
132
+ * daemon, all in parallel, however many rows each daemon has.
133
+ *
134
+ * Only a row whose daemon can answer is asked for — online and not superseded;
135
+ * an offline or replaced registration has nobody on the other end. Each read is
136
+ * bounded, because a list must not hang on one slow machine; a daemon that does
137
+ * not answer in time reads as unknown (`undefined`), never as `ready`.
138
+ *
139
+ * `asked` and `answered` let a caller tell "this daemon does not report it"
140
+ * from "nothing could be asked", which call for different next steps.
141
+ */
142
+ export declare function readinessForRows(rows: readonly ReadinessRow[], credential?: unknown, opts?: {
143
+ timeoutMs?: number;
144
+ }): Promise<{
145
+ lookup: (row: ReadinessRow) => ProviderReadiness | undefined;
146
+ /** Whether this row's daemon answered at all — so a `-` can say why. */
147
+ daemonAnswered: (row: ReadinessRow) => boolean;
148
+ asked: number;
149
+ answered: number;
150
+ }>;
77
151
  export interface ManagedCliInstallJob {
78
152
  id: string;
153
+ provider?: string;
154
+ displayName?: string;
79
155
  status: 'queued' | 'running' | 'succeeded' | 'failed';
156
+ /** `update` for an in-place update; absent from an older daemon, meaning install. */
157
+ kind?: 'install' | 'update';
158
+ previousVersion?: string | null;
159
+ installedVersion?: string | null;
80
160
  error?: string;
161
+ /**
162
+ * The daemon's job log: the LAST lines only (a sliding window, 200 lines in
163
+ * `daemon/src/cli-installers.ts`). This is the field the daemon sends.
164
+ */
165
+ logTail?: string[];
166
+ /**
167
+ * Lines the job has EVER logged (daemons with OSK-10179). With it, the lines
168
+ * new since the last poll are exactly the last `count − printed` of the tail,
169
+ * even when they repeat.
170
+ */
171
+ logLineCount?: number;
172
+ /** Never sent by any daemon; kept so an older caller's shape still types. */
81
173
  log?: string[];
82
174
  }
83
175
  export interface StartInstallResponse {
84
176
  started?: boolean;
177
+ /** The daemon declined because the CLI is already there. */
178
+ alreadyInstalled?: boolean;
179
+ alreadyRunning?: boolean;
85
180
  provider?: ManagedCliProviderState;
86
181
  job?: {
87
182
  id?: string;
@@ -89,9 +184,36 @@ export interface StartInstallResponse {
89
184
  };
90
185
  }
91
186
  export declare function startManagedCliInstall(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
187
+ /** Ask the daemon to update an installed CLI in place (OSK-10096). */
188
+ export declare function startManagedCliUpdate(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
189
+ /**
190
+ * How an update job ended, from the versions the daemon recorded on it:
191
+ * "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
192
+ * its updater found nothing newer." Null when the job does not carry both.
193
+ */
194
+ export declare function updateOutcomeLine(job: ManagedCliInstallJob): string | null;
92
195
  export declare function readManagedCliInstallJob(daemonId: string, jobId: string, credential?: unknown): Promise<{
93
196
  job?: ManagedCliInstallJob;
94
197
  }>;
198
+ /**
199
+ * The lines of a job's log tail not yet printed.
200
+ *
201
+ * The tail is a SLIDING window: once an installer writes more than the daemon
202
+ * keeps, earlier lines fall off the front, so an index into it skips or repeats
203
+ * lines. Instead, find the longest run at the start of the new tail that matches
204
+ * the end of what was already printed, and print only what follows it.
205
+ */
206
+ export declare function unseenLogLines(printed: readonly string[], tail: readonly string[]): string[];
207
+ /**
208
+ * The new lines of a job's tail when the daemon reports its running line count:
209
+ * exactly the last `total − printedCount`, and how many of those already fell
210
+ * out of the window (the daemon keeps the last 200). Unlike overlap matching,
211
+ * this cannot mistake a repeated line for one already printed.
212
+ */
213
+ export declare function newLogLinesByCount(printedCount: number, tail: readonly string[], total: number): {
214
+ lines: string[];
215
+ skipped: number;
216
+ };
95
217
  /** A 100 MB download over a hotel connection is the case this has to survive. */
96
218
  export declare const INSTALL_MAX_WAIT_MS: number;
97
219
  export declare const INSTALL_POLL_INTERVAL_MS = 2000;