@lobehub/cli 0.0.34 → 0.0.36

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
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
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
- import fs, { constants, createWriteStream, existsSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
6
+ import fs, { constants, createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
7
7
  import process$1 from "node:process";
8
8
  import crypto$1, { createHash, randomUUID } from "node:crypto";
9
9
  import os, { platform, tmpdir } from "node:os";
@@ -5217,7 +5217,8 @@ const CLI_API_KEY_ENV = "LOBEHUB_CLI_API_KEY";
5217
5217
 
5218
5218
  //#endregion
5219
5219
  //#region src/api/client.ts
5220
- let _client;
5220
+ const PERSONAL_KEY = "__personal__";
5221
+ const _clients = /* @__PURE__ */ new Map();
5221
5222
  let _toolsClient;
5222
5223
  async function getAuthAndServer() {
5223
5224
  const envJwt = process.env.LOBEHUB_JWT;
@@ -5247,15 +5248,36 @@ async function getAuthAndServer() {
5247
5248
  serverUrl
5248
5249
  };
5249
5250
  }
5250
- async function getTrpcClient() {
5251
- if (_client) return _client;
5251
+ /**
5252
+ * Resolve the workspace scope for outbound tRPC calls.
5253
+ *
5254
+ * Precedence: explicit caller arg → `LOBEHUB_WORKSPACE_ID` env (inherited
5255
+ * from a workspace-dispatched parent process, e.g. openclaw spawned by the
5256
+ * device's `runHeteroTask`) → personal mode. Without this, agentNotify
5257
+ * callbacks on workspace topics would resolve through personal-mode
5258
+ * TopicModel and 404.
5259
+ */
5260
+ function resolveWorkspaceId(explicit) {
5261
+ if (explicit) return explicit;
5262
+ const fromEnv = process.env.LOBEHUB_WORKSPACE_ID;
5263
+ return fromEnv && fromEnv.length > 0 ? fromEnv : void 0;
5264
+ }
5265
+ async function getTrpcClient(workspaceId) {
5266
+ const wsId = resolveWorkspaceId(workspaceId);
5267
+ const cacheKey = wsId ?? PERSONAL_KEY;
5268
+ const cached = _clients.get(cacheKey);
5269
+ if (cached) return cached;
5252
5270
  const { headers, serverUrl } = await getAuthAndServer();
5253
- _client = createTRPCClient({ links: [httpLink({
5254
- headers,
5271
+ const client = createTRPCClient({ links: [httpLink({
5272
+ headers: wsId ? {
5273
+ ...headers,
5274
+ "X-Workspace-Id": wsId
5275
+ } : headers,
5255
5276
  transformer: SuperJSON,
5256
5277
  url: `${serverUrl}/trpc/lambda`
5257
5278
  })] });
5258
- return _client;
5279
+ _clients.set(cacheKey, client);
5280
+ return client;
5259
5281
  }
5260
5282
  /**
5261
5283
  * Build a Lambda tRPC client from an already-resolved auth context, without
@@ -5264,9 +5286,12 @@ async function getTrpcClient() {
5264
5286
  * via env/stored creds and `process.exit(1)` when none exist, which would
5265
5287
  * abort an otherwise-valid explicit-token session.
5266
5288
  */
5267
- function createLambdaClient(auth) {
5289
+ function createLambdaClient(auth, workspaceId) {
5268
5290
  return createTRPCClient({ links: [httpLink({
5269
- headers: auth.tokenType === "apiKey" ? { "X-API-Key": auth.token } : { "Oidc-Auth": auth.token },
5291
+ headers: {
5292
+ ...auth.tokenType === "apiKey" ? { "X-API-Key": auth.token } : { "Oidc-Auth": auth.token },
5293
+ ...workspaceId ? { "X-Workspace-Id": workspaceId } : {}
5294
+ },
5270
5295
  transformer: SuperJSON,
5271
5296
  url: `${auth.serverUrl}/trpc/lambda`
5272
5297
  })] });
@@ -7642,7 +7667,9 @@ const HotkeyEnum = {
7642
7667
  DeleteLastMessage: "deleteLastMessage",
7643
7668
  EditMessage: "editMessage",
7644
7669
  NavigateToChat: "navigateToChat",
7670
+ NextTab: "nextTab",
7645
7671
  OpenChatSettings: "openChatSettings",
7672
+ PrevTab: "prevTab",
7646
7673
  OpenHotkeyHelper: "openHotkeyHelper",
7647
7674
  RegenerateMessage: "regenerateMessage",
7648
7675
  SaveDocument: "saveDocument",
@@ -7650,6 +7677,7 @@ const HotkeyEnum = {
7650
7677
  Search: "search",
7651
7678
  ShowApp: "showApp",
7652
7679
  SwitchAgent: "switchAgent",
7680
+ SwitchTab: "switchTab",
7653
7681
  ToggleLeftPanel: "toggleLeftPanel",
7654
7682
  ToggleRightPanel: "toggleRightPanel"
7655
7683
  };
@@ -7683,6 +7711,31 @@ const HOTKEYS_REGISTRATION = [
7683
7711
  nonEditable: true,
7684
7712
  scopes: [HotkeyScopeEnum.Global]
7685
7713
  },
7714
+ {
7715
+ group: HotkeyGroupEnum.Essential,
7716
+ id: HotkeyEnum.SwitchTab,
7717
+ keys: combineKeys([KeyEnum.Mod, KeyEnum.Number]),
7718
+ nonEditable: true,
7719
+ scopes: [HotkeyScopeEnum.Global]
7720
+ },
7721
+ {
7722
+ group: HotkeyGroupEnum.Essential,
7723
+ id: HotkeyEnum.NextTab,
7724
+ keys: combineKeys([KeyEnum.Ctrl, KeyEnum.Tab]),
7725
+ nonEditable: true,
7726
+ scopes: [HotkeyScopeEnum.Global]
7727
+ },
7728
+ {
7729
+ group: HotkeyGroupEnum.Essential,
7730
+ id: HotkeyEnum.PrevTab,
7731
+ keys: combineKeys([
7732
+ KeyEnum.Ctrl,
7733
+ KeyEnum.Shift,
7734
+ KeyEnum.Tab
7735
+ ]),
7736
+ nonEditable: true,
7737
+ scopes: [HotkeyScopeEnum.Global]
7738
+ },
7686
7739
  {
7687
7740
  group: HotkeyGroupEnum.Essential,
7688
7741
  id: HotkeyEnum.NavigateToChat,
@@ -8755,7 +8808,7 @@ const MEMORY_SEARCH_TOP_K_LIMITS = {
8755
8808
 
8756
8809
  //#endregion
8757
8810
  //#region ../../package.json
8758
- var version$2 = "2.2.6";
8811
+ var version$2 = "2.2.7";
8759
8812
 
8760
8813
  //#endregion
8761
8814
  //#region ../../packages/const/src/version.ts
@@ -204262,6 +204315,7 @@ var GatewayClient = class extends EventEmitter {
204262
204315
  token;
204263
204316
  tokenType;
204264
204317
  userId;
204318
+ workspaceId;
204265
204319
  serverUrl;
204266
204320
  logger;
204267
204321
  autoReconnect;
@@ -204275,6 +204329,7 @@ var GatewayClient = class extends EventEmitter {
204275
204329
  this.channel = options.channel;
204276
204330
  this.serverUrl = options.serverUrl;
204277
204331
  this.userId = options.userId;
204332
+ this.workspaceId = options.workspaceId;
204278
204333
  this.logger = options.logger || noopLogger;
204279
204334
  this.autoReconnect = options.autoReconnect ?? true;
204280
204335
  }
@@ -204380,7 +204435,8 @@ var GatewayClient = class extends EventEmitter {
204380
204435
  platform: process.platform
204381
204436
  });
204382
204437
  if (this.channel) params.set("channel", this.channel);
204383
- if (this.userId) params.set("userId", this.userId);
204438
+ if (this.workspaceId) params.set("workspaceId", this.workspaceId);
204439
+ else if (this.userId) params.set("userId", this.userId);
204384
204440
  return `${wsProtocol}://${host}/ws?${params.toString()}`;
204385
204441
  }
204386
204442
  handleOpen = () => {
@@ -204708,7 +204764,7 @@ const buildHeteroExecStdinPayload = (params) => {
204708
204764
  * instead flows back as a dispatch failure the user can see.
204709
204765
  */
204710
204766
  function spawnHeteroAgentRun(params, logger) {
204711
- const { agentType, cwd, imageList, jwt, operationId, prompt, resumeSessionId, serverUrl, systemContext, topicId } = params;
204767
+ const { agentType, args: extraArgs, cwd, imageList, jwt, operationId, prompt, resumeSessionId, serverUrl, systemContext, topicId } = params;
204712
204768
  const workDir = cwd ?? process.cwd();
204713
204769
  const cliArgs = [
204714
204770
  process.argv[1],
@@ -204726,7 +204782,8 @@ function spawnHeteroAgentRun(params, logger) {
204726
204782
  "-",
204727
204783
  "--cwd",
204728
204784
  workDir,
204729
- ...resumeSessionId ? ["--resume", resumeSessionId] : []
204785
+ ...resumeSessionId ? ["--resume", resumeSessionId] : [],
204786
+ ...extraArgs ?? []
204730
204787
  ];
204731
204788
  const stdinPayload = buildHeteroExecStdinPayload({
204732
204789
  imageList,
@@ -205682,6 +205739,34 @@ async function registerDevice(auth, identity) {
205682
205739
  platform: process.platform
205683
205740
  });
205684
205741
  }
205742
+ /**
205743
+ * Identity for a WORKSPACE device: derived from the workspaceId (namespaced) so
205744
+ * the same physical machine enrolled into a workspace is a distinct device from
205745
+ * its personal identity, and stable across reconnects.
205746
+ */
205747
+ function resolveWorkspaceDeviceIdentity(workspaceId, explicitDeviceId) {
205748
+ if (explicitDeviceId) return {
205749
+ deviceId: explicitDeviceId,
205750
+ identitySource: "fallback"
205751
+ };
205752
+ return deriveDeviceId(`workspace:${workspaceId}`);
205753
+ }
205754
+ /**
205755
+ * Mint a workspace-device connect token (owner-only on the server). The returned
205756
+ * token carries the `workspace_id` claim the gateway routes by.
205757
+ */
205758
+ async function mintWorkspaceConnectToken(auth, workspaceId) {
205759
+ return createLambdaClient(auth, workspaceId).device.mintWorkspaceConnectToken.mutate();
205760
+ }
205761
+ /** Register this machine as a device of the given workspace (owner-only). */
205762
+ async function registerWorkspaceDevice(auth, identity, workspaceId) {
205763
+ await createLambdaClient(auth, workspaceId).device.registerWorkspaceDevice.mutate({
205764
+ deviceId: identity.deviceId,
205765
+ hostname: os.hostname(),
205766
+ identitySource: identity.identitySource,
205767
+ platform: process.platform
205768
+ });
205769
+ }
205685
205770
 
205686
205771
  //#endregion
205687
205772
  //#region src/tools/checkPlatformCapability.ts
@@ -205916,9 +206001,9 @@ function resolveLhPath() {
205916
206001
  return "lh";
205917
206002
  }
205918
206003
  }
205919
- async function sendAutoNotify(topicId, taskId, text, agentId) {
206004
+ async function sendAutoNotify(topicId, taskId, text, agentId, workspaceId) {
205920
206005
  try {
205921
- await (await getTrpcClient()).agentNotify.notify.mutate({
206006
+ await (await getTrpcClient(workspaceId)).agentNotify.notify.mutate({
205922
206007
  agentId,
205923
206008
  content: text,
205924
206009
  role: "assistant",
@@ -205935,9 +206020,9 @@ async function sendAutoNotify(topicId, taskId, text, agentId) {
205935
206020
  * `sendAutoNotify` which writes an error message AND triggers completion via
205936
206021
  * the `done` flag.
205937
206022
  */
205938
- async function sendDoneSignal(topicId, agentId) {
206023
+ async function sendDoneSignal(topicId, agentId, workspaceId) {
205939
206024
  try {
205940
- await (await getTrpcClient()).agentNotify.notify.mutate({
206025
+ await (await getTrpcClient(workspaceId)).agentNotify.notify.mutate({
205941
206026
  agentId,
205942
206027
  content: "",
205943
206028
  done: true,
@@ -205966,9 +206051,13 @@ This conversation / task was sent to you by the **LobeHub platform** on behalf o
205966
206051
  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.`;
205967
206052
  }
205968
206053
  async function runHeteroTask(params) {
205969
- const { agentId, agentType, cwd, operationId, prompt, taskId, topicId } = params;
206054
+ const { agentId, agentType, cwd, operationId, prompt, taskId, topicId, workspaceId } = params;
205970
206055
  const workDir = cwd || process.cwd();
205971
206056
  const lhPath = resolveLhPath();
206057
+ const childEnv = workspaceId ? {
206058
+ ...process.env,
206059
+ LOBEHUB_WORKSPACE_ID: workspaceId
206060
+ } : { ...process.env };
205972
206061
  if (agentType === "openclaw") {
205973
206062
  const openclawAgent = process.env.OPENCLAW_AGENT_ID ?? "main";
205974
206063
  const enrichedPrompt = `${prompt}\n\n${buildNotifyProtocol(lhPath, topicId)}`;
@@ -205990,7 +206079,7 @@ async function runHeteroTask(params) {
205990
206079
  ], {
205991
206080
  cwd: workDir,
205992
206081
  detached: true,
205993
- env: { ...process.env },
206082
+ env: childEnv,
205994
206083
  stdio: "ignore"
205995
206084
  });
205996
206085
  const pid = child.pid;
@@ -206003,13 +206092,14 @@ async function runHeteroTask(params) {
206003
206092
  pid,
206004
206093
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
206005
206094
  taskId,
206006
- topicId
206095
+ topicId,
206096
+ workspaceId
206007
206097
  });
206008
206098
  log$10.info(`OpenClaw task started: taskId=${taskId} pid=${pid} agent=${openclawAgent}`);
206009
206099
  child.on("close", (code, signal) => {
206010
206100
  removeTask(taskId);
206011
- if (code !== 0 || signal !== null) sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId).finally(() => sendDoneSignal(topicId, agentId));
206012
- else sendDoneSignal(topicId, agentId);
206101
+ if (code !== 0 || signal !== null) sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId, workspaceId).finally(() => sendDoneSignal(topicId, agentId, workspaceId));
206102
+ else sendDoneSignal(topicId, agentId, workspaceId);
206013
206103
  });
206014
206104
  return JSON.stringify({
206015
206105
  pid,
@@ -206035,7 +206125,7 @@ async function runHeteroTask(params) {
206035
206125
  const child = spawn("hermes", hermesArgs, {
206036
206126
  cwd: workDir,
206037
206127
  detached: true,
206038
- env: { ...process.env },
206128
+ env: childEnv,
206039
206129
  stdio: [
206040
206130
  "ignore",
206041
206131
  "pipe",
@@ -206052,7 +206142,8 @@ async function runHeteroTask(params) {
206052
206142
  pid,
206053
206143
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
206054
206144
  taskId,
206055
- topicId
206145
+ topicId,
206146
+ workspaceId
206056
206147
  });
206057
206148
  log$10.info(`Hermes task started: taskId=${taskId} pid=${pid}`);
206058
206149
  let stdout = "";
@@ -206062,14 +206153,14 @@ async function runHeteroTask(params) {
206062
206153
  child.on("close", (code, signal) => {
206063
206154
  removeTask(taskId);
206064
206155
  if (code !== 0 || signal !== null) {
206065
- sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId).finally(() => sendDoneSignal(topicId, agentId));
206156
+ sendAutoNotify(topicId, taskId, signal ? `Task cancelled (signal: ${signal})` : `Task failed (exit code: ${code})`, agentId, workspaceId).finally(() => sendDoneSignal(topicId, agentId, workspaceId));
206066
206157
  return;
206067
206158
  }
206068
206159
  const sessionId = stdout.match(/^session_id:\s*(\S+)/m)?.[1];
206069
206160
  const response = stdout.replace(/^session_id:[^\n]*\n?/, "").trim();
206070
206161
  if (sessionId) saveHermesSessionId(topicId, sessionId);
206071
- if (response) sendAutoNotify(topicId, taskId, response, agentId).finally(() => sendDoneSignal(topicId, agentId));
206072
- else sendDoneSignal(topicId, agentId);
206162
+ if (response) sendAutoNotify(topicId, taskId, response, agentId, workspaceId).finally(() => sendDoneSignal(topicId, agentId, workspaceId));
206163
+ else sendDoneSignal(topicId, agentId, workspaceId);
206073
206164
  });
206074
206165
  return JSON.stringify({
206075
206166
  pid,
@@ -206090,7 +206181,7 @@ async function cancelHeteroTask(params) {
206090
206181
  } catch (err) {
206091
206182
  log$10.warn(`Failed to send ${signal} to pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`);
206092
206183
  removeTask(taskId);
206093
- await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId);
206184
+ await sendAutoNotify(entry.topicId, taskId, "Task already completed or cancelled", entry.agentId, entry.workspaceId);
206094
206185
  }
206095
206186
  return JSON.stringify({
206096
206187
  pid: entry.pid,
@@ -207160,7 +207251,7 @@ async function executeToolCall(apiName, argsStr, timeout) {
207160
207251
  //#endregion
207161
207252
  //#region src/commands/connect.ts
207162
207253
  function registerConnectCommand(program) {
207163
- const connectCmd = program.command("connect").description("Connect to the device gateway and listen for tool calls").option("--token <jwt>", "JWT access token").option("--gateway <url>", "Device gateway URL").option("--device-id <id>", "Device ID (auto-generated if not provided)").option("-v, --verbose", "Enable verbose logging").option("-d, --daemon", "Run as a background daemon process").option("--daemon-child", "Internal: runs as the daemon child process").action(async (options) => {
207254
+ const connectCmd = program.command("connect").description("Connect to the device gateway and listen for tool calls").option("--token <jwt>", "JWT access token").option("--gateway <url>", "Device gateway URL").option("--device-id <id>", "Device ID (auto-generated if not provided)").option("--workspace <id>", "Enroll as a device of this workspace (admin only)").option("-v, --verbose", "Enable verbose logging").option("-d, --daemon", "Run as a background daemon process").option("--daemon-child", "Internal: runs as the daemon child process").action(async (options) => {
207164
207255
  if (options.verbose) setVerbose(true);
207165
207256
  if (options.daemon) return handleDaemonStart(options);
207166
207257
  await runConnect(options, options.daemonChild || process.env.LOBEHUB_DAEMON === "1");
@@ -207228,6 +207319,7 @@ function buildDaemonArgs(options) {
207228
207319
  if (options.token) args.push("--token", options.token);
207229
207320
  if (options.gateway) args.push("--gateway", options.gateway);
207230
207321
  if (options.deviceId) args.push("--device-id", options.deviceId);
207322
+ if (options.workspace) args.push("--workspace", options.workspace);
207231
207323
  if (options.verbose) args.push("--verbose");
207232
207324
  return args;
207233
207325
  }
@@ -207245,7 +207337,25 @@ async function runConnect(options, isDaemonChild) {
207245
207337
  gatewayUrl
207246
207338
  });
207247
207339
  const resolvedGatewayUrl = gatewayUrl || "https://device-gateway.lobehub.com";
207248
- const identity = resolveDeviceIdentity(auth.userId, options.deviceId);
207340
+ const workspaceId = options.workspace;
207341
+ const identity = workspaceId ? resolveWorkspaceDeviceIdentity(workspaceId, options.deviceId) : resolveDeviceIdentity(auth.userId, options.deviceId);
207342
+ let connectToken = auth.token;
207343
+ let connectTokenType = auth.tokenType;
207344
+ if (workspaceId) {
207345
+ connectToken = (await mintWorkspaceConnectToken(auth, workspaceId)).token;
207346
+ connectTokenType = "jwt";
207347
+ }
207348
+ const refreshConnectToken = async () => {
207349
+ const refreshed = await resolveToken({});
207350
+ if (!refreshed) return void 0;
207351
+ auth = refreshed;
207352
+ if (workspaceId) {
207353
+ connectToken = (await mintWorkspaceConnectToken(auth, workspaceId)).token;
207354
+ return connectToken;
207355
+ }
207356
+ connectToken = refreshed.token;
207357
+ return connectToken;
207358
+ };
207249
207359
  const client = new GatewayClient({
207250
207360
  channel: process.env.LOBEHUB_CLI_CHANNEL || "cli",
207251
207361
  connectionId: loadOrCreateConnectionId(),
@@ -207253,9 +207363,10 @@ async function runConnect(options, isDaemonChild) {
207253
207363
  gatewayUrl: resolvedGatewayUrl,
207254
207364
  logger: isDaemonChild ? createDaemonLogger() : log$10,
207255
207365
  serverUrl: auth.serverUrl,
207256
- token: auth.token,
207257
- tokenType: auth.tokenType,
207258
- userId: auth.userId
207366
+ token: connectToken,
207367
+ tokenType: connectTokenType,
207368
+ userId: workspaceId ? void 0 : auth.userId,
207369
+ workspaceId
207259
207370
  });
207260
207371
  const info = (msg) => {
207261
207372
  if (isDaemonChild) appendLog(msg);
@@ -207347,6 +207458,7 @@ async function runConnect(options, isDaemonChild) {
207347
207458
  try {
207348
207459
  const ack = await spawnHeteroAgentRun({
207349
207460
  agentType: request.agentType,
207461
+ args: request.args,
207350
207462
  cwd: request.cwd,
207351
207463
  imageList: request.imageList,
207352
207464
  jwt: request.jwt,
@@ -207383,23 +207495,26 @@ async function runConnect(options, isDaemonChild) {
207383
207495
  client.on("reconnecting", () => {
207384
207496
  updateStatus("reconnecting");
207385
207497
  });
207386
- const startProactiveRefresh = () => scheduleProactiveRefresh(auth, (refreshed) => {
207387
- client.updateToken(refreshed.token);
207388
- auth = refreshed;
207389
- cancelRefreshTimer = startProactiveRefresh();
207498
+ const startProactiveRefresh = () => scheduleProactiveRefresh(connectToken, connectTokenType, async () => {
207499
+ const newToken = await refreshConnectToken();
207500
+ if (newToken) {
207501
+ client.updateToken(newToken);
207502
+ cancelRefreshTimer = startProactiveRefresh();
207503
+ }
207504
+ return newToken;
207390
207505
  }, info, error);
207391
207506
  let cancelRefreshTimer = startProactiveRefresh();
207392
207507
  let authFailedRefreshAttempted = false;
207393
207508
  client.on("auth_failed", async (reason) => {
207394
- if (auth.tokenType === "jwt" && !authFailedRefreshAttempted) {
207509
+ if (connectTokenType === "jwt" && !authFailedRefreshAttempted) {
207395
207510
  authFailedRefreshAttempted = true;
207396
207511
  info(`Authentication failed (${reason}). Attempting token refresh...`);
207397
207512
  try {
207398
- const refreshed = await resolveToken({});
207399
- if (refreshed && refreshed.token !== auth.token) {
207513
+ const prev = connectToken;
207514
+ const newToken = await refreshConnectToken();
207515
+ if (newToken && newToken !== prev) {
207400
207516
  info("Token refreshed successfully. Reconnecting...");
207401
- client.updateToken(refreshed.token);
207402
- auth = refreshed;
207517
+ client.updateToken(newToken);
207403
207518
  authFailedRefreshAttempted = false;
207404
207519
  cancelRefreshTimer = startProactiveRefresh();
207405
207520
  await client.reconnect();
@@ -207413,14 +207528,13 @@ async function runConnect(options, isDaemonChild) {
207413
207528
  process.exit(1);
207414
207529
  });
207415
207530
  client.on("auth_expired", async () => {
207416
- if (auth.tokenType === "apiKey") return;
207531
+ if (connectTokenType === "apiKey") return;
207417
207532
  info("Authentication expired. Attempting to refresh token...");
207418
207533
  try {
207419
- const refreshed = await resolveToken({});
207420
- if (refreshed) {
207534
+ const newToken = await refreshConnectToken();
207535
+ if (newToken) {
207421
207536
  info("Token refreshed successfully. Reconnecting...");
207422
- client.updateToken(refreshed.token);
207423
- auth = refreshed;
207537
+ client.updateToken(newToken);
207424
207538
  cancelRefreshTimer = startProactiveRefresh();
207425
207539
  await client.reconnect();
207426
207540
  return;
@@ -207450,7 +207564,8 @@ async function runConnect(options, isDaemonChild) {
207450
207564
  process.exit(0);
207451
207565
  });
207452
207566
  if (identity) try {
207453
- await registerDevice(auth, identity);
207567
+ if (workspaceId) await registerWorkspaceDevice(auth, identity, workspaceId);
207568
+ else await registerDevice(auth, identity);
207454
207569
  } catch (err) {
207455
207570
  error(`Device registration failed (non-fatal): ${err.message}`);
207456
207571
  }
@@ -207488,31 +207603,31 @@ function parseJwtExp(token) {
207488
207603
  }
207489
207604
  }
207490
207605
  /**
207491
- * Schedule a proactive token refresh before the JWT expires.
207492
- * Returns a cleanup function that cancels the scheduled timer.
207606
+ * Schedule a proactive token refresh before the (connect) token expires.
207607
+ * `refresh` performs the actual refresh — re-minting a workspace token or
207608
+ * refreshing the user token — and returns the new token. Returns a cleanup
207609
+ * function that cancels the scheduled timer.
207493
207610
  */
207494
- function scheduleProactiveRefresh(auth, onRefreshed, info, error) {
207495
- if (auth.tokenType !== "jwt") return null;
207496
- const exp = parseJwtExp(auth.token);
207611
+ function scheduleProactiveRefresh(token, tokenType, refresh, info, error) {
207612
+ if (tokenType !== "jwt") return null;
207613
+ const exp = parseJwtExp(token);
207497
207614
  if (!exp) return null;
207498
- const delay = (exp - PROACTIVE_REFRESH_BUFFER) * 1e3 - Date.now();
207499
- if (delay < 0) {
207615
+ const lifetimeMs = exp * 1e3 - Date.now();
207616
+ if (lifetimeMs <= 0) {
207500
207617
  doRefresh();
207501
207618
  return null;
207502
207619
  }
207620
+ const delay = lifetimeMs - Math.min(PROACTIVE_REFRESH_BUFFER * 1e3, lifetimeMs / 2);
207503
207621
  const timer = setTimeout(() => void doRefresh(), delay);
207504
207622
  return () => clearTimeout(timer);
207505
207623
  async function doRefresh() {
207506
207624
  try {
207507
- if (!await getValidToken(PROACTIVE_REFRESH_BUFFER)) {
207625
+ const newToken = await refresh();
207626
+ if (!newToken) {
207508
207627
  error("Proactive token refresh failed — no valid credentials.");
207509
207628
  return;
207510
207629
  }
207511
- const refreshed = await resolveToken({});
207512
- if (refreshed.token !== auth.token) {
207513
- info("Proactively refreshed token.");
207514
- onRefreshed(refreshed);
207515
- }
207630
+ if (newToken !== token) info("Proactively refreshed token.");
207516
207631
  } catch {
207517
207632
  error("Proactive token refresh failed.");
207518
207633
  }
@@ -209109,6 +209224,17 @@ var ClaudeCodeAdapter = class {
209109
209224
  sawStreamEvent = false;
209110
209225
  /** Track current message.id to detect step boundaries */
209111
209226
  currentMessageId;
209227
+ /**
209228
+ * Whether the current turn (the in-flight `currentMessageId`) has already
209229
+ * emitted a `tool_use`. When CC reuses the SAME `message.id` to stream the
209230
+ * model's post-tool answer (it continues after the `tool_result` without
209231
+ * minting a fresh id — seen on device/batch `lh hetero exec` runs), that
209232
+ * trailing text must NOT coalesce onto the tool-issuing assistant. We force a
209233
+ * step boundary so the answer anchors to its own assistant, chained after the
209234
+ * tool results — otherwise text + `tool_use` share one message and the
209235
+ * renderer drops the tool block below the answer.
209236
+ */
209237
+ currentTurnHadToolUse = false;
209112
209238
  /** message.id of the stream_event delta flow currently in flight */
209113
209239
  currentStreamEventMessageId;
209114
209240
  /**
@@ -209299,7 +209425,10 @@ var ClaudeCodeAdapter = class {
209299
209425
  if (parentToolUseId) return this.handleSubagentAssistant(raw, parentToolUseId);
209300
209426
  const events = [];
209301
209427
  const messageId = raw.message?.id;
209302
- events.push(...this.openMainMessage(messageId, raw.message?.model));
209428
+ const hasTextBlock = content.some((b) => b?.type === "text" && b.text);
209429
+ const hasToolUseBlock = content.some((b) => b?.type === "tool_use");
209430
+ const isPostToolTextReusingId = hasTextBlock && !hasToolUseBlock && messageId !== void 0 && messageId === this.currentMessageId && this.currentTurnHadToolUse;
209431
+ events.push(...this.openMainMessage(messageId, raw.message?.model, isPostToolTextReusingId));
209303
209432
  if (raw.message?.model) this.currentStreamEventModel = raw.message.model;
209304
209433
  const textParts = [];
209305
209434
  const reasoningParts = [];
@@ -209330,7 +209459,10 @@ var ClaudeCodeAdapter = class {
209330
209459
  break;
209331
209460
  }
209332
209461
  }
209333
- if (newToolCalls.length > 0) this.pendingExternalSignal = void 0;
209462
+ if (newToolCalls.length > 0) {
209463
+ this.pendingExternalSignal = void 0;
209464
+ this.currentTurnHadToolUse = true;
209465
+ }
209334
209466
  const textCompletion = this.getTrailingCompletion(messageId, textParts.join(""), this.streamedTextByMessageId);
209335
209467
  const thinkingCompletion = this.getTrailingCompletion(messageId, reasoningParts.join(""), this.streamedThinkingByMessageId);
209336
209468
  if (thinkingCompletion) events.push(this.makeChunkEvent({
@@ -209699,22 +209831,37 @@ var ClaudeCodeAdapter = class {
209699
209831
  * record the id (init already primed the executor).
209700
209832
  * - Otherwise: advance stepIndex and emit stream_end + stream_start(newStep).
209701
209833
  */
209702
- openMainMessage(messageId, model) {
209834
+ openMainMessage(messageId, model, forcePostToolBoundary = false) {
209703
209835
  if (!messageId) return [];
209704
209836
  if (!this.started) {
209705
209837
  this.started = true;
209706
209838
  this.currentMessageId = messageId;
209839
+ this.currentTurnHadToolUse = false;
209707
209840
  return [this.makeEvent("stream_start", {
209708
209841
  model,
209709
209842
  provider: "claude-code"
209710
209843
  })];
209711
209844
  }
209712
- if (messageId === this.currentMessageId) return [];
209845
+ if (messageId === this.currentMessageId) {
209846
+ if (!forcePostToolBoundary) return [];
209847
+ this.stepIndex++;
209848
+ this.currentTurnHadToolUse = false;
209849
+ this.hasUnhandledUserInput = false;
209850
+ this.pendingExternalSignal = void 0;
209851
+ return [this.makeEvent("stream_end", {}), this.makeEvent("stream_start", {
209852
+ messageId: `${messageId}:s${this.stepIndex}`,
209853
+ model,
209854
+ newStep: true,
209855
+ provider: "claude-code"
209856
+ })];
209857
+ }
209713
209858
  if (this.currentMessageId === void 0) {
209714
209859
  this.currentMessageId = messageId;
209860
+ this.currentTurnHadToolUse = false;
209715
209861
  return [];
209716
209862
  }
209717
209863
  this.currentMessageId = messageId;
209864
+ this.currentTurnHadToolUse = false;
209718
209865
  this.stepIndex++;
209719
209866
  if (!this.hasUnhandledUserInput && this.activeTasks.size > 0) {
209720
209867
  const lastTaskKey = [...this.activeTasks.keys()].at(-1);
@@ -211530,6 +211677,7 @@ var TrpcIngestSink = class {
211530
211677
  //#endregion
211531
211678
  //#region src/commands/hetero.ts
211532
211679
  const SUPPORTED_AGENT_TYPES = new Set(["claude-code", "codex"]);
211680
+ const CODEX_REASONING_EFFORT_CONFIG_KEY = "model_reasoning_effort";
211533
211681
  /**
211534
211682
  * Patterns that indicate a `--resume <sessionId>` run should be retried
211535
211683
  * without `--resume`. Two classes of failure:
@@ -211560,6 +211708,12 @@ const RESUME_RETRY_PATTERNS = [
211560
211708
  ];
211561
211709
  const looksLikeNeedsRetryWithoutResume = (text) => RESUME_RETRY_PATTERNS.some((p) => p.test(text));
211562
211710
  const collectImage = (value, previous = []) => [...previous, value];
211711
+ const collectAgentArg = (value, previous = []) => [...previous, value];
211712
+ const buildExtraArgs = (options) => {
211713
+ const selectorArgs = options.type === "codex" ? [...options.model ? ["--model", options.model] : [], ...options.effort ? ["-c", `${CODEX_REASONING_EFFORT_CONFIG_KEY}="${options.effort}"`] : []] : [...options.model ? ["--model", options.model] : [], ...options.effort ? ["--effort", options.effort] : []];
211714
+ const extraArgs = [...options.agentArg ?? [], ...selectorArgs];
211715
+ return extraArgs.length > 0 ? extraArgs : void 0;
211716
+ };
211563
211717
  const readStdin = async () => {
211564
211718
  const chunks = [];
211565
211719
  for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
@@ -211940,10 +212094,12 @@ const exec = async (options) => {
211940
212094
  };
211941
212095
  };
211942
212096
  const interceptResume = !!options.resume;
212097
+ const extraArgs = buildExtraArgs(options);
211943
212098
  const first = await runOneAgent({
211944
212099
  agentType: options.type,
211945
212100
  command: options.command,
211946
212101
  cwd: options.cwd || process.cwd(),
212102
+ extraArgs,
211947
212103
  operationId,
211948
212104
  prompt: resolved.prompt,
211949
212105
  resumeSessionId: options.resume
@@ -211955,6 +212111,7 @@ const exec = async (options) => {
211955
212111
  agentType: options.type,
211956
212112
  command: options.command,
211957
212113
  cwd: options.cwd || process.cwd(),
212114
+ extraArgs,
211958
212115
  operationId,
211959
212116
  prompt: resolved.prompt
211960
212117
  }, false, "attempt-2-noresume");
@@ -211994,7 +212151,7 @@ const exec = async (options) => {
211994
212151
  process.exit(1);
211995
212152
  };
211996
212153
  function registerHeteroCommand(program) {
211997
- program.command("hetero").description("Run heterogeneous agent CLIs (Claude Code / Codex) and stream their output").command("exec").description("Spawn a heterogeneous agent CLI and stream its events as JSONL on stdout. Standalone mode (no server ingest).").requiredOption("-t, --type <type>", `Agent type: ${[...SUPPORTED_AGENT_TYPES].join(" | ")}`).option("-p, --prompt [text]", "Prompt text. Pass `-` (or omit the value) to read from stdin.").option("-i, --image <path|url>", "Attach an image (repeatable). Accepts a local path, http(s) URL, or data: URL.", collectImage).option("--input-json <path>", "Read full multimodal prompt as JSON content blocks from a file. Use `-` for stdin.").option("-r, --resume <sessionId>", "Resume an existing agent session by its native id").option("-d, --cwd <path>", "Working directory for the spawned agent (default: process.cwd())").option("-c, --command <bin>", "Override the agent CLI binary name (default: `claude` or `codex`)").option("--operation-id <id>", "Operation id stamped onto every emitted event. Required in server-ingest mode (--topic). Generated as a UUID if omitted (standalone).").option("--topic <topicId>", "Server topic id. Enables server-ingest mode: events are batch-POSTed to aiAgent.heteroIngest. Requires --operation-id.").option("--render <mode>", "Output mode: jsonl (emit events as JSONL on stdout) | none (suppress stdout). Defaults to jsonl in standalone, none in server-ingest mode.").option("--raw-dump <dir>", "Persist the agent process RAW stdout/stderr (pre-adapter stream-json) under <dir>/<timestamp>-<operationId>/ for debugging. Each spawn attempt writes its own .stdout.jsonl / .stderr.log. Best-effort; never affects the run.").action(exec);
212154
+ program.command("hetero").description("Run heterogeneous agent CLIs (Claude Code / Codex) and stream their output").command("exec").description("Spawn a heterogeneous agent CLI and stream its events as JSONL on stdout. Standalone mode (no server ingest).").requiredOption("-t, --type <type>", `Agent type: ${[...SUPPORTED_AGENT_TYPES].join(" | ")}`).option("-p, --prompt [text]", "Prompt text. Pass `-` (or omit the value) to read from stdin.").option("-i, --image <path|url>", "Attach an image (repeatable). Accepts a local path, http(s) URL, or data: URL.", collectImage).option("--input-json <path>", "Read full multimodal prompt as JSON content blocks from a file. Use `-` for stdin.").option("-r, --resume <sessionId>", "Resume an existing agent session by its native id").option("-d, --cwd <path>", "Working directory for the spawned agent (default: process.cwd())").option("--model <model>", "Forward a resolved model selection to the agent CLI").option("--effort <level>", "Forward a resolved reasoning effort selection to the agent CLI").option("--agent-arg <arg>", "Forward one native agent CLI argument after wrapper parsing (repeatable)", collectAgentArg).option("-c, --command <bin>", "Override the agent CLI binary name (default: `claude` or `codex`)").option("--operation-id <id>", "Operation id stamped onto every emitted event. Required in server-ingest mode (--topic). Generated as a UUID if omitted (standalone).").option("--topic <topicId>", "Server topic id. Enables server-ingest mode: events are batch-POSTed to aiAgent.heteroIngest. Requires --operation-id.").option("--render <mode>", "Output mode: jsonl (emit events as JSONL on stdout) | none (suppress stdout). Defaults to jsonl in standalone, none in server-ingest mode.").option("--raw-dump <dir>", "Persist the agent process RAW stdout/stderr (pre-adapter stream-json) under <dir>/<timestamp>-<operationId>/ for debugging. Each spawn attempt writes its own .stdout.jsonl / .stderr.log. Best-effort; never affects the run.").action(exec);
211998
212155
  }
211999
212156
 
212000
212157
  //#endregion
@@ -217544,6 +217701,37 @@ function evidencePaths(evidence) {
217544
217701
  }
217545
217702
  function registerVerifyCommand(program) {
217546
217703
  const verify = program.command("verify").description("Manage the Agent Run delivery checker (criteria, rubrics, plans, results)");
217704
+ verify.command("init").description("Write the portable verify skill into a working dir (.claude/skills/verify)").option("--dir <path>", "Target working directory (default: current dir)").option("--skill <id>", "Skill identifier to pull", "verify").option("--force", "Overwrite existing skill files").option("--json [fields]", "Output JSON").action(async (options) => {
217705
+ const bundle = await (await getTrpcClient()).verify.getSkillBundle.query({ identifier: options.skill });
217706
+ const baseDir = options.dir ? path.resolve(options.dir) : process.cwd();
217707
+ const skillDir = path.join(baseDir, ".claude", "skills", bundle.identifier);
217708
+ const entries = [["SKILL.md", bundle.content], ...Object.entries(bundle.files)];
217709
+ const written = [];
217710
+ const skipped = [];
217711
+ for (const [rel, content] of entries) {
217712
+ const dest = path.join(skillDir, rel);
217713
+ if (existsSync(dest) && !options.force) {
217714
+ skipped.push(rel);
217715
+ continue;
217716
+ }
217717
+ mkdirSync(path.dirname(dest), { recursive: true });
217718
+ writeFileSync(dest, content, "utf8");
217719
+ written.push(rel);
217720
+ }
217721
+ const result = {
217722
+ dir: skillDir,
217723
+ skill: bundle.identifier,
217724
+ skipped,
217725
+ written
217726
+ };
217727
+ if (options.json !== void 0) {
217728
+ outputJson(result, typeof options.json === "string" ? options.json : void 0);
217729
+ return;
217730
+ }
217731
+ console.log(`${import_picocolors.default.green("✓")} ${import_picocolors.default.bold(bundle.name)} skill → ${import_picocolors.default.dim(path.relative(process.cwd(), skillDir) || skillDir)}`);
217732
+ console.log(` ${written.length} written${skipped.length ? `, ${skipped.length} skipped` : ""}`);
217733
+ if (skipped.length > 0) console.log(import_picocolors.default.dim(` (skipped existing — pass --force to overwrite)`));
217734
+ });
217547
217735
  const criterion = verify.command("criterion").description("Reusable pass/fail standards");
217548
217736
  criterion.command("list").description("List criteria").option("--json [fields]", "Output JSON, optionally specify fields (comma-separated)").action(async (options) => {
217549
217737
  const items = await (await getTrpcClient()).verify.listCriteria.query();
@@ -217935,10 +218123,23 @@ function registerVerifyCommand(program) {
217935
218123
  const summary = result.summary ?? {};
217936
218124
  const reportMdPath = path.join(dir, "report.md");
217937
218125
  const content = existsSync(reportMdPath) ? readFileSync(reportMdPath, "utf8") : void 0;
218126
+ const surfaces = Array.isArray(result.surfaces) ? result.surfaces.filter((s) => typeof s === "string") : void 0;
218127
+ const contextEntries = Object.entries({
218128
+ branch: typeof result.branch === "string" ? result.branch : void 0,
218129
+ commit: typeof result.commit === "string" ? result.commit : void 0,
218130
+ entry: typeof result.entry === "string" ? result.entry : void 0,
218131
+ focus: typeof result.focus === "string" ? result.focus : options.goal,
218132
+ surfaces: surfaces && surfaces.length > 0 ? surfaces : void 0,
218133
+ testedAt: typeof result.createdAt === "string" ? result.createdAt : void 0
218134
+ }).filter(([, v]) => v !== void 0);
218135
+ const context = contextEntries.length > 0 ? Object.fromEntries(contextEntries) : void 0;
218136
+ const scenario = result.scenario === "coding" ? "coding" : "coding";
217938
218137
  const client = await getTrpcClient();
217939
218138
  const run = await client.verify.createRun.mutate({
217940
- goal: options.goal,
218139
+ context,
218140
+ goal: options.goal ?? (typeof result.focus === "string" ? result.focus : void 0),
217941
218141
  operationId: options.operation,
218142
+ scenario,
217942
218143
  source: options.source,
217943
218144
  title: options.title ?? result.title
217944
218145
  });
@@ -217964,22 +218165,29 @@ function registerVerifyCommand(program) {
217964
218165
  log$10.warn(`evidence not found, skipping: ${rel}`);
217965
218166
  continue;
217966
218167
  }
217967
- const file = await uploadLocalFile(client, abs);
217968
- await client.verify.uploadEvidence.mutate({
217969
- capturedBy: "cli",
217970
- checkResultId: checkResult.id,
217971
- description: c.name ?? path.basename(abs),
217972
- fileId: file.id,
217973
- type: evidenceTypeForFile(abs)
217974
- });
217975
- uploaded += 1;
218168
+ try {
218169
+ const file = await uploadLocalFile(client, abs);
218170
+ await client.verify.uploadEvidence.mutate({
218171
+ capturedBy: "cli",
218172
+ checkResultId: checkResult.id,
218173
+ description: path.basename(abs),
218174
+ fileId: file.id,
218175
+ type: evidenceTypeForFile(abs)
218176
+ });
218177
+ uploaded += 1;
218178
+ } catch (e) {
218179
+ log$10.warn(`evidence upload failed, skipping ${path.basename(abs)}: ${String(e)}`);
218180
+ }
217976
218181
  }
217977
218182
  }
218183
+ const conclusion = typeof summary.conclusion === "string" ? summary.conclusion : typeof summary.note === "string" ? summary.note : void 0;
218184
+ const score = typeof summary.score === "number" ? Math.max(0, Math.min(1, summary.score / 100)) : void 0;
217978
218185
  await client.verify.upsertReport.mutate({
217979
218186
  content,
217980
218187
  failedChecks: summary.failed,
218188
+ overallConfidence: score,
217981
218189
  passedChecks: summary.passed,
217982
- summary: typeof summary.note === "string" ? summary.note : void 0,
218190
+ summary: conclusion,
217983
218191
  totalChecks: summary.total ?? cases.length,
217984
218192
  uncertainChecks: (summary.blocked ?? 0) + (summary.uncertain ?? 0) || void 0,
217985
218193
  verdict: summary.verdict ? toVerdict(summary.verdict) : void 0,
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.34" "User Commands"
3
+ .TH LH 1 "" "@lobehub/cli 0.0.36" "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.34",
3
+ "version": "0.0.36",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "lh": "./dist/index.js",
@@ -33,12 +33,12 @@
33
33
  "tsdown": "^0.21.4",
34
34
  "typescript": "^6.0.3",
35
35
  "ws": "^8.18.1",
36
- "@lobechat/agent-gateway-client": "1.0.0",
37
- "@lobechat/device-gateway-client": "1.0.0",
38
- "@lobechat/device-control": "1.0.0",
39
36
  "@lobechat/device-identity": "1.0.0",
40
- "@lobechat/local-file-shell": "1.0.0",
41
37
  "@lobechat/heterogeneous-agents": "1.0.0",
38
+ "@lobechat/device-control": "1.0.0",
39
+ "@lobechat/device-gateway-client": "1.0.0",
40
+ "@lobechat/agent-gateway-client": "1.0.0",
41
+ "@lobechat/local-file-shell": "1.0.0",
42
42
  "@lobechat/tool-runtime": "1.0.0"
43
43
  },
44
44
  "publishConfig": {