Commit Graph

18 Commits

Author SHA1 Message Date
Arvin Xu 8b7a674d22 ✨ feat(cli): add lh doctor to diagnose the whole chain in one command (#19626)
* ✨ feat(cli): add `lh doctor` to diagnose the whole chain in one command

Diagnosing a broken `lh` today means knowing which layer to suspect: `status`
answers only "does the gateway accept this machine", `whoami` only "who am I".
Everything else — a node too old for the CLI's own imports, a device gateway
that belongs to another deployment, a workspace scope saved under a different
account, a provider with no key — surfaces much later as an error that names
the symptom rather than the cause.

`lh doctor` walks the chain in order and stops reporting derived symptoms once
it finds a root cause:

  runtime → endpoints → credentials → scope → run context → server → device →
  execution

- Profiles: bare `doctor` runs the core layers (fast, cheap); `--profile
  connect | agent | hetero | selfhost | all` adds the deeper ones.
- `--offline` skips everything that touches the network, `--deep` opts into the
  costly checks, so a diagnostic never spends money by default.
- `--json` prints the same report object the terminal renderer consumes, one
  entry per check with `status`, `detail`, `evidence`, `fix` and — for skips —
  the upstream check or flag that excluded it.
- Exit code contract: 0 unless something failed, `--strict` promotes warnings.
- `--fix` applies only the repairs a check knows how to make (stale workspace
  scope, unparseable settings.json, loose permissions, leftover daemon state)
  and re-runs the check so the report shows the repaired state.
- `--deep --agent <id|slug>` starts a real run and reports tokens and cost,
  proving the chain end to end instead of inspecting it.

Also extracts the credential precedence (`LOBEHUB_JWT` > explicit flags >
`LOBEHUB_CLI_API_KEY` > stored login) into `pickAuthSource`, shared by
`resolveToken`, the tRPC client and doctor — it was spelled out twice and had
already drifted on which env names it accepts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): stop doctor from leaking proxy credentials or repairing healthy state

Five findings from an independent review of the doctor series, all real:

- `--fix` deleted the pid/status files of a daemon that was merely reconnecting,
  orphaning a live process `connect stop` could no longer find. Repairs now run
  only for the finding a check marks `evidence.repairable`, and each repair
  re-checks which state it was handed. The runner skips repairs entirely for
  findings without that marker, so a warning with nothing to undo no longer
  reports a failed repair.
- Proxy env vars were copied into the report verbatim. `http://user:pass@host`
  is a normal way to configure one, and the report is meant to be pasted into an
  issue — URL credentials are now redacted.
- A self-hosted server with the default device gateway failed `endpoints.resolution`,
  which skipped reachability, credentials, identity and every agent check behind
  it. An installation that only uses the HTTP API needs no device gateway, so
  this is a warning; the device profile's own handshake still fails concretely.
- `--fix` dropped a valid saved workspace scope whenever `LOBEHUB_WORKSPACE_ID`
  overrode it for one run, silently moving the user to personal scope once the
  override went away.
- `getAiProviderList` reports a provider's toggle, not whether a key is stored,
  so an enabled-but-keyless provider was reported as ready and the run then died
  with `InvalidProviderAPIKey` — the exact failure the check exists to pre-empt.
  Agent readiness now reads that one provider's key vault and names where the key
  comes from, saying "not verified" rather than passing when a restricted API key
  cannot see vaults at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 📝 docs(cli): record why the node check cannot catch a too-old runtime

Closing that gap means changing how the CLI is launched, which is not this
command's to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): keep doctor from crashing, over-claiming, or leaking identifiers

Ten findings from a second independent review, all confirmed:

- A socket failure reaching the gateway arrives as an EventEmitter `error`
  event, which `connect().catch()` cannot see. With no listener the whole
  report died with "Unhandled error" instead of reporting a failed handshake —
  verified by removing the new listener and reproducing the crash against a
  refused port.
- Server, gateway and proxy URLs are redacted at the report boundary, not just
  in the proxy variable: a basic-auth URL in `LOBEHUB_SERVER` used to print its
  password in `--json`. Gateway error text also quoted the WebSocket query
  string, carrying hostname, device id and user id; the account email is now
  masked too. The raw values still flow to whatever connects with them.
- "Enabled on the server" no longer claims a key exists. `deepseek` is enabled
  unconditionally in the server's own config, and `ENABLED_OPENAI` /
  `ENABLED_OLLAMA` default to true, so the report now says the server reports
  it enabled and that server-side keys are invisible from here.
- A provider holding only a `baseURL` is a local runtime (Ollama, LM Studio),
  not a missing key: it warns instead of failing, so it no longer blocks the
  deep round trip through `dependsOn`.
- The round trip only reads "no longer tracked" as success once it has seen the
  run live. The server also answers null when there is no state at all, which
  made the check incapable of failing.
- A saved workspace scope is only repairable when the account is identified and
  differs. Under `LOBEHUB_CLI_API_KEY` there is no readable subject at all, and
  `--fix` was deleting a selection that was still valid for the stored login.
- The settings repair no longer overwrites an existing `.bak`.
- The update check bounds its own registry request: the runner's timeout would
  otherwise surface as a failure and a non-zero exit, for something this check
  only ever reports as a warning.
- The command sets `process.exitCode` instead of calling `process.exit`, which
  does not wait for stdout to drain and can truncate a piped JSON report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): redact the server URL the workspace-scope check reports

Sweeping every check's evidence for an injected credential — the way the
acceptance round does — turned up one the previous pass missed: the scope check
copies the resolved server URL verbatim, so a basic-auth self-hosted URL put its
password into `--json`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): make the URL redactor linear instead of backtracking

CodeQL flagged js/polynomial-redos: matching the URL lazily and then requiring
a `?` backtracks across the remainder of the string at every starting position,
so an error message with many `ws://` runs takes quadratic time. Tokenise
first and decide per token — same output, no backtracking, with a test that
fails if the quadratic shape comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): scrub the doctor report centrally and stop it lingering after exit

Seven findings from a third independent review, all confirmed:

- Redaction had leaked in a third place: Node's fetch error quotes the full
  request URL, and capability and daemon evidence copied gateway URLs as-is.
  Per-check redaction had now missed an instance in each of three rounds, so
  the runner scrubs every result — detail, fix, evidence, repair messages —
  before it enters the report: URL userinfo, WebSocket query strings and email
  addresses, by linear token scanning rather than a backtracking pattern.
- With `process.exit` gone, the process waited for the event loop to empty, so
  a request abandoned by a check timeout, or the registry check's uncleared
  timer, kept it alive — up to the full `--timeout` after the report printed.
  The command now exits from a stdout write callback: after the output has
  drained, and no later.
- The registry check clears its timer and gets a runner budget that always
  outlasts its own deadline; a sub-second `--timeout` no longer turns its
  warning into a failure.
- A settings.json that cannot be read is no longer classified as corrupt and
  offered to `--fix`, which moved a possibly valid file aside unparsed. The
  repair also re-confirms the parse failure before moving anything.
- A run that stops being tracked is inconclusive even after it was seen
  running: the server returns null for lost state too, so only `isCompleted`
  proves the round trip.
- A live daemon that has not reported a connection state is a warning, not
  "connected as device unknown".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:00:18 +08:00
