@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.
@@ -0,0 +1,224 @@
1
+ import { dslog } from "./slog.js";
2
+ import { analyzeLocalMemoryInjection, createLocalMemoryAccessObserver, inspectLocalMemoryFilesWithinDeadline, localMemoryContentFacts, localMemoryFileChanged, } from "./local-memory-diagnostics.js";
3
+ function safeLog(eventType, message, fields) {
4
+ try {
5
+ dslog(eventType, message, fields);
6
+ }
7
+ catch {
8
+ // Local-memory diagnostics must never alter Agent execution.
9
+ }
10
+ }
11
+ function errorFields(error) {
12
+ const code = error?.code;
13
+ return {
14
+ error_name: error instanceof Error ? error.name : "unknown",
15
+ ...(typeof code === "string" ? { error_code: code } : {}),
16
+ };
17
+ }
18
+ export function logLocalMemoryContextPrepareFailure(identity, phase, error) {
19
+ safeLog("local_memory.context_prepare_failed", "本地记忆上下文准备失败", {
20
+ level: "WARN",
21
+ execution_id: identity.executionId,
22
+ agent_handle: identity.agentHandle,
23
+ task_key: identity.taskKey,
24
+ failure_phase: phase,
25
+ ...errorFields(error),
26
+ });
27
+ }
28
+ export function logLocalMemoryDiagnosticsFailure(identity, phase, error) {
29
+ safeLog("local_memory.diagnostics_failed", "本地记忆诊断失败并已放行", {
30
+ level: "WARN",
31
+ execution_id: identity.executionId,
32
+ agent_handle: identity.agentHandle,
33
+ task_key: identity.taskKey,
34
+ diagnostics_phase: phase,
35
+ ...errorFields(error),
36
+ });
37
+ }
38
+ function snapshotFields(snapshot) {
39
+ if (snapshot === null)
40
+ return {};
41
+ return {
42
+ memory_exists: snapshot.memory.exists,
43
+ memory_size_bytes: snapshot.memory.size_bytes,
44
+ memory_mtime_ms: snapshot.memory.mtime_ms,
45
+ memory_sha256: snapshot.memory.sha256,
46
+ memory_hash_skipped_reason: snapshot.memory.hash_skipped_reason,
47
+ memory_error_code: snapshot.memory.error_code,
48
+ memory_markdown_heading_count: snapshot.memory.markdown_heading_count,
49
+ memory_lessons_reference_count: snapshot.memory.lessons_reference_count,
50
+ lessons_exists: snapshot.lessons.exists,
51
+ lessons_size_bytes: snapshot.lessons.size_bytes,
52
+ lessons_mtime_ms: snapshot.lessons.mtime_ms,
53
+ lessons_sha256: snapshot.lessons.sha256,
54
+ lessons_hash_skipped_reason: snapshot.lessons.hash_skipped_reason,
55
+ lessons_error_code: snapshot.lessons.error_code,
56
+ lessons_markdown_heading_count: snapshot.lessons.markdown_heading_count,
57
+ };
58
+ }
59
+ function promptDelivery(runtime, stableProtocolRuntime) {
60
+ if (runtime === "claude")
61
+ return "claude_system_prompt_file";
62
+ if (!stableProtocolRuntime)
63
+ return "combined_wake_prompt";
64
+ return runtime === "codex" ? "codex_developer_instructions" : "kimi_protocol_prompt";
65
+ }
66
+ function logAccess(options, observation, sequence) {
67
+ safeLog("local_memory.file_access_observed", "观察到本地记忆文件访问", {
68
+ execution_id: options.executionId,
69
+ agent_handle: options.agentHandle,
70
+ task_key: options.taskKey,
71
+ runtime: options.runtime,
72
+ access_sequence: sequence,
73
+ ...observation,
74
+ });
75
+ }
76
+ export function createLocalMemoryTelemetry(options) {
77
+ const accessObserver = createLocalMemoryAccessObserver({
78
+ homeDir: options.agentDir,
79
+ runDir: options.runDir,
80
+ });
81
+ let beforeSnapshot = null;
82
+ let diagnosticsMs;
83
+ let accessSequence = 0;
84
+ let contextWasPrepared = false;
85
+ let runtimeStarted = false;
86
+ let runtimeReady = false;
87
+ let runtimeExitCode;
88
+ let executorCompleted = false;
89
+ return {
90
+ async captureBefore() {
91
+ const startedAt = Date.now();
92
+ try {
93
+ beforeSnapshot = await inspectLocalMemoryFilesWithinDeadline(options.agentDir, options.diagnosticsTimeoutMs);
94
+ }
95
+ catch (error) {
96
+ logLocalMemoryDiagnosticsFailure(options, "before", error);
97
+ }
98
+ finally {
99
+ diagnosticsMs = Date.now() - startedAt;
100
+ }
101
+ },
102
+ contextPrepared(context) {
103
+ try {
104
+ const sourceFacts = localMemoryContentFacts(context.memory);
105
+ const injectionFacts = analyzeLocalMemoryInjection(context.systemPrompt, context.memory);
106
+ safeLog("local_memory.context_prepared", "本地记忆上下文已准备", {
107
+ execution_id: options.executionId,
108
+ agent_handle: options.agentHandle,
109
+ task_key: options.taskKey,
110
+ runtime: options.runtime,
111
+ resumed: context.resumed,
112
+ is_memory_prune: options.isMemoryPrune,
113
+ memory_seed_created: context.memorySeedCreated,
114
+ memory_source_read: true,
115
+ memory_source_size_bytes: sourceFacts.size_bytes,
116
+ memory_source_sha256: sourceFacts.sha256,
117
+ memory_source_scan_skipped_reason: sourceFacts.scan_skipped_reason,
118
+ memory_read_matches_snapshot: sourceFacts.sha256 === undefined
119
+ || beforeSnapshot?.memory.sha256 === undefined
120
+ ? undefined
121
+ : sourceFacts.sha256 === beforeSnapshot.memory.sha256,
122
+ memory_source_nonblank: sourceFacts.nonblank,
123
+ memory_source_markdown_heading_count: sourceFacts.markdown_heading_count,
124
+ memory_source_lessons_reference_count: sourceFacts.lessons_reference_count,
125
+ memory_injection_state: injectionFacts.state,
126
+ memory_injection_source_bytes: injectionFacts.source_bytes,
127
+ memory_injection_bounded_bytes: injectionFacts.bounded_bytes,
128
+ memory_injected_bytes: injectionFacts.injected_bytes,
129
+ system_prompt_bytes: Buffer.byteLength(context.systemPrompt, "utf8"),
130
+ prompt_delivery: promptDelivery(options.runtime, options.stableProtocolRuntime),
131
+ diagnostics_ms: diagnosticsMs,
132
+ ...snapshotFields(beforeSnapshot),
133
+ });
134
+ contextWasPrepared = true;
135
+ }
136
+ catch (error) {
137
+ logLocalMemoryDiagnosticsFailure(options, "event_observe", error);
138
+ }
139
+ },
140
+ observe(event) {
141
+ try {
142
+ for (const observation of accessObserver.observe(event)) {
143
+ accessSequence += 1;
144
+ logAccess(options, observation, accessSequence);
145
+ }
146
+ }
147
+ catch (error) {
148
+ logLocalMemoryDiagnosticsFailure(options, "event_observe", error);
149
+ }
150
+ },
151
+ markRuntimeStarted() {
152
+ runtimeStarted = true;
153
+ },
154
+ markRuntimeReady() {
155
+ runtimeReady = true;
156
+ },
157
+ markRuntimeExited(exitCode) {
158
+ runtimeExitCode = exitCode;
159
+ },
160
+ markExecutorCompleted() {
161
+ executorCompleted = true;
162
+ },
163
+ async finish() {
164
+ let afterSnapshot = null;
165
+ try {
166
+ afterSnapshot = await inspectLocalMemoryFilesWithinDeadline(options.agentDir, options.diagnosticsTimeoutMs);
167
+ }
168
+ catch (error) {
169
+ logLocalMemoryDiagnosticsFailure(options, "after", error);
170
+ }
171
+ const memoryChanged = beforeSnapshot !== null && afterSnapshot !== null
172
+ ? localMemoryFileChanged(beforeSnapshot.memory, afterSnapshot.memory)
173
+ : undefined;
174
+ const lessonsChanged = beforeSnapshot !== null && afterSnapshot !== null
175
+ ? localMemoryFileChanged(beforeSnapshot.lessons, afterSnapshot.lessons)
176
+ : undefined;
177
+ const accessSummary = accessObserver.summary();
178
+ const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined
179
+ ? undefined
180
+ : memoryChanged || lessonsChanged;
181
+ const currentExecutionSharedWriteObserved = accessSummary.shared_memory_write_succeeded > 0
182
+ || accessSummary.shared_lessons_write_succeeded > 0;
183
+ safeLog("local_memory.execution_observed", "本地记忆执行观察已完成", {
184
+ execution_id: options.executionId,
185
+ agent_handle: options.agentHandle,
186
+ task_key: options.taskKey,
187
+ runtime: options.runtime,
188
+ is_memory_prune: options.isMemoryPrune,
189
+ context_prepared: contextWasPrepared,
190
+ runtime_started: runtimeStarted,
191
+ runtime_ready: runtimeReady,
192
+ runtime_exit_code: runtimeExitCode,
193
+ executor_outcome: executorCompleted ? "succeeded" : "failed",
194
+ memory_changed: memoryChanged,
195
+ lessons_changed: lessonsChanged,
196
+ shared_files_changed_during_execution: sharedFilesChanged,
197
+ unexpected_shared_write: options.isMemoryPrune
198
+ ? undefined
199
+ : currentExecutionSharedWriteObserved
200
+ ? true
201
+ : sharedFilesChanged === false ? false : undefined,
202
+ access_observation_count: accessSequence,
203
+ ...accessSummary,
204
+ ...(afterSnapshot === null ? {} : {
205
+ memory_after_exists: afterSnapshot.memory.exists,
206
+ memory_after_size_bytes: afterSnapshot.memory.size_bytes,
207
+ memory_after_mtime_ms: afterSnapshot.memory.mtime_ms,
208
+ memory_after_sha256: afterSnapshot.memory.sha256,
209
+ memory_after_hash_skipped_reason: afterSnapshot.memory.hash_skipped_reason,
210
+ memory_after_error_code: afterSnapshot.memory.error_code,
211
+ memory_after_markdown_heading_count: afterSnapshot.memory.markdown_heading_count,
212
+ memory_after_lessons_reference_count: afterSnapshot.memory.lessons_reference_count,
213
+ lessons_after_exists: afterSnapshot.lessons.exists,
214
+ lessons_after_size_bytes: afterSnapshot.lessons.size_bytes,
215
+ lessons_after_mtime_ms: afterSnapshot.lessons.mtime_ms,
216
+ lessons_after_sha256: afterSnapshot.lessons.sha256,
217
+ lessons_after_hash_skipped_reason: afterSnapshot.lessons.hash_skipped_reason,
218
+ lessons_after_error_code: afterSnapshot.lessons.error_code,
219
+ lessons_after_markdown_heading_count: afterSnapshot.lessons.markdown_heading_count,
220
+ }),
221
+ });
222
+ },
223
+ };
224
+ }
@@ -15,6 +15,8 @@ import { dirname, resolve } from "node:path";
15
15
  import { executableRuntimes } from "./runtime-capabilities.js";
16
16
  import { executionBackendCapability } from "./execution-backend.js";
17
17
  import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
18
+ import { probeHermesAcp } from "./runtimes/hermes.js";
19
+ import { probeOpenCodeRun } from "./runtimes/opencode.js";
18
20
  export { executableRuntimes } from "./runtime-capabilities.js";
19
21
  const execFileP = promisify(execFile);
20
22
  export const DAEMON_CAPABILITIES = [
@@ -39,6 +41,7 @@ const RUNTIME_BINS = [
39
41
  ["codex", "codex"],
40
42
  ["cursor", "cursor-agent"],
41
43
  ["gemini", "gemini"],
44
+ ["hermes", "hermes"],
42
45
  ["opencode", "opencode"],
43
46
  ["copilot", "copilot"],
44
47
  ["kimi", "kimi"],
@@ -59,17 +62,29 @@ export async function detectRuntimes() {
59
62
  const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
60
63
  return checks.filter((x) => x !== null);
61
64
  }
62
- async function supportsKimiAcp() {
63
- return probeKimiAcp({ bin: "kimi" });
65
+ async function supportsKimiAcp(signal) {
66
+ return probeKimiAcp({ bin: "kimi", ...(signal ? { signal } : {}) });
67
+ }
68
+ async function supportsHermesAcp(signal) {
69
+ return probeHermesAcp({ bin: "hermes", ...(signal ? { signal } : {}) });
70
+ }
71
+ async function supportsOpenCodeRun(signal) {
72
+ return probeOpenCodeRun(signal);
64
73
  }
65
74
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
66
- export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp) {
75
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, signal) {
67
76
  const present = await installed;
68
- const supported = executableRuntimes(present).filter((runtime) => runtime !== "kimi");
69
- if (present.includes("kimi") && await kimiAcpProbe())
70
- supported.push("kimi");
71
- return supported;
77
+ const [kimiReady, hermesReady, openCodeReady] = await Promise.all([
78
+ present.includes("kimi") ? kimiAcpProbe(signal) : false,
79
+ present.includes("hermes") ? hermesAcpProbe(signal) : false,
80
+ present.includes("opencode") ? openCodeProbe(signal) : false,
81
+ ]);
82
+ return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
83
+ : runtime === "hermes" ? hermesReady
84
+ : runtime === "opencode" ? openCodeReady
85
+ : true));
72
86
  }
