@botcord/daemon 0.2.35 → 0.2.37

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.
Files changed (68) hide show
  1. package/dist/config.d.ts +30 -1
  2. package/dist/config.js +27 -0
  3. package/dist/daemon-config-map.d.ts +3 -0
  4. package/dist/daemon-config-map.js +30 -0
  5. package/dist/daemon.d.ts +15 -1
  6. package/dist/daemon.js +56 -11
  7. package/dist/gateway/channels/botcord.js +44 -0
  8. package/dist/gateway/channels/http-types.d.ts +19 -0
  9. package/dist/gateway/channels/http-types.js +1 -0
  10. package/dist/gateway/channels/index.d.ts +5 -0
  11. package/dist/gateway/channels/index.js +5 -0
  12. package/dist/gateway/channels/login-session.d.ts +83 -0
  13. package/dist/gateway/channels/login-session.js +99 -0
  14. package/dist/gateway/channels/secret-store.d.ts +21 -0
  15. package/dist/gateway/channels/secret-store.js +75 -0
  16. package/dist/gateway/channels/state-store.d.ts +60 -0
  17. package/dist/gateway/channels/state-store.js +173 -0
  18. package/dist/gateway/channels/telegram.d.ts +31 -0
  19. package/dist/gateway/channels/telegram.js +371 -0
  20. package/dist/gateway/channels/text-split.d.ts +13 -0
  21. package/dist/gateway/channels/text-split.js +33 -0
  22. package/dist/gateway/channels/url-guard.d.ts +18 -0
  23. package/dist/gateway/channels/url-guard.js +53 -0
  24. package/dist/gateway/channels/wechat-http.d.ts +18 -0
  25. package/dist/gateway/channels/wechat-http.js +28 -0
  26. package/dist/gateway/channels/wechat-login.d.ts +36 -0
  27. package/dist/gateway/channels/wechat-login.js +62 -0
  28. package/dist/gateway/channels/wechat.d.ts +40 -0
  29. package/dist/gateway/channels/wechat.js +472 -0
  30. package/dist/gateway/runtimes/openclaw-acp.js +211 -6
  31. package/dist/gateway/types.d.ts +10 -0
  32. package/dist/gateway-control.d.ts +53 -0
  33. package/dist/gateway-control.js +638 -0
  34. package/dist/openclaw-discovery.js +1 -1
  35. package/dist/provision.d.ts +7 -0
  36. package/dist/provision.js +255 -5
  37. package/package.json +1 -1
  38. package/src/__tests__/gateway-control.test.ts +499 -0
  39. package/src/__tests__/openclaw-acp.test.ts +63 -0
  40. package/src/__tests__/openclaw-discovery.test.ts +36 -0
  41. package/src/__tests__/provision.test.ts +179 -0
  42. package/src/__tests__/secret-store.test.ts +70 -0
  43. package/src/__tests__/state-store.test.ts +119 -0
  44. package/src/__tests__/third-party-gateway.test.ts +126 -0
  45. package/src/__tests__/url-guard.test.ts +85 -0
  46. package/src/__tests__/wechat-channel.test.ts +1134 -0
  47. package/src/config.ts +72 -1
  48. package/src/daemon-config-map.ts +24 -0
  49. package/src/daemon.ts +70 -11
  50. package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
  51. package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
  52. package/src/gateway/channels/botcord.ts +39 -0
  53. package/src/gateway/channels/http-types.ts +22 -0
  54. package/src/gateway/channels/index.ts +22 -0
  55. package/src/gateway/channels/login-session.ts +135 -0
  56. package/src/gateway/channels/secret-store.ts +100 -0
  57. package/src/gateway/channels/state-store.ts +213 -0
  58. package/src/gateway/channels/telegram.ts +469 -0
  59. package/src/gateway/channels/text-split.ts +29 -0
  60. package/src/gateway/channels/url-guard.ts +55 -0
  61. package/src/gateway/channels/wechat-http.ts +35 -0
  62. package/src/gateway/channels/wechat-login.ts +90 -0
  63. package/src/gateway/channels/wechat.ts +572 -0
  64. package/src/gateway/runtimes/openclaw-acp.ts +211 -7
  65. package/src/gateway/types.ts +10 -0
  66. package/src/gateway-control.ts +709 -0
  67. package/src/openclaw-discovery.ts +1 -1
  68. package/src/provision.ts +336 -5
