@lobehub/cli 0.0.16-beta.1 → 0.0.16
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/index.js +558 -218
- package/man/man1/lh.1 +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11374,6 +11374,25 @@ const inferAttachmentType = (mimeType) => {
|
|
|
11374
11374
|
return "file";
|
|
11375
11375
|
};
|
|
11376
11376
|
/**
|
|
11377
|
+
* Resolve a list of `--attachment` flag values into `AttachmentInput[]`. Each
|
|
11378
|
+
* entry is either a URL or a local file path. Returns `undefined` when no
|
|
11379
|
+
* flags were passed so callers can omit the field on the wire entirely (the
|
|
11380
|
+
* TRPC schema treats absent vs empty differently). Bails the process on
|
|
11381
|
+
* load failures — a silently-dropped attachment would be worse than a
|
|
11382
|
+
* loud error here.
|
|
11383
|
+
*/
|
|
11384
|
+
const resolveAttachmentFlags = async (flags) => {
|
|
11385
|
+
if (flags.length === 0) return void 0;
|
|
11386
|
+
const out = [];
|
|
11387
|
+
for (const raw of flags) try {
|
|
11388
|
+
out.push(await parseAttachmentArg(raw));
|
|
11389
|
+
} catch (error) {
|
|
11390
|
+
log$7.error(`Failed to load attachment "${raw}": ${error.message}`);
|
|
11391
|
+
process.exit(1);
|
|
11392
|
+
}
|
|
11393
|
+
return out;
|
|
11394
|
+
};
|
|
11395
|
+
/**
|
|
11377
11396
|
* Parse a single `--attachment <value>` argument. Accepted forms:
|
|
11378
11397
|
* - `https://…` / `http://…` → fetchUrl, type inferred from extension
|
|
11379
11398
|
* - any other string → treated as a local file path;
|
|
@@ -11399,22 +11418,29 @@ const parseAttachmentArg = async (raw) => {
|
|
|
11399
11418
|
type: inferAttachmentType(mimeType)
|
|
11400
11419
|
};
|
|
11401
11420
|
};
|
|
11421
|
+
/**
|
|
11422
|
+
* Resolve the `<botIdOrAtKey>` positional argument into a `{ botId? |
|
|
11423
|
+
* messengerInstallationId? }` shape that matches the TRPC send procedures'
|
|
11424
|
+
* `exactly-one-of` constraint.
|
|
11425
|
+
*
|
|
11426
|
+
* Convention: a value prefixed with `@` is treated as a System Bot
|
|
11427
|
+
* messenger installation id (e.g. `@inst_abc123`); anything else is a
|
|
11428
|
+
* per-agent bot id. The `@` was chosen because `agent_bot_providers`.id is
|
|
11429
|
+
* always a UUID — no UUID starts with `@`, so the prefix unambiguously
|
|
11430
|
+
* disambiguates without breaking the existing UUID-only call sites.
|
|
11431
|
+
*/
|
|
11432
|
+
const resolveSendTargetArg = (value) => {
|
|
11433
|
+
if (value.startsWith("@")) return { messengerInstallationId: value.slice(1) };
|
|
11434
|
+
return { botId: value };
|
|
11435
|
+
};
|
|
11402
11436
|
function registerBotMessageCommands(bot) {
|
|
11403
11437
|
const message = bot.command("message").description("Send and manage messages on connected platforms");
|
|
11404
|
-
message.command("send <
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
attachments = [];
|
|
11408
|
-
for (const raw of options.attachment) try {
|
|
11409
|
-
attachments.push(await parseAttachmentArg(raw));
|
|
11410
|
-
} catch (error) {
|
|
11411
|
-
log$7.error(`Failed to load attachment "${raw}": ${error.message}`);
|
|
11412
|
-
process.exit(1);
|
|
11413
|
-
}
|
|
11414
|
-
}
|
|
11438
|
+
message.command("send <botIdOrAtKey>").description("Send a message to a channel. Pass a per-agent bot id, or \"@<messenger-install-id>\" to send through a System Bot messenger installation (see `lh bot messengers list`).").requiredOption("--target <channelId>", "Target channel / conversation ID").requiredOption("--message <text>", "Message content").option("--attachment <pathOrUrl>", "Attach a file by local path or remote URL (repeatable). Local paths are base64-encoded; http(s) URLs are passed as fetchUrl.", collectOptions, []).option("--reply-to <messageId>", "Reply to a specific message").option("--json", "Output JSON").action(async (botIdOrAtKey, options) => {
|
|
11439
|
+
const attachments = await resolveAttachmentFlags(options.attachment);
|
|
11440
|
+
const target = resolveSendTargetArg(botIdOrAtKey);
|
|
11415
11441
|
const result = await (await getTrpcClient()).botMessage.sendMessage.mutate({
|
|
11442
|
+
...target,
|
|
11416
11443
|
attachments,
|
|
11417
|
-
botId,
|
|
11418
11444
|
channelId: options.target,
|
|
11419
11445
|
content: options.message,
|
|
11420
11446
|
replyTo: options.replyTo
|
|
@@ -11427,6 +11453,23 @@ function registerBotMessageCommands(bot) {
|
|
|
11427
11453
|
const suffix = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
11428
11454
|
console.log(`${import_picocolors.default.green("✓")} Message sent${r.messageId ? ` (${import_picocolors.default.dim(r.messageId)})` : ""}${suffix}`);
|
|
11429
11455
|
});
|
|
11456
|
+
message.command("dm <botIdOrAtKey>").description("Send a direct message to a platform user. Pass a per-agent bot id, or \"@<messenger-install-id>\" for a System Bot install.").requiredOption("--user-id <id>", "Target user ID on the platform").requiredOption("--message <text>", "Message content").option("--attachment <pathOrUrl>", "Attach a file by local path or remote URL (repeatable). Local paths are base64-encoded; http(s) URLs are passed as fetchUrl.", collectOptions, []).option("--json", "Output JSON").action(async (botIdOrAtKey, options) => {
|
|
11457
|
+
const attachments = await resolveAttachmentFlags(options.attachment);
|
|
11458
|
+
const target = resolveSendTargetArg(botIdOrAtKey);
|
|
11459
|
+
const result = await (await getTrpcClient()).botMessage.sendDirectMessage.mutate({
|
|
11460
|
+
...target,
|
|
11461
|
+
attachments,
|
|
11462
|
+
content: options.message,
|
|
11463
|
+
userId: options.userId
|
|
11464
|
+
});
|
|
11465
|
+
if (options.json) {
|
|
11466
|
+
outputJson(result);
|
|
11467
|
+
return;
|
|
11468
|
+
}
|
|
11469
|
+
const r = result;
|
|
11470
|
+
const suffix = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
11471
|
+
console.log(`${import_picocolors.default.green("✓")} DM sent${r.messageId ? ` (${import_picocolors.default.dim(r.messageId)})` : ""}${suffix}`);
|
|
11472
|
+
});
|
|
11430
11473
|
message.command("read <botId>").description("Read messages from a channel").requiredOption("--target <channelId>", "Target channel / conversation ID").option("--limit <n>", "Max messages to fetch", String(50)).option("--before <messageId>", "Read messages before this ID").option("--after <messageId>", "Read messages after this ID").option("--start-time <timestamp>", "Start time as Unix seconds (Feishu/Lark)").option("--end-time <timestamp>", "End time as Unix seconds (Feishu/Lark)").option("--cursor <token>", "Pagination cursor from a previous response (Feishu/Lark)").option("--json", "Output JSON").action(async (botId, options) => {
|
|
11431
11474
|
const result = await (await getTrpcClient()).botMessage.readMessages.query({
|
|
11432
11475
|
after: options.after,
|
|
@@ -11627,13 +11670,17 @@ function registerBotMessageCommands(bot) {
|
|
|
11627
11670
|
"MESSAGES"
|
|
11628
11671
|
]);
|
|
11629
11672
|
});
|
|
11630
|
-
thread.command("reply <
|
|
11673
|
+
thread.command("reply <botIdOrAtKey>").description("Reply to a thread. Pass a per-agent bot id, or \"@<messenger-install-id>\" for a System Bot install.").requiredOption("--thread-id <id>", "Thread ID").requiredOption("--message <text>", "Reply content").option("--attachment <pathOrUrl>", "Attach a file by local path or remote URL (repeatable). Local paths are base64-encoded; http(s) URLs are passed as fetchUrl.", collectOptions, []).action(async (botIdOrAtKey, options) => {
|
|
11674
|
+
const attachments = await resolveAttachmentFlags(options.attachment);
|
|
11675
|
+
const target = resolveSendTargetArg(botIdOrAtKey);
|
|
11631
11676
|
const r = await (await getTrpcClient()).botMessage.replyToThread.mutate({
|
|
11632
|
-
|
|
11677
|
+
...target,
|
|
11678
|
+
attachments,
|
|
11633
11679
|
content: options.message,
|
|
11634
11680
|
threadId: options.threadId
|
|
11635
11681
|
});
|
|
11636
|
-
|
|
11682
|
+
const suffix = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
11683
|
+
console.log(`${import_picocolors.default.green("✓")} Reply sent${r.messageId ? ` (${import_picocolors.default.dim(r.messageId)})` : ""}${suffix}`);
|
|
11637
11684
|
});
|
|
11638
11685
|
const channel = message.command("channel").description("Manage channels");
|
|
11639
11686
|
channel.command("list <botId>").description("List channels").option("--server-id <id>", "Server / workspace ID").option("--filter <type>", "Filter by type").option("--json", "Output JSON").action(async (botId, options) => {
|
|
@@ -11696,6 +11743,178 @@ function collectOptions(value, previous) {
|
|
|
11696
11743
|
return [...previous, value];
|
|
11697
11744
|
}
|
|
11698
11745
|
|
|
11746
|
+
//#endregion
|
|
11747
|
+
//#region src/commands/botMessengers.ts
|
|
11748
|
+
const PLATFORMS = [
|
|
11749
|
+
"telegram",
|
|
11750
|
+
"slack",
|
|
11751
|
+
"discord"
|
|
11752
|
+
];
|
|
11753
|
+
const validatePlatform = (value) => {
|
|
11754
|
+
if (!PLATFORMS.includes(value)) throw new Error(`Unknown messenger platform: ${value}. Valid values: ${PLATFORMS.join(", ")}.`);
|
|
11755
|
+
return value;
|
|
11756
|
+
};
|
|
11757
|
+
function registerBotMessengersCommands(bot) {
|
|
11758
|
+
const messengers = bot.command("messengers").description("Manage System Bot messenger installations (Slack workspaces, Discord guilds, Telegram) and per-user account links");
|
|
11759
|
+
messengers.command("list").description("List all System Bot installations the current user has connected.").option("--json", "Output JSON").action(async (options) => {
|
|
11760
|
+
const installations = await (await getTrpcClient()).messenger.listMyInstallations.query();
|
|
11761
|
+
if (options.json) {
|
|
11762
|
+
outputJson(installations);
|
|
11763
|
+
return;
|
|
11764
|
+
}
|
|
11765
|
+
if (installations.length === 0) {
|
|
11766
|
+
console.log("No System Bot installations connected.");
|
|
11767
|
+
console.log(`\nRun ${import_picocolors.default.dim("lh bot messengers platforms")} to see what's available, then install via ${import_picocolors.default.dim("Settings → Messenger")} (OAuth requires a browser).`);
|
|
11768
|
+
return;
|
|
11769
|
+
}
|
|
11770
|
+
printTable(installations.map((i) => [
|
|
11771
|
+
i.id || "",
|
|
11772
|
+
i.platform || "",
|
|
11773
|
+
i.tenantName || i.tenantId || "(global)",
|
|
11774
|
+
i.applicationId || "",
|
|
11775
|
+
i.installedAt ? new Date(i.installedAt).toISOString().slice(0, 10) : ""
|
|
11776
|
+
]), [
|
|
11777
|
+
"INSTALLATION ID",
|
|
11778
|
+
"PLATFORM",
|
|
11779
|
+
"TENANT",
|
|
11780
|
+
"APP ID",
|
|
11781
|
+
"INSTALLED"
|
|
11782
|
+
]);
|
|
11783
|
+
console.log(`\nUse ${import_picocolors.default.dim("@<INSTALLATION ID>")} as the positional argument on ${import_picocolors.default.dim("lh bot message send/dm/thread reply")} to route through a System Bot install.`);
|
|
11784
|
+
});
|
|
11785
|
+
messengers.command("view <installationId>").description("Show detail for one installation.").option("--json", "Output JSON").action(async (installationId, options) => {
|
|
11786
|
+
const install = (await (await getTrpcClient()).messenger.listMyInstallations.query()).find((i) => i.id === installationId);
|
|
11787
|
+
if (!install) {
|
|
11788
|
+
if (options.json) outputJson(null);
|
|
11789
|
+
else console.error(import_picocolors.default.red(`Installation not found: ${installationId}`));
|
|
11790
|
+
process.exit(1);
|
|
11791
|
+
return;
|
|
11792
|
+
}
|
|
11793
|
+
if (options.json) {
|
|
11794
|
+
outputJson(install);
|
|
11795
|
+
return;
|
|
11796
|
+
}
|
|
11797
|
+
console.log(`${import_picocolors.default.bold("Installation")} ${import_picocolors.default.dim(install.id)}`);
|
|
11798
|
+
console.log(` Platform: ${install.platform}`);
|
|
11799
|
+
console.log(` Tenant: ${install.tenantName || install.tenantId || "(global)"}`);
|
|
11800
|
+
if (install.tenantId && install.tenantName) console.log(` Tenant ID: ${install.tenantId}`);
|
|
11801
|
+
console.log(` Application ID: ${install.applicationId}`);
|
|
11802
|
+
if (install.scope) console.log(` OAuth Scope: ${install.scope}`);
|
|
11803
|
+
if (install.installedAt) console.log(` Installed: ${new Date(install.installedAt).toISOString()}`);
|
|
11804
|
+
if (install.enterpriseId) console.log(` Enterprise ID: ${install.enterpriseId}`);
|
|
11805
|
+
if (install.isEnterpriseInstall) console.log(` Enterprise: yes`);
|
|
11806
|
+
});
|
|
11807
|
+
messengers.command("uninstall <installationId>").description("Revoke a workspace install. AFFECTS EVERY USER IN THAT WORKSPACE — for Slack this freezes the bot; for Discord it removes the audit entry (a guild admin must remove the bot separately). To disconnect only your own account, use `bot messengers links unlink`.").option("--yes", "Skip confirmation prompt").action(async (installationId, options) => {
|
|
11808
|
+
const client = await getTrpcClient();
|
|
11809
|
+
if (!options.yes) {
|
|
11810
|
+
const install = (await client.messenger.listMyInstallations.query()).find((i) => i.id === installationId);
|
|
11811
|
+
const label = install ? `${install.platform} (${install.tenantName || install.tenantId || "global"})` : installationId;
|
|
11812
|
+
if (!await confirm(`${import_picocolors.default.yellow("⚠")} Uninstall ${import_picocolors.default.bold(label)} — this revokes the install for the whole workspace. Continue?`)) {
|
|
11813
|
+
console.log("Aborted.");
|
|
11814
|
+
return;
|
|
11815
|
+
}
|
|
11816
|
+
}
|
|
11817
|
+
await client.messenger.uninstallInstallation.mutate({ installationId });
|
|
11818
|
+
console.log(`${import_picocolors.default.green("✓")} Installation ${import_picocolors.default.dim(installationId)} revoked.`);
|
|
11819
|
+
});
|
|
11820
|
+
messengers.command("platforms").description("List the platforms available for System Bot OAuth install.").option("--json", "Output JSON").action(async (options) => {
|
|
11821
|
+
const platforms = await (await getTrpcClient()).messenger.availablePlatforms.query();
|
|
11822
|
+
if (options.json) {
|
|
11823
|
+
outputJson(platforms);
|
|
11824
|
+
return;
|
|
11825
|
+
}
|
|
11826
|
+
if (platforms.length === 0) {
|
|
11827
|
+
console.log("No System Bot platforms are configured on this deployment.");
|
|
11828
|
+
return;
|
|
11829
|
+
}
|
|
11830
|
+
printTable(platforms.map((p) => [
|
|
11831
|
+
p.id || "",
|
|
11832
|
+
p.name || "",
|
|
11833
|
+
p.appId || "",
|
|
11834
|
+
p.botUsername || ""
|
|
11835
|
+
]), [
|
|
11836
|
+
"ID",
|
|
11837
|
+
"NAME",
|
|
11838
|
+
"APP ID",
|
|
11839
|
+
"BOT USERNAME"
|
|
11840
|
+
]);
|
|
11841
|
+
console.log(`\nInstalls are initiated via ${import_picocolors.default.dim("Settings → Messenger")} in the web UI (OAuth needs a browser).`);
|
|
11842
|
+
});
|
|
11843
|
+
const links = messengers.command("links").description("Manage per-user account links — routing of inbound IM to your agents");
|
|
11844
|
+
links.command("list").description("List all your account links across platforms and tenants.").option("--json", "Output JSON").action(async (options) => {
|
|
11845
|
+
const linkRows = await (await getTrpcClient()).messenger.listMyLinks.query();
|
|
11846
|
+
if (options.json) {
|
|
11847
|
+
outputJson(linkRows);
|
|
11848
|
+
return;
|
|
11849
|
+
}
|
|
11850
|
+
if (linkRows.length === 0) {
|
|
11851
|
+
console.log("No account links yet. Complete verify-im on a platform first.");
|
|
11852
|
+
return;
|
|
11853
|
+
}
|
|
11854
|
+
printTable(linkRows.map((l) => [
|
|
11855
|
+
l.platform || "",
|
|
11856
|
+
l.tenantId || "(global)",
|
|
11857
|
+
l.activeAgentId || import_picocolors.default.dim("(unset)"),
|
|
11858
|
+
l.platformUsername || l.platformUserId || ""
|
|
11859
|
+
]), [
|
|
11860
|
+
"PLATFORM",
|
|
11861
|
+
"TENANT",
|
|
11862
|
+
"ACTIVE AGENT",
|
|
11863
|
+
"PLATFORM USER"
|
|
11864
|
+
]);
|
|
11865
|
+
});
|
|
11866
|
+
links.command("view <platform>").description("Show one account link.").option("--tenant <id>", "Tenant scope (Slack workspace id). Omit for global-bot platforms.").option("--json", "Output JSON").action(async (platform, options) => {
|
|
11867
|
+
const client = await getTrpcClient();
|
|
11868
|
+
const platformValidated = validatePlatform(platform);
|
|
11869
|
+
const link = await client.messenger.getMyLink.query({
|
|
11870
|
+
platform: platformValidated,
|
|
11871
|
+
tenantId: options.tenant
|
|
11872
|
+
});
|
|
11873
|
+
if (!link) {
|
|
11874
|
+
console.error(import_picocolors.default.red(`No link found for ${platform}${options.tenant ? ` (tenant ${options.tenant})` : ""}`));
|
|
11875
|
+
process.exit(1);
|
|
11876
|
+
return;
|
|
11877
|
+
}
|
|
11878
|
+
if (options.json) {
|
|
11879
|
+
outputJson(link);
|
|
11880
|
+
return;
|
|
11881
|
+
}
|
|
11882
|
+
console.log(`${import_picocolors.default.bold("Link")} ${import_picocolors.default.dim(link.platform)}`);
|
|
11883
|
+
if (link.tenantId) console.log(` Tenant ID: ${link.tenantId}`);
|
|
11884
|
+
console.log(` Platform User ID: ${link.platformUserId}`);
|
|
11885
|
+
if (link.platformUsername) console.log(` Platform User: ${link.platformUsername}`);
|
|
11886
|
+
console.log(` Active Agent: ${link.activeAgentId ?? import_picocolors.default.dim("(unset)")}`);
|
|
11887
|
+
});
|
|
11888
|
+
links.command("set-agent <platform>").description("Change which agent receives inbound IM on a platform link.").requiredOption("--agent <id>", "Agent id to route to, or \"none\" to clear the active agent.").option("--tenant <id>", "Tenant scope (Slack workspace id). Omit for global-bot platforms.").action(async (platform, options) => {
|
|
11889
|
+
const client = await getTrpcClient();
|
|
11890
|
+
const platformValidated = validatePlatform(platform);
|
|
11891
|
+
const agentId = options.agent === "none" ? null : options.agent;
|
|
11892
|
+
await client.messenger.setActiveAgent.mutate({
|
|
11893
|
+
agentId,
|
|
11894
|
+
platform: platformValidated,
|
|
11895
|
+
tenantId: options.tenant
|
|
11896
|
+
});
|
|
11897
|
+
const scope = options.tenant ? ` (tenant ${options.tenant})` : "";
|
|
11898
|
+
const target = agentId === null ? "cleared" : `set to agent ${import_picocolors.default.dim(agentId)}`;
|
|
11899
|
+
console.log(`${import_picocolors.default.green("✓")} Active agent for ${platform}${scope} ${target}.`);
|
|
11900
|
+
});
|
|
11901
|
+
links.command("unlink <platform>").description("Remove your account link for a platform. Workspace install is unaffected — colleagues can still use the bot.").option("--tenant <id>", "Tenant scope (Slack workspace id). Omit for global-bot platforms.").option("--yes", "Skip confirmation prompt").action(async (platform, options) => {
|
|
11902
|
+
const client = await getTrpcClient();
|
|
11903
|
+
const platformValidated = validatePlatform(platform);
|
|
11904
|
+
if (!options.yes) {
|
|
11905
|
+
if (!await confirm(`Unlink your account from ${import_picocolors.default.bold(platform)}${options.tenant ? ` (tenant ${options.tenant})` : ""}?`)) {
|
|
11906
|
+
console.log("Aborted.");
|
|
11907
|
+
return;
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11910
|
+
await client.messenger.unlink.mutate({
|
|
11911
|
+
platform: platformValidated,
|
|
11912
|
+
tenantId: options.tenant
|
|
11913
|
+
});
|
|
11914
|
+
console.log(`${import_picocolors.default.green("✓")} Unlinked.`);
|
|
11915
|
+
});
|
|
11916
|
+
}
|
|
11917
|
+
|
|
11699
11918
|
//#endregion
|
|
11700
11919
|
//#region src/commands/bot.ts
|
|
11701
11920
|
const DM_POLICIES = [
|
|
@@ -12004,6 +12223,7 @@ function registerWatchKeywordsCommand(bot) {
|
|
|
12004
12223
|
function registerBotCommand(program) {
|
|
12005
12224
|
const bot = program.command("bot").description("Manage bot integrations");
|
|
12006
12225
|
registerBotMessageCommands(bot);
|
|
12226
|
+
registerBotMessengersCommands(bot);
|
|
12007
12227
|
bot.command("platforms").description("List supported platforms and their required credentials").option("--json", "Output JSON").action(async (options) => {
|
|
12008
12228
|
const platforms = await (await getTrpcClient()).agentBotProvider.listPlatforms.query();
|
|
12009
12229
|
if (options.json) {
|
|
@@ -16453,6 +16673,318 @@ async function resolveToken(options) {
|
|
|
16453
16673
|
process.exit(1);
|
|
16454
16674
|
}
|
|
16455
16675
|
|
|
16676
|
+
//#endregion
|
|
16677
|
+
//#region src/daemon/taskRegistry.ts
|
|
16678
|
+
function getRegistryPath() {
|
|
16679
|
+
return path.join(os.homedir(), ".lobehub", "task-registry.json");
|
|
16680
|
+
}
|
|
16681
|
+
function readRegistry() {
|
|
16682
|
+
try {
|
|
16683
|
+
return JSON.parse(fs.readFileSync(getRegistryPath(), "utf8"));
|
|
16684
|
+
} catch {
|
|
16685
|
+
return {};
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16688
|
+
function writeRegistry(entries) {
|
|
16689
|
+
const dir = path.dirname(getRegistryPath());
|
|
16690
|
+
fs.mkdirSync(dir, {
|
|
16691
|
+
mode: 448,
|
|
16692
|
+
recursive: true
|
|
16693
|
+
});
|
|
16694
|
+
fs.writeFileSync(getRegistryPath(), JSON.stringify(entries, null, 2), { mode: 384 });
|
|
16695
|
+
}
|
|
16696
|
+
function saveTask(entry) {
|
|
16697
|
+
const registry = readRegistry();
|
|
16698
|
+
registry[entry.taskId] = entry;
|
|
16699
|
+
writeRegistry(registry);
|
|
16700
|
+
}
|
|
16701
|
+
function getTask(taskId) {
|
|
16702
|
+
return readRegistry()[taskId];
|
|
16703
|
+
}
|
|
16704
|
+
function removeTask(taskId) {
|
|
16705
|
+
const registry = readRegistry();
|
|
16706
|
+
delete registry[taskId];
|
|
16707
|
+
writeRegistry(registry);
|
|
16708
|
+
}
|
|
16709
|
+
|
|
16710
|
+
//#endregion
|
|
16711
|
+
//#region src/tools/heteroTask.ts
|
|
16712
|
+
const DEFAULT_HERMES_PORT = 3456;
|
|
16713
|
+
/** Resolve the absolute path to the `lh` binary to avoid PATH issues in child processes. */
|
|
16714
|
+
function resolveLhPath() {
|
|
16715
|
+
try {
|
|
16716
|
+
return execFileSync("which", ["lh"], { encoding: "utf8" }).trim();
|
|
16717
|
+
} catch {
|
|
16718
|
+
return "lh";
|
|
16719
|
+
}
|
|
16720
|
+
}
|
|
16721
|
+
/**
|
|
16722
|
+
* Check whether an openclaw session already exists for the given topicId.
|
|
16723
|
+
* The session key format is `agent:<agentId>:explicit:<sessionId>`.
|
|
16724
|
+
* Returns false on any error so that callers default to injecting the full protocol.
|
|
16725
|
+
*/
|
|
16726
|
+
function openclawSessionExists(agentId, topicId) {
|
|
16727
|
+
try {
|
|
16728
|
+
const raw = execFileSync("openclaw", [
|
|
16729
|
+
"sessions",
|
|
16730
|
+
"--agent",
|
|
16731
|
+
agentId,
|
|
16732
|
+
"--json"
|
|
16733
|
+
], { encoding: "utf8" });
|
|
16734
|
+
const data = JSON.parse(raw);
|
|
16735
|
+
const expectedKey = `agent:${agentId}:explicit:${topicId}`;
|
|
16736
|
+
return data.sessions?.some((s) => s.key === expectedKey) ?? false;
|
|
16737
|
+
} catch {
|
|
16738
|
+
return false;
|
|
16739
|
+
}
|
|
16740
|
+
}
|
|
16741
|
+
function getHermesPort() {
|
|
16742
|
+
const env = process.env.HERMES_GATEWAY_PORT;
|
|
16743
|
+
if (env) {
|
|
16744
|
+
const parsed = Number.parseInt(env, 10);
|
|
16745
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
16746
|
+
}
|
|
16747
|
+
return DEFAULT_HERMES_PORT;
|
|
16748
|
+
}
|
|
16749
|
+
async function isHermesGatewayRunning(port) {
|
|
16750
|
+
try {
|
|
16751
|
+
return (await fetch(`http://localhost:${port}/health`)).ok;
|
|
16752
|
+
} catch {
|
|
16753
|
+
return false;
|
|
16754
|
+
}
|
|
16755
|
+
}
|
|
16756
|
+
async function startHermesGateway(port) {
|
|
16757
|
+
spawn("hermes", ["gateway", "start"], {
|
|
16758
|
+
detached: true,
|
|
16759
|
+
env: { ...process.env },
|
|
16760
|
+
stdio: "ignore"
|
|
16761
|
+
}).unref();
|
|
16762
|
+
const deadline = Date.now() + 1e4;
|
|
16763
|
+
while (Date.now() < deadline) {
|
|
16764
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
16765
|
+
if (await isHermesGatewayRunning(port)) return;
|
|
16766
|
+
}
|
|
16767
|
+
throw new Error(`Hermes gateway did not start within 10s on port ${port}`);
|
|
16768
|
+
}
|
|
16769
|
+
async function sendAutoNotify(topicId, taskId, text, agentId) {
|
|
16770
|
+
try {
|
|
16771
|
+
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
16772
|
+
agentId,
|
|
16773
|
+
content: text,
|
|
16774
|
+
role: "assistant",
|
|
16775
|
+
topicId
|
|
16776
|
+
});
|
|
16777
|
+
} catch (err) {
|
|
16778
|
+
log$7.error("Failed to send auto-notify:", err instanceof Error ? err.message : String(err));
|
|
16779
|
+
}
|
|
16780
|
+
}
|
|
16781
|
+
/**
|
|
16782
|
+
* Signal remote hetero task completion to the server so it can publish
|
|
16783
|
+
* `agent_runtime_end` to the gateway WS and close the frontend subscription.
|
|
16784
|
+
* Called on clean process exit (code=0, no signal) — error exits go through
|
|
16785
|
+
* `sendAutoNotify` which writes an error message AND triggers completion via
|
|
16786
|
+
* the `done` flag.
|
|
16787
|
+
*/
|
|
16788
|
+
async function sendDoneSignal(topicId, agentId) {
|
|
16789
|
+
try {
|
|
16790
|
+
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
16791
|
+
agentId,
|
|
16792
|
+
content: "",
|
|
16793
|
+
done: true,
|
|
16794
|
+
role: "assistant",
|
|
16795
|
+
topicId
|
|
16796
|
+
});
|
|
16797
|
+
} catch (err) {
|
|
16798
|
+
log$7.error("Failed to send done signal:", err instanceof Error ? err.message : String(err));
|
|
16799
|
+
}
|
|
16800
|
+
}
|
|
16801
|
+
/**
|
|
16802
|
+
* Build the notify protocol injected into the first message of a new hetero-agent session.
|
|
16803
|
+
* Tells the agent how to push updates back to the LobeHub user via `lh notify`.
|
|
16804
|
+
*/
|
|
16805
|
+
function buildNotifyProtocol(lhPath, topicId) {
|
|
16806
|
+
return `## Context: This task was dispatched by LobeHub
|
|
16807
|
+
|
|
16808
|
+
This conversation / task was sent to you by the **LobeHub platform** on behalf of a user. You are running as a background agent; the user is waiting for your response inside the LobeHub chat interface.
|
|
16809
|
+
|
|
16810
|
+
**When to call notify**: any time you have something meaningful to tell the user — a key finding, a decision you made, a result, a question, or your final answer. Think of it as speaking directly to the user in the chat window.
|
|
16811
|
+
|
|
16812
|
+
**What to hide**: internal work details such as tool call sequences, file reads, intermediate command output, retries, or low-level reasoning steps. The user cares about outcomes and insights, not your step-by-step mechanics.
|
|
16813
|
+
|
|
16814
|
+
## Sending messages back to the user
|
|
16815
|
+
|
|
16816
|
+
Use the \`${lhPath} notify\` command. All your updates appear as a **single message bubble** in the UI — create it once and update it in place.\n\n**Step 1 — Open the bubble on your first meaningful update** (captures the messageId):\n\`\`\`\nMSG_ID=$(${lhPath} notify --topic ${topicId} --role assistant --content "Starting..." --json | grep -o '"messageId":"[^"]*"' | cut -d'"' -f4)\n\`\`\`\n\n**Step 2 — Update the same bubble as you make progress**:\n\`\`\`\n${lhPath} notify --topic ${topicId} --role assistant --message-id "$MSG_ID" --content "Still working..."\n\`\`\`\n\n**Step 3 — Replace with your complete, final response when done**:\n\`\`\`\n${lhPath} notify --topic ${topicId} --role assistant --message-id "$MSG_ID" --content "<your full response here>"\n\`\`\`\n\nRules:\n- Always use \`--json\` on the first call and capture \`messageId\` from the output.\n- Always pass \`--message-id\` on every subsequent call so updates overwrite the same bubble.\n- Write what matters to the user — not implementation steps or internal tool calls.\n- Call notify at least once when the task is done, even if there were no intermediate updates.`;
|
|
16817
|
+
}
|
|
16818
|
+
async function runHeteroTask(params) {
|
|
16819
|
+
const { agentId, agentType, cwd, operationId, prompt, taskId, topicId } = params;
|
|
16820
|
+
const workDir = cwd || process.cwd();
|
|
16821
|
+
const lhPath = resolveLhPath();
|
|
16822
|
+
if (agentType === "openclaw") {
|
|
16823
|
+
const openclawAgent = process.env.OPENCLAW_AGENT_ID ?? "main";
|
|
16824
|
+
const child = spawn("openclaw", [
|
|
16825
|
+
"agent",
|
|
16826
|
+
"--agent",
|
|
16827
|
+
openclawAgent,
|
|
16828
|
+
"--session-id",
|
|
16829
|
+
topicId,
|
|
16830
|
+
"--message",
|
|
16831
|
+
!openclawSessionExists(openclawAgent, topicId) ? `${prompt}\n\n${buildNotifyProtocol(lhPath, topicId)}` : prompt,
|
|
16832
|
+
"--local"
|
|
16833
|
+
], {
|
|
16834
|
+
cwd: workDir,
|
|
16835
|
+
detached: true,
|
|
16836
|
+
env: { ...process.env },
|
|
16837
|
+
stdio: "ignore"
|
|
16838
|
+
});
|
|
16839
|
+
const pid = child.pid;
|
|
16840
|
+
if (pid === void 0) throw new Error("Failed to get PID for openclaw process");
|
|
16841
|
+
child.unref();
|
|
16842
|
+
saveTask({
|
|
16843
|
+
agentId,
|
|
16844
|
+
agentType,
|
|
16845
|
+
operationId,
|
|
16846
|
+
pid,
|
|
16847
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16848
|
+
taskId,
|
|
16849
|
+
topicId
|
|
16850
|
+
});
|
|
16851
|
+
log$7.info(`OpenClaw task started: taskId=${taskId} pid=${pid} agent=${openclawAgent}`);
|
|
16852
|
+
child.on("close", (code, signal) => {
|
|
16853
|
+
removeTask(taskId);
|
|
16854
|
+
if (code !== 0 || signal !== null) {
|
|
16855
|
+
sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId);
|
|
16856
|
+
sendDoneSignal(topicId, agentId);
|
|
16857
|
+
} else sendDoneSignal(topicId, agentId);
|
|
16858
|
+
});
|
|
16859
|
+
return JSON.stringify({
|
|
16860
|
+
pid,
|
|
16861
|
+
taskId
|
|
16862
|
+
});
|
|
16863
|
+
}
|
|
16864
|
+
if (agentType === "hermes") {
|
|
16865
|
+
const port = getHermesPort();
|
|
16866
|
+
if (!await isHermesGatewayRunning(port)) {
|
|
16867
|
+
log$7.info(`Hermes gateway not running on port ${port}, starting...`);
|
|
16868
|
+
await startHermesGateway(port);
|
|
16869
|
+
}
|
|
16870
|
+
const res = await fetch(`http://localhost:${port}/message`, {
|
|
16871
|
+
body: JSON.stringify({
|
|
16872
|
+
content: prompt,
|
|
16873
|
+
operationId
|
|
16874
|
+
}),
|
|
16875
|
+
headers: { "Content-Type": "application/json" },
|
|
16876
|
+
method: "POST"
|
|
16877
|
+
});
|
|
16878
|
+
if (!res.ok) throw new Error(`Hermes gateway returned ${res.status}: ${await res.text()}`);
|
|
16879
|
+
saveTask({
|
|
16880
|
+
agentId,
|
|
16881
|
+
agentType,
|
|
16882
|
+
operationId,
|
|
16883
|
+
pid: 0,
|
|
16884
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16885
|
+
taskId,
|
|
16886
|
+
topicId
|
|
16887
|
+
});
|
|
16888
|
+
log$7.info(`Hermes task dispatched: taskId=${taskId} operationId=${operationId}`);
|
|
16889
|
+
return JSON.stringify({
|
|
16890
|
+
operationId,
|
|
16891
|
+
taskId
|
|
16892
|
+
});
|
|
16893
|
+
}
|
|
16894
|
+
throw new Error(`Unsupported agentType: ${agentType}`);
|
|
16895
|
+
}
|
|
16896
|
+
async function cancelHeteroTask(params) {
|
|
16897
|
+
const { signal = "SIGINT", taskId } = params;
|
|
16898
|
+
const entry = getTask(taskId);
|
|
16899
|
+
if (!entry) return JSON.stringify({
|
|
16900
|
+
message: `No task found with taskId: ${taskId}`,
|
|
16901
|
+
success: false
|
|
16902
|
+
});
|
|
16903
|
+
if (entry.agentType === "hermes") {
|
|
16904
|
+
const port = getHermesPort();
|
|
16905
|
+
try {
|
|
16906
|
+
await fetch(`http://localhost:${port}/stop`, {
|
|
16907
|
+
body: JSON.stringify({ operationId: entry.operationId }),
|
|
16908
|
+
headers: { "Content-Type": "application/json" },
|
|
16909
|
+
method: "POST"
|
|
16910
|
+
});
|
|
16911
|
+
} catch (err) {
|
|
16912
|
+
log$7.warn(`Failed to send /stop to Hermes gateway: ${err instanceof Error ? err.message : String(err)}`);
|
|
16913
|
+
}
|
|
16914
|
+
removeTask(taskId);
|
|
16915
|
+
await sendAutoNotify(entry.topicId, taskId, "Task cancelled", entry.agentId);
|
|
16916
|
+
return JSON.stringify({ taskId });
|
|
16917
|
+
}
|
|
16918
|
+
try {
|
|
16919
|
+
process.kill(entry.pid, signal);
|
|
16920
|
+
} catch (err) {
|
|
16921
|
+
log$7.warn(`Failed to send ${signal} to pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16922
|
+
removeTask(taskId);
|
|
16923
|
+
await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId);
|
|
16924
|
+
}
|
|
16925
|
+
return JSON.stringify({
|
|
16926
|
+
pid: entry.pid,
|
|
16927
|
+
signal,
|
|
16928
|
+
taskId
|
|
16929
|
+
});
|
|
16930
|
+
}
|
|
16931
|
+
|
|
16932
|
+
//#endregion
|
|
16933
|
+
//#region src/tools/checkPlatformCapability.ts
|
|
16934
|
+
/**
|
|
16935
|
+
* Probe whether a specific agent platform is available on this device.
|
|
16936
|
+
* Dispatched by the server via `device.checkCapability` tRPC procedure.
|
|
16937
|
+
*
|
|
16938
|
+
* - openclaw: runs `openclaw --version` and parses the output
|
|
16939
|
+
* - hermes: hits the gateway health endpoint on the configured port
|
|
16940
|
+
*/
|
|
16941
|
+
async function checkPlatformCapability(params) {
|
|
16942
|
+
const { platform } = params;
|
|
16943
|
+
if (platform === "openclaw") try {
|
|
16944
|
+
return {
|
|
16945
|
+
available: true,
|
|
16946
|
+
version: execFileSync("openclaw", ["--version"], {
|
|
16947
|
+
encoding: "utf8",
|
|
16948
|
+
timeout: 5e3
|
|
16949
|
+
}).trim().split(/\s+/).at(-1)
|
|
16950
|
+
};
|
|
16951
|
+
} catch (err) {
|
|
16952
|
+
return {
|
|
16953
|
+
available: false,
|
|
16954
|
+
reason: err instanceof Error ? err.message : "openclaw not found or failed to run"
|
|
16955
|
+
};
|
|
16956
|
+
}
|
|
16957
|
+
if (platform === "hermes") {
|
|
16958
|
+
const port = getHermesPort();
|
|
16959
|
+
try {
|
|
16960
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(5e3) });
|
|
16961
|
+
if (res.ok) {
|
|
16962
|
+
let version;
|
|
16963
|
+
try {
|
|
16964
|
+
version = (await res.json()).version;
|
|
16965
|
+
} catch {}
|
|
16966
|
+
return {
|
|
16967
|
+
available: true,
|
|
16968
|
+
version
|
|
16969
|
+
};
|
|
16970
|
+
}
|
|
16971
|
+
return {
|
|
16972
|
+
available: false,
|
|
16973
|
+
reason: `Hermes gateway returned HTTP ${res.status}`
|
|
16974
|
+
};
|
|
16975
|
+
} catch (err) {
|
|
16976
|
+
return {
|
|
16977
|
+
available: false,
|
|
16978
|
+
reason: err instanceof Error ? err.message : `Hermes gateway not reachable on port ${port}`
|
|
16979
|
+
};
|
|
16980
|
+
}
|
|
16981
|
+
}
|
|
16982
|
+
return {
|
|
16983
|
+
available: false,
|
|
16984
|
+
reason: `Unknown platform: ${platform}`
|
|
16985
|
+
};
|
|
16986
|
+
}
|
|
16987
|
+
|
|
16456
16988
|
//#endregion
|
|
16457
16989
|
//#region node_modules/.pnpm/diff@8.0.4/node_modules/diff/libesm/diff/base.js
|
|
16458
16990
|
var Diff = class {
|
|
@@ -205897,6 +206429,11 @@ const TEXT_READABLE_FILE_TYPES = [
|
|
|
205897
206429
|
"scala",
|
|
205898
206430
|
"groovy",
|
|
205899
206431
|
"gradle",
|
|
206432
|
+
"tex",
|
|
206433
|
+
"sty",
|
|
206434
|
+
"cls",
|
|
206435
|
+
"bib",
|
|
206436
|
+
"bbl",
|
|
205900
206437
|
"log",
|
|
205901
206438
|
"sql",
|
|
205902
206439
|
"patch",
|
|
@@ -206605,207 +207142,6 @@ async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_b
|
|
|
206605
207142
|
}
|
|
206606
207143
|
}
|
|
206607
207144
|
|
|
206608
|
-
//#endregion
|
|
206609
|
-
//#region src/daemon/taskRegistry.ts
|
|
206610
|
-
function getRegistryPath() {
|
|
206611
|
-
return path.join(os.homedir(), ".lobehub", "task-registry.json");
|
|
206612
|
-
}
|
|
206613
|
-
function readRegistry() {
|
|
206614
|
-
try {
|
|
206615
|
-
return JSON.parse(fs.readFileSync(getRegistryPath(), "utf8"));
|
|
206616
|
-
} catch {
|
|
206617
|
-
return {};
|
|
206618
|
-
}
|
|
206619
|
-
}
|
|
206620
|
-
function writeRegistry(entries) {
|
|
206621
|
-
const dir = path.dirname(getRegistryPath());
|
|
206622
|
-
fs.mkdirSync(dir, {
|
|
206623
|
-
mode: 448,
|
|
206624
|
-
recursive: true
|
|
206625
|
-
});
|
|
206626
|
-
fs.writeFileSync(getRegistryPath(), JSON.stringify(entries, null, 2), { mode: 384 });
|
|
206627
|
-
}
|
|
206628
|
-
function saveTask(entry) {
|
|
206629
|
-
const registry = readRegistry();
|
|
206630
|
-
registry[entry.taskId] = entry;
|
|
206631
|
-
writeRegistry(registry);
|
|
206632
|
-
}
|
|
206633
|
-
function getTask(taskId) {
|
|
206634
|
-
return readRegistry()[taskId];
|
|
206635
|
-
}
|
|
206636
|
-
function removeTask(taskId) {
|
|
206637
|
-
const registry = readRegistry();
|
|
206638
|
-
delete registry[taskId];
|
|
206639
|
-
writeRegistry(registry);
|
|
206640
|
-
}
|
|
206641
|
-
|
|
206642
|
-
//#endregion
|
|
206643
|
-
//#region src/tools/heteroTask.ts
|
|
206644
|
-
const DEFAULT_HERMES_PORT = 3456;
|
|
206645
|
-
/** Resolve the absolute path to the `lh` binary to avoid PATH issues in child processes. */
|
|
206646
|
-
function resolveLhPath() {
|
|
206647
|
-
try {
|
|
206648
|
-
return execFileSync("which", ["lh"], { encoding: "utf8" }).trim();
|
|
206649
|
-
} catch {
|
|
206650
|
-
return "lh";
|
|
206651
|
-
}
|
|
206652
|
-
}
|
|
206653
|
-
function getHermesPort() {
|
|
206654
|
-
const env = process.env.HERMES_GATEWAY_PORT;
|
|
206655
|
-
if (env) {
|
|
206656
|
-
const parsed = Number.parseInt(env, 10);
|
|
206657
|
-
if (!Number.isNaN(parsed)) return parsed;
|
|
206658
|
-
}
|
|
206659
|
-
return DEFAULT_HERMES_PORT;
|
|
206660
|
-
}
|
|
206661
|
-
async function isHermesGatewayRunning(port) {
|
|
206662
|
-
try {
|
|
206663
|
-
return (await fetch(`http://localhost:${port}/health`)).ok;
|
|
206664
|
-
} catch {
|
|
206665
|
-
return false;
|
|
206666
|
-
}
|
|
206667
|
-
}
|
|
206668
|
-
async function startHermesGateway(port) {
|
|
206669
|
-
spawn("hermes", ["gateway", "start"], {
|
|
206670
|
-
detached: true,
|
|
206671
|
-
env: { ...process.env },
|
|
206672
|
-
stdio: "ignore"
|
|
206673
|
-
}).unref();
|
|
206674
|
-
const deadline = Date.now() + 1e4;
|
|
206675
|
-
while (Date.now() < deadline) {
|
|
206676
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
206677
|
-
if (await isHermesGatewayRunning(port)) return;
|
|
206678
|
-
}
|
|
206679
|
-
throw new Error(`Hermes gateway did not start within 10s on port ${port}`);
|
|
206680
|
-
}
|
|
206681
|
-
async function sendAutoNotify(topicId, taskId, text, agentId) {
|
|
206682
|
-
try {
|
|
206683
|
-
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
206684
|
-
agentId,
|
|
206685
|
-
content: JSON.stringify({
|
|
206686
|
-
stage: "error",
|
|
206687
|
-
taskId,
|
|
206688
|
-
text
|
|
206689
|
-
}),
|
|
206690
|
-
topicId
|
|
206691
|
-
});
|
|
206692
|
-
} catch (err) {
|
|
206693
|
-
log$7.error("Failed to send auto-notify:", err instanceof Error ? err.message : String(err));
|
|
206694
|
-
}
|
|
206695
|
-
}
|
|
206696
|
-
async function runHeteroTask(params) {
|
|
206697
|
-
const { agentId, agentType, cwd, operationId, prompt, taskId, topicId } = params;
|
|
206698
|
-
const workDir = cwd || process.cwd();
|
|
206699
|
-
if (agentType === "openclaw") {
|
|
206700
|
-
const enrichedPrompt = `${prompt}\n\nWhen your task is complete, run this shell command to report back:\n${resolveLhPath()} notify --topic ${topicId} --content '${JSON.stringify({
|
|
206701
|
-
stage: "done",
|
|
206702
|
-
taskId,
|
|
206703
|
-
text: "Task completed"
|
|
206704
|
-
})}'`;
|
|
206705
|
-
const openclawAgent = process.env.OPENCLAW_AGENT_ID ?? "main";
|
|
206706
|
-
const child = spawn("openclaw", [
|
|
206707
|
-
"agent",
|
|
206708
|
-
"--agent",
|
|
206709
|
-
openclawAgent,
|
|
206710
|
-
"--message",
|
|
206711
|
-
enrichedPrompt,
|
|
206712
|
-
"--local"
|
|
206713
|
-
], {
|
|
206714
|
-
cwd: workDir,
|
|
206715
|
-
detached: true,
|
|
206716
|
-
env: { ...process.env },
|
|
206717
|
-
stdio: "ignore"
|
|
206718
|
-
});
|
|
206719
|
-
const pid = child.pid;
|
|
206720
|
-
if (pid === void 0) throw new Error("Failed to get PID for openclaw process");
|
|
206721
|
-
child.unref();
|
|
206722
|
-
saveTask({
|
|
206723
|
-
agentId,
|
|
206724
|
-
agentType,
|
|
206725
|
-
operationId,
|
|
206726
|
-
pid,
|
|
206727
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
206728
|
-
taskId,
|
|
206729
|
-
topicId
|
|
206730
|
-
});
|
|
206731
|
-
log$7.info(`OpenClaw task started: taskId=${taskId} pid=${pid} agent=${openclawAgent}`);
|
|
206732
|
-
child.on("close", (code, signal) => {
|
|
206733
|
-
removeTask(taskId);
|
|
206734
|
-
if (code !== 0 || signal !== null) sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId);
|
|
206735
|
-
});
|
|
206736
|
-
return JSON.stringify({
|
|
206737
|
-
pid,
|
|
206738
|
-
taskId
|
|
206739
|
-
});
|
|
206740
|
-
}
|
|
206741
|
-
if (agentType === "hermes") {
|
|
206742
|
-
const port = getHermesPort();
|
|
206743
|
-
if (!await isHermesGatewayRunning(port)) {
|
|
206744
|
-
log$7.info(`Hermes gateway not running on port ${port}, starting...`);
|
|
206745
|
-
await startHermesGateway(port);
|
|
206746
|
-
}
|
|
206747
|
-
const res = await fetch(`http://localhost:${port}/message`, {
|
|
206748
|
-
body: JSON.stringify({
|
|
206749
|
-
content: prompt,
|
|
206750
|
-
operationId
|
|
206751
|
-
}),
|
|
206752
|
-
headers: { "Content-Type": "application/json" },
|
|
206753
|
-
method: "POST"
|
|
206754
|
-
});
|
|
206755
|
-
if (!res.ok) throw new Error(`Hermes gateway returned ${res.status}: ${await res.text()}`);
|
|
206756
|
-
saveTask({
|
|
206757
|
-
agentId,
|
|
206758
|
-
agentType,
|
|
206759
|
-
operationId,
|
|
206760
|
-
pid: 0,
|
|
206761
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
206762
|
-
taskId,
|
|
206763
|
-
topicId
|
|
206764
|
-
});
|
|
206765
|
-
log$7.info(`Hermes task dispatched: taskId=${taskId} operationId=${operationId}`);
|
|
206766
|
-
return JSON.stringify({
|
|
206767
|
-
operationId,
|
|
206768
|
-
taskId
|
|
206769
|
-
});
|
|
206770
|
-
}
|
|
206771
|
-
throw new Error(`Unsupported agentType: ${agentType}`);
|
|
206772
|
-
}
|
|
206773
|
-
async function cancelHeteroTask(params) {
|
|
206774
|
-
const { signal = "SIGINT", taskId } = params;
|
|
206775
|
-
const entry = getTask(taskId);
|
|
206776
|
-
if (!entry) return JSON.stringify({
|
|
206777
|
-
message: `No task found with taskId: ${taskId}`,
|
|
206778
|
-
success: false
|
|
206779
|
-
});
|
|
206780
|
-
if (entry.agentType === "hermes") {
|
|
206781
|
-
const port = getHermesPort();
|
|
206782
|
-
try {
|
|
206783
|
-
await fetch(`http://localhost:${port}/stop`, {
|
|
206784
|
-
body: JSON.stringify({ operationId: entry.operationId }),
|
|
206785
|
-
headers: { "Content-Type": "application/json" },
|
|
206786
|
-
method: "POST"
|
|
206787
|
-
});
|
|
206788
|
-
} catch (err) {
|
|
206789
|
-
log$7.warn(`Failed to send /stop to Hermes gateway: ${err instanceof Error ? err.message : String(err)}`);
|
|
206790
|
-
}
|
|
206791
|
-
removeTask(taskId);
|
|
206792
|
-
await sendAutoNotify(entry.topicId, taskId, "Task cancelled", entry.agentId);
|
|
206793
|
-
return JSON.stringify({ taskId });
|
|
206794
|
-
}
|
|
206795
|
-
try {
|
|
206796
|
-
process.kill(entry.pid, signal);
|
|
206797
|
-
} catch (err) {
|
|
206798
|
-
log$7.warn(`Failed to send ${signal} to pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
206799
|
-
removeTask(taskId);
|
|
206800
|
-
await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId);
|
|
206801
|
-
}
|
|
206802
|
-
return JSON.stringify({
|
|
206803
|
-
pid: entry.pid,
|
|
206804
|
-
signal,
|
|
206805
|
-
taskId
|
|
206806
|
-
});
|
|
206807
|
-
}
|
|
206808
|
-
|
|
206809
207145
|
//#endregion
|
|
206810
207146
|
//#region src/tools/shell.ts
|
|
206811
207147
|
const processManager = new ShellProcessManager();
|
|
@@ -206829,6 +207165,7 @@ async function killCommand(params) {
|
|
|
206829
207165
|
//#region src/tools/index.ts
|
|
206830
207166
|
const methodMap = {
|
|
206831
207167
|
cancelHeteroTask,
|
|
207168
|
+
checkPlatformCapability,
|
|
206832
207169
|
editFile: editLocalFile,
|
|
206833
207170
|
getCommandOutput,
|
|
206834
207171
|
globFiles: globLocalFiles,
|
|
@@ -212383,13 +212720,16 @@ function registerModelCommand(program) {
|
|
|
212383
212720
|
//#endregion
|
|
212384
212721
|
//#region src/commands/notify.ts
|
|
212385
212722
|
function registerNotifyCommand(program) {
|
|
212386
|
-
program.command("notify").description("Send a callback message to a topic and trigger the agent to process it").requiredOption("--topic <topicId>", "Target topic ID").requiredOption("-c, --content <content>", "Message content").option("--agent-id <agentId>", "Agent ID (overrides topic default)").option("--thread-id <threadId>", "Thread ID for threaded conversations").option("--json", "Output JSON").action(async (options) => {
|
|
212387
|
-
log$7.debug("notify: topic=%s, agentId=%s", options.topic, options.agentId);
|
|
212723
|
+
program.command("notify").description("Send a callback message to a topic and trigger the agent to process it").requiredOption("--topic <topicId>", "Target topic ID").requiredOption("-c, --content <content>", "Message content").option("--agent-id <agentId>", "Agent ID (overrides topic default)").option("--thread-id <threadId>", "Thread ID for threaded conversations").option("--role <role>", "Message role: user (default, triggers agent reply) | assistant (writes directly as agent message)", "user").option("--message-id <messageId>", "When --role assistant: update an existing message instead of creating a new one (keeps a single bubble)").option("--continue", "When --role assistant: trigger a follow-up agent turn after writing the message").option("--json", "Output JSON").action(async (options) => {
|
|
212724
|
+
log$7.debug("notify: topic=%s, agentId=%s, role=%s, messageId=%s", options.topic, options.agentId, options.role, options.messageId);
|
|
212388
212725
|
const client = await getTrpcClient();
|
|
212389
212726
|
try {
|
|
212390
212727
|
const result = await client.agentNotify.notify.mutate({
|
|
212391
212728
|
agentId: options.agentId,
|
|
212392
212729
|
content: options.content,
|
|
212730
|
+
continue: options.continue,
|
|
212731
|
+
messageId: options.messageId,
|
|
212732
|
+
role: options.role,
|
|
212393
212733
|
threadId: options.threadId,
|
|
212394
212734
|
topicId: options.topic
|
|
212395
212735
|
});
|
package/man/man1/lh.1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.\" Code generated by `npm run man:generate`; DO NOT EDIT.
|
|
2
2
|
.\" Manual command details come from the Commander command tree.
|
|
3
|
-
.TH LH 1 "" "@lobehub/cli 0.0.16
|
|
3
|
+
.TH LH 1 "" "@lobehub/cli 0.0.16" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
lh \- LobeHub CLI \- manage and connect to LobeHub services
|
|
6
6
|
.SH SYNOPSIS
|