@botcord/daemon 0.2.85 → 0.2.87
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 +23 -2
- package/dist/daemon-singleton.d.ts +12 -0
- package/dist/daemon-singleton.js +83 -7
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +32 -0
- package/dist/gateway/channels/botcord.js +72 -29
- package/dist/gateway/dispatcher.d.ts +16 -0
- package/dist/gateway/dispatcher.js +25 -1
- package/dist/gateway/runtimes/deepseek-tui.js +5 -1
- package/dist/index.js +27 -7
- package/dist/provision.js +37 -3
- package/dist/skill-index.d.ts +13 -0
- package/dist/skill-index.js +65 -18
- package/dist/turn-text.js +38 -1
- 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__/dispatcher-reply-to.test.ts +61 -0
- package/src/__tests__/provision.test.ts +42 -0
- package/src/__tests__/runtime-discovery.test.ts +17 -1
- package/src/__tests__/skill-index.test.ts +130 -0
- package/src/__tests__/turn-text.test.ts +121 -0
- package/src/cloud-daemon.ts +22 -2
- package/src/daemon-singleton.ts +98 -6
- package/src/daemon.ts +37 -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/dispatcher.ts +26 -1
- package/src/gateway/runtimes/deepseek-tui.ts +7 -1
- package/src/index.ts +25 -6
- package/src/provision.ts +42 -1
- package/src/skill-index.ts +87 -19
- package/src/turn-text.ts +51 -1
|
@@ -1196,27 +1196,46 @@ function extractToolResult(raw: any): { name?: string; result: string; id?: stri
|
|
|
1196
1196
|
function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
|
|
1197
1197
|
const payload = raw?.payload;
|
|
1198
1198
|
if (!payload || typeof payload !== "object") return null;
|
|
1199
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1200
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1199
1201
|
|
|
1200
|
-
if (
|
|
1201
|
-
const tool =
|
|
1202
|
+
if (event === "tool.started") {
|
|
1203
|
+
const tool = innerPayload?.tool && typeof innerPayload.tool === "object" ? innerPayload.tool : undefined;
|
|
1202
1204
|
return {
|
|
1203
|
-
name: stringField(
|
|
1204
|
-
params: parseMaybeJson(
|
|
1205
|
-
|
|
1206
|
-
|
|
1205
|
+
name: stringField(innerPayload, "name") ?? stringField(tool, "name") ?? "tool",
|
|
1206
|
+
params: parseMaybeJson(
|
|
1207
|
+
innerPayload?.input ??
|
|
1208
|
+
innerPayload?.arguments ??
|
|
1209
|
+
innerPayload?.params ??
|
|
1210
|
+
tool?.input ??
|
|
1211
|
+
tool?.rawInput ??
|
|
1212
|
+
tool?.arguments ??
|
|
1213
|
+
tool?.params,
|
|
1214
|
+
),
|
|
1215
|
+
id: stringField(innerPayload, "id") ?? stringField(tool, "id"),
|
|
1216
|
+
status: stringField(innerPayload, "status") ?? stringField(tool, "status"),
|
|
1207
1217
|
};
|
|
1208
1218
|
}
|
|
1209
1219
|
|
|
1210
|
-
if (
|
|
1211
|
-
const inner =
|
|
1212
|
-
raw?.event === "item.started"
|
|
1213
|
-
? payload
|
|
1214
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1215
|
-
? payload.payload
|
|
1216
|
-
: {};
|
|
1220
|
+
if (event === "item.started") {
|
|
1221
|
+
const inner = innerPayload ?? {};
|
|
1217
1222
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1218
1223
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1219
|
-
const
|
|
1224
|
+
const metadata = item?.metadata && typeof item.metadata === "object" ? item.metadata : undefined;
|
|
1225
|
+
const metadataCommand =
|
|
1226
|
+
metadata && (metadata.command ?? metadata.cmd)
|
|
1227
|
+
? { [metadata.command ? "command" : "cmd"]: metadata.command ?? metadata.cmd }
|
|
1228
|
+
: undefined;
|
|
1229
|
+
const itemParams = parseMaybeJson(
|
|
1230
|
+
item?.input ??
|
|
1231
|
+
item?.arguments ??
|
|
1232
|
+
item?.params ??
|
|
1233
|
+
metadata?.input ??
|
|
1234
|
+
metadata?.arguments ??
|
|
1235
|
+
metadata?.params ??
|
|
1236
|
+
metadataCommand ??
|
|
1237
|
+
item?.detail,
|
|
1238
|
+
);
|
|
1220
1239
|
const detailParams =
|
|
1221
1240
|
itemParams !== undefined
|
|
1222
1241
|
? itemParams
|
|
@@ -1240,9 +1259,18 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1240
1259
|
inner.arguments ??
|
|
1241
1260
|
inner.params ??
|
|
1242
1261
|
item?.input ??
|
|
1243
|
-
item?.arguments
|
|
1262
|
+
item?.arguments ??
|
|
1263
|
+
item?.params ??
|
|
1264
|
+
metadata?.input ??
|
|
1265
|
+
metadata?.arguments ??
|
|
1266
|
+
metadata?.params ??
|
|
1267
|
+
metadataCommand,
|
|
1244
1268
|
) ?? detailParams ?? tool ?? item,
|
|
1245
|
-
id:
|
|
1269
|
+
id:
|
|
1270
|
+
stringField(tool, "id") ??
|
|
1271
|
+
stringField(inner, "id") ??
|
|
1272
|
+
stringField(item, "id") ??
|
|
1273
|
+
stringField(payload, "item_id"),
|
|
1246
1274
|
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1247
1275
|
};
|
|
1248
1276
|
}
|
|
@@ -1253,28 +1281,26 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1253
1281
|
function extractDeepseekToolResult(raw: any): { name?: string; result: string; id?: string } | null {
|
|
1254
1282
|
const payload = raw?.payload;
|
|
1255
1283
|
if (!payload || typeof payload !== "object") return null;
|
|
1284
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1285
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1256
1286
|
|
|
1257
|
-
if (
|
|
1258
|
-
const result =
|
|
1287
|
+
if (event === "tool.completed") {
|
|
1288
|
+
const result =
|
|
1289
|
+
innerPayload?.output ??
|
|
1290
|
+
innerPayload?.result ??
|
|
1291
|
+
innerPayload?.content ??
|
|
1292
|
+
innerPayload?.error ??
|
|
1293
|
+
innerPayload ??
|
|
1294
|
+
payload;
|
|
1259
1295
|
return {
|
|
1260
|
-
name: stringField(
|
|
1296
|
+
name: stringField(innerPayload, "name"),
|
|
1261
1297
|
result: stringifyToolResult(result),
|
|
1262
|
-
id: stringField(
|
|
1298
|
+
id: stringField(innerPayload, "id"),
|
|
1263
1299
|
};
|
|
1264
1300
|
}
|
|
1265
1301
|
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
raw?.event === "item.failed" ||
|
|
1269
|
-
payload.event === "item.completed" ||
|
|
1270
|
-
payload.event === "item.failed"
|
|
1271
|
-
) {
|
|
1272
|
-
const inner =
|
|
1273
|
-
raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1274
|
-
? payload
|
|
1275
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1276
|
-
? payload.payload
|
|
1277
|
-
: {};
|
|
1302
|
+
if (event === "item.completed" || event === "item.failed") {
|
|
1303
|
+
const inner = innerPayload ?? {};
|
|
1278
1304
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1279
1305
|
const result =
|
|
1280
1306
|
item?.output ??
|
|
@@ -1295,13 +1321,35 @@ function extractDeepseekToolResult(raw: any): { name?: string; result: string; i
|
|
|
1295
1321
|
stringField(inner, "name") ??
|
|
1296
1322
|
stringField(item, "type"),
|
|
1297
1323
|
result: stringifyToolResult(result),
|
|
1298
|
-
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1324
|
+
id: stringField(item, "id") ?? stringField(inner, "id") ?? stringField(payload, "item_id"),
|
|
1299
1325
|
};
|
|
1300
1326
|
}
|
|
1301
1327
|
|
|
1302
1328
|
return null;
|
|
1303
1329
|
}
|
|
1304
1330
|
|
|
1331
|
+
function unwrapDeepseekPayload(raw: any): any {
|
|
1332
|
+
const payload = raw?.payload;
|
|
1333
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
1334
|
+
const nested = payload.payload;
|
|
1335
|
+
if (nested && typeof nested === "object") {
|
|
1336
|
+
const outerEvent = stringField(payload, "event");
|
|
1337
|
+
if (
|
|
1338
|
+
outerEvent ||
|
|
1339
|
+
nested.item ||
|
|
1340
|
+
nested.tool ||
|
|
1341
|
+
nested.turn ||
|
|
1342
|
+
nested.kind ||
|
|
1343
|
+
nested.output ||
|
|
1344
|
+
nested.result ||
|
|
1345
|
+
nested.error
|
|
1346
|
+
) {
|
|
1347
|
+
return nested;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return payload;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1305
1353
|
function formatBlockDetails(raw: unknown): string {
|
|
1306
1354
|
if (!raw || typeof raw !== "object") return "";
|
|
1307
1355
|
const r = raw as any;
|
|
@@ -208,6 +208,31 @@ function buildRuntimeRecoveryPrompt(args: {
|
|
|
208
208
|
].join("\n");
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Pick the canonical reply_to value to attach to outbound replies for a given
|
|
213
|
+
* inbound `GatewayInboundMessage`. Priority:
|
|
214
|
+
*
|
|
215
|
+
* 1. `msg.replyTo` — the inbound was itself a reply; preserve the chain so
|
|
216
|
+
* receipts and threaded replies point at the original target.
|
|
217
|
+
* 2. `raw.envelope.msg_id` — the wire-protocol identifier (UUID per a2a/0.1).
|
|
218
|
+
* This is the canonical form the hub stores in `reply_to_msg_id`.
|
|
219
|
+
* 3. `msg.id` — fallback to the hub_msg_id (`h_*`) the BotCord channel
|
|
220
|
+
* stamps on every inbound. The hub accepts this form via
|
|
221
|
+
* `_load_reply_target`'s prefix-based discriminator, but emitting it is
|
|
222
|
+
* lossy because the hub then has to resolve it back to msg_id.
|
|
223
|
+
*
|
|
224
|
+
* Exported for unit testing; production code paths use Dispatcher.providerReplyTo.
|
|
225
|
+
*/
|
|
226
|
+
export function pickReplyToTarget(msg: GatewayInboundMessage): string {
|
|
227
|
+
if (msg.replyTo) return msg.replyTo;
|
|
228
|
+
const raw = msg.raw as { envelope?: { msg_id?: unknown } } | null | undefined;
|
|
229
|
+
const envMsgId =
|
|
230
|
+
raw && typeof raw.envelope?.msg_id === "string" && raw.envelope.msg_id
|
|
231
|
+
? raw.envelope.msg_id
|
|
232
|
+
: null;
|
|
233
|
+
return envMsgId ?? msg.id;
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
/** Factory signature for building a runtime adapter at turn dispatch time. */
|
|
212
237
|
export type RuntimeFactory = (
|
|
213
238
|
runtimeId: string,
|
|
@@ -2132,7 +2157,7 @@ export class Dispatcher {
|
|
|
2132
2157
|
}
|
|
2133
2158
|
|
|
2134
2159
|
private providerReplyTo(msg: GatewayInboundMessage): string {
|
|
2135
|
-
return msg
|
|
2160
|
+
return pickReplyToTarget(msg);
|
|
2136
2161
|
}
|
|
2137
2162
|
|
|
2138
2163
|
private emitInbound(turnId: string, msg: GatewayInboundEnvelope["message"]): void {
|
|
@@ -497,9 +497,15 @@ function isDeepseekTerminalEvent(eventName: string, payload: any): boolean {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
function isToolStarted(eventName: string, payload: any): boolean {
|
|
500
|
+
const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
|
|
500
501
|
return (
|
|
501
502
|
(eventName === "item.started" &&
|
|
502
|
-
(
|
|
503
|
+
(
|
|
504
|
+
!!payload?.tool ||
|
|
505
|
+
itemKind === "tool_call" ||
|
|
506
|
+
itemKind === "command_execution" ||
|
|
507
|
+
itemKind === "file_change"
|
|
508
|
+
)) ||
|
|
503
509
|
(payload?.event === "item.started" && !!payload?.payload?.tool)
|
|
504
510
|
);
|
|
505
511
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RouteRuleMatch,
|
|
18
18
|
} from "./config.js";
|
|
19
19
|
import {
|
|
20
|
+
acquireDaemonSingletonLock,
|
|
20
21
|
ensureNoOtherDaemonFromPidFile,
|
|
21
22
|
findOtherDaemonProcesses,
|
|
22
23
|
pidAlive,
|
|
@@ -625,13 +626,22 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
625
626
|
}
|
|
626
627
|
|
|
627
628
|
// Foreground: we ARE the daemon.
|
|
629
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
628
630
|
writeCurrentPid();
|
|
629
|
-
|
|
631
|
+
let handle: Awaited<ReturnType<typeof startDaemon>>;
|
|
632
|
+
try {
|
|
633
|
+
handle = await startDaemon({ config: cfg, configPath: CONFIG_FILE_PATH });
|
|
634
|
+
} catch (err) {
|
|
635
|
+
removePidFile();
|
|
636
|
+
singletonLock.release();
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
630
639
|
|
|
631
640
|
const shutdown = async (sig: string) => {
|
|
632
641
|
log.info("signal received", { sig });
|
|
633
642
|
await handle.stop(sig);
|
|
634
643
|
removePidFile();
|
|
644
|
+
singletonLock.release();
|
|
635
645
|
process.exit(0);
|
|
636
646
|
};
|
|
637
647
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -661,6 +671,7 @@ async function cmdStartCloud(_args: ParsedArgs): Promise<void> {
|
|
|
661
671
|
daemonInstanceId: cloudConfig.daemonInstanceId,
|
|
662
672
|
hubUrl: cloudConfig.hubUrl,
|
|
663
673
|
});
|
|
674
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
664
675
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
665
676
|
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
666
677
|
writeCurrentPid();
|
|
@@ -676,16 +687,24 @@ async function cmdStartCloud(_args: ParsedArgs): Promise<void> {
|
|
|
676
687
|
saveConfig(cfg);
|
|
677
688
|
log.info("cloud mode config initialized", { configPath: CONFIG_FILE_PATH });
|
|
678
689
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
690
|
+
let handle: Awaited<ReturnType<typeof startCloudDaemon>>;
|
|
691
|
+
try {
|
|
692
|
+
handle = await startCloudDaemon({
|
|
693
|
+
cloudConfig,
|
|
694
|
+
config: cfg,
|
|
695
|
+
configPath: CONFIG_FILE_PATH,
|
|
696
|
+
});
|
|
697
|
+
} catch (err) {
|
|
698
|
+
removePidFile();
|
|
699
|
+
singletonLock.release();
|
|
700
|
+
throw err;
|
|
701
|
+
}
|
|
684
702
|
|
|
685
703
|
const shutdown = async (sig: string): Promise<void> => {
|
|
686
704
|
log.info("signal received", { sig });
|
|
687
705
|
await handle.stop(sig);
|
|
688
706
|
removePidFile();
|
|
707
|
+
singletonLock.release();
|
|
689
708
|
process.exit(0);
|
|
690
709
|
};
|
|
691
710
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
package/src/provision.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
defaultCredentialsFile,
|
|
14
14
|
derivePublicKey,
|
|
15
15
|
loadStoredCredentials,
|
|
16
|
+
normalizeTokenExpiresAt,
|
|
16
17
|
writeCredentialsFile,
|
|
17
18
|
type AgentIdentitySnapshot,
|
|
18
19
|
type ControlAck,
|
|
@@ -74,6 +75,7 @@ import { log as daemonLog } from "./log.js";
|
|
|
74
75
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
75
76
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
76
77
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
78
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
77
79
|
import {
|
|
78
80
|
buildRuntimeSelectionExtraArgs,
|
|
79
81
|
mergeRuntimeExtraArgs,
|
|
@@ -83,6 +85,16 @@ import {
|
|
|
83
85
|
type CloudGatewayTypingEmitter,
|
|
84
86
|
} from "./cloud-gateway-runtime.js";
|
|
85
87
|
|
|
88
|
+
interface ListAgentSkillsParams {
|
|
89
|
+
agentId: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runtimeForLoadedAgent(gateway: Gateway, agentId: string): string | undefined {
|
|
93
|
+
return gateway.listManagedRoutes()
|
|
94
|
+
.find((route) => route.match?.accountId === agentId)
|
|
95
|
+
?.runtime;
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
/**
|
|
87
99
|
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
88
100
|
* provision. Mirrors the credential fields the daemon's per-agent caches
|
|
@@ -486,6 +498,34 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
486
498
|
return { ok: true, result };
|
|
487
499
|
}
|
|
488
500
|
|
|
501
|
+
case "list_agent_skills": {
|
|
502
|
+
const params = (frame.params ?? {}) as unknown as ListAgentSkillsParams;
|
|
503
|
+
if (!params.agentId) {
|
|
504
|
+
return {
|
|
505
|
+
ok: false,
|
|
506
|
+
error: { code: "bad_params", message: "list_agent_skills requires params.agentId" },
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const channels = gateway.snapshot().channels;
|
|
510
|
+
if (!channels[params.agentId]) {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
error: {
|
|
514
|
+
code: "agent_not_loaded",
|
|
515
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const runtime = runtimeForLoadedAgent(gateway, params.agentId);
|
|
520
|
+
const result = collectAgentSkillSnapshot(params.agentId, { runtime });
|
|
521
|
+
daemonLog.debug("list_agent_skills", {
|
|
522
|
+
agentId: params.agentId,
|
|
523
|
+
runtime,
|
|
524
|
+
count: result.skills.length,
|
|
525
|
+
});
|
|
526
|
+
return { ok: true, result };
|
|
527
|
+
}
|
|
528
|
+
|
|
489
529
|
case "wake_agent": {
|
|
490
530
|
return handleWakeAgent(gateway, frame.params);
|
|
491
531
|
}
|
|
@@ -1227,7 +1267,8 @@ async function materializeCredentials(
|
|
|
1227
1267
|
};
|
|
1228
1268
|
if (c.displayName) record.displayName = c.displayName;
|
|
1229
1269
|
if (c.token) record.token = c.token;
|
|
1230
|
-
|
|
1270
|
+
const tokenExpiresAt = normalizeTokenExpiresAt(c.tokenExpiresAt);
|
|
1271
|
+
if (tokenExpiresAt !== undefined) record.tokenExpiresAt = tokenExpiresAt;
|
|
1231
1272
|
if (runtime) record.runtime = runtime;
|
|
1232
1273
|
const runtimeSelection = pickRuntimeSelection(params);
|
|
1233
1274
|
if (runtimeSelection.runtimeModel) record.runtimeModel = runtimeSelection.runtimeModel;
|
package/src/skill-index.ts
CHANGED
|
@@ -23,9 +23,23 @@ export interface SoftSkillEntry {
|
|
|
23
23
|
mtimeMs: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface AgentSkillSnapshotEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
source: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
mtimeMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AgentSkillSnapshot {
|
|
34
|
+
agentId: string;
|
|
35
|
+
skills: AgentSkillSnapshotEntry[];
|
|
36
|
+
probedAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
export interface SkillIndexOptions {
|
|
27
40
|
extraDirs?: string[];
|
|
28
41
|
includeGlobal?: boolean;
|
|
42
|
+
runtime?: string;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
export function defaultSkillDirs(
|
|
@@ -33,21 +47,26 @@ export function defaultSkillDirs(
|
|
|
33
47
|
opts: SkillIndexOptions = {},
|
|
34
48
|
): Array<{ dir: string; source: string }> {
|
|
35
49
|
const includeGlobal = opts.includeGlobal !== false;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const agentClaude = {
|
|
51
|
+
dir: path.join(agentWorkspaceDir(agentId), ".claude", "skills"),
|
|
52
|
+
source: "agent-claude",
|
|
53
|
+
};
|
|
54
|
+
const agentCodex = {
|
|
55
|
+
dir: path.join(agentCodexHomeDir(agentId), "skills"),
|
|
56
|
+
source: "agent-codex",
|
|
57
|
+
};
|
|
58
|
+
const dirs: Array<{ dir: string; source: string }> =
|
|
59
|
+
runtimeFamily(opts.runtime) === "codex"
|
|
60
|
+
? [agentCodex, agentClaude]
|
|
61
|
+
: [agentClaude, agentCodex];
|
|
46
62
|
|
|
47
63
|
if (includeGlobal) {
|
|
64
|
+
const globalClaude = { dir: path.join(homedir(), ".claude", "skills"), source: "global-claude" };
|
|
65
|
+
const globalCodex = { dir: path.join(homedir(), ".codex", "skills"), source: "global-codex" };
|
|
48
66
|
dirs.push(
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
...(runtimeFamily(opts.runtime) === "codex"
|
|
68
|
+
? [globalCodex, globalClaude]
|
|
69
|
+
: [globalClaude, globalCodex]),
|
|
51
70
|
);
|
|
52
71
|
}
|
|
53
72
|
|
|
@@ -56,7 +75,7 @@ export function defaultSkillDirs(
|
|
|
56
75
|
dirs.push({ dir, source: "external" });
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
return dedupeDirs(dirs);
|
|
78
|
+
return dedupeDirs(expandSkillRoots(dirs));
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
export function scanSoftSkills(
|
|
@@ -70,7 +89,7 @@ export function scanSoftSkills(
|
|
|
70
89
|
if (!existsSync(root.dir)) continue;
|
|
71
90
|
let children: string[];
|
|
72
91
|
try {
|
|
73
|
-
children = readdirSync(root.dir);
|
|
92
|
+
children = readdirSync(root.dir).sort((a, b) => a.localeCompare(b));
|
|
74
93
|
} catch {
|
|
75
94
|
continue;
|
|
76
95
|
}
|
|
@@ -98,22 +117,37 @@ export function scanSoftSkills(
|
|
|
98
117
|
description: parsed.description,
|
|
99
118
|
mtimeMs: st.mtimeMs,
|
|
100
119
|
};
|
|
101
|
-
if (!existing || priority(root.source) < priority(existing.source)) {
|
|
120
|
+
if (!existing || priority(root.source, opts.runtime) < priority(existing.source, opts.runtime)) {
|
|
102
121
|
byName.set(entry.name, entry);
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
return Array.from(byName.values())
|
|
108
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
109
|
-
|
|
127
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function collectAgentSkillSnapshot(
|
|
131
|
+
agentId: string,
|
|
132
|
+
opts: SkillIndexOptions = {},
|
|
133
|
+
): AgentSkillSnapshot {
|
|
134
|
+
return {
|
|
135
|
+
agentId,
|
|
136
|
+
skills: scanSoftSkills(agentId, opts).map((skill) => ({
|
|
137
|
+
name: skill.name,
|
|
138
|
+
source: skill.source.startsWith("agent-") ? "workspace" : "runtime-global",
|
|
139
|
+
...(skill.description ? { description: skill.description } : {}),
|
|
140
|
+
mtimeMs: skill.mtimeMs,
|
|
141
|
+
})),
|
|
142
|
+
probedAt: Date.now(),
|
|
143
|
+
};
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
export function buildSoftSkillIndexPrompt(
|
|
113
147
|
agentId: string,
|
|
114
148
|
opts: SkillIndexOptions = {},
|
|
115
149
|
): string | null {
|
|
116
|
-
const skills = scanSoftSkills(agentId, opts);
|
|
150
|
+
const skills = scanSoftSkills(agentId, opts).slice(0, MAX_SKILLS);
|
|
117
151
|
if (skills.length === 0) return null;
|
|
118
152
|
|
|
119
153
|
const lines = [
|
|
@@ -196,7 +230,41 @@ function dedupeDirs(
|
|
|
196
230
|
return out;
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
function
|
|
233
|
+
function expandSkillRoots(
|
|
234
|
+
dirs: Array<{ dir: string; source: string }>,
|
|
235
|
+
): Array<{ dir: string; source: string }> {
|
|
236
|
+
const out: Array<{ dir: string; source: string }> = [];
|
|
237
|
+
for (const entry of dirs) {
|
|
238
|
+
out.push(entry);
|
|
239
|
+
if (entry.source.includes("codex")) {
|
|
240
|
+
out.push({ dir: path.join(entry.dir, ".system"), source: entry.source });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runtimeFamily(runtime: string | undefined): "codex" | "claude" | "other" {
|
|
247
|
+
if (runtime === "codex") return "codex";
|
|
248
|
+
if (runtime === "claude-code") return "claude";
|
|
249
|
+
return "other";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function priority(source: string, runtime: string | undefined): number {
|
|
253
|
+
if (runtimeFamily(runtime) === "codex") {
|
|
254
|
+
switch (source) {
|
|
255
|
+
case "agent-codex":
|
|
256
|
+
return 0;
|
|
257
|
+
case "global-codex":
|
|
258
|
+
return 1;
|
|
259
|
+
case "agent-claude":
|
|
260
|
+
return 2;
|
|
261
|
+
case "global-claude":
|
|
262
|
+
return 3;
|
|
263
|
+
default:
|
|
264
|
+
return 4;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
200
268
|
switch (source) {
|
|
201
269
|
case "agent-claude":
|
|
202
270
|
return 0;
|
package/src/turn-text.ts
CHANGED
|
@@ -112,6 +112,18 @@ interface BatchedEntry {
|
|
|
112
112
|
source_type?: unknown;
|
|
113
113
|
source_user_name?: unknown;
|
|
114
114
|
mentioned?: unknown;
|
|
115
|
+
reply_preview?: ReplyPreviewRaw | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Structural mirror of protocol-core ReplyPreview — kept local so the
|
|
119
|
+
* composer has no cross-package import. */
|
|
120
|
+
interface ReplyPreviewRaw {
|
|
121
|
+
msg_id?: unknown;
|
|
122
|
+
sender_id?: unknown;
|
|
123
|
+
sender_display_name?: unknown;
|
|
124
|
+
text_preview?: unknown;
|
|
125
|
+
topic_id?: unknown;
|
|
126
|
+
deleted?: unknown;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
interface RoomContextRaw {
|
|
@@ -196,6 +208,40 @@ function entryText(e: BatchedEntry): string {
|
|
|
196
208
|
return "";
|
|
197
209
|
}
|
|
198
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Format the inline quote-reply context line that prefixes a message body
|
|
213
|
+
* when the inbound envelope replies to another message. Single layer — we
|
|
214
|
+
* never render a quote-of-a-quote chain. Returns `null` when the source
|
|
215
|
+
* carries no reply_preview, so the caller can skip emitting an empty line.
|
|
216
|
+
*
|
|
217
|
+
* Both sender label and preview body are sanitized; the preview is hard-
|
|
218
|
+
* capped at 120 chars to mirror the backend truncation.
|
|
219
|
+
*/
|
|
220
|
+
function formatReplyQuoteLine(raw: unknown): string | null {
|
|
221
|
+
if (!raw || typeof raw !== "object") return null;
|
|
222
|
+
const rp = (raw as { reply_preview?: ReplyPreviewRaw | null }).reply_preview;
|
|
223
|
+
if (!rp || typeof rp !== "object") return null;
|
|
224
|
+
if (rp.deleted === true) {
|
|
225
|
+
return "[quoting (deleted message)]";
|
|
226
|
+
}
|
|
227
|
+
const senderRaw =
|
|
228
|
+
typeof rp.sender_display_name === "string" && rp.sender_display_name
|
|
229
|
+
? rp.sender_display_name
|
|
230
|
+
: typeof rp.sender_id === "string" && rp.sender_id
|
|
231
|
+
? rp.sender_id
|
|
232
|
+
: "unknown";
|
|
233
|
+
const sender = sanitizeSenderName(senderRaw);
|
|
234
|
+
const previewRaw =
|
|
235
|
+
typeof rp.text_preview === "string" ? rp.text_preview : "";
|
|
236
|
+
const previewClean = sanitizeUntrustedContent(previewRaw)
|
|
237
|
+
.replace(/[\r\n]+/g, " ")
|
|
238
|
+
.slice(0, 120);
|
|
239
|
+
if (!previewClean) {
|
|
240
|
+
return `[quoting ${sender}]`;
|
|
241
|
+
}
|
|
242
|
+
return `[quoting ${sender}: "${previewClean}"]`;
|
|
243
|
+
}
|
|
244
|
+
|
|
199
245
|
function formatRoomContext(raw: unknown, fallback: { id: string; title?: string }): string[] {
|
|
200
246
|
const r = raw && typeof raw === "object" ? (raw as RoomContextRaw) : {};
|
|
201
247
|
const roomId = typeof r.room_id === "string" && r.room_id ? r.room_id : fallback.id;
|
|
@@ -293,11 +339,13 @@ export function composeBotCordUserTurn(msg: GatewayInboundMessage): string {
|
|
|
293
339
|
"agent ID and any message they attached.]"
|
|
294
340
|
: null;
|
|
295
341
|
|
|
342
|
+
const quoteLine = formatReplyQuoteLine(msg.raw);
|
|
296
343
|
const lines: string[] = [
|
|
297
344
|
headerFields.join(" | "),
|
|
298
345
|
...formatScheduleContext(msg.raw),
|
|
299
346
|
...(isGroup ? formatRoomContext(msg.raw, { id: conversation.id, title: roomTitle }) : []),
|
|
300
347
|
`<${tag} sender="${sanitizedSenderLabel}" sender_kind="${senderKindAttr}">`,
|
|
348
|
+
...(quoteLine ? [quoteLine] : []),
|
|
301
349
|
trimmed,
|
|
302
350
|
`</${tag}>`,
|
|
303
351
|
"",
|
|
@@ -350,8 +398,10 @@ function composeBatchedTurn(
|
|
|
350
398
|
// non-owner. Still sanitize defensively.
|
|
351
399
|
const safeBody = sanitizeUntrustedContent(raw);
|
|
352
400
|
const tag = kind === "human" ? "human-message" : "agent-message";
|
|
401
|
+
const quoteLine = formatReplyQuoteLine(entry);
|
|
402
|
+
const inner = quoteLine ? `${quoteLine}\n${safeBody}` : safeBody;
|
|
353
403
|
blocks.push(
|
|
354
|
-
`<${tag} sender="${safeLabel}" sender_kind="${kind}">\n${
|
|
404
|
+
`<${tag} sender="${safeLabel}" sender_kind="${kind}">\n${inner}\n</${tag}>`,
|
|
355
405
|
);
|
|
356
406
|
if (envelopeType === "contact_request") {
|
|
357
407
|
contactRequestSenders.push(safeLabel);
|