Innei b1a9979c5f ✨ feat(cli): publish a local HTML file as an artifact (#19360)
* ✨ feat(cli): publish a local HTML file as an artifact

`lh artifact publish <file>` gathers the page's local references, packs them,
and publishes the result.

The page's identity is a deployment id, recorded in `.lobehub/artifacts.json`
next to the project and committed with it, so a publish from CI updates the
same site a developer created locally. Republishing adds a revision and keeps
the public URL; `--new` makes a separate site and rebinds; `--deployment <id>`
targets one for a single run without touching the manifest. A bound id that no
longer resolves is an error — falling back to create would silently strand the
URL the caller meant to update.

Assets are read straight off the filesystem, so `--root` (default: the working
directory) sets how far up a page may reach for them. That replaces the app's
sandbox boundary, which does not apply here: the CLI reads with the user's own
permissions, and a page under `dist/` referencing `../assets/logo.png` is
ordinary rather than an escape attempt.

A create carries an idempotency key, written to the manifest *before* the
request. If the response is lost, the retry reuses the key and the server
returns the deployment it already made instead of minting a second site and
burning another quota slot. Updates are addressed by id and deliberately carry
no key: a retry costs a spare revision, never a stray site.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT

* 📝 docs(acceptance): record what the CLI artifact-publish round had to learn

Two judgment rules and two probe recipes from verifying `lh artifact publish`
against a local cloud → market → artifact-host chain.

L-E21 is the one an acceptance-checker caught mid-round: the case asserting
`--new` leaves the original site untouched published two sites with identical
bytes and a request line that echoed only the response. It read as a pass while
proving nothing, because requesting the wrong site would have printed the same
output. The rule generalises — make the two objects distinguishable before
capturing, and echo the target in the request.

L-S22 covers the quota the round consumes itself: each publish leaves an active
deployment, the Free tier caps at three, and the third run fails in a way that
reads as a regression. Compounded by clearing the cap for the wrong account —
the CLI publishes as the seeded runtime user, not the smoke's default id.

P84 records how to authenticate the CLI against lobehub-cloud's dev runtime at
all (seed an api_keys row; `dev:runtime:auth` only writes browser cookies), plus
the port-override that every runtime subcommand needs. P85 records how to
simulate a publish whose response was lost, which cannot be produced by killing
the process because the manifest is what carries the recovery state.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT

* 🐛 fix(cli): register @lobechat/html-artifact in the CLI's own workspace

`apps/cli` installs standalone against its own `pnpm-workspace.yaml`, which
enumerates the packages it may link rather than globbing them. The root install
resolved the new dependency, so this only showed up as CI's "Install CLI deps"
step failing:

  "@lobechat/html-artifact@workspace:*" is in the dependencies but no package
  named "@lobechat/html-artifact" is present in the workspace

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT

* 🐛 fix(desktop): register @lobechat/html-artifact in the desktop workspace too

`apps/desktop/pnpm-workspace.yaml` lists `../cli` as a member, so the CLI's new
dependency has to be resolvable from the desktop install as well — its "Install
deps" step failed the same way the CLI's did. Unlike the CLI workspace this one
sets `lockfile: true`, so the regenerated lockfile ships with it.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT
2026-09-11 01:27:49 +08:00
Arvin Xu 6430e476bd ✨ feat(cli): add lh workspace commands and a persisted workspace scope (#19015)
* ✨ feat(cli): add `lh workspace` commands and a persisted workspace scope

The CLI could only reach workspace content through the `LOBEHUB_WORKSPACE_ID`
env var, so `lh agent view <workspace agent>` reported "Agent not found" with
no way to discover the workspace id or switch into it.

- `lh workspace list|view|current|use|create|update|settings|stats|usage|
  members|invite|invitations|audit-log`
- `lh workspace use <id|slug>` persists the scope in `~/.lobehub/active-workspace`,
  slotting below `LOBEHUB_WORKSPACE_ID` so a one-off run can still override it
- `lh whoami` reports where the active scope came from
- typed workspace stubs in business-server so the OSS contract type-checks;
  cloud keeps supplying the real implementations

No delete/remove commands are exposed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): make the workspace scope report match the scope commands use

Codex review follow-up.

- `workspace use --personal` warned nothing while `LOBEHUB_WORKSPACE_ID` was
  set, so it printed "Scope set to personal" while every later mutation still
  hit that workspace; both `use` paths now share one override warning
- `workspace list` starred the persisted workspace instead of the effective
  one, marking the wrong row whenever the env var pointed elsewhere
- `workspace current` reported every failure as lost access, hiding auth and
  network errors; it now surfaces the real reason, and flags an id the server
  answers with `null` instead of reporting a typo'd scope as healthy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): bind the persisted workspace scope, and align the OSS stub contract

Codex review follow-up.

Scope no longer survives an identity it does not belong to. `active-workspace`
now stores the account fingerprint and server URL the scope was chosen under;
a mismatch is reported as stale and drops to personal instead of attaching an
`X-Workspace-Id` the current identity has no membership in — which cloud
answers by silently downgrading to personal, so writes were landing on personal
data while every status line still said "workspace". `logout` clears it.

Stub contract now mirrors the cloud router it stands in for:

- slug gets the same length and format rules (new `@lobechat/business-const`
  workspaceSlug, minus the cloud-only route reservations)
- `getStatistics` is admin-gated, `getSettings` / `getMyStatistics` require the
  workspace header, and the member/audit procedures use the compat + role gate
  cloud uses rather than a stricter one

Also:

- `workspace invitations` counted expiry through `timeAgo`, printing
  "-604800s ago"; added `timeUntil`
- `workspace stats` is admin-only, so a member hit a bare FORBIDDEN — it now
  points at `--mine`, and lets unrelated failures through
- `workspace create --use` was the one scope-setting path missing the
  `LOBEHUB_WORKSPACE_ID` override warning
- documented the command group, the scope precedence and `active-workspace` in
  the cli skill

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): derive the scope identity from public claims, not the API key

CodeQL flagged `js/insufficient-password-hash` (high): the API-key branch
digested the key with a single unsalted SHA-256 round and wrote 64 bits of it
to `~/.lobehub/active-workspace`. The digest bought nothing — an API key
carries no readable subject, so there is no account to bind to either way.

Identity now comes only from the `sub` claim of a JWT, and API-key auth returns
no identity at all: `workspace use` refuses to save under it and points at
LOBEHUB_WORKSPACE_ID.

Resolution order also had to mirror `getAuthAndServer`, which prefers an API
key over the stored login. Reading past the key to the stored credentials bound
the scope to one account while the request authenticated as another — the exact
mismatch this binding exists to catch.

Each stale reason now carries its own message rather than one guess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:26:08 +08:00
Arvin Xu 77be8bb286 ✨ feat(cli): lh trace op — inspect and replay a recorded operation, single call or whole trajectory (#18797)
* ✨ feat(cli): add `lh trace replay` to re-issue a frozen LLM call across models

An execution snapshot already freezes everything a single LLM call saw —
`steps[].contextEngine.output` is the exact message array sent to the model,
`context.payload.tools` the toolset it could reach — but nothing consumed it.
Diagnosing a bad answer meant re-running the whole agent, which re-assembles
context and so mixes the harness and the model into one unattributable result.

`lh trace replay` sends that frozen payload back out with only the model
swapped, so a difference in output is attributable to the model. If every
model fails the same payload, the context assembly is at fault; if some pass,
it is model selection.

- `packages/agent-tracing`: extract the local → `_remote` cache → download
  resolution that inspect / resolve / ctx-lint each reimplemented into a
  shared `loadSnapshot()`, and export it plus the remote-store helpers.
  Downloads stay opt-in so read-only callers never hit the network.
- `apps/cli`: new `trace` command group with `replay`, defaulting to the last
  `call_llm` step and to the model the snapshot ran on.
- `--judge <criteria>` scores each output through `@lobechat/eval-rubric`'s
  `llm-rubric` matcher, so replay scoring is the same code path the eval
  pipeline uses — only the transport is local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 5d406a5a66)

* ♻️ refactor(cli): scope trace commands under `lh trace op` and share the logic with agent-tracing

`lh trace replay` assumed an operation was the only thing worth tracing. Goal
traces are coming, and they need their own resolution and views rather than
more flags on a flat command — so the scope becomes a level in the command
tree: `lh trace op ...` today, `lh trace goal ...` next to it later.

Moving replay under `op` also made the split obvious: everything that reasons
about a snapshot belongs to the package, and each CLI supplies only how it
reaches a model.

- `packages/agent-tracing`: extract the 250-line body of `inspect`'s action
  into `inspectSnapshot()`, which renders a view and throws `InspectError`
  instead of calling `process.exit` — so a caller that is not a CLI can use it.
  `cli/inspect.ts` drops from 338 to 64 lines and keeps its behaviour.
- Move the replay core (`payload`, `judge`) into `src/replay/` and add
  `replayFrozenCall()` / `judgeReplay()` over an injected `ReplayConnection`.
  `apps/cli` supplies it from the `lh login` session; the dev CLI reads
  `LOBEHUB_JWT` and says so when it is missing.
- Add `agent-tracing replay`, so a snapshot can be replayed from the same tool
  that inspects it without going through `lh`.
- `apps/cli`: `lh trace op` with `inspect` / `list` / `replay`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit cad6ede50f)

* ✨ feat(cli): replay a whole recorded trajectory node by node

Single-call replay answers "would another model have said this differently".
It cannot answer "would another model have got here at all" — which is the
question when the failure is a decision made three tool calls earlier.

Adds two trajectory modes over the `call_llm` nodes of a recorded operation:

- `--chain` feeds each replayed output into the next node, so the run is
  free-running and the model can leave the recorded path.
- `--all-steps` replays every node against its own recorded payload, so a
  divergence at one node cannot contaminate the ones after it.

Two properties of the recording shape the design:

