@botcord/daemon 0.2.36 → 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.
- package/dist/config.d.ts +29 -0
- package/dist/config.js +27 -0
- package/dist/daemon-config-map.d.ts +3 -0
- package/dist/daemon-config-map.js +30 -0
- package/dist/daemon.d.ts +15 -1
- package/dist/daemon.js +56 -11
- package/dist/gateway/channels/botcord.js +44 -0
- package/dist/gateway/channels/http-types.d.ts +19 -0
- package/dist/gateway/channels/http-types.js +1 -0
- package/dist/gateway/channels/index.d.ts +5 -0
- package/dist/gateway/channels/index.js +5 -0
- package/dist/gateway/channels/login-session.d.ts +83 -0
- package/dist/gateway/channels/login-session.js +99 -0
- package/dist/gateway/channels/secret-store.d.ts +21 -0
- package/dist/gateway/channels/secret-store.js +75 -0
- package/dist/gateway/channels/state-store.d.ts +60 -0
- package/dist/gateway/channels/state-store.js +173 -0
- package/dist/gateway/channels/telegram.d.ts +31 -0
- package/dist/gateway/channels/telegram.js +371 -0
- package/dist/gateway/channels/text-split.d.ts +13 -0
- package/dist/gateway/channels/text-split.js +33 -0
- package/dist/gateway/channels/url-guard.d.ts +18 -0
- package/dist/gateway/channels/url-guard.js +53 -0
- package/dist/gateway/channels/wechat-http.d.ts +18 -0
- package/dist/gateway/channels/wechat-http.js +28 -0
- package/dist/gateway/channels/wechat-login.d.ts +36 -0
- package/dist/gateway/channels/wechat-login.js +62 -0
- package/dist/gateway/channels/wechat.d.ts +40 -0
- package/dist/gateway/channels/wechat.js +472 -0
- package/dist/gateway/runtimes/openclaw-acp.js +211 -6
- package/dist/gateway/types.d.ts +10 -0
- package/dist/gateway-control.d.ts +53 -0
- package/dist/gateway-control.js +638 -0
- package/dist/provision.d.ts +7 -0
- package/dist/provision.js +255 -5
- package/package.json +1 -1
- package/src/__tests__/gateway-control.test.ts +499 -0
- package/src/__tests__/openclaw-acp.test.ts +63 -0
- package/src/__tests__/provision.test.ts +179 -0
- package/src/__tests__/secret-store.test.ts +70 -0
- package/src/__tests__/state-store.test.ts +119 -0
- package/src/__tests__/third-party-gateway.test.ts +126 -0
- package/src/__tests__/url-guard.test.ts +85 -0
- package/src/__tests__/wechat-channel.test.ts +1134 -0
- package/src/config.ts +71 -0
- package/src/daemon-config-map.ts +24 -0
- package/src/daemon.ts +70 -11
- package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
- package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
- package/src/gateway/channels/botcord.ts +39 -0
- package/src/gateway/channels/http-types.ts +22 -0
- package/src/gateway/channels/index.ts +22 -0
- package/src/gateway/channels/login-session.ts +135 -0
- package/src/gateway/channels/secret-store.ts +100 -0
- package/src/gateway/channels/state-store.ts +213 -0
- package/src/gateway/channels/telegram.ts +469 -0
- package/src/gateway/channels/text-split.ts +29 -0
- package/src/gateway/channels/url-guard.ts +55 -0
- package/src/gateway/channels/wechat-http.ts +35 -0
- package/src/gateway/channels/wechat-login.ts +90 -0
- package/src/gateway/channels/wechat.ts +572 -0
- package/src/gateway/runtimes/openclaw-acp.ts +211 -7
- package/src/gateway/types.ts +10 -0
- package/src/gateway-control.ts +709 -0
- package/src/provision.ts +336 -5
package/src/provision.ts
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 {
|
|
@@ -56,6 +56,8 @@ import {
|
|
|
56
56
|
ensureAgentWorkspace,
|
|
57
57
|
} from "./agent-workspace.js";
|
|
58
58
|
import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
59
|
+
import { createGatewayControl } from "./gateway-control.js";
|
|
60
|
+
import type { LoginSessionStore } from "./gateway/channels/login-session.js";
|
|
59
61
|
import {
|
|
60
62
|
hermesProfileHomeDir,
|
|
61
63
|
isValidHermesProfileName,
|
|
@@ -114,6 +116,12 @@ export interface ProvisionerOptions {
|
|
|
114
116
|
* the next restart.
|
|
115
117
|
*/
|
|
116
118
|
onAgentInstalled?: OnAgentInstalledHook;
|
|
119
|
+
/**
|
|
120
|
+
* Optional shared login-session store for the third-party gateway login
|
|
121
|
+
* frames. Tests inject a stub clock; production lets `createGatewayControl`
|
|
122
|
+
* spin up a default in-memory store.
|
|
123
|
+
*/
|
|
124
|
+
loginSessions?: LoginSessionStore;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
@@ -131,6 +139,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
131
139
|
const register = opts.register ?? BotCordClient.register;
|
|
132
140
|
const policyResolver = opts.policyResolver;
|
|
133
141
|
const onAgentInstalled = opts.onAgentInstalled;
|
|
142
|
+
const gatewayControl = createGatewayControl({
|
|
143
|
+
gateway,
|
|
144
|
+
...(opts.loginSessions ? { loginSessions: opts.loginSessions } : {}),
|
|
145
|
+
});
|
|
134
146
|
|
|
135
147
|
return async (frame: ControlFrame): Promise<AckBody> => {
|
|
136
148
|
daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
|
|
@@ -310,6 +322,72 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
310
322
|
return { ok: true, result: snapshot };
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
case CONTROL_FRAME_TYPES.LIST_GATEWAYS:
|
|
326
|
+
return gatewayControl.handleList();
|
|
327
|
+
|
|
328
|
+
case CONTROL_FRAME_TYPES.UPSERT_GATEWAY: {
|
|
329
|
+
const v = validateGatewayParams(frame.params, {
|
|
330
|
+
required: ["id", "type", "accountId"],
|
|
331
|
+
});
|
|
332
|
+
if (!v.ok) return v.ack;
|
|
333
|
+
return gatewayControl.handleUpsert(
|
|
334
|
+
v.params as unknown as Parameters<typeof gatewayControl.handleUpsert>[0],
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case CONTROL_FRAME_TYPES.REMOVE_GATEWAY: {
|
|
339
|
+
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
340
|
+
if (!v.ok) return v.ack;
|
|
341
|
+
return gatewayControl.handleRemove(
|
|
342
|
+
v.params as unknown as Parameters<typeof gatewayControl.handleRemove>[0],
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case CONTROL_FRAME_TYPES.TEST_GATEWAY: {
|
|
347
|
+
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
348
|
+
if (!v.ok) return v.ack;
|
|
349
|
+
return gatewayControl.handleTest(
|
|
350
|
+
v.params as unknown as Parameters<typeof gatewayControl.handleTest>[0],
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START: {
|
|
355
|
+
const v = validateGatewayParams(frame.params, {
|
|
356
|
+
required: ["provider", "accountId"],
|
|
357
|
+
});
|
|
358
|
+
if (!v.ok) return v.ack;
|
|
359
|
+
return gatewayControl.handleLoginStart(
|
|
360
|
+
v.params as unknown as Parameters<typeof gatewayControl.handleLoginStart>[0],
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS: {
|
|
365
|
+
const v = validateGatewayParams(frame.params, {
|
|
366
|
+
required: ["provider", "loginId"],
|
|
367
|
+
});
|
|
368
|
+
if (!v.ok) return v.ack;
|
|
369
|
+
return gatewayControl.handleLoginStatus(
|
|
370
|
+
v.params as unknown as Parameters<typeof gatewayControl.handleLoginStatus>[0],
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case "list_agent_files": {
|
|
375
|
+
const params = (frame.params ?? {}) as unknown as ListAgentFilesParams;
|
|
376
|
+
if (!params.agentId) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
error: { code: "bad_params", message: "list_agent_files requires params.agentId" },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const result = listAgentRuntimeFiles(params);
|
|
383
|
+
daemonLog.debug("list_agent_files", {
|
|
384
|
+
agentId: params.agentId,
|
|
385
|
+
fileId: params.fileId ?? null,
|
|
386
|
+
count: result.files.length,
|
|
387
|
+
});
|
|
388
|
+
return { ok: true, result };
|
|
389
|
+
}
|
|
390
|
+
|
|
313
391
|
default:
|
|
314
392
|
daemonLog.warn("provision.dispatch: unknown frame type", {
|
|
315
393
|
type: frame.type,
|
|
@@ -323,6 +401,42 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
323
401
|
};
|
|
324
402
|
}
|
|
325
403
|
|
|
404
|
+
// W8: hand-written runtime validator for the third-party gateway frame
|
|
405
|
+
// params. Rejects malformed payloads with a structured `bad_params` ack
|
|
406
|
+
// before they hit the per-handler logic, so an attacker can't smuggle a
|
|
407
|
+
// non-object `params` (e.g. `null`, an array, a string) through the type
|
|
408
|
+
// cast and trigger a downstream `TypeError` we don't surface.
|
|
409
|
+
type GatewayParamAck = { ok: false; error: { code: string; message: string } };
|
|
410
|
+
type ValidateResult =
|
|
411
|
+
| { ok: true; params: Record<string, unknown> }
|
|
412
|
+
| { ok: false; ack: GatewayParamAck };
|
|
413
|
+
|
|
414
|
+
function validateGatewayParams(
|
|
415
|
+
raw: unknown,
|
|
416
|
+
spec: { required: ReadonlyArray<string> },
|
|
417
|
+
): ValidateResult {
|
|
418
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
ack: { ok: false, error: { code: "bad_params", message: "params must be an object" } },
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const params = raw as Record<string, unknown>;
|
|
425
|
+
for (const key of spec.required) {
|
|
426
|
+
const v = params[key];
|
|
427
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
ack: {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: { code: "bad_params", message: `params.${key} must be a non-empty string` },
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { ok: true, params };
|
|
438
|
+
}
|
|
439
|
+
|
|
326
440
|
interface ProvisionedAgent {
|
|
327
441
|
agentId: string;
|
|
328
442
|
hubUrl: string;
|
|
@@ -337,6 +451,192 @@ interface ProvisionCtx {
|
|
|
337
451
|
|
|
338
452
|
const openclawProvisionLocks = new Map<string, Promise<unknown>>();
|
|
339
453
|
|
|
454
|
+
const RUNTIME_FILE_READ_CAP_BYTES = 128 * 1024;
|
|
455
|
+
|
|
456
|
+
interface ListAgentFilesParams {
|
|
457
|
+
agentId: string;
|
|
458
|
+
fileId?: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
interface AgentRuntimeFile {
|
|
462
|
+
id: string;
|
|
463
|
+
name: string;
|
|
464
|
+
scope: "workspace" | "hermes" | "openclaw";
|
|
465
|
+
runtime?: string;
|
|
466
|
+
profile?: string;
|
|
467
|
+
size?: number;
|
|
468
|
+
mtimeMs?: number;
|
|
469
|
+
content?: string;
|
|
470
|
+
truncated?: boolean;
|
|
471
|
+
error?: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
interface ListAgentFilesResult {
|
|
475
|
+
agentId: string;
|
|
476
|
+
runtime?: string;
|
|
477
|
+
files: AgentRuntimeFile[];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
interface RuntimeFileCandidate {
|
|
481
|
+
id: string;
|
|
482
|
+
name: string;
|
|
483
|
+
scope: AgentRuntimeFile["scope"];
|
|
484
|
+
root: string;
|
|
485
|
+
relativePath: string;
|
|
486
|
+
runtime?: string;
|
|
487
|
+
profile?: string;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function listAgentRuntimeFiles(params: ListAgentFilesParams): ListAgentFilesResult {
|
|
491
|
+
const agentId = params.agentId;
|
|
492
|
+
const credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
|
|
493
|
+
const candidates = runtimeFileCandidates(credentials);
|
|
494
|
+
const selected = params.fileId
|
|
495
|
+
? candidates.filter((candidate) => candidate.id === params.fileId)
|
|
496
|
+
: candidates;
|
|
497
|
+
return {
|
|
498
|
+
agentId,
|
|
499
|
+
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
500
|
+
files: selected.map(readRuntimeFileCandidate).filter(Boolean) as AgentRuntimeFile[],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function runtimeFileCandidates(credentials: StoredBotCordCredentials): RuntimeFileCandidate[] {
|
|
505
|
+
const agentId = credentials.agentId;
|
|
506
|
+
const runtime = credentials.runtime;
|
|
507
|
+
const out: RuntimeFileCandidate[] = [];
|
|
508
|
+
|
|
509
|
+
addWorkspaceFiles(out, agentId, runtime);
|
|
510
|
+
|
|
511
|
+
if (runtime === "hermes-agent") {
|
|
512
|
+
addHermesFiles(out, credentials);
|
|
513
|
+
}
|
|
514
|
+
if (runtime === "openclaw-acp") {
|
|
515
|
+
addOpenclawFiles(out, credentials);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function addWorkspaceFiles(
|
|
522
|
+
out: RuntimeFileCandidate[],
|
|
523
|
+
agentId: string,
|
|
524
|
+
runtime?: string,
|
|
525
|
+
): void {
|
|
526
|
+
const root = agentWorkspaceDir(agentId);
|
|
527
|
+
for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
|
|
528
|
+
out.push({
|
|
529
|
+
id: `workspace:${file}`,
|
|
530
|
+
name: `workspace/${file}`,
|
|
531
|
+
scope: "workspace",
|
|
532
|
+
root,
|
|
533
|
+
relativePath: file,
|
|
534
|
+
...(runtime ? { runtime } : {}),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function addHermesFiles(
|
|
540
|
+
out: RuntimeFileCandidate[],
|
|
541
|
+
credentials: StoredBotCordCredentials,
|
|
542
|
+
): void {
|
|
543
|
+
if (credentials.hermesProfile) {
|
|
544
|
+
const profile = credentials.hermesProfile;
|
|
545
|
+
const root = hermesProfileHomeDir(profile);
|
|
546
|
+
for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
|
|
547
|
+
out.push({
|
|
548
|
+
id: `hermes-profile:${profile}:${file}`,
|
|
549
|
+
name: `hermes/${profile}/${file}`,
|
|
550
|
+
scope: "hermes",
|
|
551
|
+
root,
|
|
552
|
+
relativePath: file,
|
|
553
|
+
runtime: "hermes-agent",
|
|
554
|
+
profile,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
const root = path.join(agentHomeDir(credentials.agentId), "hermes-home");
|
|
559
|
+
for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
|
|
560
|
+
out.push({
|
|
561
|
+
id: `hermes-home:${file}`,
|
|
562
|
+
name: `hermes-home/${file}`,
|
|
563
|
+
scope: "hermes",
|
|
564
|
+
root,
|
|
565
|
+
relativePath: file,
|
|
566
|
+
runtime: "hermes-agent",
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
out.push({
|
|
572
|
+
id: "hermes-workspace:AGENTS.md",
|
|
573
|
+
name: "hermes-workspace/AGENTS.md",
|
|
574
|
+
scope: "hermes",
|
|
575
|
+
root: path.join(agentHomeDir(credentials.agentId), "hermes-workspace"),
|
|
576
|
+
relativePath: "AGENTS.md",
|
|
577
|
+
runtime: "hermes-agent",
|
|
578
|
+
...(credentials.hermesProfile ? { profile: credentials.hermesProfile } : {}),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function addOpenclawFiles(
|
|
583
|
+
out: RuntimeFileCandidate[],
|
|
584
|
+
credentials: StoredBotCordCredentials,
|
|
585
|
+
): void {
|
|
586
|
+
if (!credentials.openclawAgent) return;
|
|
587
|
+
const local = readLocalOpenclawAgents();
|
|
588
|
+
if (!local) return;
|
|
589
|
+
const profile = local.find((entry) => entry.id === credentials.openclawAgent);
|
|
590
|
+
if (!profile?.workspace) return;
|
|
591
|
+
for (const file of ["SOUL.md", "MEMORY.md", "AGENTS.md"]) {
|
|
592
|
+
out.push({
|
|
593
|
+
id: `openclaw:${credentials.openclawAgent}:${file}`,
|
|
594
|
+
name: `openclaw/${credentials.openclawAgent}/${file}`,
|
|
595
|
+
scope: "openclaw",
|
|
596
|
+
root: profile.workspace,
|
|
597
|
+
relativePath: file,
|
|
598
|
+
runtime: "openclaw-acp",
|
|
599
|
+
profile: credentials.openclawAgent,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function readRuntimeFileCandidate(candidate: RuntimeFileCandidate): AgentRuntimeFile | null {
|
|
605
|
+
const resolved = path.resolve(candidate.root, candidate.relativePath);
|
|
606
|
+
const root = path.resolve(candidate.root);
|
|
607
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const base: AgentRuntimeFile = {
|
|
612
|
+
id: candidate.id,
|
|
613
|
+
name: candidate.name,
|
|
614
|
+
scope: candidate.scope,
|
|
615
|
+
...(candidate.runtime ? { runtime: candidate.runtime } : {}),
|
|
616
|
+
...(candidate.profile ? { profile: candidate.profile } : {}),
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const lst = lstatSync(resolved);
|
|
621
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return null;
|
|
622
|
+
const st = statSync(resolved);
|
|
623
|
+
base.size = st.size;
|
|
624
|
+
base.mtimeMs = st.mtimeMs;
|
|
625
|
+
if (st.size > RUNTIME_FILE_READ_CAP_BYTES) {
|
|
626
|
+
base.truncated = true;
|
|
627
|
+
return base;
|
|
628
|
+
}
|
|
629
|
+
base.content = readFileSync(resolved, "utf8");
|
|
630
|
+
return base;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
633
|
+
return {
|
|
634
|
+
...base,
|
|
635
|
+
error: err instanceof Error ? err.message : String(err),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
340
640
|
async function provisionAgent(
|
|
341
641
|
params: ProvisionAgentParams,
|
|
342
642
|
ctx: ProvisionCtx,
|
|
@@ -1556,10 +1856,15 @@ function readLocalOpenclawAgents(): Array<{
|
|
|
1556
1856
|
}> | null {
|
|
1557
1857
|
try {
|
|
1558
1858
|
const file = path.join(homedir(), ".openclaw", "openclaw.json");
|
|
1559
|
-
if (!existsSync(file)) return [{ id: "default" }];
|
|
1859
|
+
if (!existsSync(file)) return readLocalOpenclawAgentDirs() ?? [{ id: "default" }];
|
|
1560
1860
|
const cfg = JSON.parse(readFileSync(file, "utf8")) as any;
|
|
1561
1861
|
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
1562
|
-
const
|
|
1862
|
+
const explicitDefaultId =
|
|
1863
|
+
typeof cfg?.agents?.defaults?.id === "string" && cfg.agents.defaults.id
|
|
1864
|
+
? cfg.agents.defaults.id
|
|
1865
|
+
: null;
|
|
1866
|
+
const dirAgents = readLocalOpenclawAgentDirs();
|
|
1867
|
+
const defaultId = explicitDefaultId ?? (list.length === 0 && !dirAgents ? "default" : null);
|
|
1563
1868
|
const seen = new Set<string>();
|
|
1564
1869
|
const out: Array<{ id: string; name?: string; workspace?: string; model?: { name?: string; provider?: string } }> = [];
|
|
1565
1870
|
const push = (raw: any, fallbackId?: string): void => {
|
|
@@ -1581,15 +1886,41 @@ function readLocalOpenclawAgents(): Array<{
|
|
|
1581
1886
|
}
|
|
1582
1887
|
out.push(row);
|
|
1583
1888
|
};
|
|
1584
|
-
//
|
|
1585
|
-
push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
|
|
1889
|
+
// Explicit default agent first so it surfaces at the top of the dropdown.
|
|
1890
|
+
if (defaultId) push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
|
|
1586
1891
|
for (const entry of list) push(entry);
|
|
1892
|
+
for (const entry of dirAgents ?? []) push(entry);
|
|
1587
1893
|
return out;
|
|
1588
1894
|
} catch {
|
|
1589
1895
|
return null;
|
|
1590
1896
|
}
|
|
1591
1897
|
}
|
|
1592
1898
|
|
|
1899
|
+
function readLocalOpenclawAgentDirs(): Array<{
|
|
1900
|
+
id: string;
|
|
1901
|
+
workspace?: string;
|
|
1902
|
+
}> | null {
|
|
1903
|
+
try {
|
|
1904
|
+
const dir = path.join(homedir(), ".openclaw", "agents");
|
|
1905
|
+
if (!existsSync(dir)) return null;
|
|
1906
|
+
const agents = readdirSync(dir, { withFileTypes: true })
|
|
1907
|
+
.filter((entry) => entry.isDirectory() && entry.name.length > 0)
|
|
1908
|
+
.map((entry) => ({
|
|
1909
|
+
id: entry.name,
|
|
1910
|
+
workspace: path.join(dir, entry.name),
|
|
1911
|
+
}));
|
|
1912
|
+
if (agents.length === 0) return null;
|
|
1913
|
+
agents.sort((a, b) => {
|
|
1914
|
+
if (a.id === "main") return -1;
|
|
1915
|
+
if (b.id === "main") return 1;
|
|
1916
|
+
return a.id.localeCompare(b.id);
|
|
1917
|
+
});
|
|
1918
|
+
return agents;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1593
1924
|
function resolveOpenclawIdentityName(
|
|
1594
1925
|
agentId: string,
|
|
1595
1926
|
workspace?: string,
|