@nowcrew/daemon 0.5.45 → 0.5.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -193,8 +193,8 @@ dispatch reports unavailable instead of running on another compatible daemon. An
193
193
  the global compatible-machine pool.
194
194
 
195
195
  The daemon intersects requested permission with local policy, checks advertised resource limits, prepares
196
- the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, or
197
- `kimi`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
196
+ the local workspace/environment, and launches only a built-in runtime adapter (`claude`, `codex`, `kimi`,
197
+ `hermes`, or `opencode`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
198
198
  behavior. Those are server responsibilities.
199
199
 
200
200
  The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
@@ -204,6 +204,8 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
204
204
  | Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
205
205
  | Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume` |
206
206
  | Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
207
+ | Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
208
+ | OpenCode | `run --format json` | stdin | `--session` |
207
209
 
208
210
  Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
209
211
  the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
@@ -211,7 +213,20 @@ CLI and carries the same native session id; unrelated ACP failures stay failures
211
213
  subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
212
214
  durable Job Object ownership is available.
213
215
 
214
- Codex and Kimi sessions use the same bounded per-task persistence and keyed lease as Claude when they
216
+ Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
217
+ a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
218
+ machine-local. A missing resumed ACP session is retried once as a fresh session.
219
+
220
+ OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
221
+ workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
222
+ compatibility fallback). A missing resumed session is retried once without `--session`; malformed,
223
+ empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
224
+ `opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
225
+ only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
226
+ `--model`, `--variant`, and `--session`; older installations remain visible as detected CLIs until
227
+ they are upgraded, rather than falling back to a permission override that user config could weaken.
228
+
229
+ Codex, Kimi, Hermes, and OpenCode sessions use the same bounded per-task persistence and keyed lease as Claude when they
215
230
  run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
216
231
  semantically silent; after the first semantic event, the execution's configured total timeout remains
217
232
  authoritative so a legitimate long-running tool is not killed for quiet output.
@@ -219,6 +234,10 @@ authoritative so a legitimate long-running tool is not killed for quiet output.
219
234
  Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
220
235
  found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
221
236
  adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
237
+ The daemon sends a conservative first hello without waiting for third-party handshakes, then refreshes it
238
+ after the optional Kimi/Hermes/OpenCode probes finish. Work targeting those optional runtimes waits for the
239
+ same full probe result; reconnects share one in-flight probe, and shutdown aborts any unfinished probe and
240
+ its child process.
222
241
  Old daemons that omit `executionRuntimes` are conservatively interpreted as the intersection of installed
223
242
  CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
224
243
  Protocol-0 `agent:start` remains only for the server-governed compatibility window.
@@ -1,35 +1,126 @@
1
1
  import { createAgentMemoryClient } from "./client.js";
2
2
  import { buildCaptureMessages, extractIncomingMessage, rankMemoryItems, renderMemoryContext, } from "./policy.js";
3
+ import { dslog } from "../slog.js";
4
+ function safeReport(report, eventType, message, fields) {
5
+ try {
6
+ report(eventType, message, fields);
7
+ }
8
+ catch {
9
+ // Diagnostics must never affect optional memory recall or capture.
10
+ }
11
+ }
3
12
  export function createAgentMemoryBridge(config, dependencies = {}) {
4
13
  const client = dependencies.client ?? createAgentMemoryClient(config);
14
+ const report = dependencies.report ?? dslog;
5
15
  const blockId = `chat_memory-${config.teamId}-${config.agentId}`;
6
16
  return {
7
- recall: async (agentHandle, wakePrompt) => {
8
- const incoming = agentHandle === config.agentHandle ? extractIncomingMessage(wakePrompt) : null;
9
- if (incoming === null)
17
+ recall: async (agentHandle, wakePrompt, executionId) => {
18
+ const startedAt = Date.now();
19
+ if (agentHandle !== config.agentHandle) {
20
+ safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
21
+ execution_id: executionId,
22
+ agent_handle: agentHandle,
23
+ outcome: "skipped",
24
+ skip_reason: "handle_mismatch",
25
+ duration_ms: Date.now() - startedAt,
26
+ });
27
+ return "";
28
+ }
29
+ const incoming = extractIncomingMessage(wakePrompt);
30
+ if (incoming === null) {
31
+ safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已跳过", {
32
+ execution_id: executionId,
33
+ agent_handle: agentHandle,
34
+ outcome: "skipped",
35
+ skip_reason: "wake_unparsed",
36
+ duration_ms: Date.now() - startedAt,
37
+ });
10
38
  return "";
39
+ }
11
40
  try {
12
41
  const [core, atomic] = await Promise.all([
13
42
  client.layer(blockId, "L3", 1),
14
43
  client.layer(blockId, "L1", config.recallLimit),
15
44
  ]);
16
- return renderMemoryContext(core, rankMemoryItems(incoming, atomic, config.recallLimit));
45
+ const selected = rankMemoryItems(incoming, atomic, config.recallLimit);
46
+ const rendered = renderMemoryContext(core, selected);
47
+ safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 已完成", {
48
+ execution_id: executionId,
49
+ agent_handle: agentHandle,
50
+ outcome: "succeeded",
51
+ core_count: core.length,
52
+ atomic_count: atomic.length,
53
+ selected_atomic_count: selected.length,
54
+ rendered_bytes: Buffer.byteLength(rendered, "utf8"),
55
+ duration_ms: Date.now() - startedAt,
56
+ });
57
+ return rendered;
17
58
  }