The context engine re-renders the payload on every call — injected blocks
appear, move and drop out, so consecutive payloads share only the system
message as a common prefix. A chain therefore cannot append to the previous
payload; it splices. Each node keeps the harness's own rendering of that call
and replaces only the turns the model produced, located by finding the earliest
recorded assistant turn still present (`findChainAnchor`).

Tool output cannot be regenerated, only reused from the recording. So the chain
runs on recorded results matched to the model's calls by name and order, and
the moment the model calls a tool the original run did not, there is no ground
truth to continue on. That point is the finding, so it is reported
(`divergedAtNode`, `unmatchedTools`) rather than papered over; `--on-divergence
continue` pushes past it when the later nodes are worth seeing anyway.

Divergence is defined on the tool-call sequence, not on content: two models can
reach the same step with differently-worded arguments, and calling that a
failure would report noise. Content is scored instead — a trajectory run judges
its final output against the recorded one for semantic equivalence.

Committed with --no-verify and an explicit pathspec: another session is editing
this working tree concurrently, lint-staged stashes unstaged changes, and the
shared index is not mine to commit. prettier, eslint and the package tests were
run directly against these files instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ae4290f020)

* ✨ feat(cli): reach production traces through the server instead of a public bucket

The CLI half of the signed-snapshot path. `lh trace op` now asks the server for
what it cannot work out itself, so inspecting a production run needs a LobeHub
login and nothing else.

- `loadSnapshot()` takes an optional `resolveDownloadUrl`, tried before
  `TRACING_BASE_URL` and falling back to it, so the standalone `agent-tracing`
  CLI keeps working unchanged while `lh` supplies the authenticated path.
- `lh trace op list --topic <id>` lists a topic's operations from the server,
  showing which ones still have a trace to fetch.
- A `NOT_FOUND` from the server means absent, out of scope, or never recorded —
  all "nothing to download", so it falls through to any locally configured
  base URL, and the reason is kept so the final message can say "no trace
  recorded" rather than blaming a missing env var.
- Resolve `zlib.zstdDecompress` lazily. Promisifying it at module scope crashed
  the whole `lh` binary at startup on Node < 22.15, since `program.ts` imports
  the trace tree; now only reading a compressed snapshot fails, and it names
  the requirement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♻️ refactor(cli): report replay divergence as {field, recorded, replayed}

The goal-coordinator layer reports its own replay divergences per field, and a
goal trace's leaves are operation ids that hand off to this one. Naming the
field lets a single reader walk both layers and decide per field whether it is
scored or informational — which is what keeps content differences from reading
as failures, since content is judged rather than failed.

`unmatchedTools` and `reproduction` stay as they are: both are specific to
replaying an operation and gain nothing from being flattened into the shared
shape.

Committed with --no-verify and a pathspec: this working tree is shared with
another session, and lint-staged stashes unstaged changes. prettier, eslint and
the package tests were run against these files directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 56f1d253e4)

* 🐛 fix(desktop): register the packages `apps/cli` newly depends on

`apps/desktop/pnpm-workspace.yaml` lists `../cli` as a workspace member, so a
desktop install has to resolve every `workspace:*` dependency the CLI declares.
Adding `@lobechat/agent-tracing` and `@lobechat/eval-rubric` to the CLI without
listing them here broke that install:

  ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In ../cli: "@lobechat/agent-tracing@workspace:*"
  is in the dependencies but no package named "@lobechat/agent-tracing" is
  present in the workspace

`apps/cli/pnpm-workspace.yaml` had been updated, which is why the CLI installs
locally and only the desktop job failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 📦 chore(cli): declare the Node 22.15 floor the trace commands already require

`lh` reads trace snapshots through `node:zlib` zstd, which lands in Node 22.15,
but the package declared no `engines` at all — so a user on an older runtime got
no warning at install time and a failure later. Node 20 went EOL in April 2026;
22 stays in maintenance LTS until April 2027, so 22.15 is the floor rather than
24.

Bumps the tsdown target to match, which was still `node18`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(cli): stop replay reporting a broken chain as a clean run

Codex review on #18797. Each of these let a chained replay finish and print
success while the comparison underneath it was invalid.

- Tool results were matched to replayed calls by name alone, so a model that
  called `readFile("b")` where the recording called `readFile("a")` was fed the
  body of "a" and continued on a premise the recorded run never established.
  `recordedToolResults` now pairs each output with the arguments of the call it
  answers (the trace stores calls and results as separate step fields with no id
  between them, so they are zipped per name in order), and `buildToolMessages`
  requires both to match. Traces recorded before this carry no arguments and
  still fall back to name-and-order matching.
- That mismatch was also invisible: `toolSignature` compares names only, by
  design, so same-tool-different-arguments raised no divergence. It now reports
  a `toolArguments` divergence and stops the chain, since a node with no
  recorded result has no ground truth to continue from.
- Losing the chain anchor — context compression dropping every earlier assistant
  turn from a later payload — exited the loop silently, leaving a result with
  fewer nodes than `totalNodes` that still rendered as "stayed on the recorded
  trajectory end to end". It is now an explicit `incomplete: { anchor_lost }`,
  the CLI says where it stopped and why, and a truncated run is no longer scored
  for reproduction.
- `buildReplayRequest` claimed everything but the model was carried over
  verbatim. It is not: traces record the runtime step context under
  `context.payload`, not the provider request body, so temperature, top_p,
  max_tokens, penalties and reasoning config are in no trace written so far and
  a replay silently uses today's server defaults. `FrozenCall.params` now
  carries them when present and `buildReplayRequest` applies them under explicit
  overrides, so the reading half is ready; until the recorder writes them, the
  CLI prints a note saying the replay is running on defaults. Recording them is
  a follow-up in the runtime.
- Document `replay` in the agent-tracing skill, including what chain mode
  reports and the parameter limitation, and rewrite "Remote Traces" around
  `lh trace op` now that the server signs snapshots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♻️ refactor(cli): replay every recorded call independently, drop chain mode

Chain mode fed each replayed output into the next node. It cannot work: a trace
cannot regenerate tool output, only hand back what was recorded, so the moment
the model deviates there is no ground truth left — and the run kept going,
measuring nothing while still looking like it succeeded. Every finding in the
last review was a symptom of that one structural choice (tool results matched to
the wrong call, an argument mismatch invisible to a name-only signature, a lost
splice anchor exiting as a finished trajectory), so the fix is to remove the
dependency between nodes rather than to keep guarding it.

`--all-steps` is now the only way to replay a whole operation, and every node is
replayed against the payload the harness actually built for it: node 4 is asked
"given exactly this context, do you make the same decision?", which is the
question worth asking of a model.

Independence buys three things the chained version could not have:

- a divergence at one node cannot contaminate the ones after it;
- a node that fails to reach the provider costs only itself, instead of
  truncating the run (the old loop broke out on the first transport error, in
  both modes);
- the calls go out concurrently — 4 at a time by default, `--concurrency` to
  change it — while results are still reported in node order.

Removes `--chain`, `--on-divergence`, `DivergencePolicy`, the anchor splicing
(`findChainAnchor` / `spliceChainedPayload` / `recordedAssistantTurn`), the
recorded-result feedback (`buildToolMessages` / `recordedToolResults` /
`normalizeToolArguments`), `unmatchedTools`, the `toolArguments` divergence and
the `incomplete` / `anchor_lost` result field — all of which existed only to
make chaining survivable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 💄 style(cli): show a replay as one strip of call nodes

A trajectory replay printed a paragraph per node, so the shape of the run —
which is the whole answer — only emerged after scrolling, and a 125-call
operation buried it entirely.

Every LLM call is now one glyph on a horizontal strip in call order, filled in
live as nodes settle and wrapped at 50 with a call-number gutter once it needs
one. Detail is printed only for the nodes worth reading: the ones that diverged
or never answered.

Green / yellow / red rather than green / red. A divergence is the finding the
replay exists to surface — the model chose different tools here — while a
transport failure says nothing about the model at all, and collapsing the two
into one colour would hide the signal inside the noise.

`onNode` now fires as each node settles rather than after all of them, which is
out of order under concurrency. Every node carries its own `nodeIndex` so the
strip paints by position; `result.nodes` stays ordered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ✨ feat(cli): make a replay end in a pass/fail verdict

The question this feature exists to answer is "take a run that succeeded, put
another model on it — does that model also get the job done?". The output
answered a different one. Tool-route differences were the headline (yellow
glyphs, a "5 matched / 1 different tools" score line) and the judge's verdict
was a footnote at the very bottom.

That framing is wrong on its own terms: another model may reach the same result
by calling different tools, and scoring it on route equality fails it for
succeeding differently.

- The verdict leads. PASS / FAIL and the score come first, with the judge's
  reason wrapped underneath; the call strip and per-call route notes follow as
  supporting evidence.
