@lobehub/cli 0.0.16-beta.1 → 0.0.17

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 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 <botId>").description("Send a message to a channel").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 (botId, options) => {
11405
- let attachments;
11406
- if (options.attachment.length > 0) {
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 <botId>").description("Reply to a thread").requiredOption("--thread-id <id>", "Thread ID").requiredOption("--message <text>", "Reply content").action(async (botId, options) => {
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
- botId,
11677
+ ...target,
11678
+ attachments,
11633
11679
  content: options.message,
11634
11680
  threadId: options.threadId
11635
11681
  });
11636
- console.log(`${import_picocolors.default.green("✓")} Reply sent${r.messageId ? ` (${import_picocolors.default.dim(r.messageId)})` : ""}`);
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",
@@ -206606,204 +207143,65 @@ async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_b
206606
207143
  }
206607
207144
 
206608
207145
  //#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 {};
207146
+ //#region src/tools/getAgentProfile.ts
207147
+ const IDENTITY_FILES$1 = ["IDENTITY.md", "SOUL.md"];
207148
+ /**
207149
+ * Try to extract a description from the workspace identity file.
207150
+ * Looks for Creature / Vibe / Description fields in IDENTITY.md or SOUL.md.
207151
+ */
207152
+ function readDescriptionFromWorkspace(workspacePath) {
207153
+ for (const filename of IDENTITY_FILES$1) {
207154
+ const filePath = path.join(workspacePath, filename);
207155
+ if (!fs.existsSync(filePath)) continue;
207156
+ const match = fs.readFileSync(filePath, "utf8").match(/\*{0,2}(?:Creature|Vibe|Description):?\*{0,2}\s*(.+)/i);
207157
+ if (!match) continue;
207158
+ const value = match[1].trim();
207159
+ if (/^[_*((].*[))*_]$|^(?:tbd|todo|n\/?a|none|待定|未定)$/i.test(value)) continue;
207160
+ return value;
206618
207161
  }
206619
207162
  }
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() {
207163
+ function getOpenClawProfile(agentId) {
207164
+ let output;
206647
207165
  try {
206648
- return execFileSync("which", ["lh"], { encoding: "utf8" }).trim();
207166
+ output = execFileSync("openclaw", [
207167
+ "agents",
207168
+ "list",
207169
+ "--json"
207170
+ ], {
207171
+ encoding: "utf8",
207172
+ timeout: 5e3
207173
+ });
206649
207174
  } 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;
207175
+ return {};
206658
207176
  }
206659
- return DEFAULT_HERMES_PORT;
206660
- }
206661
- async function isHermesGatewayRunning(port) {
207177
+ let agents;
206662
207178
  try {
206663
- return (await fetch(`http://localhost:${port}/health`)).ok;
207179
+ agents = JSON.parse(output);
206664
207180
  } 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
- });
207181
+ return {};
206770
207182
  }
206771
- throw new Error(`Unsupported agentType: ${agentType}`);
207183
+ const agent = agentId ? agents.find((a) => a.id === agentId) : agents.find((a) => a.isDefault) ?? agents[0];
207184
+ if (!agent) return {};
207185
+ const title = agent.identityName || void 0;
207186
+ return {
207187
+ avatar: agent.identityEmoji || "🦞",
207188
+ description: agent.workspace ? readDescriptionFromWorkspace(agent.workspace) : void 0,
207189
+ title
207190
+ };
206772
207191
  }
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
- });
207192
+ /**
207193
+ * Fetch the agent profile (title, avatar, description) from the platform
207194
+ * installed on this device. Dispatched by the server via `device.getAgentProfile`.
207195
+ *
207196
+ * - openclaw: `openclaw agents list --json` for name + emoji, workspace
207197
+ * IDENTITY.md for description fallback
207198
+ * - hermes: not yet implemented — returns empty profile
207199
+ */
207200
+ async function getAgentProfile(params) {
207201
+ const { platform, agentId } = params;
207202
+ if (platform === "openclaw") return getOpenClawProfile(agentId);
207203
+ if (platform === "hermes") return {};
207204
+ return {};
206807
207205
  }
206808
207206
 
206809
207207
  //#endregion
@@ -206829,6 +207227,8 @@ async function killCommand(params) {
206829
207227
  //#region src/tools/index.ts
206830
207228
  const methodMap = {
206831
207229
  cancelHeteroTask,
207230
+ checkPlatformCapability,
207231
+ getAgentProfile,
206832
207232
  editFile: editLocalFile,
206833
207233
  getCommandOutput,
206834
207234
  globFiles: globLocalFiles,
@@ -212383,13 +212783,16 @@ function registerModelCommand(program) {
212383
212783
  //#endregion
212384
212784
  //#region src/commands/notify.ts
212385
212785
  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);
212786
+ 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) => {
212787
+ log$7.debug("notify: topic=%s, agentId=%s, role=%s, messageId=%s", options.topic, options.agentId, options.role, options.messageId);
212388
212788
  const client = await getTrpcClient();
212389
212789
  try {
212390
212790
  const result = await client.agentNotify.notify.mutate({
212391
212791
  agentId: options.agentId,
212392
212792
  content: options.content,
212793
+ continue: options.continue,
212794
+ messageId: options.messageId,
212795
+ role: options.role,
212393
212796
  threadId: options.threadId,
212394
212797
  topicId: options.topic
212395
212798
  });
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\-beta.1" "User Commands"
3
+ .TH LH 1 "" "@lobehub/cli 0.0.17" "User Commands"
4
4
  .SH NAME
5
5
  lh \- LobeHub CLI \- manage and connect to LobeHub services
6
6
  .SH SYNOPSIS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/cli",
3
- "version": "0.0.16-beta.1",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "lh": "./dist/index.js",