18
- catch {
59
+ catch (error) {
60
+ safeReport(report, "external_agent_memory.recall_observed", "外部 Agent Memory recall 失败并已放行", {
61
+ level: "WARN",
62
+ execution_id: executionId,
63
+ agent_handle: agentHandle,
64
+ outcome: "failed",
65
+ error_name: error instanceof Error ? error.name : "unknown",
66
+ duration_ms: Date.now() - startedAt,
67
+ });
19
68
  return "";
20
69
  }
21
70
  },
22
71
  capture: async (agentHandle, executionId, wakePrompt, finalText) => {
23
- const incoming = agentHandle === config.agentHandle ? extractIncomingMessage(wakePrompt) : null;
24
- if (incoming === null)
72
+ const startedAt = Date.now();
73
+ if (agentHandle !== config.agentHandle) {
74
+ safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
75
+ execution_id: executionId,
76
+ agent_handle: agentHandle,
77
+ outcome: "skipped",
78
+ skip_reason: "handle_mismatch",
79
+ duration_ms: Date.now() - startedAt,
80
+ });
25
81
  return;
82
+ }
83
+ const incoming = extractIncomingMessage(wakePrompt);
84
+ if (incoming === null) {
85
+ safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
86
+ execution_id: executionId,
87
+ agent_handle: agentHandle,
88
+ outcome: "skipped",
89
+ skip_reason: "wake_unparsed",
90
+ duration_ms: Date.now() - startedAt,
91
+ });
92
+ return;
93
+ }
26
94
  const messages = buildCaptureMessages(incoming, finalText);
27
- if (messages === null)
95
+ if (messages === null) {
96
+ safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已跳过", {
97
+ execution_id: executionId,
98
+ agent_handle: agentHandle,
99
+ outcome: "skipped",
100
+ skip_reason: "content_filtered",
101
+ duration_ms: Date.now() - startedAt,
102
+ });
28
103
  return;
104
+ }
29
105
  try {
30
- await client.importConversation(`nowwork-${executionId}`, messages);
106
+ const result = await client.importConversation(`nowwork-${executionId}`, messages);
107
+ safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 已完成", {
108
+ execution_id: executionId,
109
+ agent_handle: agentHandle,
110
+ outcome: "succeeded",
111
+ accepted_count: result.acceptedCount,
112
+ duration_ms: Date.now() - startedAt,
113
+ });
31
114
  }
32
- catch {
115
+ catch (error) {
116
+ safeReport(report, "external_agent_memory.capture_observed", "外部 Agent Memory capture 失败并已放行", {
117
+ level: "WARN",
118
+ execution_id: executionId,
119
+ agent_handle: agentHandle,
120
+ outcome: "failed",
121
+ error_name: error instanceof Error ? error.name : "unknown",
122
+ duration_ms: Date.now() - startedAt,
123
+ });
33
124
  // External memory is an optional side effect and cannot alter execution completion.
34
125
  }
35
126
  },
@@ -4,6 +4,12 @@ const DaemonUpdateMessageSchema = z.object({
4
4
  updateId: z.string().uuid(),
5
5
  targetVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
6
6
  }).strict();
7
+ const DaemonRestartMessageSchema = z.object({
8
+ type: z.literal("daemon:restart"),
9
+ updateId: z.string().uuid(),
10
+ targetVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
11
+ }).strict();
12
+ const DaemonControlMessageSchema = z.union([DaemonUpdateMessageSchema, DaemonRestartMessageSchema]);
7
13
  export function createDaemonUpdateController(deps) {
8
14
  const handled = new Set();
9
15
  let running = null;
@@ -17,16 +23,18 @@ export function createDaemonUpdateController(deps) {
17
23
  failed(message.updateId, "ineligible");
18
24
  return;
19
25
  }
20
- const installed = await deps.install({
21
- targetVersion: message.targetVersion,
22
- packageRoot: eligibility.packageRoot,
23
- npmPrefix: eligibility.npmPrefix,
24
- onInstalling: () => {
25
- deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
26
- },
27
- });
28
- if (!installed.ok) {
29
- failed(message.updateId, installed.errorCode);
26
+ const preparation = message.type === "daemon:restart"
27
+ ? await deps.prepareRestart()
28
+ : await deps.install({
29
+ targetVersion: message.targetVersion,
30
+ packageRoot: eligibility.packageRoot,
31
+ npmPrefix: eligibility.npmPrefix,
32
+ onInstalling: () => {
33
+ deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
34
+ },
35
+ });
36
+ if (!preparation.ok) {
37
+ failed(message.updateId, preparation.errorCode);
30
38
  return;
31
39
  }
32
40
  let released = false;
@@ -34,7 +42,7 @@ export function createDaemonUpdateController(deps) {
34
42
  if (released)
35
43
  return;
36
44
  released = true;
37
- await installed.release();
45
+ await preparation.release();
38
46
  };
39
47
  deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "restarting" });
40
48
  try {
@@ -66,7 +74,7 @@ export function createDaemonUpdateController(deps) {
66
74
  }
67
75
  },