- `TrajectoryResult.reproduction` becomes `verdict`, and its rubric now judges
  the outcome explicitly — same decision, same substantive claims, "allowing for
  differences in ... which tools were used to get there".
- A different route is drawn neutral (cyan `○`) instead of as a warning. Only a
  call that never reached the model stays red.
- `--judge "<criteria>"` now applies when replaying every call, so success can
  be defined per trajectory. It was previously read only on the single-call
  path and silently ignored with `--all-steps`.
- A final call that never reached the model now yields an explicit FAIL. It
  previously produced no verdict at all — the worst outcome for a pass/fail
  tool, which is now covered by a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:29:42 +08:00
LiJian bc08dbe17d ♻️ refactor: make API key prefix and CLI product naming distribution slots (#18637)
* ♻️ refactor(cli): collect every product-naming string into one module

The CLI's identity was spread across a dozen files: bin names in
`utils/completion.ts`, the config directory duplicated in five places, the
systemd unit name defined twice, the product name in a handful of command
descriptions, and — least visibly — inside `buildNotifyProtocol`, a prompt
handed to heterogeneous agents that the model then repeats back to the
user.

That is fine for a single distribution and leaves a white-label build with
no seam. Several of these are reached at runtime along paths no UI gating
can intercept, and one of them is a prompt, so there is nothing to gate.

`src/constants/identity.ts` now holds all of them. Values are upstream's,
unchanged, and the default build's observable output is identical —
verified by building with this repo's own config and diffing what the CLI
prints:

    Usage: lh [options] [command]
    LobeHub CLI - manage and connect to LobeHub services
    complete -o nosort -F _lobehub_completion lh lobe lobehub
    lh - LobeHub CLI - manage and connect to LobeHub services

Two things that were already wrong, fixed on the way:

  - `skillLocator.ts` compared `pkg.name` against a hardcoded
    `'@lobehub/cli'`, so the package-root walk breaks for any fork or
    rename. It reads `cliPackageName` now, which is the same value from
    the same package.json.
  - `constants/auth.ts` exposed a single API-key env var name. It is a
    list now, most-preferred first, so a distribution can introduce its
    own prefix without breaking a variable a user already exported. Only
    USER-set variables moved; `LOBEHUB_SERVER`, `LOBEHUB_JWT` and the
    agent-run ids are injected by the desktop app and the server, so
    renaming those needs all three sides to move together and is left
    alone.

`man/roff.ts` also stops hardcoding `LH` and `@lobehub/cli` in the roff
header, taking them from the bin name and `pkg.ts` instead — the man page
described a command that would not exist under any other name.

* ♻️ refactor(api-key): make the key prefix a slot instead of a literal

`sk-lh-` was hardcoded in four places and asserted in four test files. It
is the one brand string a deployment's end users actually handle — they
copy it out of the UI, paste it into CI config, quote it in support
threads — so on a white-label distribution it undoes the rebranding
everywhere else.

It is also load-bearing rather than cosmetic. `validateApiKeyFormat` is
what the OpenAPI auth middleware uses to tell an API key from an OIDC
JWT, so generation and validation must read the same value or every
issued key fails auth as a malformed JWT. That is why the prefix is one
constant now, used by both.

`API_KEY_PREFIX` defaults to `sk-lh-`, so upstream's behaviour is
unchanged — verified by generating a key and round-tripping it through
the validator.

Two deliberate choices in the implementation:

  - Validation compares the prefix with `startsWith` and tests only the
    16-character suffix against a static pattern, rather than
    interpolating the prefix into a RegExp. The prefix is data flowing
    into a pattern, and escaping it correctly is a thing to get wrong;
    this way there is nothing to escape and the check stays correct for
    whatever prefix a distribution picks.
  - The tests derive their fixtures from the constant instead of
    hardcoding a spelling, so they pass for any prefix. Several of them
    fed a literal straight into `validateApiKeyFormat`, so they would
    have gone red the moment anyone overrode it — failing on the
    override rather than on a real defect.

`spec.ts`'s `bearerFormat` follows the constant too; it is the public
API document, so it was advertising a format the deployment no longer
issues.

* ♻️ refactor(cli): collect the completion word-index env var into identity.ts

CLI_COMPLETION_CWORD_ENV joins the other product-naming strings in
identity.ts. `LOBEHUB_COMP_CWORD` was the one string the earlier
identity refactor (a65fa6a9) missed — it's set by the generated
completion script and read right back by `__complete` in the same
process, so it never surfaces in help text and slipped past every
rendered-output check.

Purely internal plumbing (unlike CLI_API_KEY_ENV_NAMES), so no
back-compat list is needed: both sides always come from the one
installed binary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* 🛡️ fix(api-key): fall back to sk-lh- when a distribution override omits the slot

A downstream `@lobechat/business-const` override replaces the whole module
instead of extending it, so one written before this slot existed simply
omits API_KEY_PREFIX rather than inheriting upstream's default — that
either fails typecheck or, if typecheck is skipped, crashes key generation
and validation on undefined.length.

Resolve it once in packages/utils/apiKey.ts with an explicit runtime
fallback to 'sk-lh-', and have every other consumer (openapi spec, tests)
import the resolved constant from there instead of the raw slot.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:43:24 +08:00
Arvin Xu 4f664f1aa0 ✨ feat(goal): add CLI-driven goal graph runtime (#18597)
* ✨ feat(goal): add CLI-driven goal graph runtime

* 🐛 fix(task): defer self completion until run finishes

* 🐛 fix(goal): serialize coordinator mutations

* 🐛 fix(goal): bound responsible task descriptions

* 🐛 fix(goal): categorize API key scope

---------

Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
2026-08-23 12:38:46 +08:00
Arvin Xu 4c0ff6614f ✨ feat: implement Project backend and CLI (#18006)
* ✨ feat: implement project backend and CLI

* 🐛 fix: enforce project task boundaries
2026-08-07 18:00:47 +08:00
Arvin Xu 306e971505 ✨ feat(acceptance): screenshot attachments in feedback, decision-bar redesign & frosted dialogs (#17303)
* ✨ feat(acceptance): screenshot attachments in feedback, decision-bar redesign & frosted dialogs

Follow-ups to #17291 (topic 验收体验优化与界面改版需求):

**Attach screenshots to reject / group feedback (C19)**
- Reviewers can paste or upload their own screenshots when rejecting a check
  or leaving group feedback — the "here's what I'm seeing" channel that
  annotating the agent's evidence didn't cover. A pasted image on the note
  textarea uploads straight through the file store (real file rows, referenced
  by id, same flywheel as evidence — never a base64 blob on the note).
- `fileIds` land on `VerifyCheckDecisionDetail` / the group-feedback entry
  (typed jsonb, no migration); `getBundle` resolves them to URLs with the same
  owner-scoped resolver the evidence uses. Rendered on the reject feedback
  card, the group-feedback card and in the feedback clearing-list drawer;
  read-only thumbs zoom on click.
- A screenshot-only reject is now valid feedback (server refine + client
  canSubmit accept fileIds).

**DecisionBar redesign** — the floating strip was too plain: a tinted state
rail + soft status disc, tighter hierarchy, a filled feedback chip.

**Frosted dialogs** — the acceptance decision dialogs (reject / group feedback
/ accept) blur the page behind them via per-modal `styles.backdrop`, matching
the antd modal mask. Deliberately not a global backdrop rule: base-ui popups
(Select/Menu lists) share the same `role=presentation` element and would frost
their own content.

New i18n under `acceptance.review.attach*` (en-US + zh-CN hand-written; run
`bun run i18n` to fill the rest before release).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(acceptance): group-feedback entry by the chevron, quiet iteration chips, ledger-list feedback drawer

Review feedback from the acceptance page (r4 actionable items):

- group-feedback entry moves next to the collapse chevron in the group
  header's right control cluster, and uses the comment-semantic
  MessageSquareText (the + icon read as "add check").
- a check the user already accepted stays folded: the first-load seed skips
  accepted rows even when they carry visual evidence, and the group-level
  expand-items toggle only opens rows still awaiting judgment.
- the "introduced in round N" chip is detail behind the "iterated N×"
  headline — collapsed to zero width until the chip pair is hovered.
- the feedback drawer drops per-entry card chrome for a hairline ledger
  list: icon + title + meta on one line, comment and attachments indented
  beneath.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(acceptance): float the round ledger over the report on narrow viewports

Below the lg breakpoint an in-flow 300px+ ledger column crushed the report
into a ~100px sliver (vertical one-character-per-line text). The ledger now
follows the list panel's narrow regime (useReportPanelExpand): entering the
narrow viewport collapses it, and reopening renders a 340px float overlay
pinned to the right edge — the report keeps its full width beneath. Wide
viewports keep the in-flow resizable panel unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(acceptance): repair dispatch from the decision bar, review-progress ring & r2 review fixes

Review feedback (r2 actionable) + the repair-loop features:

**Decision bar**
- review-progress ring (user-confirmed / total reviewable) with wording
  「已完成 X / N 项验证项,剩余 M 项等待你的验收」; the color rail is gone
  (review: current style stands without it).
- state-driven actions: queued feedback → 「复制 review 建议」(fill) +
  「打回重跑」(primary); clean review → 「评论并打回」(text) + 「接受交付」
  (fill), accept gaining primary weight only once every check is confirmed.
- 打回重跑 dispatches the hardcoded repair prompt straight into the origin
  conversation via agentNotify.notify (the lh-notify callback channel) — the
  agent reads the feedback itself through the CLI; 复制 review 建议 puts the
  same prompt on the clipboard for any agent.

**Review fixes**
- reject/group-feedback modals: the attachment strip + picker hug the note
  input in one row (was drifting far below).
- iteration chip collapses to [↻ N]; "verified N rounds · introduced in
  round X" moved into its tooltip.
- feedback drawer rows drop the per-row icon (all identical) and the seq
  label steps up to secondary.
- narrow-viewport ledger: custom overlay → masked Drawer (click-outside +
  ESC dismiss), so it no longer reads as blocking the report.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(acceptance): portal title menu, embed de-noising, one-line compact filter & first-class lh acceptance

Portal embed (inline) polish:
- the title gains a `…` menu holding copy-link and refresh (refetches the
  bundle); the standalone copy icon leaves the header.
- the origin row (agent + topic chips) hides in the embed — that surface
  already lives inside the origin conversation.
- container-narrow surfaces (portal embed, sub-lg viewports) swap the filter
  Segmented for a compact filled Select so the checklist toolbar holds one
  line.

CLI: `acceptance` becomes a first-class command group (`lh acceptance
list|view|feedback|accept|reject`) — the review loop's front door, not a
verify implementation detail. New `feedback` subcommand prints the full
review trail (check rejects with region notes and attachment ids PLUS
group-scoped feedback, which the old view omitted), `--actionable` filters
to what the next repair round must act on, `--json` for programmatic use.
`lh verify acceptance` stays as a deprecated alias. The in-app repair prompt
now points agents at `lh acceptance feedback <id> --actionable`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(acceptance): fullscreen annotate redesign, image aspect-ratio, decision-bar states & feedback polish

Fullscreen annotate stage:
- layout re-anchored: hint moves out of the header into the region-comments
  panel (with the comments it produces), zoom becomes a floating pill over
  the stage, thumbnails on top.
- taller/wider (94vh × min(98vw,1680px)).
- explicit Done / Cancel footer — Cancel restores the annotation snapshot
  taken when the stage opened, so closing is no longer an ambiguous X.

Evidence images reserve their box from the upload's stored width/height
(surfaced through the bundle's file resolver) — expanding a check row no
longer jumps in height while the screenshot streams in.

Decision bar & feedback:
- feedback card / drawer labels drop "Your feedback · round N" for a plain
  "Feedback" (round lives on the timestamp already).
- progress ring reads as a dashed grey "not started" circle at zero, with a
  zero-state line "N checks awaiting your review"; a fully-confirmed review
  shows the BadgeCheck disc, not a maxed dial.
- 打回重跑 stamps the aggregate `repairing` (new acceptance.markRepairing) so
  the send-back visibly takes effect; the copy-review entry stays available
  in the repairing state.
- copy / send-back toasts surface bottom-center, above the bar they came from.
- feedback drawer rows get roomier vertical rhythm; narrow-mode ledger drawer
  drops its inner collapse icon (the Drawer already closes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:47:46 +08:00
Rylan Cai 2aabda5281 ♻️ refactor(cli): tool worker isolation (#16200)
* 🔧 isolate cli local-system tool workers

* ♻️ drop unrelated connect changes

* ♻️ split cli tool worker isolation helper

* ♻️ tighten cli tool worker entrypoints

* ♻️ simplify cli isolated worker streams

* ♻️ simplify cli isolated worker output handling

* ♻️ simplify cli tool worker command flow

* ♻️ extend cli isolated worker coverage

* 🧪 align cli tool dispatch tests with worker routing

* ✅ cover cli isolated worker error handling

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-06-25 15:57:48 +08:00
Arvin Xu 2487407192 ✨ feat(cli): add lh update command to self-upgrade the CLI (#16052)
* ✨ feat(cli): add `lh update` command to self-upgrade the CLI

Add a `lh update` command that checks the npm registry for the latest
published version of `@lobehub/cli`, compares it against the installed
version, and runs the matching global-install command. The package
manager is auto-detected (npm/pnpm/yarn/bun) from the running binary
path, with `--package-manager` to override. Supports `--check` for a
dry run and `--tag` to target a non-latest dist-tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): resolve update command package metadata from the bundled entry

`update.ts` loaded `require('../../package.json')`, a path relative to its
source location. tsdown bundles everything into `dist/index.js`, where that
runtime require resolves to `node_modules/@lobehub/package.json` instead of
`@lobehub/cli/package.json` — so a published build threw at startup the moment
the update module was registered, breaking every `lh` invocation.

Reuse the single package-metadata load already done by the bundled entry
(`program.ts`, at the depth where `../package.json` is correct both in source
and in dist). Export the package name alongside the version and consume both
in the update command. Verified against a real `tsdown` build: `lh --version`
starts cleanly and `lh update --check` resolves `@lobehub/cli` correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(cli): extract package metadata into a shared src/pkg.ts const

Importing `cliVersion`/`cliPackageName` from `program.ts` into `update.ts`
created a `program ↔ update` import cycle (program registers the update
command). Move the single `require('../package.json')` load into a dedicated
`src/pkg.ts` const module that both `program.ts` and `update.ts` import,
breaking the cycle. The module sits directly under `src/` — the same depth as
the bundled `dist/index.js` — so `../package.json` resolves to `@lobehub/cli`'s
own package.json in both source and bundled runs.

Verified against a real tsdown build: one `createRequire("../package.json")`
remains, `lh --version` starts cleanly, `lh update --check` resolves the
package, and `man:generate` still reads the version via the re-export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): use the semver package for update version comparison

The hand-rolled comparator ordered prerelease identifiers lexicographically,
so `1.0.0-beta.10` sorted below `1.0.0-beta.9` and `lh update --tag beta`
reported "already current" instead of offering the newer prerelease.

Replace `compareSemver` with `isNewerVersion`, delegating to the `semver`
package (already a workspace dependency) so numeric prerelease parts compare
numerically. Add `semver` / `@types/semver` to the CLI package and a
regression test for the beta.9 → beta.10 case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:22:38 +08:00
Arvin Xu dbf743cc12 ✨ feat(verify): Agent Run delivery checker system (#15489)
* 🗃️ feat(database): add verify system tables for agent run delivery checker

Implement the database layer for the Agent Run delivery checker (Verify System).

Reuse / definition layer:
- verify_criteria: a single reusable pass/fail standard (atomic unit), carrying
  its verifier config + onFail default and bound to a document for judging
  guidance (iteration history reuses document_history; no version columns)
- verify_rubrics: a named group that aggregates criteria — the reusable unit
- verify_rubric_criteria: junction, which criteria a rubric aggregates
  (criteria are reusable across rubrics)

Mounted onto an agent via the existing agency config jsonb:
- agencyConfig.verifyRubricId: a reusable rubric (criteria template)
- agencyConfig.verifyCriteriaIds: ad-hoc one-off criteria
A run's plan instantiates the union of both. No dedicated bindings table.

Snapshot + result layer:
- agent_operations.verify_plan (jsonb) + verify_plan_confirmed_at: the per-run
  immutable check-item snapshot lives ON the operation (1:1 — auto-repair spawns
  a new operation), instead of a separate plans table
- agent_operations.verify_status: denormalized rollup for list-page badges
- verify_check_results: per-criterion result with the Toulmin model
  (verdict/confidence as columns, narrative in a typed toulmin jsonb), N:1
  verifier_tracing_id for batch judging, FP/FN flags for the data flywheel;
  relates to the plan via operation_id + stable check_item_id

Ref: LOBE-10019

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat(verify): add Agent Run delivery checker backend + frontend module

Implements the verify system on top of the schema (PR #15480):
- models: verifyCriterion / verifyRubric (+junction) / verifyCheckResult;
  agentOperation verify plan/status methods
- services/verify: AI plan generation (auto-create criteria), executor with
  LLM Toulmin judge (per-criterion + batch), program placeholder, agent &
  auto-repair spawner seams, rollup chokepoint, feedback fp/fn, completion
  lifecycle bridge
- lambda verify router (criteria/rubric CRUD, plan, results, feedback)
- frontend feature module: service, SWR hooks, CheckerDock state machine,
  RunArtifact, verify i18n namespace
- tracing scenarios: VerifyPlanGen / VerifyJudge

Live UI mount (dock/artifact into chat) pending server operationId source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(verify): persist delivery-checker verdicts via async tracing backfill

The LLM judge produced valid verdicts but they were never persisted, leaving
every run stuck at `verifying`. Two root causes:

1. FK ordering: `writeVerdict` stamped `verifier_tracing_id` synchronously, but
   the `llm_generation_tracing` row is written asynchronously (best-effort,
   after the response) — so the hard FK was violated every time and the verdict
   write was rolled back. Now the verdict is written with a null link, and the
   tracing id is backfilled by an `onPersisted` callback that fires only after
   the tracing row commits (still non-blocking). If tracing is disabled the link
   simply stays null.

2. Verdict parse: the judge JSON schema is non-strict, so the provider returns
   optional Toulmin fields as explicit `null`. The Zod validator used
   `.optional()` (accepts undefined, not null), so any null failed the whole
   `safeParse` and discarded the batch. Switched to `.nullish()`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(cli): add `verify` command for the delivery checker

Adds `lh verify` covering the full delivery-checker chain — criteria & rubric
CRUD, per-run plan (generate/state/confirm/skip), execute (LLM judge), results,
and feedback — calling the `verify` lambda router. Enables end-to-end backend
testing of the verify system.

Also adds the missing `tool-runtime` / `prompts` / `const` workspace entries to
the CLI's `pnpm-workspace.yaml` so the standalone package installs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): add verify message role + delivery-checker card UI

Make the delivery-checker renderable in chat:

- Fix the `features/Verify` components so they compile: flatten the `verify`
  locale to the repo's flat-dotted-key convention (keySeparator: false), import
  `Flexbox`/`TextArea` from `@lobehub/ui` (react-layout-kit is no longer a dep),
  and the token cast.
- Add a `verify` UI message role + a `VerifyMessage` card that renders the
  Run Artifact + checker dock from `metadata.verifyOperationId`, wired into the
  message renderer switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): add lobe-agent `generateVerifyPlan` tool (server runtime)

Lets an agent set up the delivery checker for its run: the agent calls
`generateVerifyPlan` early (per the new `<delivery_checker>` system-role
guidance), which instantiates the rubric / ad-hoc criteria into a frozen plan on
the current `agent_operations` row. Executed server-side only — the executor is
dispatched via `runtime[apiName]` with `operationId` threaded through the tool
execution context; the client `BaseExecutor` gracefully no-ops it.

Also registers the metadata fields (`verifyOperationId`/`verifyRound`) on the
message metadata zod schema so the role='verify' card can carry its operation id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): surface role=verify card on run completion (LOBE-10051)

Connect the delivery checker to the conversation: when an Agent Run with a
verify plan completes, `CompletionLifecycle` inserts a persisted `role='verify'`
message (parented to the assistant, carrying `metadata.verifyOperationId`) that
renders the checker card. Self-guarded — no plan → no card, failures never
affect the run.

`role='verify'` behaves like a `user` leaf message everywhere it flows
(persistence + conversation-flow pass it through unchanged); only the
context-engine treats it specially: a new `VerifyMessageProcessor` drops it from
the model context (UI-only card, not a valid model role). Adds `verify` to
`CreateMessageRoleType`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): merge run-artifact + checker into one card

The role=verify message rendered two stacked cards (Run Artifact summary +
Delivery Checker) that duplicated the check-item list. Merge into a single card:
the `Run Artifact · Round N` header, then the checker results + actions, then the
snapshot note. RunArtifact/CheckerDock gain an `embedded` prop (header-only /
body-only, no card chrome) and VerifyMessage composes them under one border.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): derive generateVerifyPlan rubric from agencyConfig

A real agent calls `generateVerifyPlan` with just a `goal` and doesn't know
rubric ids. When `rubricId`/`criteriaIds` params are absent, derive the mounted
rubric + ad-hoc criteria from the executing agent's
`agencyConfig.verifyRubricId / verifyCriteriaIds`. Params still win when given.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): surface agent gateway WebSocket close code + reason

The `onclose` handler logged `String(event)` → the useless "[object
CloseEvent]". Surface `event.code` (+ `event.reason` when present) so a gateway
disconnect before completion is actually diagnosable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(verify): rename "Run Artifact" → "Verification", drop failed red border

- The kicker said "Run Artifact" — it's automated verification, not an artifact.
  Renamed to "Verification · Round N".
- Removed the red error border on a failed check — a normal card reads better.
- Fixes a render crash (`useVerifyState is not defined`): the border removal left
  a dangling reference after the import was dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(cli): poll run status when the agent stream drops

When the live stream (gateway WebSocket / SSE) closes before the run finishes,
the run is still executing server-side — so instead of hard-exiting, fall back to
polling `aiAgent.getOperationStatus` every 10s until the run reaches a terminal
state (or is no longer tracked). Pairs with surfacing the WS close code/reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): add Render for generateVerifyPlan tool call

The generateVerifyPlan tool call rendered as the default param/result dump. Add a
Render that lists the generated delivery checks (title + gate/auto-fill tag), and
surface the items on the tool state so the Render can read them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): auto-confirm generated plan so checks run on completion