87
+ export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, signal);
73
88
  export function daemonVersion() {
74
89
  try {
75
90
  const here = dirname(fileURLToPath(import.meta.url));
package/dist/main.js CHANGED
@@ -113,7 +113,12 @@ async function main() {
113
113
  process.exit(2);
114
114
  }
115
115
  process.stdout.write(formatDaemonLogLine(`🚀 ${td("Waking agent")} "${values.agent}" ${td("for channel")} ${values.channel}`) + "\n");
116
- initSlog(config.serverUrl, config.machineToken); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
116
+ initSlog(config.serverUrl, config.machineToken, {
117
+ daemonVersion: daemonVersion(),
118
+ cliVersion: cliVersion(),
119
+ ...(values.profile === undefined ? {} : { profileName: values.profile }),
120
+ agentsRoot: config.agentsRoot,
121
+ }); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
117
122
  const result = await runAgent(config, {
118
123
  handle: values.agent,
119
124
  channelId: values.channel,
package/dist/normalize.js CHANGED
@@ -34,7 +34,8 @@ export function classifyCommand(command) {
34
34
  /** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
35
35
  export function extractFinalText(event) {
36
36
  const e = (event ?? {});
37
- if (e.type === "kimi.acp.text_delta" && e.text)
37
+ if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
38
+ || e.type === "opencode.text_delta") && e.text)
38
39
  return e.text;
39
40
  if (e.type === "result" && !e.is_error && e.result?.trim())
40
41
  return e.result.trim();
@@ -61,13 +62,16 @@ function parseKimiBashCommand(args) {
61
62
  /** 把一个 stream-json 事件归一化为 0..N 个活动。 */
62
63
  export function normalizeEvent(event) {
63
64
  const e = (event ?? {});
64
- if (e.type === "kimi.acp.text_delta" && e.text?.trim()) {
65
+ if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
66
+ || e.type === "opencode.text_delta") && e.text?.trim()) {
65
67
  return [{ kind: "text", label: "思考/说明", detail: e.text }];
66
68
  }
67
- if (e.type === "kimi.acp.tool_call") {
69
+ if (e.type === "kimi.acp.tool_call" || e.type === "hermes.acp.tool_call"
70
+ || e.type === "opencode.tool_call") {
68
71
  return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
69
72
  }
70
- if (e.type === "kimi.acp.tool_result") {
73
+ if (e.type === "kimi.acp.tool_result" || e.type === "hermes.acp.tool_result"
74
+ || e.type === "opencode.tool_result") {
71
75
  return [{ kind: "tool_result", label: "工具返回" }];
72
76
  }
73
77
  if (e.type === "system" && e.subtype === "init") {
@@ -86,6 +90,12 @@ export function normalizeEvent(event) {
86
90
  if (e.type === "turn.completed") {
87
91
  return [{ kind: "done", label: "本轮结束" }];
88
92
  }
93
+ if (e.type === "opencode.error") {
94
+ const detail = typeof event.message === "string"
95
+ ? event.message
96
+ : undefined;
97
+ return [{ kind: "error", label: "运行出错", ...(detail ? { detail } : {}) }];
98
+ }
89
99
  if (e.type === "result") {
90
100
  return e.is_error
91
101
  ? [{ kind: "error", label: "运行出错", ...(e.result ? { detail: e.result } : {}) }]
@@ -1,4 +1,4 @@
1
- export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi"];
1
+ export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi", "hermes", "opencode"];
2
2
  export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
3
3
  claude: Object.freeze({
4
4
  transport: "claude-stream-json",
@@ -15,6 +15,16 @@ export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
15
15
  nativeResume: true,
16
16
  systemPromptTransport: "protocol",
17
17
  }),
18
+ hermes: Object.freeze({
19
+ transport: "hermes-acp",
20
+ nativeResume: true,
21
+ systemPromptTransport: "file",
22
+ }),
23
+ opencode: Object.freeze({
24
+ transport: "opencode-json",
25
+ nativeResume: true,
26
+ systemPromptTransport: "file",
27
+ }),
18
28
  });
19
29
  export function runtimeCapability(runtime) {
20
30
  return LOCAL_RUNTIME_CAPABILITIES[runtime];
@@ -0,0 +1,26 @@
1
+ import { executableRuntimes } from "./runtime-capabilities.js";
2
+ export function conservativeExecutionRuntimes(installed) {
3
+ return executableRuntimes(installed)
4
+ .filter((runtime) => runtime === "claude" || runtime === "codex");
5
+ }
6
+ export function createRuntimeProbeCoordinator() {
7
+ const abort = new AbortController();
8
+ const flights = new Map();
9
+ return {
10
+ detect(installed, detector) {
11
+ const key = [...installed].sort().join("\0");
12
+ const existing = flights.get(key);
13
+ if (existing)
14
+ return existing;
15
+ const pending = detector(installed, abort.signal).catch((error) => {
16
+ flights.delete(key);
17
+ throw error;
18
+ });
19
+ flights.set(key, pending);
20
+ return pending;
21
+ },
22
+ stop() {
23
+ abort.abort();
24
+ },
25
+ };
26
+ }
@@ -77,6 +77,8 @@ export function createRuntimeStartupGate(limits, now = Date.now) {
77
77
  claude: activeByRuntime.get("claude") ?? 0,
78
78
  codex: activeByRuntime.get("codex") ?? 0,
79
79
  kimi: activeByRuntime.get("kimi") ?? 0,
80
+ hermes: activeByRuntime.get("hermes") ?? 0,
81
+ opencode: activeByRuntime.get("opencode") ?? 0,
80
82
  },
81
83
  }),
82
84
  };
@@ -193,10 +193,20 @@ export function mapCodexNotification(method, params) {
193
193
  type: "command_execution",
194
194
  command: value.item.command,
195
195
  ...(value.item.status === undefined ? {} : { status: value.item.status }),
196
+ ...(typeof value.item.exitCode === "number" ? { exit_code: value.item.exitCode } : {}),
196
197
  ...(value.item.aggregatedOutput == null ? {} : { aggregated_output: value.item.aggregatedOutput }),
197
198
  },
198
199
  }];
199
200
  }
201
+ if (value.item.type === "fileChange" && Array.isArray(value.item.changes)) {
202
+ return [{
203
+ type: "diagnostic.file_change",
204
+ ...(value.item.status === undefined ? {} : { status: value.item.status }),
205
+ changes: value.item.changes.flatMap((change) => typeof change.path === "string"
206
+ ? [{ path: change.path, ...(change.kind === undefined ? {} : { kind: change.kind }) }]
207
+ : []),
208
+ }];
209
+ }
200
210
  return [];
201
211
  }
202
212
  async function readRunnerInput() {
@@ -0,0 +1,117 @@
1
+ import { once } from "node:events";
2
+ import { Readable, Writable } from "node:stream";
3
+ import spawn from "cross-spawn";
4
+ import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
5
+ import { augmentedPath } from "../runtime-path.js";
6
+ const HERMES_MODEL_PROBE_TIMEOUT_MS = 8_000;
7
+ function nonEmptyString(value) {
8
+ if (typeof value !== "string")
9
+ return null;
10
+ const trimmed = value.trim();
11
+ return trimmed.length > 0 ? trimmed : null;
12
+ }
13
+ function selectOptions(options, category) {
14
+ const aliases = category === "model"
15
+ ? new Set(["model"])
16
+ : new Set(["reasoning", "effort", "thought level", "thinking level"]);
17
+ const matches = (options ?? []).filter((option) => (option.type === "select" && (option.category === category
18
+ || aliases.has(option.id.toLowerCase())
19
+ || aliases.has(option.name.toLowerCase()))));
20
+ if (matches.length !== 1)
21
+ return null;
22
+ const match = matches[0];
23
+ const values = match.options.flatMap((option) => "options" in option ? option.options : [option]);
24
+ return {
25
+ current: nonEmptyString(match.currentValue),
26
+ values: values.map((option) => ({ modelId: option.value, name: option.name })),
27
+ };
28
+ }
29
+ export function normalizeHermesModels(session) {
30
+ const modelConfig = selectOptions(session.configOptions, "model");
31
+ const effortConfig = selectOptions(session.configOptions, "thought_level");
32
+ const extension = session.models;
33
+ const rows = extension?.availableModels ?? extension?.available_models ?? modelConfig?.values ?? [];
34
+ const current = nonEmptyString(extension?.currentModelId)
35
+ ?? nonEmptyString(extension?.current_model_id)
36
+ ?? modelConfig?.current
37
+ ?? null;
38
+ const reasoning = [...new Set((effortConfig?.values ?? [])
39
+ .map((option) => nonEmptyString(option.modelId ?? option.model_id ?? option.id))
40
+ .filter((value) => value !== null))];
41
+ const seen = new Set();
42
+ return rows.flatMap((row) => {
43
+ const id = nonEmptyString(row.modelId ?? row.model_id ?? row.id);
44
+ if (id === null || seen.has(id))
45
+ return [];
46
+ seen.add(id);
47
+ return [{
48
+ id,
49
+ label: nonEmptyString(row.name) ?? id,
50
+ ...(id === current ? { default: true } : {}),
51
+ ...(reasoning.length > 0 ? { reasoning } : {}),
52
+ }];
53
+ });
54
+ }
55
+ async function stopChild(child) {
56
+ if (child.exitCode !== null || child.signalCode !== null)
57
+ return;
58
+ const closed = once(child, "close").then(() => undefined);
59
+ child.kill("SIGTERM");
60
+ let timer;
61
+ const graceful = await Promise.race([
62
+ closed.then(() => true),
63
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
64
+ ]);
65
+ if (timer !== undefined)
66
+ clearTimeout(timer);
67
+ if (!graceful && child.exitCode === null && child.signalCode === null) {
68
+ child.kill("SIGKILL");
69
+ await closed;
70
+ }
71
+ }
72
+ export async function listHermesModels(options = {}) {
73
+ const spawnProcess = options.spawnProcess ?? spawn;
74
+ const child = spawnProcess("hermes", ["acp"], {
75
+ cwd: process.cwd(),
76
+ env: { ...process.env, PATH: augmentedPath() },
77
+ stdio: ["pipe", "pipe", "pipe"],
78
+ });
79
+ if (child.stdin === null || child.stdout === null || child.stderr === null)
80
+ return [];
81
+ child.stderr.resume();
82
+ const app = client({ name: "nowcrew-daemon-hermes-models" })
83
+ .onRequest(methods.client.session.requestPermission, () => ({
84
+ outcome: { outcome: "cancelled" },
85
+ }))
86
+ .onNotification(methods.client.session.update, () => undefined);
87
+ let timer;
88
+ try {
89
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
90
+ const discovery = app.connectWith(stream, async (context) => {
91
+ await context.request(methods.agent.initialize, {
92
+ protocolVersion: PROTOCOL_VERSION,
93
+ clientCapabilities: {},
94
+ clientInfo: { name: "nowcrew-daemon", version: "1" },
95
+ });
96
+ const session = await context.request(methods.agent.session.new, {
97
+ cwd: process.cwd(),
98
+ mcpServers: [],
99
+ });
100
+ return normalizeHermesModels(session);
101
+ });
102
+ return await Promise.race([
103
+ discovery,
104
+ new Promise((resolve) => {
105
+ timer = setTimeout(() => {
106
+ void stopChild(child);
107
+ resolve([]);
108
+ }, HERMES_MODEL_PROBE_TIMEOUT_MS);
109
+ }),
110
+ ]);
111
+ }
112
+ finally {
113
+ if (timer !== undefined)
114
+ clearTimeout(timer);
115
+ await stopChild(child);
116
+ }
117
+ }
@@ -0,0 +1,6 @@
1
+ import spawn from "cross-spawn";
2
+ import { probeKimiAcp } from "./kimi-acp-runner.js";
3
+ /** Hermes and Kimi expose the same ACP initialize contract; execution policy stays provider-specific. */
4
+ export function probeHermesAcp(options, spawnProcess = spawn) {
5
+ return probeKimiAcp({ ...options, provider: "hermes" }, spawnProcess);
6
+ }