feat(cli): agent-friendly output — signature tools listing with filter/paging, table-driven global flags, --pretty/--verbose, compact JSON - #67
Merged
Conversation
This was referenced Sep 20, 2026
… JSON and no meta by default Replaces the scattered --json/--no-color plumbing with a single declarative GlobalFlags table (cli/global-flags.ts) that also registers cac options and resolves flags from argv for the pre-parse-failure fallback path. Adds two new global flags, --pretty and --verbose, and collapses the ad-hoc `io` bag into one CliEnv object (`env`) threaded through every route. Centralizes the two pieces of logic that used to be duplicated across runner.ts: the result envelope (cli/envelope.ts's finalizeResult, which owns the `meta` block) and JSON serialization (global-flags.ts's formatJson, which owns compact-vs-indented output). Breaking (CLI): - `--json` output is now compact (single-line) by default; `--pretty` restores the old 2-space indentation. - The `meta` block (command/timestamp/duration_ms) is no longer emitted by default, in either human or --json output; `--verbose` restores it. NDJSON event lines (`appduct events`) stay single-line always, regardless of --pretty, per the streaming-consumer contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zpvXWBBSJZnNVrqbzpFB4
…fset; agent skill defaults to plain text
`appduct tools` now renders each tool as a one-line call signature
(`name(params) -> result`, via new `@appduct/shared` renderToolSignature)
plus the description's first line, instead of a bare name/description
table, and gains --filter/--limit/--offset so a large registry stays
cheap for an agent to read. `tools.list` sorts by name, filters, and
pages daemon-side, and now returns `{ tools, total }` instead of a bare
array (a breaking CLI --json change) so the CLI can report how many
tools were left off a page. The appduct agent skill's example commands
default to plain text, adding --json only where a script will parse
the output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zpvXWBBSJZnNVrqbzpFB4
… verbatim Parsing positionals and flags in a route body let a usage error escape the runner, so the built CLI crashed with a stack trace on `tools --limit 0`, `invoke` with no tool, or too many positionals. Parse inside the handler instead, before the version check. cac never enforces a <value> placeholder, so `--limit -1` and a bare `--limit` arrived as true and were read as 1; reject them. cac also coerces numeric-looking values to numbers, which silently dropped `--filter 404` and turned `--filter 007` into 7; recover the verbatim text from argv, now carried on RouteContext.
…riptions An --offset past the end printed "No tools registered" for a non-empty registry; it now says which offset was empty and how many tools match. The single-arg form that resolves to a tool name silently ignored --filter/--limit/--offset; it now fails the same way an explicit <selector> <name> does. The listed description summary now breaks on any line terminator, drops control characters, and never cuts through a surrogate pair.
…chemas Nested array items recursed without bound (a cyclic items overflowed the stack), a BigInt const or default threw from JSON.stringify, and a throwing getter escaped. Cap array depth, stringify defensively, and fall back to name(...) on anything else. Enum and const literals are cut at 40 characters, and a property name that is not identifier-like is JSON-quoted so a newline or escape sequence cannot break or hijack the line.
appduct/client runs no daemon version check, so a newer client against an already-running older daemon got undefined from tools(). Accept both shapes.
The SDKs omit input_schema for a tool that takes no input, and the MCP server already maps that to an empty object schema. Printing (...) sent agents to fetch a full schema that does not exist.
…daemon
Read the bound wss port from daemon status now that test state dirs ask for
wssPort 0, and answer tools.list from the fake daemon with the sorted
{ tools, total } result.
V3RON
force-pushed
the
claude/charming-noether-f7mm0t
branch
from
September 21, 2026 08:27
16279c0 to
1c54fbd
Compare
This was referenced Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The CLI's output is increasingly read by agents, where every token counts, and
appduct toolshad no answer for an app that registers hundreds of tools. Three things cost tokens on every invocation without informing anyone: the full input and output schema of every tool in the listing (about 300 tokens per tool as--json), 2-space indented JSON under--json, and a trailingMetablock that nothing acts on. This PR makes the listing a signature per tool, adds daemon-side filter and paging, makes the other two opt-in, and turns the global output flags into one declarative table so the next flag is a one-entry change.Measured on a realistic Zod-exported tool: the old
--jsonlisting costs about 306 tokens per tool; the new text listing costs about 50.What changed
appduct toolslisting (commit 2)A signature per tool. The text listing prints one call signature plus the first line of the description per tool:
The renderer is
renderToolSignaturein@appduct/shared(packages/shared/src/domains/tool-signature.ts). It is pure and total: required and optional params, short defaults, enums, const, type arrays,T[], nested objects expanded one level, and...for anything it cannot summarise (anyOf/oneOf/allOf/not/$ref, a non-object input root, a missing schema). Schema internals are never validated anywhere in the codebase, so every branch is defensive. A[prompt]/[deny]tag follows tools whose effective policy is notallow. Descriptions are cut to their first line and 120 characters.--filter,--limit,--offset. Applied daemon-side in thetools.listhandler: the registry is sorted by name with a code-point comparison (notlocaleCompare, so order does not depend on the daemon's locale), filtered by case-insensitive substring on name and description, and sliced. A truncated listing ends withShowing n of total tools (offset o). Narrow with --filter <text> or page with --offset <n>.The single-tool lookup (tools <name>, and the ambiguous single-arg probe) always asks for the unpaged registry so a name lookup can never miss because of paging; combining<name>with any of the three flags is a usage error.tools.listreturns{ tools, total }.totalis the post-filter, pre-paging count. The MCP server andappduct/clientunwrap it;AppClient.tools()keeps returningToolDescriptor[].Skill.
skills/appduct/SKILL.mdnow defaults every example to plain text, explains the signature format,...,--filterand paging, and says--jsonis for scripts that parse output, not for the agent reading it. Text output is documented as not a stable contract.Global flags (commit 1)
One table.
cli/global-flags.tsdeclares--json,--pretty,--verboseand--no-colorwith theircacspec, help text, and how to derive each from parsed options or raw argv.create-cli.tsregisters from it;dispatch.tsresolves it.One environment object.
RouteContext.iobecameRouteContext.env: CliEnv = { flags, stdout, stderr, clock }. A new flag added to the table is available asenv.flags.<name>in every route with no plumbing change.The envelope owns
meta.cli/envelope.tsholdscreateCommandMetaandfinalizeResult, which attachesmetaonly under--verboseand otherwise returns the result with nometakey. Rendering keys off presence.One JSON formatter.
formatJsonis compact by default and indents under--pretty, for the--jsonresult, the late-failure JSON on stderr, and JSON embedded in human output. NDJSON event lines stay compact regardless. TheinitMCP snippet stays indented because it is meant to be pasted.Breaking changes (CLI only)
appduct tools --jsonfor a listing returns{ tools, total }instead of a bare array. The single-tool form is unchanged.--jsonoutput is compact, one line. JSON parsers are unaffected;--prettyrestores indentation.metablock is gone from both human and--jsonoutput by default.--verboserestores it.No change to the app↔daemon wire protocol, the
ToolDescriptortype, the SDKs, or theappduct/clientpublic API. All documented inCHANGELOG.mdunder Unreleased, plus the README (new "appduct tools: a signature per tool" section) and ARCHITECTURE §5 and §10.Tests
tool-signature.test.ts(30 table-driven cases including hostile schemas),global-flags.test.ts,envelope.test.ts,global-flags.integration.test.ts(daemon-freerunClicoverage of every flag).totalbefore paging, slicing, bad-param rejection intool-invocation.integration.test.ts.cli-v2.integration.test.tscovering--filter,--limit/--offset, name lookup under paging, the usage error, and the human signature and "Showing" lines.output.test.tsand snapshot,runner.test.ts,router.test.ts, MCP, client and e2e tests for the{ tools, total }shape.Verification
The one e2e failure,
daemon-restart.e2e.test.ts("SIGKILL mid-session → next command auto-spawns a fresh daemon"), fails identically on an untouched build ofmainin this sandbox, so it is environmental and unrelated.Smoke-tested the built
dist/bin.js:tools --helplists the new flags;bogus --jsonis one line with nometa;--prettyindents;--verboseaddsmetain both modes.🤖 Generated with Claude Code
https://claude.ai/code/session_018zpvXWBBSJZnNVrqbzpFB4