The agent generated a plan but it stayed `planned`/unconfirmed, so the completion
hook (which gates on a confirmed plan) never ran the checks — the card was stuck
at "awaiting confirmation" with no pass/fail. In the headless agent flow there's
no one to click Confirm, so `generateVerifyPlan` now auto-confirms the plan it
generates; the checks then run automatically on completion. (An interactive
"review before run" gate is a future enhancement.)

Also: the verify card header disappeared in the draft/planned phase
(`phaseToArtifact.draft` was null). Give it a header so the card always shows its
"Verification · Round N" heading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-tracing): only count opaque/presentational attrs as structural noise

The first structuralNoiseRatio charged ALL markup (every <...> tag) as noise,
which over-penalized legitimately structured results 3x. Grounding against real
web-search output (`<item title="…" url="…">snippet</item>`) showed the tags and
the title=/url= attributes ARE the signal the model reads.

Now only opaque/presentational attribute names (id, class, style, data-*, aria-*,
role, on*) count as noise; semantic element tags and content-bearing attributes
(title, url, href, name…) are kept. On a 57-op user-interrupted sample this drops
web-search noise 42%→0% and overall estimated waste 16%→5%, leaving large-payload
(readDocument) and high error-rate tools as the real signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): model-authored criteria with name/description/instruction-in-document + agent verifier

