@lobehub/cli 0.0.20 → 0.0.21
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 +446 -372
- package/man/man1/lh.1 +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { EventEmitter } from "node:events";
|
|
3
|
+
import { EventEmitter, once } from "node:events";
|
|
4
4
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import path, { basename, extname } from "node:path";
|
|
6
6
|
import fs, { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -5023,8 +5023,8 @@ const log$7 = {
|
|
|
5023
5023
|
|
|
5024
5024
|
//#endregion
|
|
5025
5025
|
//#region src/settings/index.ts
|
|
5026
|
-
const LOBEHUB_DIR_NAME$
|
|
5027
|
-
const SETTINGS_DIR = path.join(os.homedir(), LOBEHUB_DIR_NAME$
|
|
5026
|
+
const LOBEHUB_DIR_NAME$2 = process.env.LOBEHUB_CLI_HOME || ".lobehub";
|
|
5027
|
+
const SETTINGS_DIR = path.join(os.homedir(), LOBEHUB_DIR_NAME$2);
|
|
5028
5028
|
const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
|
|
5029
5029
|
function normalizeUrl(url) {
|
|
5030
5030
|
return url ? url.replace(/\/$/, "") : void 0;
|
|
@@ -5083,8 +5083,8 @@ function loadSettings() {
|
|
|
5083
5083
|
|
|
5084
5084
|
//#endregion
|
|
5085
5085
|
//#region src/auth/credentials.ts
|
|
5086
|
-
const LOBEHUB_DIR_NAME = process.env.LOBEHUB_CLI_HOME || ".lobehub";
|
|
5087
|
-
const CREDENTIALS_DIR = path.join(os.homedir(), LOBEHUB_DIR_NAME);
|
|
5086
|
+
const LOBEHUB_DIR_NAME$1 = process.env.LOBEHUB_CLI_HOME || ".lobehub";
|
|
5087
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), LOBEHUB_DIR_NAME$1);
|
|
5088
5088
|
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
5089
5089
|
function deriveKey() {
|
|
5090
5090
|
const material = `lobehub-cli:${os.hostname()}:${os.userInfo().username}`;
|
|
@@ -5437,6 +5437,7 @@ async function streamAgentEventsViaWebSocket(options) {
|
|
|
5437
5437
|
const jsonEvents = [];
|
|
5438
5438
|
const ctx = createRenderContext();
|
|
5439
5439
|
let heartbeatTimer;
|
|
5440
|
+
let isSettled = false;
|
|
5440
5441
|
let jsonPrinted = false;
|
|
5441
5442
|
const cleanup = () => {
|
|
5442
5443
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
@@ -5479,6 +5480,8 @@ async function streamAgentEventsViaWebSocket(options) {
|
|
|
5479
5480
|
jsonPrinted = true;
|
|
5480
5481
|
console.log(JSON.stringify(jsonEvents, null, 2));
|
|
5481
5482
|
} else if (!streamOpts.json) renderEnd(agentEvent);
|
|
5483
|
+
if (isSettled) return;
|
|
5484
|
+
isSettled = true;
|
|
5482
5485
|
cleanup();
|
|
5483
5486
|
resolve();
|
|
5484
5487
|
return;
|
|
@@ -5498,21 +5501,31 @@ async function streamAgentEventsViaWebSocket(options) {
|
|
|
5498
5501
|
jsonPrinted = true;
|
|
5499
5502
|
console.log(JSON.stringify(jsonEvents, null, 2));
|
|
5500
5503
|
}
|
|
5504
|
+
if (isSettled) return;
|
|
5505
|
+
isSettled = true;
|
|
5501
5506
|
cleanup();
|
|
5502
5507
|
resolve();
|
|
5503
5508
|
}
|
|
5504
5509
|
};
|
|
5505
5510
|
ws.onerror = (err) => {
|
|
5506
5511
|
cleanup();
|
|
5507
|
-
|
|
5512
|
+
if (isSettled) return;
|
|
5513
|
+
if (streamOpts.json && jsonEvents.length > 0 && !jsonPrinted) {
|
|
5514
|
+
jsonPrinted = true;
|
|
5515
|
+
console.log(JSON.stringify(jsonEvents, null, 2));
|
|
5516
|
+
}
|
|
5517
|
+
isSettled = true;
|
|
5518
|
+
reject(/* @__PURE__ */ new Error(`Agent gateway WebSocket failed: ${String(err)}`));
|
|
5508
5519
|
};
|
|
5509
|
-
ws.onclose = () => {
|
|
5520
|
+
ws.onclose = (event) => {
|
|
5510
5521
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
5522
|
+
if (isSettled) return;
|
|
5511
5523
|
if (streamOpts.json && jsonEvents.length > 0 && !jsonPrinted) {
|
|
5512
5524
|
jsonPrinted = true;
|
|
5513
5525
|
console.log(JSON.stringify(jsonEvents, null, 2));
|
|
5514
5526
|
}
|
|
5515
|
-
|
|
5527
|
+
isSettled = true;
|
|
5528
|
+
reject(/* @__PURE__ */ new Error(`Agent gateway WebSocket closed before completion: ${String(event)}`));
|
|
5516
5529
|
};
|
|
5517
5530
|
});
|
|
5518
5531
|
}
|
|
@@ -7351,12 +7364,14 @@ const DEFAULT_AGENT_CHAT_CONFIG = {
|
|
|
7351
7364
|
enableAgentMode: true,
|
|
7352
7365
|
enableCompressHistory: true,
|
|
7353
7366
|
enableContextCompression: true,
|
|
7367
|
+
enableFollowUpChips: false,
|
|
7354
7368
|
enableHistoryCount: false,
|
|
7355
7369
|
enableStreaming: true,
|
|
7356
7370
|
historyCount: 20,
|
|
7357
7371
|
reasoningBudgetToken: 1024,
|
|
7358
7372
|
searchFCModel: DEFAULT_AGENT_SEARCH_FC_MODEL,
|
|
7359
|
-
searchMode: "auto"
|
|
7373
|
+
searchMode: "auto",
|
|
7374
|
+
selfIteration: { enabled: false }
|
|
7360
7375
|
};
|
|
7361
7376
|
const DEFAULT_AGENT_CONFIG = {
|
|
7362
7377
|
chatConfig: DEFAULT_AGENT_CHAT_CONFIG,
|
|
@@ -7574,11 +7589,20 @@ const DEFAULT_INPUT_COMPLETION_SYSTEM_AGENT_ITEM = {
|
|
|
7574
7589
|
model: DEFAULT_MINI_SYSTEM_AGENT_ITEM.model,
|
|
7575
7590
|
provider: DEFAULT_MINI_SYSTEM_AGENT_ITEM.provider
|
|
7576
7591
|
};
|
|
7592
|
+
const DEFAULT_FOLLOW_UP_ACTION_SYSTEM_AGENT_ITEM = {
|
|
7593
|
+
enabled: false,
|
|
7594
|
+
model: DEFAULT_MINI_SYSTEM_AGENT_ITEM.model,
|
|
7595
|
+
provider: DEFAULT_MINI_SYSTEM_AGENT_ITEM.provider
|
|
7596
|
+
};
|
|
7577
7597
|
|
|
7578
7598
|
//#endregion
|
|
7579
7599
|
//#region ../../packages/const/src/discover.ts
|
|
7580
7600
|
const DEFAULT_CREATED_AT = (/* @__PURE__ */ new Date()).toISOString();
|
|
7581
7601
|
|
|
7602
|
+
//#endregion
|
|
7603
|
+
//#region ../../packages/const/src/empty.ts
|
|
7604
|
+
const EMPTY_ARRAY = Object.freeze([]);
|
|
7605
|
+
|
|
7582
7606
|
//#endregion
|
|
7583
7607
|
//#region ../../packages/const/src/interests.ts
|
|
7584
7608
|
const INTEREST_AREA_KEYS = [
|
|
@@ -16673,250 +16697,6 @@ async function resolveToken(options) {
|
|
|
16673
16697
|
process.exit(1);
|
|
16674
16698
|
}
|
|
16675
16699
|
|
|
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
|
-
function listTasks() {
|
|
16710
|
-
return Object.values(readRegistry());
|
|
16711
|
-
}
|
|
16712
|
-
|
|
16713
|
-
//#endregion
|
|
16714
|
-
//#region src/tools/heteroTask.ts
|
|
16715
|
-
const DEFAULT_HERMES_PORT = 3456;
|
|
16716
|
-
/** Resolve the absolute path to the `lh` binary to avoid PATH issues in child processes. */
|
|
16717
|
-
function resolveLhPath() {
|
|
16718
|
-
try {
|
|
16719
|
-
return execFileSync("which", ["lh"], { encoding: "utf8" }).trim();
|
|
16720
|
-
} catch {
|
|
16721
|
-
return "lh";
|
|
16722
|
-
}
|
|
16723
|
-
}
|
|
16724
|
-
function getHermesPort() {
|
|
16725
|
-
const env = process.env.HERMES_GATEWAY_PORT;
|
|
16726
|
-
if (env) {
|
|
16727
|
-
const parsed = Number.parseInt(env, 10);
|
|
16728
|
-
if (!Number.isNaN(parsed)) return parsed;
|
|
16729
|
-
}
|
|
16730
|
-
return DEFAULT_HERMES_PORT;
|
|
16731
|
-
}
|
|
16732
|
-
async function isHermesGatewayRunning(port) {
|
|
16733
|
-
try {
|
|
16734
|
-
return (await fetch(`http://localhost:${port}/health`)).ok;
|
|
16735
|
-
} catch {
|
|
16736
|
-
return false;
|
|
16737
|
-
}
|
|
16738
|
-
}
|
|
16739
|
-
async function startHermesGateway(port) {
|
|
16740
|
-
spawn("hermes", ["gateway", "start"], {
|
|
16741
|
-
detached: true,
|
|
16742
|
-
env: { ...process.env },
|
|
16743
|
-
stdio: "ignore"
|
|
16744
|
-
}).unref();
|
|
16745
|
-
const deadline = Date.now() + 1e4;
|
|
16746
|
-
while (Date.now() < deadline) {
|
|
16747
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
16748
|
-
if (await isHermesGatewayRunning(port)) return;
|
|
16749
|
-
}
|
|
16750
|
-
throw new Error(`Hermes gateway did not start within 10s on port ${port}`);
|
|
16751
|
-
}
|
|
16752
|
-
async function sendAutoNotify(topicId, taskId, text, agentId) {
|
|
16753
|
-
try {
|
|
16754
|
-
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
16755
|
-
agentId,
|
|
16756
|
-
content: text,
|
|
16757
|
-
role: "assistant",
|
|
16758
|
-
topicId
|
|
16759
|
-
});
|
|
16760
|
-
} catch (err) {
|
|
16761
|
-
log$7.error("Failed to send auto-notify:", err instanceof Error ? err.message : String(err));
|
|
16762
|
-
}
|
|
16763
|
-
}
|
|
16764
|
-
/**
|
|
16765
|
-
* Signal remote hetero task completion to the server so it can publish
|
|
16766
|
-
* `agent_runtime_end` to the gateway WS and close the frontend subscription.
|
|
16767
|
-
* Called on clean process exit (code=0, no signal) — error exits go through
|
|
16768
|
-
* `sendAutoNotify` which writes an error message AND triggers completion via
|
|
16769
|
-
* the `done` flag.
|
|
16770
|
-
*/
|
|
16771
|
-
async function sendDoneSignal(topicId, agentId) {
|
|
16772
|
-
try {
|
|
16773
|
-
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
16774
|
-
agentId,
|
|
16775
|
-
content: "",
|
|
16776
|
-
done: true,
|
|
16777
|
-
role: "assistant",
|
|
16778
|
-
topicId
|
|
16779
|
-
});
|
|
16780
|
-
} catch (err) {
|
|
16781
|
-
log$7.error("Failed to send done signal:", err instanceof Error ? err.message : String(err));
|
|
16782
|
-
}
|
|
16783
|
-
}
|
|
16784
|
-
/**
|
|
16785
|
-
* Build the notify protocol injected into the first message of a new hetero-agent session.
|
|
16786
|
-
* Tells the agent how to push updates back to the LobeHub user via `lh notify`.
|
|
16787
|
-
*/
|
|
16788
|
-
function buildNotifyProtocol(lhPath, topicId) {
|
|
16789
|
-
return `## Context: This task was dispatched by LobeHub
|
|
16790
|
-
|
|
16791
|
-
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.
|
|
16792
|
-
|
|
16793
|
-
**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.
|
|
16794
|
-
|
|
16795
|
-
**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.
|
|
16796
|
-
|
|
16797
|
-
## Sending messages back to the user
|
|
16798
|
-
|
|
16799
|
-
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.`;
|
|
16800
|
-
}
|
|
16801
|
-
async function runHeteroTask(params) {
|
|
16802
|
-
const { agentId, agentType, cwd, operationId, prompt, taskId, topicId } = params;
|
|
16803
|
-
const workDir = cwd || process.cwd();
|
|
16804
|
-
const lhPath = resolveLhPath();
|
|
16805
|
-
if (agentType === "openclaw") {
|
|
16806
|
-
const openclawAgent = process.env.OPENCLAW_AGENT_ID ?? "main";
|
|
16807
|
-
const enrichedPrompt = `${prompt}\n\n${buildNotifyProtocol(lhPath, topicId)}`;
|
|
16808
|
-
for (const existing of listTasks()) if (existing.topicId === topicId && existing.agentType === "openclaw") {
|
|
16809
|
-
try {
|
|
16810
|
-
process.kill(existing.pid, "SIGTERM");
|
|
16811
|
-
} catch {}
|
|
16812
|
-
removeTask(existing.taskId);
|
|
16813
|
-
}
|
|
16814
|
-
const child = spawn("openclaw", [
|
|
16815
|
-
"agent",
|
|
16816
|
-
"--agent",
|
|
16817
|
-
openclawAgent,
|
|
16818
|
-
"--session-id",
|
|
16819
|
-
topicId,
|
|
16820
|
-
"--message",
|
|
16821
|
-
enrichedPrompt,
|
|
16822
|
-
"--local"
|
|
16823
|
-
], {
|
|
16824
|
-
cwd: workDir,
|
|
16825
|
-
detached: true,
|
|
16826
|
-
env: { ...process.env },
|
|
16827
|
-
stdio: "ignore"
|
|
16828
|
-
});
|
|
16829
|
-
const pid = child.pid;
|
|
16830
|
-
if (pid === void 0) throw new Error("Failed to get PID for openclaw process");
|
|
16831
|
-
child.unref();
|
|
16832
|
-
saveTask({
|
|
16833
|
-
agentId,
|
|
16834
|
-
agentType,
|
|
16835
|
-
operationId,
|
|
16836
|
-
pid,
|
|
16837
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16838
|
-
taskId,
|
|
16839
|
-
topicId
|
|
16840
|
-
});
|
|
16841
|
-
log$7.info(`OpenClaw task started: taskId=${taskId} pid=${pid} agent=${openclawAgent}`);
|
|
16842
|
-
child.on("close", (code, signal) => {
|
|
16843
|
-
removeTask(taskId);
|
|
16844
|
-
if (code !== 0 || signal !== null) sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId).finally(() => sendDoneSignal(topicId, agentId));
|
|
16845
|
-
else sendDoneSignal(topicId, agentId);
|
|
16846
|
-
});
|
|
16847
|
-
return JSON.stringify({
|
|
16848
|
-
pid,
|
|
16849
|
-
taskId
|
|
16850
|
-
});
|
|
16851
|
-
}
|
|
16852
|
-
if (agentType === "hermes") {
|
|
16853
|
-
const port = getHermesPort();
|
|
16854
|
-
if (!await isHermesGatewayRunning(port)) {
|
|
16855
|
-
log$7.info(`Hermes gateway not running on port ${port}, starting...`);
|
|
16856
|
-
await startHermesGateway(port);
|
|
16857
|
-
}
|
|
16858
|
-
const res = await fetch(`http://localhost:${port}/message`, {
|
|
16859
|
-
body: JSON.stringify({
|
|
16860
|
-
content: prompt,
|
|
16861
|
-
operationId
|
|
16862
|
-
}),
|
|
16863
|
-
headers: { "Content-Type": "application/json" },
|
|
16864
|
-
method: "POST"
|
|
16865
|
-
});
|
|
16866
|
-
if (!res.ok) throw new Error(`Hermes gateway returned ${res.status}: ${await res.text()}`);
|
|
16867
|
-
saveTask({
|
|
16868
|
-
agentId,
|
|
16869
|
-
agentType,
|
|
16870
|
-
operationId,
|
|
16871
|
-
pid: 0,
|
|
16872
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16873
|
-
taskId,
|
|
16874
|
-
topicId
|
|
16875
|
-
});
|
|
16876
|
-
log$7.info(`Hermes task dispatched: taskId=${taskId} operationId=${operationId}`);
|
|
16877
|
-
return JSON.stringify({
|
|
16878
|
-
operationId,
|
|
16879
|
-
taskId
|
|
16880
|
-
});
|
|
16881
|
-
}
|
|
16882
|
-
throw new Error(`Unsupported agentType: ${agentType}`);
|
|
16883
|
-
}
|
|
16884
|
-
async function cancelHeteroTask(params) {
|
|
16885
|
-
const { signal = "SIGINT", taskId } = params;
|
|
16886
|
-
const entry = getTask(taskId);
|
|
16887
|
-
if (!entry) return JSON.stringify({
|
|
16888
|
-
message: `No task found with taskId: ${taskId}`,
|
|
16889
|
-
success: false
|
|
16890
|
-
});
|
|
16891
|
-
if (entry.agentType === "hermes") {
|
|
16892
|
-
const port = getHermesPort();
|
|
16893
|
-
try {
|
|
16894
|
-
await fetch(`http://localhost:${port}/stop`, {
|
|
16895
|
-
body: JSON.stringify({ operationId: entry.operationId }),
|
|
16896
|
-
headers: { "Content-Type": "application/json" },
|
|
16897
|
-
method: "POST"
|
|
16898
|
-
});
|
|
16899
|
-
} catch (err) {
|
|
16900
|
-
log$7.warn(`Failed to send /stop to Hermes gateway: ${err instanceof Error ? err.message : String(err)}`);
|
|
16901
|
-
}
|
|
16902
|
-
removeTask(taskId);
|
|
16903
|
-
await sendAutoNotify(entry.topicId, taskId, "Task cancelled", entry.agentId);
|
|
16904
|
-
return JSON.stringify({ taskId });
|
|
16905
|
-
}
|
|
16906
|
-
try {
|
|
16907
|
-
process.kill(entry.pid, signal);
|
|
16908
|
-
} catch (err) {
|
|
16909
|
-
log$7.warn(`Failed to send ${signal} to pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16910
|
-
removeTask(taskId);
|
|
16911
|
-
await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId);
|
|
16912
|
-
}
|
|
16913
|
-
return JSON.stringify({
|
|
16914
|
-
pid: entry.pid,
|
|
16915
|
-
signal,
|
|
16916
|
-
taskId
|
|
16917
|
-
});
|
|
16918
|
-
}
|
|
16919
|
-
|
|
16920
16700
|
//#endregion
|
|
16921
16701
|
//#region src/tools/checkPlatformCapability.ts
|
|
16922
16702
|
/**
|
|
@@ -16942,30 +16722,21 @@ async function checkPlatformCapability(params) {
|
|
|
16942
16722
|
reason: err instanceof Error ? err.message : "openclaw not found or failed to run"
|
|
16943
16723
|
};
|
|
16944
16724
|
}
|
|
16945
|
-
if (platform === "hermes") {
|
|
16946
|
-
const
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
available: false,
|
|
16961
|
-
reason: `Hermes gateway returned HTTP ${res.status}`
|
|
16962
|
-
};
|
|
16963
|
-
} catch (err) {
|
|
16964
|
-
return {
|
|
16965
|
-
available: false,
|
|
16966
|
-
reason: err instanceof Error ? err.message : `Hermes gateway not reachable on port ${port}`
|
|
16967
|
-
};
|
|
16968
|
-
}
|
|
16725
|
+
if (platform === "hermes") try {
|
|
16726
|
+
const output = execFileSync("hermes", ["--version"], {
|
|
16727
|
+
encoding: "utf8",
|
|
16728
|
+
timeout: 5e3
|
|
16729
|
+
}).trim();
|
|
16730
|
+
const versionMatch = output.match(/v(\d+\.\d+\.\d+)/);
|
|
16731
|
+
return {
|
|
16732
|
+
available: true,
|
|
16733
|
+
version: versionMatch ? versionMatch[1] : output.split(/\s+/).at(-1)
|
|
16734
|
+
};
|
|
16735
|
+
} catch (err) {
|
|
16736
|
+
return {
|
|
16737
|
+
available: false,
|
|
16738
|
+
reason: err instanceof Error ? err.message : "hermes not found or failed to run"
|
|
16739
|
+
};
|
|
16969
16740
|
}
|
|
16970
16741
|
return {
|
|
16971
16742
|
available: false,
|
|
@@ -207213,20 +206984,327 @@ function getOpenClawProfile(agentId) {
|
|
|
207213
206984
|
};
|
|
207214
206985
|
}
|
|
207215
206986
|
/**
|
|
206987
|
+
* Read the active Hermes profile name from `hermes profile list` output.
|
|
206988
|
+
* The active profile is marked with ◆ in the first column.
|
|
206989
|
+
*/
|
|
206990
|
+
function getActiveHermesProfileName() {
|
|
206991
|
+
try {
|
|
206992
|
+
return execFileSync("hermes", ["profile", "list"], {
|
|
206993
|
+
encoding: "utf8",
|
|
206994
|
+
timeout: 5e3
|
|
206995
|
+
}).match(/◆(\S+)/)?.[1];
|
|
206996
|
+
} catch {
|
|
206997
|
+
return;
|
|
206998
|
+
}
|
|
206999
|
+
}
|
|
207000
|
+
/**
|
|
207001
|
+
* Read the filesystem path of a Hermes profile from `hermes profile show <name>`.
|
|
207002
|
+
*/
|
|
207003
|
+
function getHermesProfilePath(profileName) {
|
|
207004
|
+
try {
|
|
207005
|
+
return (execFileSync("hermes", [
|
|
207006
|
+
"profile",
|
|
207007
|
+
"show",
|
|
207008
|
+
profileName
|
|
207009
|
+
], {
|
|
207010
|
+
encoding: "utf8",
|
|
207011
|
+
timeout: 5e3
|
|
207012
|
+
}).match(/^Path:\s+(.+)/m)?.[1]?.trim())?.replace(/^~(?=\/|$)/, os.homedir());
|
|
207013
|
+
} catch {
|
|
207014
|
+
return;
|
|
207015
|
+
}
|
|
207016
|
+
}
|
|
207017
|
+
/**
|
|
207018
|
+
* Extract a one-line description from a Hermes SOUL.md file.
|
|
207019
|
+
* Strips HTML comments and Markdown headings, then returns the first
|
|
207020
|
+
* non-empty line of actual content.
|
|
207021
|
+
*/
|
|
207022
|
+
function readHermesSoulDescription(soulPath) {
|
|
207023
|
+
try {
|
|
207024
|
+
let stripped = fs.readFileSync(soulPath, "utf8");
|
|
207025
|
+
let previous;
|
|
207026
|
+
do {
|
|
207027
|
+
previous = stripped;
|
|
207028
|
+
stripped = stripped.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/[<>]/g, "").replaceAll(/^#+\s.*$/gm, "");
|
|
207029
|
+
} while (stripped !== previous);
|
|
207030
|
+
return stripped.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || void 0;
|
|
207031
|
+
} catch {
|
|
207032
|
+
return;
|
|
207033
|
+
}
|
|
207034
|
+
}
|
|
207035
|
+
function getHermesProfile() {
|
|
207036
|
+
const profileName = getActiveHermesProfileName();
|
|
207037
|
+
if (!profileName) return {};
|
|
207038
|
+
const profilePath = getHermesProfilePath(profileName);
|
|
207039
|
+
return {
|
|
207040
|
+
avatar: "⚡",
|
|
207041
|
+
description: profilePath ? readHermesSoulDescription(path.join(profilePath, "SOUL.md")) : void 0,
|
|
207042
|
+
title: profileName
|
|
207043
|
+
};
|
|
207044
|
+
}
|
|
207045
|
+
/**
|
|
207216
207046
|
* Fetch the agent profile (title, avatar, description) from the platform
|
|
207217
207047
|
* installed on this device. Dispatched by the server via `device.getAgentProfile`.
|
|
207218
207048
|
*
|
|
207219
207049
|
* - openclaw: `openclaw agents list --json` for name + emoji, workspace
|
|
207220
207050
|
* IDENTITY.md for description fallback
|
|
207221
|
-
* - hermes:
|
|
207051
|
+
* - hermes: active profile name + SOUL.md description
|
|
207222
207052
|
*/
|
|
207223
207053
|
async function getAgentProfile(params) {
|
|
207224
207054
|
const { platform, agentId } = params;
|
|
207225
207055
|
if (platform === "openclaw") return getOpenClawProfile(agentId);
|
|
207226
|
-
if (platform === "hermes") return
|
|
207056
|
+
if (platform === "hermes") return getHermesProfile();
|
|
207227
207057
|
return {};
|
|
207228
207058
|
}
|
|
207229
207059
|
|
|
207060
|
+
//#endregion
|
|
207061
|
+
//#region src/daemon/taskRegistry.ts
|
|
207062
|
+
function getRegistryPath() {
|
|
207063
|
+
return path.join(os.homedir(), ".lobehub", "task-registry.json");
|
|
207064
|
+
}
|
|
207065
|
+
function readRegistry() {
|
|
207066
|
+
try {
|
|
207067
|
+
return JSON.parse(fs.readFileSync(getRegistryPath(), "utf8"));
|
|
207068
|
+
} catch {
|
|
207069
|
+
return {};
|
|
207070
|
+
}
|
|
207071
|
+
}
|
|
207072
|
+
function writeRegistry(entries) {
|
|
207073
|
+
const dir = path.dirname(getRegistryPath());
|
|
207074
|
+
fs.mkdirSync(dir, {
|
|
207075
|
+
mode: 448,
|
|
207076
|
+
recursive: true
|
|
207077
|
+
});
|
|
207078
|
+
fs.writeFileSync(getRegistryPath(), JSON.stringify(entries, null, 2), { mode: 384 });
|
|
207079
|
+
}
|
|
207080
|
+
function saveTask(entry) {
|
|
207081
|
+
const registry = readRegistry();
|
|
207082
|
+
registry[entry.taskId] = entry;
|
|
207083
|
+
writeRegistry(registry);
|
|
207084
|
+
}
|
|
207085
|
+
function getTask(taskId) {
|
|
207086
|
+
return readRegistry()[taskId];
|
|
207087
|
+
}
|
|
207088
|
+
function removeTask(taskId) {
|
|
207089
|
+
const registry = readRegistry();
|
|
207090
|
+
delete registry[taskId];
|
|
207091
|
+
writeRegistry(registry);
|
|
207092
|
+
}
|
|
207093
|
+
function listTasks() {
|
|
207094
|
+
return Object.values(readRegistry());
|
|
207095
|
+
}
|
|
207096
|
+
|
|
207097
|
+
//#endregion
|
|
207098
|
+
//#region src/tools/heteroTask.ts
|
|
207099
|
+
const LOBEHUB_DIR_NAME = process.env.LOBEHUB_CLI_HOME || ".lobehub";
|
|
207100
|
+
const HERMES_SESSIONS_FILE = path.join(os.homedir(), LOBEHUB_DIR_NAME, "hermes-sessions.json");
|
|
207101
|
+
function getHermesSessionId(topicId) {
|
|
207102
|
+
try {
|
|
207103
|
+
return JSON.parse(fs.readFileSync(HERMES_SESSIONS_FILE, "utf8"))[topicId];
|
|
207104
|
+
} catch {
|
|
207105
|
+
return;
|
|
207106
|
+
}
|
|
207107
|
+
}
|
|
207108
|
+
function saveHermesSessionId(topicId, sessionId) {
|
|
207109
|
+
let data = {};
|
|
207110
|
+
try {
|
|
207111
|
+
data = JSON.parse(fs.readFileSync(HERMES_SESSIONS_FILE, "utf8"));
|
|
207112
|
+
} catch {}
|
|
207113
|
+
data[topicId] = sessionId;
|
|
207114
|
+
fs.mkdirSync(path.dirname(HERMES_SESSIONS_FILE), { recursive: true });
|
|
207115
|
+
fs.writeFileSync(HERMES_SESSIONS_FILE, JSON.stringify(data), "utf8");
|
|
207116
|
+
}
|
|
207117
|
+
/** Resolve the absolute path to the `lh` binary to avoid PATH issues in child processes. */
|
|
207118
|
+
function resolveLhPath() {
|
|
207119
|
+
try {
|
|
207120
|
+
return execFileSync("which", ["lh"], { encoding: "utf8" }).trim();
|
|
207121
|
+
} catch {
|
|
207122
|
+
return "lh";
|
|
207123
|
+
}
|
|
207124
|
+
}
|
|
207125
|
+
async function sendAutoNotify(topicId, taskId, text, agentId) {
|
|
207126
|
+
try {
|
|
207127
|
+
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
207128
|
+
agentId,
|
|
207129
|
+
content: text,
|
|
207130
|
+
role: "assistant",
|
|
207131
|
+
topicId
|
|
207132
|
+
});
|
|
207133
|
+
} catch (err) {
|
|
207134
|
+
log$7.error("Failed to send auto-notify:", err instanceof Error ? err.message : String(err));
|
|
207135
|
+
}
|
|
207136
|
+
}
|
|
207137
|
+
/**
|
|
207138
|
+
* Signal remote hetero task completion to the server so it can publish
|
|
207139
|
+
* `agent_runtime_end` to the gateway WS and close the frontend subscription.
|
|
207140
|
+
* Called on clean process exit (code=0, no signal) — error exits go through
|
|
207141
|
+
* `sendAutoNotify` which writes an error message AND triggers completion via
|
|
207142
|
+
* the `done` flag.
|
|
207143
|
+
*/
|
|
207144
|
+
async function sendDoneSignal(topicId, agentId) {
|
|
207145
|
+
try {
|
|
207146
|
+
await (await getTrpcClient()).agentNotify.notify.mutate({
|
|
207147
|
+
agentId,
|
|
207148
|
+
content: "",
|
|
207149
|
+
done: true,
|
|
207150
|
+
role: "assistant",
|
|
207151
|
+
topicId
|
|
207152
|
+
});
|
|
207153
|
+
} catch (err) {
|
|
207154
|
+
log$7.error("Failed to send done signal:", err instanceof Error ? err.message : String(err));
|
|
207155
|
+
}
|
|
207156
|
+
}
|
|
207157
|
+
/**
|
|
207158
|
+
* Build the notify protocol injected into the first message of a new hetero-agent session.
|
|
207159
|
+
* Tells the agent how to push updates back to the LobeHub user via `lh notify`.
|
|
207160
|
+
*/
|
|
207161
|
+
function buildNotifyProtocol(lhPath, topicId) {
|
|
207162
|
+
return `## Context: This task was dispatched by LobeHub
|
|
207163
|
+
|
|
207164
|
+
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.
|
|
207165
|
+
|
|
207166
|
+
**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.
|
|
207167
|
+
|
|
207168
|
+
**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.
|
|
207169
|
+
|
|
207170
|
+
## Sending messages back to the user
|
|
207171
|
+
|
|
207172
|
+
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.`;
|
|
207173
|
+
}
|
|
207174
|
+
async function runHeteroTask(params) {
|
|
207175
|
+
const { agentId, agentType, cwd, operationId, prompt, taskId, topicId } = params;
|
|
207176
|
+
const workDir = cwd || process.cwd();
|
|
207177
|
+
const lhPath = resolveLhPath();
|
|
207178
|
+
if (agentType === "openclaw") {
|
|
207179
|
+
const openclawAgent = process.env.OPENCLAW_AGENT_ID ?? "main";
|
|
207180
|
+
const enrichedPrompt = `${prompt}\n\n${buildNotifyProtocol(lhPath, topicId)}`;
|
|
207181
|
+
for (const existing of listTasks()) if (existing.topicId === topicId && existing.agentType === "openclaw") {
|
|
207182
|
+
try {
|
|
207183
|
+
process.kill(existing.pid, "SIGTERM");
|
|
207184
|
+
} catch {}
|
|
207185
|
+
removeTask(existing.taskId);
|
|
207186
|
+
}
|
|
207187
|
+
const child = spawn("openclaw", [
|
|
207188
|
+
"agent",
|
|
207189
|
+
"--agent",
|
|
207190
|
+
openclawAgent,
|
|
207191
|
+
"--session-id",
|
|
207192
|
+
topicId,
|
|
207193
|
+
"--message",
|
|
207194
|
+
enrichedPrompt,
|
|
207195
|
+
"--local"
|
|
207196
|
+
], {
|
|
207197
|
+
cwd: workDir,
|
|
207198
|
+
detached: true,
|
|
207199
|
+
env: { ...process.env },
|
|
207200
|
+
stdio: "ignore"
|
|
207201
|
+
});
|
|
207202
|
+
const pid = child.pid;
|
|
207203
|
+
if (pid === void 0) throw new Error("Failed to get PID for openclaw process");
|
|
207204
|
+
child.unref();
|
|
207205
|
+
saveTask({
|
|
207206
|
+
agentId,
|
|
207207
|
+
agentType,
|
|
207208
|
+
operationId,
|
|
207209
|
+
pid,
|
|
207210
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
207211
|
+
taskId,
|
|
207212
|
+
topicId
|
|
207213
|
+
});
|
|
207214
|
+
log$7.info(`OpenClaw task started: taskId=${taskId} pid=${pid} agent=${openclawAgent}`);
|
|
207215
|
+
child.on("close", (code, signal) => {
|
|
207216
|
+
removeTask(taskId);
|
|
207217
|
+
if (code !== 0 || signal !== null) sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId).finally(() => sendDoneSignal(topicId, agentId));
|
|
207218
|
+
else sendDoneSignal(topicId, agentId);
|
|
207219
|
+
});
|
|
207220
|
+
return JSON.stringify({
|
|
207221
|
+
pid,
|
|
207222
|
+
taskId
|
|
207223
|
+
});
|
|
207224
|
+
}
|
|
207225
|
+
if (agentType === "hermes") {
|
|
207226
|
+
for (const existing of listTasks()) if (existing.topicId === topicId && existing.agentType === "hermes") {
|
|
207227
|
+
try {
|
|
207228
|
+
process.kill(existing.pid, "SIGTERM");
|
|
207229
|
+
} catch {}
|
|
207230
|
+
removeTask(existing.taskId);
|
|
207231
|
+
}
|
|
207232
|
+
const existingSessionId = getHermesSessionId(topicId);
|
|
207233
|
+
const hermesArgs = [
|
|
207234
|
+
"chat",
|
|
207235
|
+
"--query",
|
|
207236
|
+
prompt,
|
|
207237
|
+
"--quiet",
|
|
207238
|
+
"--accept-hooks"
|
|
207239
|
+
];
|
|
207240
|
+
if (existingSessionId) hermesArgs.push("--resume", existingSessionId);
|
|
207241
|
+
const child = spawn("hermes", hermesArgs, {
|
|
207242
|
+
cwd: workDir,
|
|
207243
|
+
detached: true,
|
|
207244
|
+
env: { ...process.env },
|
|
207245
|
+
stdio: [
|
|
207246
|
+
"ignore",
|
|
207247
|
+
"pipe",
|
|
207248
|
+
"ignore"
|
|
207249
|
+
]
|
|
207250
|
+
});
|
|
207251
|
+
const pid = child.pid;
|
|
207252
|
+
if (pid === void 0) throw new Error("Failed to get PID for hermes process");
|
|
207253
|
+
child.unref();
|
|
207254
|
+
saveTask({
|
|
207255
|
+
agentId,
|
|
207256
|
+
agentType,
|
|
207257
|
+
operationId,
|
|
207258
|
+
pid,
|
|
207259
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
207260
|
+
taskId,
|
|
207261
|
+
topicId
|
|
207262
|
+
});
|
|
207263
|
+
log$7.info(`Hermes task started: taskId=${taskId} pid=${pid}`);
|
|
207264
|
+
let stdout = "";
|
|
207265
|
+
child.stdout.on("data", (chunk) => {
|
|
207266
|
+
stdout += chunk.toString();
|
|
207267
|
+
});
|
|
207268
|
+
child.on("close", (code, signal) => {
|
|
207269
|
+
removeTask(taskId);
|
|
207270
|
+
if (code !== 0 || signal !== null) {
|
|
207271
|
+
sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId).finally(() => sendDoneSignal(topicId, agentId));
|
|
207272
|
+
return;
|
|
207273
|
+
}
|
|
207274
|
+
const sessionId = stdout.match(/^session_id:\s*(\S+)/m)?.[1];
|
|
207275
|
+
const response = stdout.replace(/^session_id:[^\n]*\n?/, "").trim();
|
|
207276
|
+
if (sessionId) saveHermesSessionId(topicId, sessionId);
|
|
207277
|
+
if (response) sendAutoNotify(topicId, taskId, response, agentId).finally(() => sendDoneSignal(topicId, agentId));
|
|
207278
|
+
else sendDoneSignal(topicId, agentId);
|
|
207279
|
+
});
|
|
207280
|
+
return JSON.stringify({
|
|
207281
|
+
pid,
|
|
207282
|
+
taskId
|
|
207283
|
+
});
|
|
207284
|
+
}
|
|
207285
|
+
throw new Error(`Unsupported agentType: ${agentType}`);
|
|
207286
|
+
}
|
|
207287
|
+
async function cancelHeteroTask(params) {
|
|
207288
|
+
const { signal = "SIGINT", taskId } = params;
|
|
207289
|
+
const entry = getTask(taskId);
|
|
207290
|
+
if (!entry) return JSON.stringify({
|
|
207291
|
+
message: `No task found with taskId: ${taskId}`,
|
|
207292
|
+
success: false
|
|
207293
|
+
});
|
|
207294
|
+
try {
|
|
207295
|
+
process.kill(entry.pid, signal);
|
|
207296
|
+
} catch (err) {
|
|
207297
|
+
log$7.warn(`Failed to send ${signal} to pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
207298
|
+
removeTask(taskId);
|
|
207299
|
+
await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId);
|
|
207300
|
+
}
|
|
207301
|
+
return JSON.stringify({
|
|
207302
|
+
pid: entry.pid,
|
|
207303
|
+
signal,
|
|
207304
|
+
taskId
|
|
207305
|
+
});
|
|
207306
|
+
}
|
|
207307
|
+
|
|
207230
207308
|
//#endregion
|
|
207231
207309
|
//#region src/tools/shell.ts
|
|
207232
207310
|
const processManager = new ShellProcessManager();
|
|
@@ -210753,83 +210831,6 @@ const spawnAgent = async (options) => {
|
|
|
210753
210831
|
};
|
|
210754
210832
|
};
|
|
210755
210833
|
|
|
210756
|
-
//#endregion
|
|
210757
|
-
//#region src/utils/BatchIngester.ts
|
|
210758
|
-
var NoopIngestSink = class {
|
|
210759
|
-
async finish(_params) {}
|
|
210760
|
-
async ingest(_events) {}
|
|
210761
|
-
};
|
|
210762
|
-
const MAX_BATCH = 50;
|
|
210763
|
-
const FLUSH_INTERVAL_MS = 250;
|
|
210764
|
-
const MAX_RETRIES = 5;
|
|
210765
|
-
const sleep$1 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
210766
|
-
/**
|
|
210767
|
-
* Buffers `AgentStreamEvent`s and flushes them in batches to an `IngestSink`.
|
|
210768
|
-
*
|
|
210769
|
-
* Flush triggers:
|
|
210770
|
-
* - Buffer reaches MAX_BATCH (50) → immediate flush
|
|
210771
|
-
* - FLUSH_INTERVAL_MS (250ms) timer fires → flush whatever is buffered
|
|
210772
|
-
*
|
|
210773
|
-
* Each batch is retried up to MAX_RETRIES (5) times with exponential back-off
|
|
210774
|
-
* starting at 500ms, doubling up to 8s. After the final retry the error is
|
|
210775
|
-
* stored and re-thrown by `drain()`, allowing the caller to call
|
|
210776
|
-
* `sink.finish({ result: 'error' })` and exit(1).
|
|
210777
|
-
*
|
|
210778
|
-
* Call order: push() repeatedly → drain() once (before finish()).
|
|
210779
|
-
*/
|
|
210780
|
-
var BatchIngester = class {
|
|
210781
|
-
buffer = [];
|
|
210782
|
-
fatalError = null;
|
|
210783
|
-
inflightFlush = Promise.resolve();
|
|
210784
|
-
timer = null;
|
|
210785
|
-
constructor(sink) {
|
|
210786
|
-
this.sink = sink;
|
|
210787
|
-
}
|
|
210788
|
-
push(event) {
|
|
210789
|
-
if (this.fatalError) return;
|
|
210790
|
-
this.buffer.push(event);
|
|
210791
|
-
if (this.buffer.length >= MAX_BATCH) {
|
|
210792
|
-
if (this.timer) {
|
|
210793
|
-
clearTimeout(this.timer);
|
|
210794
|
-
this.timer = null;
|
|
210795
|
-
}
|
|
210796
|
-
this.triggerFlush();
|
|
210797
|
-
} else if (!this.timer) this.timer = setTimeout(() => {
|
|
210798
|
-
this.timer = null;
|
|
210799
|
-
this.triggerFlush();
|
|
210800
|
-
}, FLUSH_INTERVAL_MS);
|
|
210801
|
-
}
|
|
210802
|
-
/** Flush remaining buffer and wait for all in-flight sends to settle. */
|
|
210803
|
-
async drain() {
|
|
210804
|
-
if (this.timer) {
|
|
210805
|
-
clearTimeout(this.timer);
|
|
210806
|
-
this.timer = null;
|
|
210807
|
-
}
|
|
210808
|
-
this.triggerFlush();
|
|
210809
|
-
await this.inflightFlush;
|
|
210810
|
-
if (this.fatalError) throw this.fatalError;
|
|
210811
|
-
}
|
|
210812
|
-
async sendWithRetry(batch) {
|
|
210813
|
-
let delay = 500;
|
|
210814
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) try {
|
|
210815
|
-
await this.sink.ingest(batch);
|
|
210816
|
-
return;
|
|
210817
|
-
} catch (err) {
|
|
210818
|
-
if (attempt === MAX_RETRIES) {
|
|
210819
|
-
this.fatalError = err instanceof Error ? err : new Error(String(err));
|
|
210820
|
-
throw this.fatalError;
|
|
210821
|
-
}
|
|
210822
|
-
await sleep$1(delay);
|
|
210823
|
-
delay = Math.min(delay * 2, 8e3);
|
|
210824
|
-
}
|
|
210825
|
-
}
|
|
210826
|
-
triggerFlush() {
|
|
210827
|
-
if (this.fatalError || this.buffer.length === 0) return;
|
|
210828
|
-
const batch = this.buffer.splice(0);
|
|
210829
|
-
this.inflightFlush = this.inflightFlush.then(() => this.sendWithRetry(batch)).catch(() => {});
|
|
210830
|
-
}
|
|
210831
|
-
};
|
|
210832
|
-
|
|
210833
210834
|
//#endregion
|
|
210834
210835
|
//#region src/utils/TrpcIngestSink.ts
|
|
210835
210836
|
/**
|
|
@@ -210840,11 +210841,12 @@ var BatchIngester = class {
|
|
|
210840
210841
|
* injected by the server before spawning the sandbox / desktop process).
|
|
210841
210842
|
*/
|
|
210842
210843
|
var TrpcIngestSink = class {
|
|
210843
|
-
constructor(client, agentType, operationId, topicId) {
|
|
210844
|
+
constructor(client, agentType, operationId, topicId, assistantMessageId) {
|
|
210844
210845
|
this.client = client;
|
|
210845
210846
|
this.agentType = agentType;
|
|
210846
210847
|
this.operationId = operationId;
|
|
210847
210848
|
this.topicId = topicId;
|
|
210849
|
+
this.assistantMessageId = assistantMessageId;
|
|
210848
210850
|
}
|
|
210849
210851
|
async finish(params) {
|
|
210850
210852
|
await this.client.aiAgent.heteroFinish.mutate({
|
|
@@ -210857,6 +210859,7 @@ var TrpcIngestSink = class {
|
|
|
210857
210859
|
async ingest(events) {
|
|
210858
210860
|
await this.client.aiAgent.heteroIngest.mutate({
|
|
210859
210861
|
agentType: this.agentType,
|
|
210862
|
+
assistantMessageId: this.assistantMessageId,
|
|
210860
210863
|
events,
|
|
210861
210864
|
operationId: this.operationId,
|
|
210862
210865
|
topicId: this.topicId
|
|
@@ -211002,6 +211005,69 @@ const resolvePrompt = async (options) => {
|
|
|
211002
211005
|
};
|
|
211003
211006
|
return buildPromptFromText(raw, images);
|
|
211004
211007
|
};
|
|
211008
|
+
var SerialServerIngester = class {
|
|
211009
|
+
accumulatedText = "";
|
|
211010
|
+
fatalError = null;
|
|
211011
|
+
inflight = Promise.resolve();
|
|
211012
|
+
nextSnapshotSeq = 0;
|
|
211013
|
+
pendingTextEvent;
|
|
211014
|
+
timer = null;
|
|
211015
|
+
constructor(sink, snapshotFlushMs = 200) {
|
|
211016
|
+
this.sink = sink;
|
|
211017
|
+
this.snapshotFlushMs = snapshotFlushMs;
|
|
211018
|
+
}
|
|
211019
|
+
push(event) {
|
|
211020
|
+
if (this.fatalError) return;
|
|
211021
|
+
if (event.type === "stream_chunk" && event.data?.chunkType === "text" && typeof event.data?.content === "string") {
|
|
211022
|
+
this.accumulatedText += event.data.content;
|
|
211023
|
+
this.pendingTextEvent = event;
|
|
211024
|
+
if (this.timer) clearTimeout(this.timer);
|
|
211025
|
+
this.timer = setTimeout(() => {
|
|
211026
|
+
this.timer = null;
|
|
211027
|
+
this.queuePendingTextSnapshot();
|
|
211028
|
+
}, this.snapshotFlushMs);
|
|
211029
|
+
return;
|
|
211030
|
+
}
|
|
211031
|
+
this.queuePendingTextSnapshot();
|
|
211032
|
+
this.enqueue(async () => {
|
|
211033
|
+
await this.sink.ingest([event]);
|
|
211034
|
+
});
|
|
211035
|
+
}
|
|
211036
|
+
async drain() {
|
|
211037
|
+
this.queuePendingTextSnapshot();
|
|
211038
|
+
try {
|
|
211039
|
+
await this.inflight;
|
|
211040
|
+
} catch {}
|
|
211041
|
+
if (this.fatalError) throw this.fatalError;
|
|
211042
|
+
}
|
|
211043
|
+
enqueue(task) {
|
|
211044
|
+
this.inflight = this.inflight.then(task).catch((err) => {
|
|
211045
|
+
this.fatalError = err instanceof Error ? err : new Error(String(err));
|
|
211046
|
+
throw this.fatalError;
|
|
211047
|
+
});
|
|
211048
|
+
}
|
|
211049
|
+
queuePendingTextSnapshot() {
|
|
211050
|
+
if (!this.pendingTextEvent || this.fatalError) return;
|
|
211051
|
+
if (this.timer) {
|
|
211052
|
+
clearTimeout(this.timer);
|
|
211053
|
+
this.timer = null;
|
|
211054
|
+
}
|
|
211055
|
+
const baseEvent = this.pendingTextEvent;
|
|
211056
|
+
this.pendingTextEvent = void 0;
|
|
211057
|
+
const snapshotEvent = {
|
|
211058
|
+
...baseEvent,
|
|
211059
|
+
data: {
|
|
211060
|
+
...baseEvent.data,
|
|
211061
|
+
content: this.accumulatedText,
|
|
211062
|
+
snapshotMode: "replace",
|
|
211063
|
+
snapshotSeq: ++this.nextSnapshotSeq
|
|
211064
|
+
}
|
|
211065
|
+
};
|
|
211066
|
+
this.enqueue(async () => {
|
|
211067
|
+
await this.sink.ingest([snapshotEvent]);
|
|
211068
|
+
});
|
|
211069
|
+
}
|
|
211070
|
+
};
|
|
211005
211071
|
const exec = async (options) => {
|
|
211006
211072
|
if (!SUPPORTED_AGENT_TYPES.has(options.type)) {
|
|
211007
211073
|
log$7.error(`Unsupported --type "${options.type}". Supported: ${[...SUPPORTED_AGENT_TYPES].join(", ")}`);
|
|
@@ -211027,11 +211093,13 @@ const exec = async (options) => {
|
|
|
211027
211093
|
const emitJsonl = options.render === "jsonl" || options.render === void 0 && !serverIngest;
|
|
211028
211094
|
const agentType = options.type;
|
|
211029
211095
|
let sink;
|
|
211030
|
-
|
|
211031
|
-
|
|
211032
|
-
|
|
211096
|
+
let serverIngester;
|
|
211097
|
+
if (serverIngest) {
|
|
211098
|
+
sink = new TrpcIngestSink(await getTrpcClient(), agentType, operationId, options.topic, process.env.LOBEHUB_ASSISTANT_MESSAGE_ID);
|
|
211099
|
+
serverIngester = new SerialServerIngester(sink);
|
|
211100
|
+
}
|
|
211033
211101
|
/**
|
|
211034
|
-
* Spawn one agent process and stream all its events into
|
|
211102
|
+
* Spawn one agent process and stream all its events into the server ingester.
|
|
211035
211103
|
*
|
|
211036
211104
|
* When `interceptResumeErrors` is true, any `error`-type event whose
|
|
211037
211105
|
* message matches `RESUME_RETRY_PATTERNS` is withheld from the
|
|
@@ -211057,6 +211125,7 @@ const exec = async (options) => {
|
|
|
211057
211125
|
}
|
|
211058
211126
|
const STDERR_CAP = 8 * 1024;
|
|
211059
211127
|
let stderrContent = "";
|
|
211128
|
+
const stderrEnded = once(handle.stderr, "end").then(() => void 0);
|
|
211060
211129
|
handle.stderr.on("data", (chunk) => {
|
|
211061
211130
|
if (stderrContent.length < STDERR_CAP) stderrContent += chunk.toString();
|
|
211062
211131
|
});
|
|
@@ -211069,22 +211138,22 @@ const exec = async (options) => {
|
|
|
211069
211138
|
}
|
|
211070
211139
|
interrupted = true;
|
|
211071
211140
|
handle.kill("SIGINT");
|
|
211072
|
-
if (
|
|
211073
|
-
await
|
|
211141
|
+
if (serverIngester && sink) try {
|
|
211142
|
+
await serverIngester.drain();
|
|
211074
211143
|
await sink.finish({ result: "cancelled" });
|
|
211075
211144
|
} catch {}
|
|
211076
211145
|
};
|
|
211077
211146
|
const onSigterm = async () => {
|
|
211078
211147
|
handle.kill("SIGTERM");
|
|
211079
|
-
if (
|
|
211080
|
-
await
|
|
211148
|
+
if (serverIngester && sink) try {
|
|
211149
|
+
await serverIngester.drain();
|
|
211081
211150
|
await sink.finish({ result: "cancelled" });
|
|
211082
211151
|
} catch {}
|
|
211083
211152
|
};
|
|
211084
211153
|
process.on("SIGINT", onSigint);
|
|
211085
211154
|
process.on("SIGTERM", onSigterm);
|
|
211086
211155
|
let resumeNotFound = false;
|
|
211087
|
-
|
|
211156
|
+
const ingestError = false;
|
|
211088
211157
|
try {
|
|
211089
211158
|
for await (const event of handle.events) {
|
|
211090
211159
|
if (interceptResumeErrors && event.type === "error") {
|
|
@@ -211096,12 +211165,12 @@ const exec = async (options) => {
|
|
|
211096
211165
|
}
|
|
211097
211166
|
}
|
|
211098
211167
|
if (emitJsonl) process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
211099
|
-
|
|
211168
|
+
serverIngester?.push(event);
|
|
211100
211169
|
}
|
|
211101
211170
|
} catch (err) {
|
|
211102
211171
|
log$7.error("Stream error from agent process:", err instanceof Error ? err.message : String(err));
|
|
211103
|
-
if (
|
|
211104
|
-
await
|
|
211172
|
+
if (serverIngester && sink) try {
|
|
211173
|
+
await serverIngester.drain();
|
|
211105
211174
|
await sink.finish({
|
|
211106
211175
|
error: {
|
|
211107
211176
|
message: String(err),
|
|
@@ -211116,6 +211185,7 @@ const exec = async (options) => {
|
|
|
211116
211185
|
process.off("SIGTERM", onSigterm);
|
|
211117
211186
|
}
|
|
211118
211187
|
const { code, signal } = await handle.exit;
|
|
211188
|
+
await stderrEnded;
|
|
211119
211189
|
if (interceptResumeErrors && !resumeNotFound && code !== 0 && looksLikeNeedsRetryWithoutResume(stderrContent)) resumeNotFound = true;
|
|
211120
211190
|
return {
|
|
211121
211191
|
code,
|
|
@@ -211147,9 +211217,9 @@ const exec = async (options) => {
|
|
|
211147
211217
|
}, false);
|
|
211148
211218
|
}
|
|
211149
211219
|
const { code, signal, sessionId } = result;
|
|
211150
|
-
if (
|
|
211220
|
+
if (serverIngester && sink) {
|
|
211151
211221
|
try {
|
|
211152
|
-
await
|
|
211222
|
+
await serverIngester.drain();
|
|
211153
211223
|
} catch (err) {
|
|
211154
211224
|
log$7.error("Failed to flush events to server:", err instanceof Error ? err.message : String(err));
|
|
211155
211225
|
result = {
|
|
@@ -215037,7 +215107,11 @@ function createProgram() {
|
|
|
215037
215107
|
|
|
215038
215108
|
//#endregion
|
|
215039
215109
|
//#region src/index.ts
|
|
215040
|
-
createProgram().
|
|
215110
|
+
createProgram().parseAsync(process.argv, { from: "node" }).catch((error) => {
|
|
215111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215112
|
+
log$7.error(message);
|
|
215113
|
+
process.exit(1);
|
|
215114
|
+
});
|
|
215041
215115
|
|
|
215042
215116
|
//#endregion
|
|
215043
215117
|
export { };
|
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.
|
|
3
|
+
.TH LH 1 "" "@lobehub/cli 0.0.21" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
lh \- LobeHub CLI \- manage and connect to LobeHub services
|
|
6
6
|
.SH SYNOPSIS
|