@botcord/daemon 0.2.84 → 0.2.86
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/LICENSE +21 -0
- package/dist/cloud-daemon.js +18 -2
- package/dist/daemon-singleton.d.ts +12 -0
- package/dist/daemon-singleton.js +83 -7
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +24 -0
- package/dist/gateway/channels/botcord.js +72 -29
- package/dist/gateway/runtimes/deepseek-tui.js +5 -1
- package/dist/index.js +27 -7
- package/dist/provision.js +30 -3
- package/dist/runtime-models.js +48 -6
- package/dist/skill-index.d.ts +12 -0
- package/dist/skill-index.js +15 -4
- package/package.json +10 -11
- package/src/__tests__/cloud-daemon.test.ts +79 -0
- package/src/__tests__/daemon-singleton.test.ts +59 -1
- package/src/__tests__/provision.test.ts +42 -0
- package/src/__tests__/runtime-discovery.test.ts +17 -1
- package/src/__tests__/runtime-models.test.ts +50 -0
- package/src/__tests__/skill-index.test.ts +91 -0
- package/src/cloud-daemon.ts +18 -2
- package/src/daemon-singleton.ts +98 -6
- package/src/daemon.ts +28 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +79 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +49 -0
- package/src/gateway/channels/botcord.ts +81 -33
- package/src/gateway/runtimes/deepseek-tui.ts +7 -1
- package/src/index.ts +25 -6
- package/src/provision.ts +34 -1
- package/src/runtime-models.ts +46 -6
- package/src/skill-index.ts +32 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BotLearn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cloud-daemon.js
CHANGED
|
@@ -17,7 +17,7 @@ import { ControlChannel } from "./control-channel.js";
|
|
|
17
17
|
import { toGatewayConfig } from "./daemon-config-map.js";
|
|
18
18
|
import { log as daemonLog } from "./log.js";
|
|
19
19
|
import { createProvisioner } from "./provision.js";
|
|
20
|
-
import { createDaemonChannel, pushRuntimeSnapshot } from "./daemon.js";
|
|
20
|
+
import { createDaemonChannel, pushAgentSkillSnapshot, pushRuntimeSnapshot } from "./daemon.js";
|
|
21
21
|
import { SnapshotWriter } from "./snapshot-writer.js";
|
|
22
22
|
import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
23
23
|
import { readWorkingMemorySnapshot } from "./working-memory.js";
|
|
@@ -157,7 +157,20 @@ export async function startCloudDaemon(opts) {
|
|
|
157
157
|
text: msg.text,
|
|
158
158
|
});
|
|
159
159
|
};
|
|
160
|
+
const installedAgentIds = new Set();
|
|
161
|
+
let controlChannel = null;
|
|
162
|
+
const pushInstalledAgentSkillSnapshot = (agentId, reason) => {
|
|
163
|
+
if (!controlChannel)
|
|
164
|
+
return;
|
|
165
|
+
const pushed = pushAgentSkillSnapshot(controlChannel, agentId);
|
|
166
|
+
logger.info("cloud control-channel: agent_skill_snapshot pushed", {
|
|
167
|
+
agentId,
|
|
168
|
+
reason,
|
|
169
|
+
ok: pushed,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
160
172
|
const onAgentInstalled = (info) => {
|
|
173
|
+
installedAgentIds.add(info.agentId);
|
|
161
174
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
162
175
|
if (info.hubUrl)
|
|
163
176
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -173,6 +186,7 @@ export async function startCloudDaemon(opts) {
|
|
|
173
186
|
loopRiskBuilder: () => null,
|
|
174
187
|
}));
|
|
175
188
|
}
|
|
189
|
+
pushInstalledAgentSkillSnapshot(info.agentId, "agent_installed");
|
|
176
190
|
};
|
|
177
191
|
const gateway = new Gateway({
|
|
178
192
|
config: gwConfig,
|
|
@@ -197,7 +211,6 @@ export async function startCloudDaemon(opts) {
|
|
|
197
211
|
});
|
|
198
212
|
await gateway.start();
|
|
199
213
|
logger.info("cloud daemon gateway started (zero agents at boot)");
|
|
200
|
-
let controlChannel = null;
|
|
201
214
|
if (!opts.disableControlChannel) {
|
|
202
215
|
const auth = asUserAuthManager(new CloudAuthManager(cloudCfg));
|
|
203
216
|
const provisionerFactory = opts.provisionerFactory ?? createProvisioner;
|
|
@@ -247,6 +260,9 @@ export async function startCloudDaemon(opts) {
|
|
|
247
260
|
logger.info("cloud control-channel started; runtime_snapshot pushed", {
|
|
248
261
|
ok: pushed,
|
|
249
262
|
});
|
|
263
|
+
for (const agentId of installedAgentIds) {
|
|
264
|
+
pushInstalledAgentSkillSnapshot(agentId, "control_channel_started");
|
|
265
|
+
}
|
|
250
266
|
}
|
|
251
267
|
catch (err) {
|
|
252
268
|
logger.warn("cloud control-channel start failed; daemon will retry", {
|
|
@@ -2,6 +2,11 @@ export interface SingletonLogger {
|
|
|
2
2
|
info(message: string, meta?: Record<string, unknown>): void;
|
|
3
3
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
4
4
|
}
|
|
5
|
+
export interface DaemonSingletonLock {
|
|
6
|
+
lockPath: string;
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function defaultLockPath(pidPath?: string): string;
|
|
5
10
|
export declare function readPid(pidPath?: string): number | null;
|
|
6
11
|
export declare function pidAlive(pid: number): boolean;
|
|
7
12
|
export interface DaemonProcessInfo {
|
|
@@ -23,6 +28,13 @@ export declare function stopDaemonFromPidFileForRestart(opts?: {
|
|
|
23
28
|
currentPid?: number;
|
|
24
29
|
logger?: SingletonLogger;
|
|
25
30
|
}): Promise<void>;
|
|
31
|
+
export declare function acquireDaemonSingletonLock(opts?: {
|
|
32
|
+
lockPath?: string;
|
|
33
|
+
pidPath?: string;
|
|
34
|
+
currentPid?: number;
|
|
35
|
+
logger?: SingletonLogger;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<DaemonSingletonLock>;
|
|
26
38
|
export declare function stopOtherDaemonProcessesForRestart(opts?: {
|
|
27
39
|
currentPid?: number;
|
|
28
40
|
logger?: SingletonLogger;
|
package/dist/daemon-singleton.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PID_PATH } from "./config.js";
|
|
5
5
|
const noopLogger = {
|
|
@@ -10,6 +10,11 @@ const noopLogger = {
|
|
|
10
10
|
// noop
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
|
+
const DEFAULT_LOCK_WAIT_MS = 15_000;
|
|
14
|
+
const DEFAULT_LOCK_RETRY_MS = 50;
|
|
15
|
+
export function defaultLockPath(pidPath = PID_PATH) {
|
|
16
|
+
return `${pidPath}.lock`;
|
|
17
|
+
}
|
|
13
18
|
export function readPid(pidPath = PID_PATH) {
|
|
14
19
|
if (!existsSync(pidPath))
|
|
15
20
|
return null;
|
|
@@ -17,6 +22,9 @@ export function readPid(pidPath = PID_PATH) {
|
|
|
17
22
|
const pid = Number(raw);
|
|
18
23
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
19
24
|
}
|
|
25
|
+
function readLockOwner(lockPath) {
|
|
26
|
+
return readPid(path.join(lockPath, "owner.pid"));
|
|
27
|
+
}
|
|
20
28
|
export function pidAlive(pid) {
|
|
21
29
|
try {
|
|
22
30
|
process.kill(pid, 0);
|
|
@@ -98,6 +106,71 @@ export async function stopDaemonFromPidFileForRestart(opts = {}) {
|
|
|
98
106
|
await stopExistingDaemonForRestart(existing, opts);
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
export async function acquireDaemonSingletonLock(opts = {}) {
|
|
110
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
111
|
+
const lockPath = opts.lockPath ?? defaultLockPath(pidPath);
|
|
112
|
+
const currentPid = opts.currentPid ?? process.pid;
|
|
113
|
+
const logger = opts.logger ?? noopLogger;
|
|
114
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_LOCK_WAIT_MS;
|
|
115
|
+
const deadline = Date.now() + timeoutMs;
|
|
116
|
+
ensureParentDir(lockPath);
|
|
117
|
+
while (true) {
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
120
|
+
writeFileSync(path.join(lockPath, "owner.pid"), String(currentPid), { mode: 0o600 });
|
|
121
|
+
return {
|
|
122
|
+
lockPath,
|
|
123
|
+
release() {
|
|
124
|
+
const owner = readLockOwner(lockPath);
|
|
125
|
+
if (owner !== null && owner !== currentPid)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const code = err.code;
|
|
138
|
+
if (code !== "EEXIST")
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
const owner = readLockOwner(lockPath);
|
|
142
|
+
if (owner === currentPid) {
|
|
143
|
+
return {
|
|
144
|
+
lockPath,
|
|
145
|
+
release() {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (owner !== null && pidAlive(owner)) {
|
|
156
|
+
logger.info("daemon singleton lock owner found; restarting", { pid: owner });
|
|
157
|
+
await stopExistingDaemonForRestart(owner, { pidPath, currentPid, logger });
|
|
158
|
+
}
|
|
159
|
+
const refreshedOwner = readLockOwner(lockPath);
|
|
160
|
+
if (refreshedOwner === null || !pidAlive(refreshedOwner)) {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// another starter may have removed/recreated it
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (Date.now() >= deadline) {
|
|
169
|
+
throw new Error(`timed out acquiring daemon singleton lock at ${lockPath}`);
|
|
170
|
+
}
|
|
171
|
+
await delay(DEFAULT_LOCK_RETRY_MS);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
101
174
|
export async function stopOtherDaemonProcessesForRestart(opts = {}) {
|
|
102
175
|
const currentPid = opts.currentPid ?? process.pid;
|
|
103
176
|
const logger = opts.logger ?? noopLogger;
|
|
@@ -142,12 +215,7 @@ export function writeCurrentPid(opts = {}) {
|
|
|
142
215
|
// Cloud-mode startup writes the PID file before `saveConfig` runs, so
|
|
143
216
|
// the daemon dir may not exist yet. mkdir its parent (0700) so the
|
|
144
217
|
// first write doesn't crash with ENOENT.
|
|
145
|
-
|
|
146
|
-
mkdirSync(path.dirname(pidPath), { recursive: true, mode: 0o700 });
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// best-effort — writeFileSync below will surface the real error
|
|
150
|
-
}
|
|
218
|
+
ensureParentDir(pidPath);
|
|
151
219
|
writeFileSync(pidPath, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
152
220
|
}
|
|
153
221
|
export function removePidFile(pidPath = PID_PATH) {
|
|
@@ -186,3 +254,11 @@ export function isBotCordDaemonStartCommand(command) {
|
|
|
186
254
|
function delay(ms) {
|
|
187
255
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
256
|
}
|
|
257
|
+
function ensureParentDir(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// best-effort — the next filesystem operation will surface real errors
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ export interface RuntimeSnapshotSink {
|
|
|
65
65
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
66
66
|
*/
|
|
67
67
|
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink, liveSnapshot?: GatewayRuntimeSnapshot): boolean;
|
|
68
|
+
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string): boolean;
|
|
68
69
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
69
70
|
export interface DaemonRuntimeOptions {
|
|
70
71
|
config: DaemonConfig;
|
package/dist/daemon.js
CHANGED
|
@@ -22,6 +22,7 @@ import { PolicyResolver } from "./gateway/policy-resolver.js";
|
|
|
22
22
|
import { scanMention } from "./mention-scan.js";
|
|
23
23
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
24
24
|
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
25
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
25
26
|
/**
|
|
26
27
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
27
28
|
* tasks routinely exceed 10 minutes, so daemon-hosted agents get a larger
|
|
@@ -164,6 +165,22 @@ export function pushRuntimeSnapshot(sink, liveSnapshot) {
|
|
|
164
165
|
}
|
|
165
166
|
return ok;
|
|
166
167
|
}
|
|
168
|
+
export function pushAgentSkillSnapshot(sink, agentId) {
|
|
169
|
+
const snap = collectAgentSkillSnapshot(agentId);
|
|
170
|
+
const ok = sink.send({
|
|
171
|
+
id: `skill_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
172
|
+
type: "agent_skill_snapshot",
|
|
173
|
+
params: snap,
|
|
174
|
+
ts: Date.now(),
|
|
175
|
+
});
|
|
176
|
+
if (!ok) {
|
|
177
|
+
daemonLog.warn("agent-skill-snapshot: control-channel send returned false", {
|
|
178
|
+
agentId,
|
|
179
|
+
skills: snap.skills.length,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return ok;
|
|
183
|
+
}
|
|
167
184
|
/**
|
|
168
185
|
* Adapt daemon's file-based `log` module into the gateway logger contract.
|
|
169
186
|
* Writes go to `~/.botcord/logs/daemon.log` + stderr, preserving the format
|
|
@@ -483,6 +500,13 @@ export async function startDaemon(opts) {
|
|
|
483
500
|
logger.info("control-channel: initial runtime_snapshot push", {
|
|
484
501
|
ok: pushed,
|
|
485
502
|
});
|
|
503
|
+
for (const agentId of agentIds) {
|
|
504
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId);
|
|
505
|
+
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
506
|
+
agentId,
|
|
507
|
+
ok: skillsPushed,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
486
510
|
}
|
|
487
511
|
catch (err) {
|
|
488
512
|
logger.warn("control-channel failed to start; continuing without it", {
|
|
@@ -1044,24 +1044,39 @@ function extractDeepseekToolCall(raw) {
|
|
|
1044
1044
|
const payload = raw?.payload;
|
|
1045
1045
|
if (!payload || typeof payload !== "object")
|
|
1046
1046
|
return null;
|
|
1047
|
-
|
|
1048
|
-
|
|
1047
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1048
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1049
|
+
if (event === "tool.started") {
|
|
1050
|
+
const tool = innerPayload?.tool && typeof innerPayload.tool === "object" ? innerPayload.tool : undefined;
|
|
1049
1051
|
return {
|
|
1050
|
-
name: stringField(
|
|
1051
|
-
params: parseMaybeJson(
|
|
1052
|
-
|
|
1053
|
-
|
|
1052
|
+
name: stringField(innerPayload, "name") ?? stringField(tool, "name") ?? "tool",
|
|
1053
|
+
params: parseMaybeJson(innerPayload?.input ??
|
|
1054
|
+
innerPayload?.arguments ??
|
|
1055
|
+
innerPayload?.params ??
|
|
1056
|
+
tool?.input ??
|
|
1057
|
+
tool?.rawInput ??
|
|
1058
|
+
tool?.arguments ??
|
|
1059
|
+
tool?.params),
|
|
1060
|
+
id: stringField(innerPayload, "id") ?? stringField(tool, "id"),
|
|
1061
|
+
status: stringField(innerPayload, "status") ?? stringField(tool, "status"),
|
|
1054
1062
|
};
|
|
1055
1063
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
const inner =
|
|
1058
|
-
? payload
|
|
1059
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1060
|
-
? payload.payload
|
|
1061
|
-
: {};
|
|
1064
|
+
if (event === "item.started") {
|
|
1065
|
+
const inner = innerPayload ?? {};
|
|
1062
1066
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1063
1067
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1064
|
-
const
|
|
1068
|
+
const metadata = item?.metadata && typeof item.metadata === "object" ? item.metadata : undefined;
|
|
1069
|
+
const metadataCommand = metadata && (metadata.command ?? metadata.cmd)
|
|
1070
|
+
? { [metadata.command ? "command" : "cmd"]: metadata.command ?? metadata.cmd }
|
|
1071
|
+
: undefined;
|
|
1072
|
+
const itemParams = parseMaybeJson(item?.input ??
|
|
1073
|
+
item?.arguments ??
|
|
1074
|
+
item?.params ??
|
|
1075
|
+
metadata?.input ??
|
|
1076
|
+
metadata?.arguments ??
|
|
1077
|
+
metadata?.params ??
|
|
1078
|
+
metadataCommand ??
|
|
1079
|
+
item?.detail);
|
|
1065
1080
|
const detailParams = itemParams !== undefined
|
|
1066
1081
|
? itemParams
|
|
1067
1082
|
: typeof item?.detail === "string" && item.detail.trim()
|
|
@@ -1082,8 +1097,16 @@ function extractDeepseekToolCall(raw) {
|
|
|
1082
1097
|
inner.arguments ??
|
|
1083
1098
|
inner.params ??
|
|
1084
1099
|
item?.input ??
|
|
1085
|
-
item?.arguments
|
|
1086
|
-
|
|
1100
|
+
item?.arguments ??
|
|
1101
|
+
item?.params ??
|
|
1102
|
+
metadata?.input ??
|
|
1103
|
+
metadata?.arguments ??
|
|
1104
|
+
metadata?.params ??
|
|
1105
|
+
metadataCommand) ?? detailParams ?? tool ?? item,
|
|
1106
|
+
id: stringField(tool, "id") ??
|
|
1107
|
+
stringField(inner, "id") ??
|
|
1108
|
+
stringField(item, "id") ??
|
|
1109
|
+
stringField(payload, "item_id"),
|
|
1087
1110
|
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1088
1111
|
};
|
|
1089
1112
|
}
|
|
@@ -1093,23 +1116,23 @@ function extractDeepseekToolResult(raw) {
|
|
|
1093
1116
|
const payload = raw?.payload;
|
|
1094
1117
|
if (!payload || typeof payload !== "object")
|
|
1095
1118
|
return null;
|
|
1096
|
-
|
|
1097
|
-
|
|
1119
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1120
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1121
|
+
if (event === "tool.completed") {
|
|
1122
|
+
const result = innerPayload?.output ??
|
|
1123
|
+
innerPayload?.result ??
|
|
1124
|
+
innerPayload?.content ??
|
|
1125
|
+
innerPayload?.error ??
|
|
1126
|
+
innerPayload ??
|
|
1127
|
+
payload;
|
|
1098
1128
|
return {
|
|
1099
|
-
name: stringField(
|
|
1129
|
+
name: stringField(innerPayload, "name"),
|
|
1100
1130
|
result: stringifyToolResult(result),
|
|
1101
|
-
id: stringField(
|
|
1131
|
+
id: stringField(innerPayload, "id"),
|
|
1102
1132
|
};
|
|
1103
1133
|
}
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
payload.event === "item.completed" ||
|
|
1107
|
-
payload.event === "item.failed") {
|
|
1108
|
-
const inner = raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1109
|
-
? payload
|
|
1110
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1111
|
-
? payload.payload
|
|
1112
|
-
: {};
|
|
1134
|
+
if (event === "item.completed" || event === "item.failed") {
|
|
1135
|
+
const inner = innerPayload ?? {};
|
|
1113
1136
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1114
1137
|
const result = item?.output ??
|
|
1115
1138
|
item?.result ??
|
|
@@ -1128,11 +1151,31 @@ function extractDeepseekToolResult(raw) {
|
|
|
1128
1151
|
stringField(inner, "name") ??
|
|
1129
1152
|
stringField(item, "type"),
|
|
1130
1153
|
result: stringifyToolResult(result),
|
|
1131
|
-
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1154
|
+
id: stringField(item, "id") ?? stringField(inner, "id") ?? stringField(payload, "item_id"),
|
|
1132
1155
|
};
|
|
1133
1156
|
}
|
|
1134
1157
|
return null;
|
|
1135
1158
|
}
|
|
1159
|
+
function unwrapDeepseekPayload(raw) {
|
|
1160
|
+
const payload = raw?.payload;
|
|
1161
|
+
if (!payload || typeof payload !== "object")
|
|
1162
|
+
return undefined;
|
|
1163
|
+
const nested = payload.payload;
|
|
1164
|
+
if (nested && typeof nested === "object") {
|
|
1165
|
+
const outerEvent = stringField(payload, "event");
|
|
1166
|
+
if (outerEvent ||
|
|
1167
|
+
nested.item ||
|
|
1168
|
+
nested.tool ||
|
|
1169
|
+
nested.turn ||
|
|
1170
|
+
nested.kind ||
|
|
1171
|
+
nested.output ||
|
|
1172
|
+
nested.result ||
|
|
1173
|
+
nested.error) {
|
|
1174
|
+
return nested;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return payload;
|
|
1178
|
+
}
|
|
1136
1179
|
function formatBlockDetails(raw) {
|
|
1137
1180
|
if (!raw || typeof raw !== "object")
|
|
1138
1181
|
return "";
|
|
@@ -426,8 +426,12 @@ function isDeepseekTerminalEvent(eventName, payload) {
|
|
|
426
426
|
embedded === "done");
|
|
427
427
|
}
|
|
428
428
|
function isToolStarted(eventName, payload) {
|
|
429
|
+
const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
|
|
429
430
|
return ((eventName === "item.started" &&
|
|
430
|
-
(!!payload?.tool ||
|
|
431
|
+
(!!payload?.tool ||
|
|
432
|
+
itemKind === "tool_call" ||
|
|
433
|
+
itemKind === "command_execution" ||
|
|
434
|
+
itemKind === "file_change")) ||
|
|
431
435
|
(payload?.event === "item.started" && !!payload?.payload?.tool));
|
|
432
436
|
}
|
|
433
437
|
function isToolCompleted(eventName, payload) {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir, hostname } from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { augmentProcessPath } from "./path-env.js";
|
|
7
7
|
import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, SNAPSHOT_PATH, CONFIG_FILE_PATH, CONFIG_MISSING, } from "./config.js";
|
|
8
|
-
import { ensureNoOtherDaemonFromPidFile, findOtherDaemonProcesses, pidAlive, readPid, removePidFile, stopDaemonFromPidFileForRestart, stopOtherDaemonProcessesForRestart, writeCurrentPid, } from "./daemon-singleton.js";
|
|
8
|
+
import { acquireDaemonSingletonLock, ensureNoOtherDaemonFromPidFile, findOtherDaemonProcesses, pidAlive, readPid, removePidFile, stopDaemonFromPidFileForRestart, stopOtherDaemonProcessesForRestart, writeCurrentPid, } from "./daemon-singleton.js";
|
|
9
9
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
10
10
|
import { defaultTranscriptRoot, resolveTranscriptEnabled, transcriptAgentRoot, transcriptFilePath, } from "./gateway/index.js";
|
|
11
11
|
import { startDaemon } from "./daemon.js";
|
|
@@ -506,12 +506,22 @@ async function cmdStart(args) {
|
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
508
|
// Foreground: we ARE the daemon.
|
|
509
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
509
510
|
writeCurrentPid();
|
|
510
|
-
|
|
511
|
+
let handle;
|
|
512
|
+
try {
|
|
513
|
+
handle = await startDaemon({ config: cfg, configPath: CONFIG_FILE_PATH });
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
removePidFile();
|
|
517
|
+
singletonLock.release();
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
511
520
|
const shutdown = async (sig) => {
|
|
512
521
|
log.info("signal received", { sig });
|
|
513
522
|
await handle.stop(sig);
|
|
514
523
|
removePidFile();
|
|
524
|
+
singletonLock.release();
|
|
515
525
|
process.exit(0);
|
|
516
526
|
};
|
|
517
527
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -539,6 +549,7 @@ async function cmdStartCloud(_args) {
|
|
|
539
549
|
daemonInstanceId: cloudConfig.daemonInstanceId,
|
|
540
550
|
hubUrl: cloudConfig.hubUrl,
|
|
541
551
|
});
|
|
552
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
542
553
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
543
554
|
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
544
555
|
writeCurrentPid();
|
|
@@ -552,15 +563,24 @@ async function cmdStartCloud(_args) {
|
|
|
552
563
|
};
|
|
553
564
|
saveConfig(cfg);
|
|
554
565
|
log.info("cloud mode config initialized", { configPath: CONFIG_FILE_PATH });
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
566
|
+
let handle;
|
|
567
|
+
try {
|
|
568
|
+
handle = await startCloudDaemon({
|
|
569
|
+
cloudConfig,
|
|
570
|
+
config: cfg,
|
|
571
|
+
configPath: CONFIG_FILE_PATH,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
removePidFile();
|
|
576
|
+
singletonLock.release();
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
560
579
|
const shutdown = async (sig) => {
|
|
561
580
|
log.info("signal received", { sig });
|
|
562
581
|
await handle.stop(sig);
|
|
563
582
|
removePidFile();
|
|
583
|
+
singletonLock.release();
|
|
564
584
|
process.exit(0);
|
|
565
585
|
};
|
|
566
586
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
package/dist/provision.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
10
|
+
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, normalizeTokenExpiresAt, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
11
11
|
import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js";
|
|
12
12
|
import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
|
|
13
13
|
import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscoveryConfigEnabled, } from "./openclaw-discovery.js";
|
|
@@ -19,6 +19,7 @@ import { log as daemonLog } from "./log.js";
|
|
|
19
19
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
20
20
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
21
21
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
22
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
22
23
|
import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
|
|
23
24
|
import { handleCloudGatewayRuntimeInbound, } from "./cloud-gateway-runtime.js";
|
|
24
25
|
/**
|
|
@@ -315,6 +316,31 @@ export function createProvisioner(opts) {
|
|
|
315
316
|
});
|
|
316
317
|
return { ok: true, result };
|
|
317
318
|
}
|
|
319
|
+
case "list_agent_skills": {
|
|
320
|
+
const params = (frame.params ?? {});
|
|
321
|
+
if (!params.agentId) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: { code: "bad_params", message: "list_agent_skills requires params.agentId" },
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const channels = gateway.snapshot().channels;
|
|
328
|
+
if (!channels[params.agentId]) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: {
|
|
332
|
+
code: "agent_not_loaded",
|
|
333
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const result = collectAgentSkillSnapshot(params.agentId);
|
|
338
|
+
daemonLog.debug("list_agent_skills", {
|
|
339
|
+
agentId: params.agentId,
|
|
340
|
+
count: result.skills.length,
|
|
341
|
+
});
|
|
342
|
+
return { ok: true, result };
|
|
343
|
+
}
|
|
318
344
|
case "wake_agent": {
|
|
319
345
|
return handleWakeAgent(gateway, frame.params);
|
|
320
346
|
}
|
|
@@ -921,8 +947,9 @@ async function materializeCredentials(params, cfg, ctx, explicitCwd) {
|
|
|
921
947
|
record.displayName = c.displayName;
|
|
922
948
|
if (c.token)
|
|
923
949
|
record.token = c.token;
|
|
924
|
-
|
|
925
|
-
|
|
950
|
+
const tokenExpiresAt = normalizeTokenExpiresAt(c.tokenExpiresAt);
|
|
951
|
+
if (tokenExpiresAt !== undefined)
|
|
952
|
+
record.tokenExpiresAt = tokenExpiresAt;
|
|
926
953
|
if (runtime)
|
|
927
954
|
record.runtime = runtime;
|
|
928
955
|
const runtimeSelection = pickRuntimeSelection(params);
|
package/dist/runtime-models.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
const MODEL_LIST_TIMEOUT_MS = 5000;
|
|
6
6
|
const MODEL_LIST_MAX_BUFFER = 16 * 1024 * 1024;
|
|
7
|
-
const RUNTIME_CATALOG_CACHE_VERSION =
|
|
7
|
+
const RUNTIME_CATALOG_CACHE_VERSION = 2;
|
|
8
8
|
const RUNTIME_CATALOG_CACHE_FRESH_MS = 10 * 60 * 1000;
|
|
9
9
|
const DEFAULT_RUNTIME_CATALOG_CACHE_DIR = path.join(homedir(), ".botcord", "daemon", "runtime-catalog-cache");
|
|
10
10
|
const CLAUDE_ALIAS_MODELS = [
|
|
@@ -102,7 +102,7 @@ function runtimeCatalogStrategy(entry) {
|
|
|
102
102
|
discoverFresh: () => discoverDeepseekCatalog(entry.result.path),
|
|
103
103
|
fallback: () => ({
|
|
104
104
|
models: DEEPSEEK_FALLBACK_MODELS.slice(),
|
|
105
|
-
parameters: discoverDeepseekParameters(),
|
|
105
|
+
parameters: discoverDeepseekParameters(entry.result.path),
|
|
106
106
|
}),
|
|
107
107
|
};
|
|
108
108
|
case "kimi-cli":
|
|
@@ -392,7 +392,7 @@ function discoverCodexParameters(rawCatalog) {
|
|
|
392
392
|
function discoverDeepseekCatalog(command) {
|
|
393
393
|
return {
|
|
394
394
|
models: discoverDeepseekModels(command),
|
|
395
|
-
parameters: discoverDeepseekParameters(),
|
|
395
|
+
parameters: discoverDeepseekParameters(command),
|
|
396
396
|
};
|
|
397
397
|
}
|
|
398
398
|
export function discoverDeepseekModels(command) {
|
|
@@ -416,8 +416,9 @@ export function parseDeepseekModelList(raw) {
|
|
|
416
416
|
}
|
|
417
417
|
return out.length ? out : undefined;
|
|
418
418
|
}
|
|
419
|
-
function discoverDeepseekParameters() {
|
|
419
|
+
function discoverDeepseekParameters(command) {
|
|
420
420
|
const config = readConfigScalars(path.join(homedir(), ".deepseek", "config.toml"));
|
|
421
|
+
const reasoningEffortValues = discoverDeepseekReasoningEffortValues(command);
|
|
421
422
|
return [
|
|
422
423
|
compactParameter({
|
|
423
424
|
id: "model",
|
|
@@ -439,8 +440,9 @@ function discoverDeepseekParameters() {
|
|
|
439
440
|
compactParameter({
|
|
440
441
|
id: "reasoning_effort",
|
|
441
442
|
displayName: "Reasoning effort",
|
|
442
|
-
type: "string",
|
|
443
|
-
flag: "
|
|
443
|
+
type: reasoningEffortValues.length > 0 ? "enum" : "string",
|
|
444
|
+
flag: "--reasoning-effort",
|
|
445
|
+
values: reasoningEffortValues.length > 0 ? reasoningEffortValues : undefined,
|
|
444
446
|
defaultValue: config.reasoning_effort,
|
|
445
447
|
source: config.reasoning_effort ? "config" : "cli",
|
|
446
448
|
}),
|
|
@@ -462,6 +464,46 @@ function discoverDeepseekParameters() {
|
|
|
462
464
|
}),
|
|
463
465
|
];
|
|
464
466
|
}
|
|
467
|
+
function discoverDeepseekReasoningEffortValues(command) {
|
|
468
|
+
const candidates = deepseekRuntimeTemplateCandidates(command);
|
|
469
|
+
const values = new Set();
|
|
470
|
+
for (const candidate of candidates) {
|
|
471
|
+
try {
|
|
472
|
+
const raw = readFileSync(candidate)
|
|
473
|
+
.toString("latin1")
|
|
474
|
+
.replace(/[^\x20-\x7E]+/g, "\n");
|
|
475
|
+
const templateRe = /Thinking mode \(DeepSeek V4 reasoning effort\):[\s\S]{0,256}?#\s*((?:"[^"]+"\s*(?:\|\s*)?)+)/g;
|
|
476
|
+
for (const match of raw.matchAll(templateRe)) {
|
|
477
|
+
const line = match[1] ?? "";
|
|
478
|
+
for (const valueMatch of line.matchAll(/"([^"]+)"/g)) {
|
|
479
|
+
const value = valueMatch[1]?.trim();
|
|
480
|
+
if (value && /^[A-Za-z0-9_.-]+$/.test(value))
|
|
481
|
+
values.add(value);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// Try the next candidate; runtime discovery should stay best-effort.
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return Array.from(values);
|
|
490
|
+
}
|
|
491
|
+
function deepseekRuntimeTemplateCandidates(command) {
|
|
492
|
+
if (!command)
|
|
493
|
+
return [];
|
|
494
|
+
const candidates = new Set();
|
|
495
|
+
if (existsSync(command))
|
|
496
|
+
candidates.add(command);
|
|
497
|
+
const dir = path.dirname(command);
|
|
498
|
+
for (const candidate of [
|
|
499
|
+
path.join(dir, "deepseek-tui"),
|
|
500
|
+
path.join(dir, "downloads", "deepseek-tui"),
|
|
501
|
+
]) {
|
|
502
|
+
if (existsSync(candidate))
|
|
503
|
+
candidates.add(candidate);
|
|
504
|
+
}
|
|
505
|
+
return Array.from(candidates);
|
|
506
|
+
}
|
|
465
507
|
function discoverKimiCatalog() {
|
|
466
508
|
const configPath = path.join(homedir(), ".kimi", "config.toml");
|
|
467
509
|
if (!existsSync(configPath))
|