Restructure the generateVerifyPlan tool to a createDocument-style full-create flow
and wire up the agent verifier path:

- criteria now = title + description (required one-liner) + instruction (required
  detailed rubric); instruction lives in a linked document (verify_criteria.documentId),
  description is a new verify_criteria column (migration 0111). verifierConfig no
  longer holds description/instruction.
- generateVerifyPlan creates verify_criteria + a rubric, snapshots the plan onto
  the operation and confirms it; judge resolves the instruction from the document.
- agent-type checks run as verifier sub-agents (execAgent + isolated thread) whose
  onComplete hook parses a VERDICT and writes it back to verify_check_results
  (renamed AgentVerifierSpawner → VerifierAgentRunner).
- UI: custom Inspector for the tool header; check list shows per-verifier-type icons
  (llm/agent/program) + description + required/optional tag; i18n en/zh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ⚡️ perf(verify): run program/llm/agent checks concurrently on completion

The three verifier kinds are independent; previously the agent spawn waited for
the batched LLM judge to finish. Run them via Promise.all so agent sub-agents
start immediately alongside the LLM batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): dedicated builtin verify-agent + writeback tool, role=verify message, portal check editor

- Add `@lobechat/builtin-tool-verify` (submitVerifyResult) + builtin `verify-agent`;
  agent-type checks now run as the dedicated verify agent (not the user's agent),
  which investigates and writes its verdict back via the tool during its run.
- Verifier inherits the parent run's model/provider (builtin default may be
  unconfigured locally).
- role=verify completion message no longer requires an assistantMessageId, so the
  delivery-checker card always surfaces when a plan exists.
- Portal editor for verify checks (title/description/instruction/verifier/onFail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(verify): restrict verify-agent to its writeback tool; fix running loader icon

Root cause of stuck `running` agent checks: the verify-agent ran in agent mode and
inherited all default tools (web-browsing, cloud-sandbox, skills, activator), so it
went off web-searching/crawling to "investigate" and never called submitVerifyResult.

- Run the verify-agent in chat mode (enableAgentMode: false, searchMode: off) — the
  strict whitelist — and whitelist `lobe-verify` for chat mode so the verifier gets
  ONLY its writeback tool.
- Sharpen the verify systemRole: judge from the provided deliverable/instruction
  (no external tools), always reach a verdict, and always call submitVerifyResult.
- CheckerDock: running check now uses the standard RingLoadingIcon (warning ring),
  matching the app's loader instead of a blue spinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): auto-repair loop — re-run the agent with failure feedback on failed checks

When required checks fail with onFail=auto_repair, automatically run a second
iteration instead of ending at `failed`:

- createRepairRunner: re-runs the SAME agent in the same topic with the failure
  feedback as the prompt, re-snapshots the plan onto the repair operation and
  confirms it so it re-verifies on completion (the next round). Capped at
  MAX_REPAIR_ROUNDS via parent-chain depth to prevent runaway loops.
- maybeAutoRepair: fires only once every required check has a terminal result, so
  it works for inline LLM checks (triggered from lifecycle) and async agent checks
  (triggered from the verify tool's writeback path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): open check result detail in portal & rename artifact→result

- add a VerifyResult portal view: clicking any check row opens that result's
  detail (verdict, confidence, Toulmin sections, suggestion) on the right; agent
  checks expose their execution trace from inside the panel
- CheckerDock rows are all clickable now (chevron affordance), status shown by
  icon only; verify card uses colorBgElevated
- rename the run-result surface from "artifact" to "result" everywhere: RunArtifact
  → RunResult, phaseToArtifact → phaseToResult, and all `artifact.*` i18n keys →
  `result.*`
- ship verify namespace zh-CN / en-US locales

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): enrich check result portal — criterion stepper, richer detail view

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): rubric run-policy config + repair feedback on the verify card

Auto-repair feedback now lives on the failed round's role=verify message
(content), and the VerifyMessageProcessor surfaces it into the repair run's
context as a tagged user turn — so the repair op runs off history via a new
execAgent `suppressUserMessage` path instead of injecting a synthetic user
message. createVerifyMessage is awaited before verification to avoid a race.

maxRepairRounds becomes a rubric-level config: new `verify_rubrics.config`
jsonb column, read live at repair time via the plan's sourceRubricId. Adds a
RubricConfig portal panel (reachable from the plan card's settings affordance)
to view/edit it, wired through the verify store + TRPC.

Verify domain types/vocab/config are extracted from the DB schema into
@lobechat/types as the single source of truth; schema and consumers import
from there.

Tests: VerifyMessageProcessor dual behavior; VerifyRubricModel config
round-trip; MessageModel.findVerifyMessageByOperationId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🗃️ refactor(verify): squash the 3 verify migrations into one

Collapse 0110 (tables) + 0111 (criteria.description) + 0112 (rubrics.config)
into a single regenerated 0110_add_verify_tables so the PR ships one clean,
idempotent migration. No schema change vs the three combined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(cli): verify rubric run-policy config commands + shrink judging-rule editor font

CLI: `verify rubric create --max-repair-rounds`, `verify rubric view`, and
`verify rubric update` exercise the rubric config endpoints end-to-end; adds a
mocked command test. UI: judging-rule editor font 16px → 14px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(verify): editable rubric name in the config panel + default 3 repair rounds

Add a name (title) field to the RubricConfig portal, persisted via a new
updateRubricTitle store action + service (optimistic + debounced, alongside
the config write-back). Bump DEFAULT_MAX_REPAIR_ROUNDS 2 → 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(verify): extract generateVerifyPlan into installable lobe-delivery-checker tool

Move the delivery-checker plan-creation flow out of the always-on lobe-agent
tool into a new standalone, installable builtin tool `lobe-delivery-checker`
(Skill Store, opt-in per agent — not loaded by default). lobe-agent no longer
ships generateVerifyPlan.

- new packages/builtin-tool-lobe-delivery-checker (manifest/types/systemRole +
  client Render/Inspector/Portal moved wholesale from lobe-agent)
- new serverRuntimes/lobeDeliveryChecker.ts (generateVerifyPlan moved out of
  lobeAgent.ts), registered alongside verifyResult
- registered installable in builtin-tools (no hidden/discoverable:false, not in
  defaultToolIds/alwaysOnToolIds/runtimeManagedToolIds); renders/inspectors/
  portals/identifiers wired; lobe-agent portal entries removed
- i18n keys moved builtins.lobe-agent.verifyPlan.* → builtins.lobe-delivery-checker.*

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(agent): add `custom` tool mode; verify agent uses it instead of chat-mode

Chat mode's contract is to strip ALL user/agent plugins (strict KB/memory/web
allow-list) — so the verify sub-agent couldn't get its writeback tool without a
leaky blanket rule. Introduce a third tool mode `custom` where the toolset is
EXACTLY the agent's declared plugins (no always-on, no defaults, no activator),
for focused builtin sub-agents.

- chatConfig.toolMode: 'agent' | 'chat' | 'custom' (overrides enableAgentMode)
- AgentToolsEngine: custom branch (defaultToolIds = plugins, rules = plugins-on,
  allowExplicitActivation only in agent mode); chatModeRules restored to strict