68
76
  handle: async (input) => {
69
- const parsed = DaemonUpdateMessageSchema.safeParse(input);
77
+ const parsed = DaemonControlMessageSchema.safeParse(input);
70
78
  if (!parsed.success)
71
79
  return false;
72
80
  const message = parsed.data;
@@ -10,6 +10,16 @@ import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
10
10
  import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
11
11
  import { inspectWindowsServiceUpdateScope } from "./windows-scheduled-task.js";
12
12
  import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
13
+ export async function managedDaemonCapabilities(eligibility) {
14
+ try {
15
+ return (await eligibility()).eligible
16
+ ? ["daemon_update_v1", "daemon_restart_v1"]
17
+ : [];
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
13
23
  function defaults() {
14
24
  return {
15
25
  platform: process.platform,
@@ -3,6 +3,27 @@ import { resolve } from "node:path";
3
3
  import { systemCommandRunner } from "./computer-service.js";
4
4
  import { daemonGlobalInstallation } from "./daemon-installation.js";
5
5
  const RELEASED_VERSION_RE = /^\d+\.\d+\.\d+$/;
6
+ export async function prepareDaemonRestart(input) {
7
+ const local = input.localSlots.tryAcquireExclusive();
8
+ if (local === null)
9
+ return { ok: false, errorCode: "runtime_busy" };
10
+ const host = await input.hostCoordinator.tryAcquireExclusiveExecution();
11
+ if (host === null) {
12
+ local.release();
13
+ return { ok: false, errorCode: "runtime_busy" };
14
+ }
15
+ let released = false;
16
+ return {
17
+ ok: true,
18
+ release: async () => {
19
+ if (released)
20
+ return;
21
+ released = true;
22
+ await host.release();
23
+ local.release();
24
+ },
25
+ };
26
+ }
6
27
  async function readPackageVersion(packageRoot) {
7
28
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
8
29
  return typeof body.version === "string" ? body.version : "";
@@ -7,7 +7,7 @@ import { JournalLockedError, createJournalLease, } from "./execution-journal-loc
7
7
  export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
8
8
  const ExecutionIdSchema = z.string().uuid();
9
9
  const TimestampSchema = z.string().datetime({ offset: true });
10
- const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
10
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
11
11
  const RUNTIME_READY_SUFFIX = ".runtime-ready";
12
12
  const RawJournalEntrySchema = z.object({
13
13
  executionId: ExecutionIdSchema,
@@ -8,7 +8,7 @@ const MIN_SIGNED_32_INTEGER = -2_147_483_648;
8
8
  const SequenceSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
9
9
  const TokenCountSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
10
10
  const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_INTEGER);
11
- const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
11
+ const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode"]);
12
12
  const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
13
13
  const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
14
14
  export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
@@ -22,16 +22,8 @@ const ProjectSkillRefSchema = z.object({
22
22
  projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
23
23
  skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
24
24
  }).strict();
25
- export const ReasoningSchema = z.enum([
26
- "default",
27
- "none",
28
- "minimal",
29
- "low",
30
- "medium",
31
- "high",
32
- "xhigh",
33
- "max",
34
- ]);
25
+ export const ReasoningSchema = z.string().min(1).max(64)
26
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u, "invalid runtime reasoning token");
35
27
  export const PermissionSchema = z.enum([
36
28
  "local_default",
37
29
  "sandboxed",
@@ -169,7 +169,10 @@ function validReasoning(spec) {
169
169
  if (spec.runtime.name === "codex") {
170
170
  return CODEX_EFFORT_LEVELS.includes(reasoning);
171
171
  }
172
- return KIMI_EFFORT_LEVELS.includes(reasoning);
172
+ if (spec.runtime.name === "kimi") {
173
+ return KIMI_EFFORT_LEVELS.includes(reasoning);
174
+ }
175
+ return true;
173
176
  }
174
177
  function launchProviderConfig(config) {
175
178
  if (config === undefined)
@@ -241,8 +244,14 @@ function admission(spec, config, dependencies, at) {
241
244
  return { rejected: rejection(spec.executionId, "resource_limit", "Local execution capacity is exhausted", at) };
242
245
  }
243
246
  const permission = effectivePermission(spec, config);
244
- if (spec.runtime.name === "kimi" && permission !== "full_access") {
245
- return { rejected: rejection(spec.executionId, "local_policy_denied", `Kimi cannot enforce ${permission} permission`, at) };
247
+ if ((spec.runtime.name === "kimi" || spec.runtime.name === "hermes" || spec.runtime.name === "opencode")
248
+ && permission !== "full_access") {
249
+ const displayName = spec.runtime.name === "kimi"
250
+ ? "Kimi"
251
+ : spec.runtime.name === "hermes" ? "Hermes" : "OpenCode";
252
+ return {
253
+ rejected: rejection(spec.executionId, "local_policy_denied", `${displayName} cannot enforce ${permission} permission`, at),
254
+ };
246
255
  }
247
256
  return { spec, permission };
248
257
  }
@@ -404,7 +413,7 @@ export async function runExecution(config, input, dependencies) {
404
413
  let recalledMemory = "";
405
414
  if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
406
415
  try {
407
- recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt), dependencies.cancellation);
416
+ recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt, spec.executionId), dependencies.cancellation);
408
417
  }
409
418
  catch (error) {
410
419
  if (error instanceof ExecutionCancelledError)
@@ -62,6 +62,10 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
62
62
  ? decoder.push(candidate.item.text)
63
63
  : [];
64
64
  }
65
+ if ((runtime === "hermes" && candidate.type === "hermes.acp.text_delta")
66
+ || (runtime === "opencode" && candidate.type === "opencode.text_delta")) {
67
+ return typeof candidate.text === "string" ? decoder.push(candidate.text) : [];
68
+ }
65
69
  return candidate.type === undefined
66
70
  && candidate.role === "assistant"
67
71
  && typeof candidate.content === "string"
@@ -1,6 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { isWin } from "./platform.js";
4
+ import { listHermesModels } from "./runtimes/hermes-models.js";
4
5
  const execFileRaw = promisify(execFile);
5
6
  const MODEL_PROBE_TIMEOUT_MS = 8_000;
6
7
  const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
@@ -11,14 +12,16 @@ const execFileP = (bin, args) => execFileRaw(bin, args, {
11
12
  killSignal: "SIGKILL",
12
13
  maxBuffer: MODEL_PROBE_MAX_BUFFER_BYTES,
13
14
  });
14
- export async function listRuntimeModels(runtime) {
15
+ export async function listRuntimeModels(runtime, options = {}) {
15
16
  switch (runtime) {
16
17
  case "codex":
17
18
  return parseCodexModels((await execFileP("codex", ["debug", "models"])).stdout);
18
19
  case "cursor":
19
20
  return parseCursorModels((await execFileP("cursor-agent", ["--list-models"])).stdout);
21
+ case "hermes":
22
+ return listHermesModels(options);
20
23
  case "opencode":
21
- return parseOpencodeModels((await execFileP("opencode", ["models"])).stdout);
24
+ return listOpencodeModels();
22
25
  case "pi":
23
26
  return parsePiModels((await execFileP("pi", ["--list-models"])).stdout);
24
27
  default:
@@ -76,12 +79,103 @@ function parseCursorModels(stdout) {
76
79
  return { id: id.trim(), label: (label || id).trim(), ...(index === 0 ? { default: true } : {}) };
77
80
  });
78
81
  }
79
- function parseOpencodeModels(stdout) {
80
- return stdout
81
- .split(/\r?\n/)
82
- .map((line) => line.trim())
83
- .filter((line) => line.length > 0 && !line.startsWith("warning:"))
84
- .map((line, index) => ({ id: line, label: line, ...(index === 0 ? { default: true } : {}) }));
82
+ async function outputEvenOnFailure(bin, args) {
83
+ try {
84
+ return (await execFileP(bin, args)).stdout;
85
+ }
86
+ catch (error) {
87
+ const stdout = error.stdout;
88
+ if (typeof stdout === "string")
89
+ return stdout;
90
+ if (Buffer.isBuffer(stdout))
91
+ return stdout.toString("utf8");
92
+ return "";
93
+ }
94
+ }
95
+ async function listOpencodeModels() {
96
+ const verbose = parseOpencodeModels(await outputEvenOnFailure("opencode", ["models", "--verbose"]));
97
+ if (verbose.length > 0)
98
+ return verbose;
99
+ return parseOpencodeModels(await outputEvenOnFailure("opencode", ["models"]));
100
+ }
101
+ const VARIANT_ORDER = new Map([
102
+ ["none", 0], ["minimal", 1], ["low", 2], ["medium", 3],
103
+ ["high", 4], ["xhigh", 5], ["max", 6],
104
+ ]);
105
+ function modelIdLine(line) {
106
+ const id = line.trim().split(/\s+/, 1)[0] ?? "";
107
+ if (!id.includes("/") || /^["{[]/.test(id) || id === id.toUpperCase())
108
+ return null;
109
+ return id;
110
+ }
111
+ function collectJson(lines, start) {
112
+ let raw = "";
113
+ for (let index = start; index < lines.length; index += 1) {
114
+ if (index > start && modelIdLine(lines[index]) !== null)
115
+ return { raw, next: index };
116
+ raw += `${raw ? "\n" : ""}${lines[index]}`;
117
+ try {
118
+ JSON.parse(raw);
119
+ return { raw, next: index + 1 };
120
+ }
121
+ catch {
122
+ // A pretty-printed block is complete only when JSON.parse succeeds.
123
+ }
124
+ }
125
+ return { raw, next: lines.length };
126
+ }
127
+ function parseVariants(raw) {
128
+ try {
129
+ const meta = JSON.parse(raw);
130
+ const variants = meta.variants ?? {};
131
+ const looksReasoning = meta.reasoning === true || Object.entries(variants).some(([name, value]) => VARIANT_ORDER.has(name) || typeof value.reasoningEffort === "string" || value.thinking != null);
132
+ if (!looksReasoning)
133
+ return [];
134
+ return Object.entries(variants)
135
+ .filter(([name, value]) => name.length > 0 && value.disabled !== true)
136
+ .map(([name]) => name)
137
+ .sort((left, right) => {
138
+ const leftOrder = VARIANT_ORDER.get(left);
139
+ const rightOrder = VARIANT_ORDER.get(right);
140
+ if (leftOrder !== undefined && rightOrder !== undefined)
141
+ return leftOrder - rightOrder;
142
+ if (leftOrder !== undefined)
143
+ return -1;
144
+ if (rightOrder !== undefined)
145
+ return 1;
146
+ return left.localeCompare(right);
147
+ });
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
153
+ export function parseOpencodeModels(stdout) {
154
+ const lines = stdout.split(/\r?\n/);
155
+ const models = [];
156
+ const byId = new Map();
157
+ for (let index = 0; index < lines.length; index += 1) {
158
+ const id = modelIdLine(lines[index]);
159
+ if (id === null)
160
+ continue;
161
+ let modelIndex = byId.get(id);
162
+ if (modelIndex === undefined) {
163
+ modelIndex = models.length;
164
+ byId.set(id, modelIndex);
165
+ models.push({ id, label: id, ...(modelIndex === 0 ? { default: true } : {}) });
166
+ }
167
+ let next = index + 1;
168
+ while (next < lines.length && lines[next].trim() === "")
169
+ next += 1;
170
+ if (next >= lines.length || !lines[next].trim().startsWith("{"))
171
+ continue;
172
+ const block = collectJson(lines, next);
173
+ const reasoning = parseVariants(block.raw);
174
+ if (reasoning.length > 0)
175
+ models[modelIndex] = { ...models[modelIndex], reasoning };
176
+ index = block.next - 1;
177
+ }
178
+ return models;
85
179
  }
86
180
  function parsePiModels(stdout) {
87
181
  return stdout