package/dist/provision.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * side effects (register agent, write credentials, load route, add/remove
5
5
  * gateway channel) and return an ack payload.
6
6
  */
7
- import { existsSync, readdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
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
10
  import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
@@ -12,6 +12,7 @@ import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js"
12
12
  import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
13
13
  import { agentHomeDir, agentStateDir, agentWorkspaceDir, applyAgentIdentity, ensureAttachedHermesProfileSkills, ensureAgentWorkspace, } from "./agent-workspace.js";
14
14
  import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
15
+ import { createGatewayControl } from "./gateway-control.js";
15
16
  import { hermesProfileHomeDir, isValidHermesProfileName, listHermesProfiles, } from "./gateway/runtimes/hermes-agent.js";
16
17
  import { log as daemonLog } from "./log.js";
17
18
  import { discoverAgentCredentials } from "./agent-discovery.js";
@@ -25,6 +26,10 @@ export function createProvisioner(opts) {
25
26
  const register = opts.register ?? BotCordClient.register;
26
27
  const policyResolver = opts.policyResolver;
27
28
  const onAgentInstalled = opts.onAgentInstalled;
29
+ const gatewayControl = createGatewayControl({
30
+ gateway,
31
+ ...(opts.loginSessions ? { loginSessions: opts.loginSessions } : {}),
32
+ });
28
33
  return async (frame) => {
29
34
  daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
30
35
  switch (frame.type) {
@@ -196,6 +201,60 @@ export function createProvisioner(opts) {
196
201
  daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
197
202
  return { ok: true, result: snapshot };
198
203
  }
204
+ case CONTROL_FRAME_TYPES.LIST_GATEWAYS:
205
+ return gatewayControl.handleList();
206
+ case CONTROL_FRAME_TYPES.UPSERT_GATEWAY: {
207
+ const v = validateGatewayParams(frame.params, {
208
+ required: ["id", "type", "accountId"],
209
+ });
210
+ if (!v.ok)
211
+ return v.ack;
212
+ return gatewayControl.handleUpsert(v.params);
213
+ }
214
+ case CONTROL_FRAME_TYPES.REMOVE_GATEWAY: {
215
+ const v = validateGatewayParams(frame.params, { required: ["id"] });
216
+ if (!v.ok)
217
+ return v.ack;
218
+ return gatewayControl.handleRemove(v.params);
219
+ }
220
+ case CONTROL_FRAME_TYPES.TEST_GATEWAY: {
221
+ const v = validateGatewayParams(frame.params, { required: ["id"] });
222
+ if (!v.ok)
223
+ return v.ack;
224
+ return gatewayControl.handleTest(v.params);
225
+ }
226
+ case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START: {
227
+ const v = validateGatewayParams(frame.params, {
228
+ required: ["provider", "accountId"],
229
+ });
230
+ if (!v.ok)
231
+ return v.ack;
232
+ return gatewayControl.handleLoginStart(v.params);
233
+ }
234
+ case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS: {
235
+ const v = validateGatewayParams(frame.params, {
236
+ required: ["provider", "loginId"],
237
+ });
238
+ if (!v.ok)
239
+ return v.ack;
240
+ return gatewayControl.handleLoginStatus(v.params);
241
+ }
242
+ case "list_agent_files": {
243
+ const params = (frame.params ?? {});
244
+ if (!params.agentId) {
245
+ return {
246
+ ok: false,
247
+ error: { code: "bad_params", message: "list_agent_files requires params.agentId" },
248
+ };
249
+ }
250
+ const result = listAgentRuntimeFiles(params);
251
+ daemonLog.debug("list_agent_files", {
252
+ agentId: params.agentId,
253
+ fileId: params.fileId ?? null,
254
+ count: result.files.length,
255
+ });
256
+ return { ok: true, result };
257
+ }
199
258
  default:
200
259
  daemonLog.warn("provision.dispatch: unknown frame type", {
201
260
  type: frame.type,
@@ -208,7 +267,165 @@ export function createProvisioner(opts) {
208
267
  }
209
268
  };
210
269
  }
270
+ function validateGatewayParams(raw, spec) {
271
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
272
+ return {
273
+ ok: false,
274
+ ack: { ok: false, error: { code: "bad_params", message: "params must be an object" } },
275
+ };
276
+ }
277
+ const params = raw;
278
+ for (const key of spec.required) {
279
+ const v = params[key];
280
+ if (typeof v !== "string" || v.length === 0) {
281
+ return {
282
+ ok: false,
283
+ ack: {
284
+ ok: false,
285
+ error: { code: "bad_params", message: `params.${key} must be a non-empty string` },
286
+ },
287
+ };
288
+ }
289
+ }
290
+ return { ok: true, params };
291
+ }
211
292
  const openclawProvisionLocks = new Map();
293
+ const RUNTIME_FILE_READ_CAP_BYTES = 128 * 1024;
294
+ function listAgentRuntimeFiles(params) {
295
+ const agentId = params.agentId;
296
+ const credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
297
+ const candidates = runtimeFileCandidates(credentials);
298
+ const selected = params.fileId
299
+ ? candidates.filter((candidate) => candidate.id === params.fileId)
300
+ : candidates;
301
+ return {
302
+ agentId,
303
+ ...(credentials.runtime ? { runtime: credentials.runtime } : {}),
304
+ files: selected.map(readRuntimeFileCandidate).filter(Boolean),
305
+ };
306
+ }
307
+ function runtimeFileCandidates(credentials) {
308
+ const agentId = credentials.agentId;
309
+ const runtime = credentials.runtime;
310
+ const out = [];
311
+ addWorkspaceFiles(out, agentId, runtime);
312
+ if (runtime === "hermes-agent") {
313
+ addHermesFiles(out, credentials);
314
+ }
315
+ if (runtime === "openclaw-acp") {
316
+ addOpenclawFiles(out, credentials);
317
+ }
318
+ return out;
319
+ }
320
+ function addWorkspaceFiles(out, agentId, runtime) {
321
+ const root = agentWorkspaceDir(agentId);
322
+ for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
323
+ out.push({
324
+ id: `workspace:${file}`,
325
+ name: `workspace/${file}`,
326
+ scope: "workspace",
327
+ root,
328
+ relativePath: file,
329
+ ...(runtime ? { runtime } : {}),
330
+ });
331
+ }
332
+ }
333
+ function addHermesFiles(out, credentials) {
334
+ if (credentials.hermesProfile) {
335
+ const profile = credentials.hermesProfile;
336
+ const root = hermesProfileHomeDir(profile);
337
+ for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
338
+ out.push({
339
+ id: `hermes-profile:${profile}:${file}`,
340
+ name: `hermes/${profile}/${file}`,
341
+ scope: "hermes",
342
+ root,
343
+ relativePath: file,
344
+ runtime: "hermes-agent",
345
+ profile,
346
+ });
347
+ }
348
+ }
349
+ else {
350
+ const root = path.join(agentHomeDir(credentials.agentId), "hermes-home");
351
+ for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
352
+ out.push({
353
+ id: `hermes-home:${file}`,
354
+ name: `hermes-home/${file}`,
355
+ scope: "hermes",
356
+ root,
357
+ relativePath: file,
358
+ runtime: "hermes-agent",
359
+ });
360
+ }
361
+ }
362
+ out.push({
363
+ id: "hermes-workspace:AGENTS.md",
364
+ name: "hermes-workspace/AGENTS.md",
365
+ scope: "hermes",
366
+ root: path.join(agentHomeDir(credentials.agentId), "hermes-workspace"),
367
+ relativePath: "AGENTS.md",
368
+ runtime: "hermes-agent",
369
+ ...(credentials.hermesProfile ? { profile: credentials.hermesProfile } : {}),
370
+ });
371
+ }
372
+ function addOpenclawFiles(out, credentials) {
373
+ if (!credentials.openclawAgent)
374
+ return;
375
+ const local = readLocalOpenclawAgents();
376
+ if (!local)
377
+ return;
378
+ const profile = local.find((entry) => entry.id === credentials.openclawAgent);
379
+ if (!profile?.workspace)
380
+ return;
381
+ for (const file of ["SOUL.md", "MEMORY.md", "AGENTS.md"]) {
382
+ out.push({
383
+ id: `openclaw:${credentials.openclawAgent}:${file}`,
384
+ name: `openclaw/${credentials.openclawAgent}/${file}`,
385
+ scope: "openclaw",
386
+ root: profile.workspace,
387
+ relativePath: file,
388
+ runtime: "openclaw-acp",
389
+ profile: credentials.openclawAgent,
390
+ });
391
+ }
392
+ }
393
+ function readRuntimeFileCandidate(candidate) {
394
+ const resolved = path.resolve(candidate.root, candidate.relativePath);
395
+ const root = path.resolve(candidate.root);
396
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) {
397
+ return null;
398
+ }
399
+ const base = {
400
+ id: candidate.id,
401
+ name: candidate.name,
402
+ scope: candidate.scope,
403
+ ...(candidate.runtime ? { runtime: candidate.runtime } : {}),
404
+ ...(candidate.profile ? { profile: candidate.profile } : {}),
405
+ };
406
+ try {
407
+ const lst = lstatSync(resolved);
408
+ if (lst.isSymbolicLink() || !lst.isFile())
409
+ return null;
410
+ const st = statSync(resolved);
411
+ base.size = st.size;
412
+ base.mtimeMs = st.mtimeMs;
413
+ if (st.size > RUNTIME_FILE_READ_CAP_BYTES) {
414
+ base.truncated = true;
415
+ return base;
416
+ }
417
+ base.content = readFileSync(resolved, "utf8");
418
+ return base;
419
+ }
420
+ catch (err) {
421
+ if (err.code === "ENOENT")
422
+ return null;
423
+ return {
424
+ ...base,
425
+ error: err instanceof Error ? err.message : String(err),
426
+ };
427
+ }
428
+ }
212
429
  async function provisionAgent(params, ctx) {
213
430
  // Validate both caller-supplied cwd sources up front. Previously only
214
431
  // `params.cwd` was checked, so `params.credentials.cwd` could smuggle an
@@ -1290,10 +1507,14 @@ function readLocalOpenclawAgents() {
1290
1507
  try {
1291
1508
  const file = path.join(homedir(), ".openclaw", "openclaw.json");
1292
1509
  if (!existsSync(file))
1293
- return [{ id: "default" }];
1510
+ return readLocalOpenclawAgentDirs() ?? [{ id: "default" }];
1294
1511
  const cfg = JSON.parse(readFileSync(file, "utf8"));
1295
1512
  const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
1296
- const defaultId = typeof cfg?.agents?.defaults?.id === "string" ? cfg.agents.defaults.id : "default";
1513
+ const explicitDefaultId = typeof cfg?.agents?.defaults?.id === "string" && cfg.agents.defaults.id
1514
+ ? cfg.agents.defaults.id
1515
+ : null;
1516
+ const dirAgents = readLocalOpenclawAgentDirs();
1517
+ const defaultId = explicitDefaultId ?? (list.length === 0 && !dirAgents ? "default" : null);
1297
1518
  const seen = new Set();
1298
1519
  const out = [];
1299
1520
  const push = (raw, fallbackId) => {
@@ -1323,16 +1544,45 @@ function readLocalOpenclawAgents() {
1323
1544
  }
1324
1545
  out.push(row);
1325
1546
  };
1326
- // Default agent first so it surfaces at the top of the dropdown.
1327
- push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
1547
+ // Explicit default agent first so it surfaces at the top of the dropdown.
1548
+ if (defaultId)
1549
+ push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
1328
1550
  for (const entry of list)
1329
1551
  push(entry);
1552
+ for (const entry of dirAgents ?? [])
1553
+ push(entry);
1330
1554
  return out;
1331
1555
  }
1332
1556
  catch {
1333
1557
  return null;
1334
1558
  }
1335
1559
  }
1560
+ function readLocalOpenclawAgentDirs() {
1561
+ try {
1562
+ const dir = path.join(homedir(), ".openclaw", "agents");
1563
+ if (!existsSync(dir))
1564
+ return null;
1565
+ const agents = readdirSync(dir, { withFileTypes: true })
1566
+ .filter((entry) => entry.isDirectory() && entry.name.length > 0)
1567
+ .map((entry) => ({
1568
+ id: entry.name,
1569
+ workspace: path.join(dir, entry.name),
1570
+ }));
1571
+ if (agents.length === 0)
1572
+ return null;
1573
+ agents.sort((a, b) => {
1574
+ if (a.id === "main")
1575
+ return -1;
1576
+ if (b.id === "main")
1577
+ return 1;
1578
+ return a.id.localeCompare(b.id);
1579
+ });
1580
+ return agents;
1581
+ }
1582
+ catch {
1583
+ return null;
1584
+ }
1585
+ }
1336
1586
  function resolveOpenclawIdentityName(agentId, workspace, cfg) {
1337
1587
  const root = workspace ?? resolveOpenclawWorkspace(agentId, cfg);
1338
1588
  if (!root)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {