@botcord/daemon 0.2.89 → 0.2.91
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/dist/cloud-daemon.js +18 -4
- package/dist/daemon.d.ts +2 -3
- package/dist/daemon.js +21 -6
- package/dist/gateway/channels/botcord.js +35 -22
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +103 -9
- package/dist/runtime-models.js +46 -0
- package/dist/self-restart.d.ts +29 -0
- package/dist/self-restart.js +172 -0
- package/dist/skill-index.d.ts +14 -2
- package/dist/skill-index.js +109 -41
- package/dist/skill-installer.d.ts +61 -0
- package/dist/skill-installer.js +340 -0
- package/dist/system-context.d.ts +6 -0
- package/dist/system-context.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/provision.test.ts +23 -0
- package/src/__tests__/runtime-discovery.test.ts +6 -3
- package/src/__tests__/runtime-models.test.ts +53 -0
- package/src/__tests__/self-restart.test.ts +57 -0
- package/src/__tests__/skill-index.test.ts +130 -13
- package/src/__tests__/skill-installer.test.ts +224 -0
- package/src/cloud-daemon.ts +17 -4
- package/src/daemon.ts +23 -8
- package/src/gateway/__tests__/botcord-channel.test.ts +38 -0
- package/src/gateway/channels/botcord.ts +41 -22
- package/src/provision.ts +111 -14
- package/src/runtime-models.ts +47 -0
- package/src/self-restart.ts +218 -0
- package/src/skill-index.ts +130 -53
- package/src/skill-installer.ts +472 -0
- package/src/system-context.ts +7 -1
package/dist/cloud-daemon.js
CHANGED
|
@@ -76,6 +76,15 @@ export async function startCloudDaemon(opts) {
|
|
|
76
76
|
const credentialPathByAgentId = new Map();
|
|
77
77
|
const hubUrlByAgentId = new Map();
|
|
78
78
|
const displayNameByAgent = new Map();
|
|
79
|
+
const runtimeByAgentId = new Map();
|
|
80
|
+
const hermesProfileByAgentId = new Map();
|
|
81
|
+
const skillIndexOptionsForAgent = (agentId) => {
|
|
82
|
+
const hermesProfile = hermesProfileByAgentId.get(agentId);
|
|
83
|
+
return {
|
|
84
|
+
runtime: runtimeByAgentId.get(agentId) ?? opts.config.defaultRoute.adapter,
|
|
85
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
79
88
|
// Seed each per-agent hub URL with the cloud-mode value so that even
|
|
80
89
|
// before the first credential file is written the room-context fetcher
|
|
81
90
|
// has somewhere sensible to point.
|
|
@@ -158,16 +167,16 @@ export async function startCloudDaemon(opts) {
|
|
|
158
167
|
});
|
|
159
168
|
};
|
|
160
169
|
const installedAgentIds = new Set();
|
|
161
|
-
const runtimeByAgentId = new Map();
|
|
162
170
|
let controlChannel = null;
|
|
163
171
|
const pushInstalledAgentSkillSnapshot = (agentId, reason) => {
|
|
164
172
|
if (!controlChannel)
|
|
165
173
|
return;
|
|
166
|
-
const
|
|
167
|
-
const pushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
174
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
175
|
+
const pushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
168
176
|
logger.info("cloud control-channel: agent_skill_snapshot pushed", {
|
|
169
177
|
agentId,
|
|
170
|
-
runtime,
|
|
178
|
+
runtime: skillIndexOptions.runtime,
|
|
179
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
171
180
|
reason,
|
|
172
181
|
ok: pushed,
|
|
173
182
|
});
|
|
@@ -176,6 +185,10 @@ export async function startCloudDaemon(opts) {
|
|
|
176
185
|
installedAgentIds.add(info.agentId);
|
|
177
186
|
if (info.runtime)
|
|
178
187
|
runtimeByAgentId.set(info.agentId, info.runtime);
|
|
188
|
+
if (info.hermesProfile)
|
|
189
|
+
hermesProfileByAgentId.set(info.agentId, info.hermesProfile);
|
|
190
|
+
else if (info.runtime)
|
|
191
|
+
hermesProfileByAgentId.delete(info.agentId);
|
|
179
192
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
180
193
|
if (info.hubUrl)
|
|
181
194
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -186,6 +199,7 @@ export async function startCloudDaemon(opts) {
|
|
|
186
199
|
agentId: info.agentId,
|
|
187
200
|
activityTracker,
|
|
188
201
|
roomContextBuilder,
|
|
202
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
189
203
|
// Cloud daemons run isolated — no loop-risk guard wired in PR1;
|
|
190
204
|
// the runtime adapter's wall-time budget enforces the equivalent.
|
|
191
205
|
loopRiskBuilder: () => null,
|
package/dist/daemon.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { DaemonConfig } from "./config.js";
|
|
|
3
3
|
import { type BootAgentsResult } from "./agent-discovery.js";
|
|
4
4
|
import { ensureAgentWorkspace } from "./agent-workspace.js";
|
|
5
5
|
import { UserAuthManager } from "./user-auth.js";
|
|
6
|
+
import { type SkillIndexOptions } from "./skill-index.js";
|
|
6
7
|
import { classifyActivitySender } from "./sender-classify.js";
|
|
7
8
|
export { classifyActivitySender };
|
|
8
9
|
/** Minimal activity-tracker surface the inbound observer uses. */
|
|
@@ -65,9 +66,7 @@ export interface RuntimeSnapshotSink {
|
|
|
65
66
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
66
67
|
*/
|
|
67
68
|
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink, liveSnapshot?: GatewayRuntimeSnapshot): boolean;
|
|
68
|
-
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string, opts?:
|
|
69
|
-
runtime?: string;
|
|
70
|
-
}): boolean;
|
|
69
|
+
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string, opts?: SkillIndexOptions): boolean;
|
|
71
70
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
72
71
|
export interface DaemonRuntimeOptions {
|
|
73
72
|
config: DaemonConfig;
|
package/dist/daemon.js
CHANGED
|
@@ -221,6 +221,13 @@ export async function startDaemon(opts) {
|
|
|
221
221
|
const agentIds = boot.agents.map((a) => a.agentId);
|
|
222
222
|
const { credentialPathByAgentId, agentRuntimes } = backfillBootAgents(boot.agents, { logger });
|
|
223
223
|
const gwConfig = toGatewayConfig(opts.config, { agentIds, agentRuntimes });
|
|
224
|
+
const skillIndexOptionsForAgent = (agentId) => {
|
|
225
|
+
const meta = agentRuntimes[agentId];
|
|
226
|
+
return {
|
|
227
|
+
runtime: meta?.runtime ?? opts.config.defaultRoute.adapter,
|
|
228
|
+
...(meta?.hermesProfile ? { hermesProfile: meta.hermesProfile } : {}),
|
|
229
|
+
};
|
|
230
|
+
};
|
|
224
231
|
// Per-agent hub URL — read from each credential file at boot. Used to
|
|
225
232
|
// populate `BOTCORD_HUB` for runtime CLI subprocesses so the bundled
|
|
226
233
|
// `botcord` CLI talks to the same hub the agent is registered against,
|
|
@@ -273,6 +280,7 @@ export async function startDaemon(opts) {
|
|
|
273
280
|
activityTracker,
|
|
274
281
|
roomContextBuilder,
|
|
275
282
|
loopRiskBuilder,
|
|
283
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(aid),
|
|
276
284
|
}));
|
|
277
285
|
}
|
|
278
286
|
const buildSystemContext = (message) => {
|
|
@@ -372,11 +380,16 @@ export async function startDaemon(opts) {
|
|
|
372
380
|
// next room-context fetch re-loads the BotCordClient against the new
|
|
373
381
|
// credential file.
|
|
374
382
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
375
|
-
if (info.runtime) {
|
|
376
|
-
|
|
383
|
+
if (info.runtime || info.hermesProfile) {
|
|
384
|
+
const next = {
|
|
377
385
|
...(agentRuntimes[info.agentId] ?? {}),
|
|
378
|
-
runtime: info.runtime,
|
|
386
|
+
...(info.runtime ? { runtime: info.runtime } : {}),
|
|
387
|
+
...(info.hermesProfile ? { hermesProfile: info.hermesProfile } : {}),
|
|
379
388
|
};
|
|
389
|
+
if (info.runtime && !info.hermesProfile) {
|
|
390
|
+
delete next.hermesProfile;
|
|
391
|
+
}
|
|
392
|
+
agentRuntimes[info.agentId] = next;
|
|
380
393
|
}
|
|
381
394
|
if (info.hubUrl)
|
|
382
395
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -388,6 +401,7 @@ export async function startDaemon(opts) {
|
|
|
388
401
|
activityTracker,
|
|
389
402
|
roomContextBuilder,
|
|
390
403
|
loopRiskBuilder,
|
|
404
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
391
405
|
}));
|
|
392
406
|
}
|
|
393
407
|
};
|
|
@@ -507,11 +521,12 @@ export async function startDaemon(opts) {
|
|
|
507
521
|
ok: pushed,
|
|
508
522
|
});
|
|
509
523
|
for (const agentId of agentIds) {
|
|
510
|
-
const
|
|
511
|
-
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
524
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
525
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
512
526
|
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
513
527
|
agentId,
|
|
514
|
-
runtime,
|
|
528
|
+
runtime: skillIndexOptions.runtime,
|
|
529
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
515
530
|
ok: skillsPushed,
|
|
516
531
|
});
|
|
517
532
|
}
|
|
@@ -187,6 +187,35 @@ function normalizeInboxBatch(msgs, options) {
|
|
|
187
187
|
raw: { ...latest, batch: msgs },
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Fire-and-forget authenticated POST for presence/streaming control requests
|
|
192
|
+
* (`/hub/typing`, `/hub/stream-block`). Mirrors `BotCordClient.hubFetch`'s 401
|
|
193
|
+
* handling: a stale-but-unexpired token (e.g. after a Hub JWT secret rotation,
|
|
194
|
+
* which `ensureToken()` won't refresh because it only refreshes near expiry) is
|
|
195
|
+
* refreshed once and the request retried. Without this, typing/stream-block
|
|
196
|
+
* silently 401 in a loop until the next actual message send happens to refresh
|
|
197
|
+
* the token — leaving the conversation with no typing indicator or live stream.
|
|
198
|
+
*/
|
|
199
|
+
async function postControlWithRefresh(client, hubUrl, path, body) {
|
|
200
|
+
let token = await client.ensureToken();
|
|
201
|
+
for (let attempt = 0; attempt <= 1; attempt++) {
|
|
202
|
+
const resp = await fetch(`${hubUrl}${path}`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
Authorization: `Bearer ${token}`,
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify(body),
|
|
209
|
+
signal: AbortSignal.timeout(10_000),
|
|
210
|
+
});
|
|
211
|
+
if (resp.status === 401 && attempt === 0) {
|
|
212
|
+
token = await client.refreshToken();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
return resp;
|
|
216
|
+
}
|
|
217
|
+
throw new Error("postControlWithRefresh: exhausted retries");
|
|
218
|
+
}
|
|
190
219
|
/**
|
|
191
220
|
* Construct a BotCord channel adapter.
|
|
192
221
|
*
|
|
@@ -758,21 +787,12 @@ export function createBotCordChannel(options) {
|
|
|
758
787
|
const client = ensureClient();
|
|
759
788
|
const hubUrl = options.hubBaseUrl ?? client.getHubUrl();
|
|
760
789
|
try {
|
|
761
|
-
const token = await client.ensureToken();
|
|
762
790
|
const block = ctx.block;
|
|
763
791
|
const seq = typeof block?.seq === "number" ? block.seq : 0;
|
|
764
|
-
const resp = await
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
Authorization: `Bearer ${token}`,
|
|
769
|
-
},
|
|
770
|
-
body: JSON.stringify({
|
|
771
|
-
trace_id: ctx.traceId,
|
|
772
|
-
seq,
|
|
773
|
-
block: normalizeBlockForHub(block, seq),
|
|
774
|
-
}),
|
|
775
|
-
signal: AbortSignal.timeout(10_000),
|
|
792
|
+
const resp = await postControlWithRefresh(client, hubUrl, "/hub/stream-block", {
|
|
793
|
+
trace_id: ctx.traceId,
|
|
794
|
+
seq,
|
|
795
|
+
block: normalizeBlockForHub(block, seq),
|
|
776
796
|
});
|
|
777
797
|
if (!resp.ok && resp.status !== 204) {
|
|
778
798
|
const body = await resp.text().catch(() => "");
|
|
@@ -790,15 +810,8 @@ export function createBotCordChannel(options) {
|
|
|
790
810
|
const client = ensureClient();
|
|
791
811
|
const hubUrl = options.hubBaseUrl ?? client.getHubUrl();
|
|
792
812
|
try {
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
method: "POST",
|
|
796
|
-
headers: {
|
|
797
|
-
"Content-Type": "application/json",
|
|
798
|
-
Authorization: `Bearer ${token}`,
|
|
799
|
-
},
|
|
800
|
-
body: JSON.stringify({ room_id: ctx.conversationId }),
|
|
801
|
-
signal: AbortSignal.timeout(10_000),
|
|
813
|
+
const resp = await postControlWithRefresh(client, hubUrl, "/hub/typing", {
|
|
814
|
+
room_id: ctx.conversationId,
|
|
802
815
|
});
|
|
803
816
|
if (!resp.ok && resp.status !== 204) {
|
|
804
817
|
const body = await resp.text().catch(() => "");
|
package/dist/provision.d.ts
CHANGED
package/dist/provision.js
CHANGED
|
@@ -20,12 +20,26 @@ import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
|
20
20
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
21
21
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
22
22
|
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
23
|
+
import { installAgentSkillManifest, installBotLearnArchiveManifest, installVercelSkillsForAgent, } from "./skill-installer.js";
|
|
23
24
|
import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
|
|
24
25
|
import { handleCloudGatewayRuntimeInbound, } from "./cloud-gateway-runtime.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
?.
|
|
26
|
+
import { scheduleDaemonSelfRestart } from "./self-restart.js";
|
|
27
|
+
function skillIndexOptionsForLoadedAgent(gateway, agentId) {
|
|
28
|
+
const route = gateway.listManagedRoutes()
|
|
29
|
+
.find((entry) => entry.match?.accountId === agentId);
|
|
30
|
+
let credentials = null;
|
|
31
|
+
try {
|
|
32
|
+
credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
credentials = null;
|
|
36
|
+
}
|
|
37
|
+
const runtime = route?.runtime ?? credentials?.runtime;
|
|
38
|
+
const hermesProfile = route?.hermesProfile ?? credentials?.hermesProfile;
|
|
39
|
+
return {
|
|
40
|
+
...(runtime ? { runtime } : {}),
|
|
41
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
45
|
* Build a dispatcher function that routes a `ControlFrame` to the right
|
|
@@ -227,6 +241,16 @@ export function createProvisioner(opts) {
|
|
|
227
241
|
daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
|
|
228
242
|
return { ok: true, result: snapshot };
|
|
229
243
|
}
|
|
244
|
+
case CONTROL_FRAME_TYPES.RESTART_DAEMON: {
|
|
245
|
+
const plan = scheduleDaemonSelfRestart({ update: true });
|
|
246
|
+
daemonLog.warn("restart_daemon: scheduled self restart", {
|
|
247
|
+
updateRequested: plan.updateRequested,
|
|
248
|
+
updateSupported: plan.updateSupported,
|
|
249
|
+
installPrefix: plan.installPrefix,
|
|
250
|
+
packageSpec: plan.packageSpec,
|
|
251
|
+
});
|
|
252
|
+
return { ok: true, result: plan };
|
|
253
|
+
}
|
|
230
254
|
case "list_gateways":
|
|
231
255
|
return gatewayControl.handleList();
|
|
232
256
|
case "upsert_gateway": {
|
|
@@ -339,15 +363,76 @@ export function createProvisioner(opts) {
|
|
|
339
363
|
},
|
|
340
364
|
};
|
|
341
365
|
}
|
|
342
|
-
const
|
|
343
|
-
const result = collectAgentSkillSnapshot(params.agentId,
|
|
366
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
367
|
+
const result = collectAgentSkillSnapshot(params.agentId, skillIndexOptions);
|
|
344
368
|
daemonLog.debug("list_agent_skills", {
|
|
345
369
|
agentId: params.agentId,
|
|
346
|
-
runtime,
|
|
370
|
+
runtime: skillIndexOptions.runtime,
|
|
371
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
347
372
|
count: result.skills.length,
|
|
348
373
|
});
|
|
349
374
|
return { ok: true, result };
|
|
350
375
|
}
|
|
376
|
+
case CONTROL_FRAME_TYPES.INSTALL_AGENT_SKILL: {
|
|
377
|
+
const params = (frame.params ?? {});
|
|
378
|
+
if (!params.agentId) {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: { code: "bad_params", message: "install_agent_skill requires params.agentId" },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const channels = gateway.snapshot().channels;
|
|
385
|
+
if (!channels[params.agentId]) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
error: {
|
|
389
|
+
code: "agent_not_loaded",
|
|
390
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
395
|
+
const runtime = skillIndexOptions.runtime;
|
|
396
|
+
const modes = [params.manifest, params.archiveManifest, params.vercel].filter(Boolean).length;
|
|
397
|
+
if (modes !== 1) {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: {
|
|
401
|
+
code: "bad_params",
|
|
402
|
+
message: "install_agent_skill requires exactly one of manifest, archiveManifest, or vercel",
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
let result;
|
|
407
|
+
try {
|
|
408
|
+
result = params.vercel
|
|
409
|
+
? await installVercelSkillsForAgent({
|
|
410
|
+
agentId: params.agentId,
|
|
411
|
+
packageSpec: params.vercel.packageSpec,
|
|
412
|
+
skills: params.vercel.skills,
|
|
413
|
+
runtime,
|
|
414
|
+
})
|
|
415
|
+
: params.archiveManifest
|
|
416
|
+
? installBotLearnArchiveManifest(params.agentId, params.archiveManifest, { runtime })
|
|
417
|
+
: installAgentSkillManifest(params.agentId, params.manifest, { runtime });
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: {
|
|
423
|
+
code: "skill_install_failed",
|
|
424
|
+
message: err instanceof Error ? err.message : String(err),
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
daemonLog.debug("install_agent_skill", {
|
|
429
|
+
agentId: params.agentId,
|
|
430
|
+
runtime,
|
|
431
|
+
installed: result.installed.map((s) => s.name),
|
|
432
|
+
snapshotCount: result.snapshot.skills.length,
|
|
433
|
+
});
|
|
434
|
+
return { ok: true, result };
|
|
435
|
+
}
|
|
351
436
|
case "wake_agent": {
|
|
352
437
|
return handleWakeAgent(gateway, frame.params);
|
|
353
438
|
}
|
|
@@ -456,8 +541,15 @@ async function handleWakeAgent(gateway, raw) {
|
|
|
456
541
|
streamable: false,
|
|
457
542
|
},
|
|
458
543
|
};
|
|
459
|
-
|
|
460
|
-
|
|
544
|
+
void gateway.injectInbound(msg).catch((err) => {
|
|
545
|
+
daemonLog.error("wake_agent: async inject failed", {
|
|
546
|
+
agentId,
|
|
547
|
+
scheduleId: scheduleId ?? null,
|
|
548
|
+
runId,
|
|
549
|
+
error: err instanceof Error ? err.message : String(err),
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
return { ok: true, result: { agent_id: agentId, queued: true } };
|
|
461
553
|
}
|
|
462
554
|
function validateGatewayParams(raw, spec) {
|
|
463
555
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -823,6 +915,7 @@ async function installLocalAgent(credentials, ctx) {
|
|
|
823
915
|
hubUrl: credentials.hubUrl,
|
|
824
916
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
825
917
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
918
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
826
919
|
});
|
|
827
920
|
}
|
|
828
921
|
catch (err) {
|
|
@@ -904,6 +997,7 @@ async function installExistingOpenclawBinding(agentId, ctx) {
|
|
|
904
997
|
hubUrl: credentials.hubUrl,
|
|
905
998
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
906
999
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
1000
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
907
1001
|
});
|
|
908
1002
|
}
|
|
909
1003
|
catch (err) {
|
package/dist/runtime-models.js
CHANGED
|
@@ -54,6 +54,36 @@ const KIMI_FALLBACK_MODELS = [
|
|
|
54
54
|
{ id: "kimi-k2-5-preview", displayName: "kimi-k2-5-preview", provider: "kimi", source: "builtin" },
|
|
55
55
|
{ id: "kimi-k2-0711", displayName: "kimi-k2-0711", provider: "kimi", source: "builtin" },
|
|
56
56
|
];
|
|
57
|
+
// Gemini CLI has no `gemini models list` command — the model set is
|
|
58
|
+
// hard-coded inside the bundle's `VALID_GEMINI_MODELS` (see `@google/gemini-cli`
|
|
59
|
+
// bundle `chunk-BE42OOYM.js:275832`). We mirror the documented user-facing
|
|
60
|
+
// names here as a static catalog; the actual availability depends on the
|
|
61
|
+
// user's auth tier (gcloud ADC / Vertex / GEMINI_API_KEY) and project quota.
|
|
62
|
+
// The `auto` alias is the default — it lets gemini pick the best model.
|
|
63
|
+
const GEMINI_FALLBACK_MODELS = [
|
|
64
|
+
{ id: "auto", displayName: "Auto (let Gemini pick)", provider: "google", source: "builtin", isDefault: true },
|
|
65
|
+
{ id: "pro", displayName: "Pro alias", provider: "google", source: "builtin" },
|
|
66
|
+
{ id: "flash", displayName: "Flash alias", provider: "google", source: "builtin" },
|
|
67
|
+
{ id: "flash-lite", displayName: "Flash Lite alias", provider: "google", source: "builtin" },
|
|
68
|
+
{ id: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", provider: "google", source: "builtin" },
|
|
69
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", provider: "google", source: "builtin" },
|
|
70
|
+
{ id: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", provider: "google", source: "builtin" },
|
|
71
|
+
{ id: "gemini-3-pro-preview", displayName: "Gemini 3 Pro (preview)", provider: "google", source: "builtin" },
|
|
72
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (preview)", provider: "google", source: "builtin" },
|
|
73
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (preview)", provider: "google", source: "builtin" },
|
|
74
|
+
{
|
|
75
|
+
id: "gemini-3.1-pro-preview-customtools",
|
|
76
|
+
displayName: "Gemini 3.1 Pro Custom Tools (preview)",
|
|
77
|
+
provider: "google",
|
|
78
|
+
source: "builtin",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
82
|
+
displayName: "Gemini 3.1 Flash Lite (preview)",
|
|
83
|
+
provider: "google",
|
|
84
|
+
source: "builtin",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
57
87
|
const backgroundRefreshes = new Set();
|
|
58
88
|
export function discoverRuntimeModelCatalog(entry) {
|
|
59
89
|
if (!entry.result.available)
|
|
@@ -126,6 +156,22 @@ function runtimeCatalogStrategy(entry) {
|
|
|
126
156
|
],
|
|
127
157
|
}),
|
|
128
158
|
};
|
|
159
|
+
case "gemini":
|
|
160
|
+
// Gemini CLI exposes no runtime discovery command and its thinking
|
|
161
|
+
// budget / level (see bundle `ThinkingLevel`, `thinkingBudget`) is
|
|
162
|
+
// configured per-installation in `~/.gemini/settings.json`, not via
|
|
163
|
+
// any CLI flag — so we ship a static model catalog with no parameter
|
|
164
|
+
// controls. settings.json + BOTCORD_GEMINI_BIN feed the cache key so
|
|
165
|
+
// user-side reconfig (e.g. switching auth type) busts the cache.
|
|
166
|
+
return {
|
|
167
|
+
id: entry.id,
|
|
168
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
169
|
+
settings: fileStatKey(path.join(homedir(), ".gemini", "settings.json")),
|
|
170
|
+
env: pickEnv(["BOTCORD_GEMINI_BIN", "GEMINI_CLI_HOME"]),
|
|
171
|
+
}),
|
|
172
|
+
discoverFresh: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
173
|
+
fallback: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
174
|
+
};
|
|
129
175
|
default:
|
|
130
176
|
return null;
|
|
131
177
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export interface DaemonRestartPlan {
|
|
3
|
+
scheduled: boolean;
|
|
4
|
+
updateRequested: boolean;
|
|
5
|
+
updateSupported: boolean;
|
|
6
|
+
installPrefix: string | null;
|
|
7
|
+
packageSpec: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ScheduleDaemonSelfRestartOptions {
|
|
10
|
+
update?: boolean;
|
|
11
|
+
packageSpec?: string;
|
|
12
|
+
delayMs?: number;
|
|
13
|
+
forceExitAfterMs?: number;
|
|
14
|
+
entrypoint?: string;
|
|
15
|
+
restartArgs?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface ScheduleDaemonSelfRestartDeps {
|
|
18
|
+
spawn?: typeof spawn;
|
|
19
|
+
setTimeout?: typeof setTimeout;
|
|
20
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
21
|
+
exit?: (code?: number) => never;
|
|
22
|
+
pid?: number;
|
|
23
|
+
execPath?: string;
|
|
24
|
+
argv?: string[];
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
}
|
|
27
|
+
export declare function findDaemonInstallPrefix(entrypoint?: string): string | null;
|
|
28
|
+
export declare function resolveNpmBin(nodePath?: string): string;
|
|
29
|
+
export declare function scheduleDaemonSelfRestart(opts?: ScheduleDaemonSelfRestartOptions, deps?: ScheduleDaemonSelfRestartDeps): DaemonRestartPlan;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const DEFAULT_DAEMON_PACKAGE = "@botcord/daemon@latest";
|
|
5
|
+
const DEFAULT_SHUTDOWN_DELAY_MS = 750;
|
|
6
|
+
const DEFAULT_FORCE_EXIT_MS = 10_000;
|
|
7
|
+
const DEFAULT_PARENT_EXIT_WAIT_MS = 30_000;
|
|
8
|
+
const DEFAULT_INSTALL_TIMEOUT_MS = 120_000;
|
|
9
|
+
export function findDaemonInstallPrefix(entrypoint) {
|
|
10
|
+
if (!entrypoint)
|
|
11
|
+
return null;
|
|
12
|
+
const candidates = [entrypoint, safeRealpath(entrypoint)];
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
if (!candidate)
|
|
15
|
+
continue;
|
|
16
|
+
const prefix = installPrefixFromPath(candidate);
|
|
17
|
+
if (prefix)
|
|
18
|
+
return prefix;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function resolveNpmBin(nodePath = process.execPath) {
|
|
23
|
+
const name = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
24
|
+
const sibling = path.join(path.dirname(nodePath), name);
|
|
25
|
+
return existsSync(sibling) ? sibling : name;
|
|
26
|
+
}
|
|
27
|
+
export function scheduleDaemonSelfRestart(opts = {}, deps = {}) {
|
|
28
|
+
const env = deps.env ?? process.env;
|
|
29
|
+
const argv = deps.argv ?? process.argv;
|
|
30
|
+
const entrypoint = opts.entrypoint ?? argv[1];
|
|
31
|
+
if (!entrypoint) {
|
|
32
|
+
throw new Error("cannot restart daemon: process entrypoint is unknown");
|
|
33
|
+
}
|
|
34
|
+
const updateRequested = opts.update !== false;
|
|
35
|
+
const installPrefix = updateRequested ? findDaemonInstallPrefix(entrypoint) : null;
|
|
36
|
+
const packageSpec = opts.packageSpec ?? env.BOTCORD_DAEMON_PACKAGE ?? DEFAULT_DAEMON_PACKAGE;
|
|
37
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
38
|
+
const pid = deps.pid ?? process.pid;
|
|
39
|
+
const restartArgs = opts.restartArgs ?? ["start", "--foreground"];
|
|
40
|
+
const supervisorEnv = {
|
|
41
|
+
...env,
|
|
42
|
+
BOTCORD_DAEMON_CHILD: "1",
|
|
43
|
+
BOTCORD_RESTART_PARENT_PID: String(pid),
|
|
44
|
+
BOTCORD_RESTART_ENTRYPOINT: entrypoint,
|
|
45
|
+
BOTCORD_RESTART_ARGS_JSON: JSON.stringify(restartArgs),
|
|
46
|
+
BOTCORD_RESTART_NODE: execPath,
|
|
47
|
+
BOTCORD_RESTART_NPM_BIN: resolveNpmBin(execPath),
|
|
48
|
+
BOTCORD_RESTART_UPDATE: updateRequested && installPrefix ? "1" : "0",
|
|
49
|
+
BOTCORD_RESTART_INSTALL_PREFIX: installPrefix ?? "",
|
|
50
|
+
BOTCORD_RESTART_PACKAGE: packageSpec,
|
|
51
|
+
BOTCORD_RESTART_PARENT_EXIT_WAIT_MS: String(DEFAULT_PARENT_EXIT_WAIT_MS),
|
|
52
|
+
BOTCORD_RESTART_INSTALL_TIMEOUT_MS: String(DEFAULT_INSTALL_TIMEOUT_MS),
|
|
53
|
+
};
|
|
54
|
+
const spawnImpl = deps.spawn ?? spawn;
|
|
55
|
+
const child = spawnImpl(execPath, ["-e", RESTART_SUPERVISOR_SCRIPT], {
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: "ignore",
|
|
58
|
+
env: supervisorEnv,
|
|
59
|
+
});
|
|
60
|
+
child.unref();
|
|
61
|
+
const setTimer = deps.setTimeout ?? setTimeout;
|
|
62
|
+
const kill = deps.kill ?? process.kill.bind(process);
|
|
63
|
+
const exit = deps.exit ?? process.exit.bind(process);
|
|
64
|
+
const delayMs = opts.delayMs ?? DEFAULT_SHUTDOWN_DELAY_MS;
|
|
65
|
+
const forceExitAfterMs = opts.forceExitAfterMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
66
|
+
const shutdownTimer = setTimer(() => {
|
|
67
|
+
try {
|
|
68
|
+
kill(pid, "SIGTERM");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
exit(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const exitTimer = setTimer(() => exit(0), forceExitAfterMs);
|
|
75
|
+
unrefTimer(exitTimer);
|
|
76
|
+
}, delayMs);
|
|
77
|
+
unrefTimer(shutdownTimer);
|
|
78
|
+
return {
|
|
79
|
+
scheduled: true,
|
|
80
|
+
updateRequested,
|
|
81
|
+
updateSupported: installPrefix !== null,
|
|
82
|
+
installPrefix,
|
|
83
|
+
packageSpec,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function safeRealpath(input) {
|
|
87
|
+
try {
|
|
88
|
+
return realpathSync(input);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function installPrefixFromPath(input) {
|
|
95
|
+
const parts = path.resolve(input).split(path.sep);
|
|
96
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
97
|
+
if (parts[i] !== "node_modules" ||
|
|
98
|
+
parts[i + 1] !== "@botcord" ||
|
|
99
|
+
parts[i + 2] !== "daemon") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const prefix = parts.slice(0, i).join(path.sep) || path.sep;
|
|
103
|
+
const packageJson = path.join(prefix, "node_modules", "@botcord", "daemon", "package.json");
|
|
104
|
+
return existsSync(packageJson) ? prefix : null;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function unrefTimer(timer) {
|
|
109
|
+
const maybe = timer;
|
|
110
|
+
if (typeof maybe.unref === "function") {
|
|
111
|
+
maybe.unref();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const RESTART_SUPERVISOR_SCRIPT = `
|
|
115
|
+
const cp = require("node:child_process");
|
|
116
|
+
|
|
117
|
+
function sleep(ms) {
|
|
118
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function alive(pid) {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
const parentPid = Number(process.env.BOTCORD_RESTART_PARENT_PID || "0");
|
|
132
|
+
const waitMs = Number(process.env.BOTCORD_RESTART_PARENT_EXIT_WAIT_MS || "30000");
|
|
133
|
+
const deadline = Date.now() + waitMs;
|
|
134
|
+
while (parentPid > 0 && alive(parentPid) && Date.now() < deadline) {
|
|
135
|
+
await sleep(250);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const update = process.env.BOTCORD_RESTART_UPDATE === "1";
|
|
139
|
+
const installPrefix = process.env.BOTCORD_RESTART_INSTALL_PREFIX || "";
|
|
140
|
+
if (update && installPrefix) {
|
|
141
|
+
const npmBin = process.env.BOTCORD_RESTART_NPM_BIN || "npm";
|
|
142
|
+
const packageSpec = process.env.BOTCORD_RESTART_PACKAGE || "${DEFAULT_DAEMON_PACKAGE}";
|
|
143
|
+
const timeout = Number(process.env.BOTCORD_RESTART_INSTALL_TIMEOUT_MS || "120000");
|
|
144
|
+
cp.spawnSync(npmBin, ["install", "--prefix", installPrefix, packageSpec], {
|
|
145
|
+
stdio: "ignore",
|
|
146
|
+
env: process.env,
|
|
147
|
+
timeout,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const node = process.env.BOTCORD_RESTART_NODE || process.execPath;
|
|
152
|
+
const entrypoint = process.env.BOTCORD_RESTART_ENTRYPOINT;
|
|
153
|
+
if (!entrypoint) process.exit(1);
|
|
154
|
+
let args = ["start", "--foreground"];
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(process.env.BOTCORD_RESTART_ARGS_JSON || "[]");
|
|
157
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
158
|
+
args = parsed;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// keep default args
|
|
162
|
+
}
|
|
163
|
+
const child = cp.spawn(node, [entrypoint, ...args], {
|
|
164
|
+
detached: true,
|
|
165
|
+
stdio: "ignore",
|
|
166
|
+
env: { ...process.env, BOTCORD_DAEMON_CHILD: "1" },
|
|
167
|
+
});
|
|
168
|
+
child.unref();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch(() => process.exit(1));
|
|
172
|
+
`;
|