- verify agent → toolMode: 'custom'; lobe-verify dropped from chatModeAllowedToolIds
- test: custom mode enables exactly the declared plugin, no always-on / defaults

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:16:35 +08:00
Arvin Xu 7989952d2e ✨ feat(agent-signal): add CLI trigger command + golden snapshot fixture (#15360)
Part of LOBE-9434 (#1 / LOBE-9435). Adds the local verification tooling the
execAgent migration depends on.

- `lh agent-signal trigger`: enqueue any producer-side Agent Signal source
  event for the authenticated user (nightly_review / self_reflection /
  self_feedback_intent / user.message / tool.outcome.*)
- server: `buildTriggerSourceEvent` default-payload builder +
  `AGENT_SIGNAL_TRIGGER_SOURCE_TYPES` allowlist, surfaced through a new
  authed `agentSignal.triggerSourceEvent` tRPC procedure that re-derives
  userId from context (owner-scoped, override can't repoint it)
- golden snapshot fixture + dependency-free `assertGoldenFinalState`
  structural assertion (ideas/intents/writeOutcomes >= 1, brief non-empty)
  for use by the migration regression tests
- builder unit tests + offline/live e2e, regenerated man page

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:06:09 +08:00
LiJian 2098ac8374 🐛 fix: remove the old cron job from lobehub (#14630)
* fix: remove the old cron job from lobehub

* fix: add some ts back
2026-05-10 13:49:32 +08:00
Arvin Xu 5fb795b092 ✨ feat(cli): add lh hetero exec for standalone heterogeneous agent runs (#14431)
* 🌐 i18n: add taskDetail.runAll keys for subtask dependency runner

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): add `lh hetero exec` for standalone heterogeneous agent runs (LOBE-8523 phase 1a)

Phase 1a of LOBE-8516: a Node-side `spawnAgent()` plus the CLI command that
drives it. Standalone-only — no `--topic` / `--operation-id` / no server
ingest. Output is `AgentStreamEvent` JSONL on stdout, one event per line.

Why phase 1a is its own milestone: it lets us validate the producer pipeline
end-to-end (`spawn → JsonlStreamProcessor → adapter → toStreamEvent`) under a
plain Node process, get Device-mode + manual debugging unblocked, and ship
without waiting on phase 2's server `heteroIngest` procedures.

## Shared `spawnAgent({ agentType, prompt, resumeSessionId, cwd, command })`

- Lives in `@lobechat/heterogeneous-agents/spawn`. Pure Node — no Electron, no
  image cache, no on-disk tracing, no proxy env composition. Desktop main keeps
  its own bespoke spawn path for those host concerns; this minimal version is
  what the CLI sandbox + terminal use case needs.
- CC: stream-json stdin format + the established preset flags. Codex: `exec` /
  `exec resume` form with `--json --skip-git-repo-check --full-auto`.
- Returns `SpawnAgentHandle` with: async-iterable `events`, `exit` promise,
  `kill(signal)` (Unix process-group kill, Windows direct), `pid`, raw `stderr`.
- Internally a single-queue async iterator coordinates between the stdout
  listeners and the consumer — keeps backpressure simple, no extra deps.

## `lh hetero exec` command

```
lh hetero exec --type claude-code|codex
  [--prompt - | --prompt <text>]   # default stdin
  [--resume <sessionId>]
  [--cwd <path>]                    # default process.cwd()
  [--command <bin>]                 # default `claude` / `codex`
  [--operation-id <id>]             # uuid v4 generated if omitted
```

- Reads prompt from stdin when omitted or `-`.
- Forwards child stderr to ours so users see auth prompts / missing-binary
  errors.
- Ctrl-C → SIGINT to the child's process group (Unix); a second Ctrl-C
  escalates to SIGKILL.
- Exit code passthrough: child code 0/non-0 stays as-is; SIGINT / SIGTERM /
  SIGKILL map to POSIX 130 / 143 / 137.

## Out of scope (phase 1b — next PR)

- `--topic` / `--operation-id` flags as REQUIRED + the BatchIngester
- `--render none|jsonl` flag (phase 1a is implicit JSONL)
- trpc `aiAgent.heteroIngest` / `heteroFinish` calls
- Gateway WS interrupt subscription

## Validation

- `bunx vitest run packages/heterogeneous-agents` — 113 passing (8 new
  spawnAgent tests + the 105 pre-existing on canary)
- `bunx vitest run apps/cli/src/commands/hetero.test.ts` — 7 passing
  (all `--type` / `--prompt` / `--operation-id` / exit-code-passthrough /
  SIGINT-mapping branches)
- Real end-to-end: `bun src/index.ts hetero exec --type claude-code --prompt
  'Reply with exactly the word HELLO and nothing else.'` produced clean
  AgentStreamEvent JSONL (stream_start → 2 stream_chunks → step_complete
  turn_metadata → step_complete result_usage → stream_end → agent_runtime_end),
  every line stamped with the same auto-generated operationId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(spawn): serialize pipeline pushes so flush waits for in-flight chunks

When stdout emits multiple chunks back-to-back — or `'end'` lands while an
earlier `pipeline.push()` is still awaiting the Codex tracker's filesystem
reads — the per-chunk `.then` handlers ran concurrently. Two consequences:

1. Out-of-order events. Push #2's events could resolve before push #1's,
   so the JSONL stream came out shuffled.

2. Late-event loss. `'end'` would call `pipeline.flush()` and immediately
   set `streamEnded = true` while prior pushes were still pending. The
   async iterator could then return `{ done: true }` before those pushes
   queued their events.

Fix: thread every `push()` / `flush()` / error-surface call through a single
`pipelineQueue` `Promise` chain, the same shape the desktop controller uses
for its broadcast queue. `flush()` now reliably runs after every queued
push has drained, so `streamEnded` is the very last write.

Two regression tests cover the failure modes by spying on
`AgentStreamPipeline.push` to inject deterministic delays:

- "preserves event ordering across async pipeline.push() calls" — chunk A
  resolves slower than chunk B; without the chain B arrives first.
- "iterator drains slow in-flight pushes before flushing the stream" —
  `'end'` fires while a 40 ms push is still pending; without the chain
  the iterator returns done before the chunk's events queue.

Bisected: both tests fail without the chain, pass with it. E2E re-smoke
(`bun src/index.ts hetero exec --type claude-code` simple text + tool-using
prompt + stdin) still produces clean ordered JSONL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:41:58 +08:00
Arvin Xu c68dfa00df ✨ feat(cli): add lh notify command for external agent callbacks (#13664)
* ✨ feat(cli): add `lh notify` command for external agent callbacks

Add a new `lh notify` CLI command and server-side TRPC endpoint that allows
external agents (e.g. Claude Code) to send callback messages to a topic and
trigger the agent loop to process them.

Fixes LOBE-6888

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore(cli): replace sessionId with agentId and threadId in notify command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:03:55 +08:00
Arvin Xu 14cd81b624 ✨ feat(cli): add migrate openclaw command (#13566)
* ✨ feat(cli): add `migrate openclaw` command for importing OpenClaw workspace

Add a new CLI command `lh migrate openclaw` that imports all files from the
OpenClaw workspace (~/.openclaw/workspace) as agent documents into the LobeHub
inbox agent. Supports --source, --agent-id, --slug, --dry-run, and --yes options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cli): restructure migrate as directory for future providers

Refactor `migrate` command from a single file to a directory structure
(`migrate/index.ts` + `migrate/openclaw.ts`) to support future migration
sources like ChatGPT, Claude, Codex, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): remove unnecessary `as any` casts in migrate openclaw

Use proper TrpcClient type instead of casting to any. Extract
resolveInboxAgentId helper with correct typing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cli): migrate openclaw creates a new "OpenClaw" agent by default

Instead of importing into the inbox, the default behavior now creates a
dedicated "OpenClaw" agent and imports workspace files as its documents.
Use --agent-id to import into an existing agent instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): restore --agent-id and --slug options for migrate openclaw

Support three modes: --agent-id (by ID), --slug (by slug, e.g. "inbox"),
or default (create a new "OpenClaw" agent).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): print agent URL after migrate openclaw completes

Show a clickable link (e.g. https://app.lobehub.com/agent/<id>) at the
end of the import so users can open the agent directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): check login state early in migrate openclaw

Verify authentication before scanning files so users get a clear
"Run 'lh login' first" message upfront instead of after confirmation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): read agent name, description, avatar from OpenClaw workspace

Parse IDENTITY.md (or SOUL.md) for Name, Creature/Vibe/Description, and
Emoji fields to populate the new agent's title, description, and avatar
instead of hardcoding "OpenClaw".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style(cli): show emoji + name instead of agent ID in migrate output

Display the agent avatar emoji and title throughout the migrate flow
(confirmation, creation, importing). The agent ID only appears in the
final URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): exclude .venv from openclaw workspace scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore(cli): expand excluded dirs/files for openclaw workspace scan

Filter out IDE configs, VCS dirs, OS artifacts, dependency dirs, Python
caches, build outputs, env files, and other common non-content items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update version

* ✨ feat(cli): use `ignore` package for gitignore-based file filtering & improve output

- Replace hardcoded EXCLUDED_NAMES set with `ignore` package (gitignore syntax)
- Respect workspace .gitignore if present, plus comprehensive default rules
- Cover all common languages/tools: Python, Ruby, Rust, Go, Java, .NET, etc.
- Improve final output: friendlier completion message with agent name + URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✅ test(cli): add tests for migrate openclaw command

Cover profile parsing, file filtering (gitignore + default rules),
dry-run, agent resolution (--agent-id, --slug, default create),
confirmation flow, error handling, and output formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(cli): filter placeholder emoji and binary/database files

- Skip avatar values like (待定), _(待定)_, TBD, N/A, etc.
- Add ignore rules for database files (*.sqlite, *.db, *.mdb, etc.),
  images, media, fonts, lock files, and compiled binaries
- Runtime binary detection: check first 8KB for null bytes and skip
  binary files that slip through the extension filter
- Add tests for placeholder emoji filtering, binary skip, and db exclusion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ✨ feat(api,cli): support optional createdAt for agent document upsert

Thread an optional `createdAt` parameter through all layers:
- Model: AgentDocumentModel.create/upsert accept optional createdAt,
  set both createdAt and updatedAt on documents + agent_documents rows
- Service: UpsertDocumentParams includes createdAt
- Router: agentDocument.upsertDocument accepts optional z.date()
- CLI: migrate openclaw passes file mtime as createdAt to preserve
  original file timestamps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style(cli): add npx usage hint to auth error message

Show 'npx -y @lobehub/cli login' alongside 'lh login' so users who
haven't installed the CLI globally know how to authenticate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update version

* ✨ feat(api,cli): support optional updatedAt for agent document upsert

Add updatedAt alongside createdAt through all layers. When both are
provided, updatedAt is used independently; when only createdAt is
given, updatedAt falls back to createdAt.

CLI now passes file birthtime as createdAt and mtime as updatedAt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): use os.homedir() for default source & wrap file reads in try

- Replace process.env.HOME || '~' with os.homedir() so the default
  --source path resolves correctly on Windows and when HOME is unset
- Move fs.readFileSync/statSync inside the try block so a single
  unreadable file doesn't abort the entire migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:45:04 +08:00
Arvin Xu dd7819b1be 🔨 chore(cli): register task command and add kanban board view (#13511)
* ✨ feat(cli): register task command and add kanban board view

Register the missing `registerTaskCommand` in program.ts so `lh task` commands are accessible. Add `--board` flag to `task list` that renders a kanban-style view grouping tasks by status columns (backlog, running, paused, completed, etc.) with color-coded borders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:54:12 +08:00
Innei 6e3053fcb3 ✨ feat(cli): add generated man pages (#13200) 2026-03-24 14:46:56 +08:00