@botcord/daemon 0.2.90 → 0.2.92
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/gateway/channels/botcord.js +35 -22
- package/dist/gateway-control.js +80 -40
- package/dist/provision.js +20 -2
- package/dist/self-restart.d.ts +29 -0
- package/dist/self-restart.js +172 -0
- package/dist/skill-index.js +33 -12
- package/package.json +3 -3
- package/src/__tests__/gateway-control.test.ts +372 -0
- package/src/__tests__/provision.test.ts +23 -0
- package/src/__tests__/self-restart.test.ts +57 -0
- package/src/__tests__/skill-index.test.ts +41 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +38 -0
- package/src/gateway/channels/botcord.ts +41 -22
- package/src/gateway-control.ts +91 -43
- package/src/provision.ts +21 -2
- package/src/self-restart.ts +218 -0
- package/src/skill-index.ts +34 -13
package/src/gateway-control.ts
CHANGED
|
@@ -271,6 +271,9 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
const cfg = cfgIO.load();
|
|
274
|
+
const existingProfiles = cfg.thirdPartyGateways ?? [];
|
|
275
|
+
const prevProfile = existingProfiles.find((g) => g.id === params.id);
|
|
276
|
+
const hadExistingProfile = prevProfile !== undefined;
|
|
274
277
|
|
|
275
278
|
// accountId must belong to a daemon-bound agent. An empty agent set
|
|
276
279
|
// (no agents provisioned yet) is itself a hard reject — otherwise we
|
|
@@ -349,43 +352,66 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
349
352
|
} else if (params.type === "feishu") {
|
|
350
353
|
const loginId = params.loginId;
|
|
351
354
|
if (!loginId) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
355
|
+
if (
|
|
356
|
+
!prevProfile ||
|
|
357
|
+
prevProfile.type !== "feishu" ||
|
|
358
|
+
prevProfile.accountId !== params.accountId ||
|
|
359
|
+
!prevProfile.appId
|
|
360
|
+
) {
|
|
361
|
+
return badParams("upsert_gateway: feishu requires loginId");
|
|
362
|
+
}
|
|
363
|
+
const existing = loadGatewaySecret<{ appSecret?: string }>(params.id);
|
|
364
|
+
if (!existing?.appSecret) {
|
|
365
|
+
return badParams("upsert_gateway: feishu requires loginId");
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
params.settings?.domain !== undefined &&
|
|
369
|
+
params.settings.domain !== (prevProfile.domain ?? "feishu")
|
|
370
|
+
) {
|
|
371
|
+
return badParams("upsert_gateway: feishu domain change requires a fresh loginId");
|
|
372
|
+
}
|
|
373
|
+
secretPayload = { appSecret: existing.appSecret };
|
|
374
|
+
tokenPreviewSource = existing.appSecret;
|
|
375
|
+
feishuAppId = prevProfile.appId;
|
|
376
|
+
feishuDomain = params.settings?.domain ?? prevProfile.domain ?? "feishu";
|
|
377
|
+
feishuUserOpenId = prevProfile.userOpenId;
|
|
378
|
+
} else {
|
|
379
|
+
const resolved = sessions.resolve(loginId);
|
|
380
|
+
if (resolved.state !== "live") {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
error:
|
|
384
|
+
resolved.state === "missing"
|
|
385
|
+
? { code: "login_missing", message: `feishu login session "${loginId}" not found` }
|
|
386
|
+
: { code: "login_expired", message: `feishu login session "${loginId}" expired` },
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const session = resolved.session!;
|
|
390
|
+
if (session.provider !== "feishu") {
|
|
391
|
+
return badParams(`upsert_gateway: login session provider "${session.provider}" != "feishu"`);
|
|
392
|
+
}
|
|
393
|
+
if (session.accountId !== params.accountId) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
error: {
|
|
397
|
+
code: "login_account_mismatch",
|
|
398
|
+
message: "feishu login session accountId does not match upsert request",
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (!session.appId || !session.appSecret) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: { code: "login_unconfirmed", message: "feishu login session has no app credentials yet" },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
secretPayload = { appSecret: session.appSecret };
|
|
409
|
+
tokenPreviewSource = session.appSecret;
|
|
410
|
+
feishuAppId = session.appId;
|
|
411
|
+
feishuDomain = session.domain ?? params.settings?.domain ?? "feishu";
|
|
412
|
+
feishuUserOpenId = session.userOpenId;
|
|
413
|
+
sessions.update(loginId, { gatewayId: params.id });
|
|
382
414
|
}
|
|
383
|
-
secretPayload = { appSecret: session.appSecret };
|
|
384
|
-
tokenPreviewSource = session.appSecret;
|
|
385
|
-
feishuAppId = session.appId;
|
|
386
|
-
feishuDomain = session.domain ?? params.settings?.domain ?? "feishu";
|
|
387
|
-
feishuUserOpenId = session.userOpenId;
|
|
388
|
-
sessions.update(loginId, { gatewayId: params.id });
|
|
389
415
|
} else {
|
|
390
416
|
return badParams(`upsert_gateway: unknown provider "${(params as { type: string }).type}"`);
|
|
391
417
|
}
|
|
@@ -393,12 +419,9 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
393
419
|
// W3/W6: remember whether a profile already exists for this id BEFORE we
|
|
394
420
|
// write the secret/config. For UPDATE path, capture previous profile +
|
|
395
421
|
// previous secret so addChannel failure can restore prior state.
|
|
396
|
-
const existingProfiles = cfg.thirdPartyGateways ?? [];
|
|
397
|
-
const hadExistingProfile = existingProfiles.some((g) => g.id === params.id);
|
|
398
|
-
const prevProfile = existingProfiles.find((g) => g.id === params.id);
|
|
399
422
|
// W6: load the previous secret for UPDATE rollback BEFORE overwriting.
|
|
400
423
|
const prevSecret = hadExistingProfile
|
|
401
|
-
? loadGatewaySecret<{ botToken?: string }>(params.id)
|
|
424
|
+
? loadGatewaySecret<{ botToken?: string; appSecret?: string }>(params.id)
|
|
402
425
|
: null;
|
|
403
426
|
|
|
404
427
|
// Persist secret first (so a config write that succeeds is never
|
|
@@ -458,20 +481,27 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
458
481
|
}
|
|
459
482
|
try {
|
|
460
483
|
if (prevProfile) {
|
|
461
|
-
cfgIO.save(
|
|
484
|
+
cfgIO.save(replaceProfileInConfig(cfgIO.load(), prevProfile));
|
|
462
485
|
}
|
|
463
486
|
} catch {
|
|
464
487
|
// best-effort
|
|
465
488
|
}
|
|
466
489
|
try {
|
|
467
|
-
if (
|
|
490
|
+
if (
|
|
491
|
+
prevProfile &&
|
|
492
|
+
((prevProfile.type === "telegram" && prevSecret?.botToken) ||
|
|
493
|
+
(prevProfile.type === "feishu" && prevSecret?.appSecret))
|
|
494
|
+
) {
|
|
468
495
|
await ctx.gateway.addChannel(
|
|
469
496
|
buildChannelConfig(
|
|
470
497
|
{
|
|
471
498
|
...params,
|
|
472
499
|
type: prevProfile.type as typeof params.type,
|
|
500
|
+
accountId: prevProfile.accountId,
|
|
473
501
|
enabled: prevProfile.enabled !== false,
|
|
474
|
-
|
|
502
|
+
...(prevProfile.type === "telegram"
|
|
503
|
+
? { secret: { botToken: prevSecret.botToken } }
|
|
504
|
+
: {}),
|
|
475
505
|
settings: {
|
|
476
506
|
baseUrl: prevProfile.baseUrl,
|
|
477
507
|
allowedSenderIds: prevProfile.allowedSenderIds,
|
|
@@ -1074,6 +1104,9 @@ function validateOutboundConversation(
|
|
|
1074
1104
|
},
|
|
1075
1105
|
};
|
|
1076
1106
|
}
|
|
1107
|
+
if (profile.type === "feishu" && (profile.allowedChatIds ?? []).length === 0) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1077
1110
|
const allowed = new Set((profile.allowedChatIds ?? []).map(String));
|
|
1078
1111
|
if (!allowed.has(chatId)) {
|
|
1079
1112
|
return {
|
|
@@ -1161,6 +1194,21 @@ function upsertProfileInConfig(
|
|
|
1161
1194
|
return { ...cfg, thirdPartyGateways: list };
|
|
1162
1195
|
}
|
|
1163
1196
|
|
|
1197
|
+
function replaceProfileInConfig(
|
|
1198
|
+
cfg: DaemonConfig,
|
|
1199
|
+
profile: ThirdPartyGatewayProfile,
|
|
1200
|
+
): DaemonConfig {
|
|
1201
|
+
const list = (cfg.thirdPartyGateways ?? []).slice();
|
|
1202
|
+
const idx = list.findIndex((g) => g.id === profile.id);
|
|
1203
|
+
const compact = compactProfile(profile);
|
|
1204
|
+
if (idx >= 0) {
|
|
1205
|
+
list[idx] = compact;
|
|
1206
|
+
} else {
|
|
1207
|
+
list.push(compact);
|
|
1208
|
+
}
|
|
1209
|
+
return { ...cfg, thirdPartyGateways: list };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1164
1212
|
function compactProfile(p: ThirdPartyGatewayProfile): ThirdPartyGatewayProfile {
|
|
1165
1213
|
const out: ThirdPartyGatewayProfile = {
|
|
1166
1214
|
id: p.id,
|
package/src/provision.ts
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
handleCloudGatewayRuntimeInbound,
|
|
92
92
|
type CloudGatewayTypingEmitter,
|
|
93
93
|
} from "./cloud-gateway-runtime.js";
|
|
94
|
+
import { scheduleDaemonSelfRestart } from "./self-restart.js";
|
|
94
95
|
|
|
95
96
|
function skillIndexOptionsForLoadedAgent(gateway: Gateway, agentId: string): SkillIndexOptions {
|
|
96
97
|
const route = gateway.listManagedRoutes()
|
|
@@ -397,6 +398,17 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
397
398
|
return { ok: true, result: snapshot };
|
|
398
399
|
}
|
|
399
400
|
|
|
401
|
+
case CONTROL_FRAME_TYPES.RESTART_DAEMON: {
|
|
402
|
+
const plan = scheduleDaemonSelfRestart({ update: true });
|
|
403
|
+
daemonLog.warn("restart_daemon: scheduled self restart", {
|
|
404
|
+
updateRequested: plan.updateRequested,
|
|
405
|
+
updateSupported: plan.updateSupported,
|
|
406
|
+
installPrefix: plan.installPrefix,
|
|
407
|
+
packageSpec: plan.packageSpec,
|
|
408
|
+
});
|
|
409
|
+
return { ok: true, result: plan };
|
|
410
|
+
}
|
|
411
|
+
|
|
400
412
|
case "list_gateways":
|
|
401
413
|
return gatewayControl.handleList();
|
|
402
414
|
|
|
@@ -729,8 +741,15 @@ async function handleWakeAgent(gateway: Gateway, raw: unknown): Promise<AckBody>
|
|
|
729
741
|
},
|
|
730
742
|
};
|
|
731
743
|
|
|
732
|
-
|
|
733
|
-
|
|
744
|
+
void gateway.injectInbound(msg).catch((err) => {
|
|
745
|
+
daemonLog.error("wake_agent: async inject failed", {
|
|
746
|
+
agentId,
|
|
747
|
+
scheduleId: scheduleId ?? null,
|
|
748
|
+
runId,
|
|
749
|
+
error: err instanceof Error ? err.message : String(err),
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
return { ok: true, result: { agent_id: agentId, queued: true } };
|
|
734
753
|
}
|
|
735
754
|
|
|
736
755
|
// W8: hand-written runtime validator for the third-party gateway frame
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DAEMON_PACKAGE = "@botcord/daemon@latest";
|
|
6
|
+
const DEFAULT_SHUTDOWN_DELAY_MS = 750;
|
|
7
|
+
const DEFAULT_FORCE_EXIT_MS = 10_000;
|
|
8
|
+
const DEFAULT_PARENT_EXIT_WAIT_MS = 30_000;
|
|
9
|
+
const DEFAULT_INSTALL_TIMEOUT_MS = 120_000;
|
|
10
|
+
|
|
11
|
+
export interface DaemonRestartPlan {
|
|
12
|
+
scheduled: boolean;
|
|
13
|
+
updateRequested: boolean;
|
|
14
|
+
updateSupported: boolean;
|
|
15
|
+
installPrefix: string | null;
|
|
16
|
+
packageSpec: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScheduleDaemonSelfRestartOptions {
|
|
20
|
+
update?: boolean;
|
|
21
|
+
packageSpec?: string;
|
|
22
|
+
delayMs?: number;
|
|
23
|
+
forceExitAfterMs?: number;
|
|
24
|
+
entrypoint?: string;
|
|
25
|
+
restartArgs?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ScheduleDaemonSelfRestartDeps {
|
|
29
|
+
spawn?: typeof spawn;
|
|
30
|
+
setTimeout?: typeof setTimeout;
|
|
31
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
32
|
+
exit?: (code?: number) => never;
|
|
33
|
+
pid?: number;
|
|
34
|
+
execPath?: string;
|
|
35
|
+
argv?: string[];
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findDaemonInstallPrefix(entrypoint?: string): string | null {
|
|
40
|
+
if (!entrypoint) return null;
|
|
41
|
+
const candidates = [entrypoint, safeRealpath(entrypoint)];
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
if (!candidate) continue;
|
|
44
|
+
const prefix = installPrefixFromPath(candidate);
|
|
45
|
+
if (prefix) return prefix;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveNpmBin(nodePath = process.execPath): string {
|
|
51
|
+
const name = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
52
|
+
const sibling = path.join(path.dirname(nodePath), name);
|
|
53
|
+
return existsSync(sibling) ? sibling : name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scheduleDaemonSelfRestart(
|
|
57
|
+
opts: ScheduleDaemonSelfRestartOptions = {},
|
|
58
|
+
deps: ScheduleDaemonSelfRestartDeps = {},
|
|
59
|
+
): DaemonRestartPlan {
|
|
60
|
+
const env = deps.env ?? process.env;
|
|
61
|
+
const argv = deps.argv ?? process.argv;
|
|
62
|
+
const entrypoint = opts.entrypoint ?? argv[1];
|
|
63
|
+
if (!entrypoint) {
|
|
64
|
+
throw new Error("cannot restart daemon: process entrypoint is unknown");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const updateRequested = opts.update !== false;
|
|
68
|
+
const installPrefix = updateRequested ? findDaemonInstallPrefix(entrypoint) : null;
|
|
69
|
+
const packageSpec = opts.packageSpec ?? env.BOTCORD_DAEMON_PACKAGE ?? DEFAULT_DAEMON_PACKAGE;
|
|
70
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
71
|
+
const pid = deps.pid ?? process.pid;
|
|
72
|
+
const restartArgs = opts.restartArgs ?? ["start", "--foreground"];
|
|
73
|
+
const supervisorEnv: NodeJS.ProcessEnv = {
|
|
74
|
+
...env,
|
|
75
|
+
BOTCORD_DAEMON_CHILD: "1",
|
|
76
|
+
BOTCORD_RESTART_PARENT_PID: String(pid),
|
|
77
|
+
BOTCORD_RESTART_ENTRYPOINT: entrypoint,
|
|
78
|
+
BOTCORD_RESTART_ARGS_JSON: JSON.stringify(restartArgs),
|
|
79
|
+
BOTCORD_RESTART_NODE: execPath,
|
|
80
|
+
BOTCORD_RESTART_NPM_BIN: resolveNpmBin(execPath),
|
|
81
|
+
BOTCORD_RESTART_UPDATE: updateRequested && installPrefix ? "1" : "0",
|
|
82
|
+
BOTCORD_RESTART_INSTALL_PREFIX: installPrefix ?? "",
|
|
83
|
+
BOTCORD_RESTART_PACKAGE: packageSpec,
|
|
84
|
+
BOTCORD_RESTART_PARENT_EXIT_WAIT_MS: String(DEFAULT_PARENT_EXIT_WAIT_MS),
|
|
85
|
+
BOTCORD_RESTART_INSTALL_TIMEOUT_MS: String(DEFAULT_INSTALL_TIMEOUT_MS),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const spawnImpl = deps.spawn ?? spawn;
|
|
89
|
+
const child = spawnImpl(execPath, ["-e", RESTART_SUPERVISOR_SCRIPT], {
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
env: supervisorEnv,
|
|
93
|
+
}) as ChildProcess;
|
|
94
|
+
child.unref();
|
|
95
|
+
|
|
96
|
+
const setTimer = deps.setTimeout ?? setTimeout;
|
|
97
|
+
const kill = deps.kill ?? process.kill.bind(process);
|
|
98
|
+
const exit = deps.exit ?? process.exit.bind(process);
|
|
99
|
+
const delayMs = opts.delayMs ?? DEFAULT_SHUTDOWN_DELAY_MS;
|
|
100
|
+
const forceExitAfterMs = opts.forceExitAfterMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
101
|
+
const shutdownTimer = setTimer(() => {
|
|
102
|
+
try {
|
|
103
|
+
kill(pid, "SIGTERM");
|
|
104
|
+
} catch {
|
|
105
|
+
exit(0);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const exitTimer = setTimer(() => exit(0), forceExitAfterMs);
|
|
109
|
+
unrefTimer(exitTimer);
|
|
110
|
+
}, delayMs);
|
|
111
|
+
unrefTimer(shutdownTimer);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
scheduled: true,
|
|
115
|
+
updateRequested,
|
|
116
|
+
updateSupported: installPrefix !== null,
|
|
117
|
+
installPrefix,
|
|
118
|
+
packageSpec,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function safeRealpath(input: string): string | null {
|
|
123
|
+
try {
|
|
124
|
+
return realpathSync(input);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function installPrefixFromPath(input: string): string | null {
|
|
131
|
+
const parts = path.resolve(input).split(path.sep);
|
|
132
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
133
|
+
if (
|
|
134
|
+
parts[i] !== "node_modules" ||
|
|
135
|
+
parts[i + 1] !== "@botcord" ||
|
|
136
|
+
parts[i + 2] !== "daemon"
|
|
137
|
+
) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const prefix = parts.slice(0, i).join(path.sep) || path.sep;
|
|
141
|
+
const packageJson = path.join(
|
|
142
|
+
prefix,
|
|
143
|
+
"node_modules",
|
|
144
|
+
"@botcord",
|
|
145
|
+
"daemon",
|
|
146
|
+
"package.json",
|
|
147
|
+
);
|
|
148
|
+
return existsSync(packageJson) ? prefix : null;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
154
|
+
const maybe = timer as { unref?: () => void };
|
|
155
|
+
if (typeof maybe.unref === "function") {
|
|
156
|
+
maybe.unref();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const RESTART_SUPERVISOR_SCRIPT = `
|
|
161
|
+
const cp = require("node:child_process");
|
|
162
|
+
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function alive(pid) {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(pid, 0);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const parentPid = Number(process.env.BOTCORD_RESTART_PARENT_PID || "0");
|
|
178
|
+
const waitMs = Number(process.env.BOTCORD_RESTART_PARENT_EXIT_WAIT_MS || "30000");
|
|
179
|
+
const deadline = Date.now() + waitMs;
|
|
180
|
+
while (parentPid > 0 && alive(parentPid) && Date.now() < deadline) {
|
|
181
|
+
await sleep(250);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const update = process.env.BOTCORD_RESTART_UPDATE === "1";
|
|
185
|
+
const installPrefix = process.env.BOTCORD_RESTART_INSTALL_PREFIX || "";
|
|
186
|
+
if (update && installPrefix) {
|
|
187
|
+
const npmBin = process.env.BOTCORD_RESTART_NPM_BIN || "npm";
|
|
188
|
+
const packageSpec = process.env.BOTCORD_RESTART_PACKAGE || "${DEFAULT_DAEMON_PACKAGE}";
|
|
189
|
+
const timeout = Number(process.env.BOTCORD_RESTART_INSTALL_TIMEOUT_MS || "120000");
|
|
190
|
+
cp.spawnSync(npmBin, ["install", "--prefix", installPrefix, packageSpec], {
|
|
191
|
+
stdio: "ignore",
|
|
192
|
+
env: process.env,
|
|
193
|
+
timeout,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const node = process.env.BOTCORD_RESTART_NODE || process.execPath;
|
|
198
|
+
const entrypoint = process.env.BOTCORD_RESTART_ENTRYPOINT;
|
|
199
|
+
if (!entrypoint) process.exit(1);
|
|
200
|
+
let args = ["start", "--foreground"];
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(process.env.BOTCORD_RESTART_ARGS_JSON || "[]");
|
|
203
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
204
|
+
args = parsed;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// keep default args
|
|
208
|
+
}
|
|
209
|
+
const child = cp.spawn(node, [entrypoint, ...args], {
|
|
210
|
+
detached: true,
|
|
211
|
+
stdio: "ignore",
|
|
212
|
+
env: { ...process.env, BOTCORD_DAEMON_CHILD: "1" },
|
|
213
|
+
});
|
|
214
|
+
child.unref();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch(() => process.exit(1));
|
|
218
|
+
`;
|
package/src/skill-index.ts
CHANGED
|
@@ -74,6 +74,18 @@ export function defaultSkillDirs(
|
|
|
74
74
|
source: "agent-codex",
|
|
75
75
|
runtime: "codex",
|
|
76
76
|
};
|
|
77
|
+
const agentGemini = [
|
|
78
|
+
{
|
|
79
|
+
dir: path.join(agentWorkspaceDir(agentId), ".gemini", "skills"),
|
|
80
|
+
source: "agent-gemini",
|
|
81
|
+
runtime: "gemini",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
dir: path.join(agentWorkspaceDir(agentId), ".agents", "skills"),
|
|
85
|
+
source: "agent-agents",
|
|
86
|
+
runtime: "gemini",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
77
89
|
const agentHermes = hermesSkillRoot(agentId, opts.hermesProfile);
|
|
78
90
|
|
|
79
91
|
const dirs: SkillRoot[] = [];
|
|
@@ -91,6 +103,23 @@ export function defaultSkillDirs(
|
|
|
91
103
|
case "hermes":
|
|
92
104
|
dirs.push(agentHermes);
|
|
93
105
|
break;
|
|
106
|
+
case "gemini":
|
|
107
|
+
dirs.push(...agentGemini);
|
|
108
|
+
if (includeGlobal) {
|
|
109
|
+
dirs.push(
|
|
110
|
+
{
|
|
111
|
+
dir: path.join(homedir(), ".gemini", "skills"),
|
|
112
|
+
source: "global-gemini",
|
|
113
|
+
runtime: "gemini",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
dir: path.join(homedir(), ".agents", "skills"),
|
|
117
|
+
source: "global-agents",
|
|
118
|
+
runtime: "gemini",
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
94
123
|
case "claude":
|
|
95
124
|
dirs.push(agentClaude);
|
|
96
125
|
if (includeGlobal) {
|
|
@@ -308,8 +337,9 @@ function hermesSkillRoot(agentId: string, profile: string | undefined): SkillRoo
|
|
|
308
337
|
};
|
|
309
338
|
}
|
|
310
339
|
|
|
311
|
-
function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "hermes" | "other" {
|
|
340
|
+
function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "gemini" | "hermes" | "other" {
|
|
312
341
|
if (runtime === "codex") return "codex";
|
|
342
|
+
if (runtime === "gemini") return "gemini";
|
|
313
343
|
if (runtime === "hermes-agent") return "hermes";
|
|
314
344
|
if (!runtime) return "claude";
|
|
315
345
|
if (runtime === "claude-code") return "claude";
|
|
@@ -317,18 +347,9 @@ function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "herme
|
|
|
317
347
|
}
|
|
318
348
|
|
|
319
349
|
function priority(source: string, _runtime: string | undefined): number {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
case "agent-hermes":
|
|
324
|
-
case "agent-hermes-profile":
|
|
325
|
-
return 0;
|
|
326
|
-
case "global-claude":
|
|
327
|
-
case "global-codex":
|
|
328
|
-
return 1;
|
|
329
|
-
default:
|
|
330
|
-
return 2;
|
|
331
|
-
}
|
|
350
|
+
if (source.startsWith("agent-")) return 0;
|
|
351
|
+
if (source.startsWith("global-")) return 1;
|
|
352
|
+
return 2;
|
|
332
353
|
}
|
|
333
354
|
|
|
334
355
|
function snapshotSource(source: string): "workspace" | "runtime-global" {
|