Skip to content

Releases: callstackincubator/appduct

v0.13.0

Choose a tag to compare

@V3RON V3RON released this 28 Sep 13:13
762bd4f

0.13.0 (2026-09-28)

  • Fix: the iOS SDK connects with the Expo development network inspector enabled. Appduct's
    WebSocket connection no longer fails with NSURLErrorDomain -1005 when that inspector is on.
  • Breaking: the CLI commands are now noun-verb, with no aliases. ls is now sessions ls,
    revoke is sessions revoke, link is sessions link, tools is tools ls, tools <name>
    is tools describe <name>, invoke is tools call, events is events tail, and
    events --since <cursor> is events since <cursor>; a removed command's error names its
    replacement.
  • Breaking: appduct_events returns flat events. Each event is now
    { name, payload, ts, seq, sessionId, alias } instead of { kind, data }, and limit now
    defaults to 50.
  • New: filter events by name. appduct_events accepts name, a glob such as "cart.*"
    (* matches any run of characters), to return only events whose name matches.
  • Breaking: appduct_events truncates a payload over 4096 bytes by default. A truncated event
    comes back as { name, payloadPreview, truncated: true, payloadBytes } instead of
    { name, payload }; pass payloadMaxBytes to raise or lower the cap.
  • New: appduct_events and app.events() report dropped and remaining. dropped counts
    events evicted from the retention buffer before you asked, and remaining counts events still
    waiting after the page you got back.
  • New: appduct events tail and appduct events since accept --name <glob> and
    --payload-max-bytes <n>.
    They filter events by name and cap payload size; payloads print
    whole unless you pass --payload-max-bytes.
  • New: appduct events since's trailing cursor line reports dropped and remaining.
    dropped counts events evicted from the retention buffer before this pull, and remaining
    counts matching events still waiting beyond this page.
  • New: app.events() accepts payloadMaxBytes to cap an event's payload size. Without it every
    event keeps its full payload; with it, an oversized payload comes back as payloadPreview/
    payloadBytes and you check truncated before reading payload.
  • Breaking: appduct/client's AppEvent and EventsResult types may now be truncated. Code
    that annotates a variable or parameter with AppEvent/EventsResult and reads .payload
    unconditionally should switch to FullAppEvent/FullEventsResult, which appduct/client now
    exports alongside TruncatedAppEvent.
  • Breaking: appduct_wait_for_event's name is now a glob, and match is removed. name
    matches the same whole-name glob appduct_events does (* waits for the next event of any
    name); pass match and the call now fails with invalid_request.
  • Breaking: appduct_wait_for_event truncates a payload over 4096 bytes by default and reports
    dropped.
    Same shape as appduct_events: a truncated result comes back as
    { name, payloadPreview, truncated: true, payloadBytes, ... } instead of { name, payload, ... };
    pass payloadMaxBytes to raise or lower the cap.
  • Breaking: app.waitForEvent()'s name is now a glob, WaitForEventOptions.match is removed,
    and it now accepts payloadMaxBytes.
    app.waitForEvent("cart.*") waits for any name matching
    the glob and match is gone; pass payloadMaxBytes to cap the resolved event's payload the same
    way app.events() does, otherwise it comes back whole as before.
  • Docs: the shipped skill covers draining and waiting for app events. skills/appduct now
    shows the appduct_events drain loop (name glob, cursor, remaining, dropped) and the
    appduct_wait_for_event background wait.

v0.12.0

Choose a tag to compare

@V3RON V3RON released this 24 Sep 12:32
5044793

0.12.0 (2026-09-24)

  • Breaking: appduct events, appduct_events and appduct_wait_for_event show only the events
    your app posts.
    Device connections and tool calls no longer appear and kinds is rejected;
    to wait for a device, use appduct ls --json or appduct_wait_for_session.
  • Fix: app events are no longer lost after about 128 tool calls. Each device keeps its last
    256 app events (eventBufferSize) however many tool calls run in between.
  • New: the daemon writes its own events to ~/.appduct/events.log. Session, link and tool-call
    events land there as JSON lines for debugging Appduct, never your app's events; the file rotates to
    events.log.1 past eventsLogMaxBytes (default 10 MiB).

v0.11.1

Choose a tag to compare

@V3RON V3RON released this 22 Sep 20:03
977e6b8

0.11.1 (2026-09-22)

  • Fix: an ungrouped tool reports group: null to every reader. appduct_describe_tool
    dropped the key instead of reporting null, so it disagreed with appduct_list_tools about
    the same tool. ToolDescriptor.group is
    string | undefined again — it is the registration type, and registering null has always been
    rejected — so the type app authors write against (appduct/client, the React Native SDK's
    getRegisteredTools()) no longer admits a value that throws. Code reading a tools.list entry
    takes the new ListedToolDescriptor (or ToolsListEntry, which adds policy), where group is
    string | null; appduct/client's tools() now returns those entries.
  • Fix: the first Appduct command on a clean machine no longer fails with a bare ENOENT.
    Nothing created the state directory before the auto-spawn path wrote into it: ~/.appduct is
    created by startDaemon, but the spawn-lock and daemon.log's fd are opened by the parent
    process, before the daemon it spawns exists. So with no ~/.appduct yet, every command that
    auto-spawns a daemon — appduct ls, appduct daemon start|status, and appduct mcp, which
    died before an MCP client could finish initialize — failed with
    ENOENT: ... open '~/.appduct/daemon.spawn.lock' until someone ran appduct daemon run in the
    foreground once. The auto-spawn path now creates the directory (mode 0700, same as the daemon
    would) before taking the lock.

v0.11.0

Choose a tag to compare

@V3RON V3RON released this 22 Sep 11:23
  • Docs: designing tools for agents. docs/TOOLS.md gains a "Designing tools for agents"
    section (name by intent, annotate, pair mutations with observers, declare outputSchema,
    describe parameters, coarse over fine, timeoutMs, no tools that wait on a person), linked from
    the React Native, iOS and Android READMEs and the website. The Appduct skill is split into a short
    SKILL.md (the CLI loop, chaining calls in one shell invocation, errors) plus on-demand
    references for the CLI, writing tools and setup. The playground tools now follow the rules:
    reset_counter is also idempotentHint, throwing_tool is readOnlyHint, and descriptions
    name their side effects.

  • New: tool groups. A tool can declare an optional group — a top-level group ("cart")
    or one subgroup below it ("checkout/payment"); each part matches the tool-name pattern
    [a-zA-Z0-9_-]{1,64}. Set it with registerTool/useAppductTool's group option (a change
    to it re-registers the tool), with createToolGroup("cart") to bind one group for a whole
    feature module, or with the group parameter of the Swift and Kotlin register calls. An
    invalid group invalidates the registry snapshot exactly like an invalid timeout_ms, so all
    three SDKs reject it at registration — an explicit null included, so an ungrouped tool omits
    the option rather than passing null. A daemon that predates groups ignores the field.

  • New: appduct tools --group <name> and appduct tools --groups. --group checkout lists
    the checkout group and all its subgroups (checkout/payment lists just that subgroup) and
    combines with --filter/--limit/--offset; --groups lists only the groups and their tool
    counts. Without --group, a registry with groups is listed under group headings, and a
    truncated listing's footer names the top-level groups to narrow to. tools.list gains a
    group param (applied before total and paging) and a groups summary of the whole registry
    on every result, so appduct tools --json now returns { tools, total, groups }. Every entry
    carries a group, null for an ungrouped tool — the same value the groups summary uses for
    its own ungrouped row, so one test answers "ungrouped" in either half of the result.

  • New: groups over MCP. appduct_list_tools takes a group (same matching as --group),
    shows each tool's group, and returns the groups summary on every result, so an agent can
    see an app's areas and list one of them. appduct_describe_tool includes the tool's group.

  • Fixed (iOS): a tool name with a trailing newline ("tool\n") is now rejected, matching
    @appduct/shared and Android. The Swift core's name check accepted it because ICU's $ also
    matches before a final line terminator; the daemon would then have rejected the snapshot.

  • Breaking (MCP): the app's tools are no longer listed as MCP tools. An agent reaches them
    through three built-ins that mirror the CLI: appduct_list_tools (one-line signatures and
    each tool's policy, with filter/limit/offset, like appduct tools),
    appduct_describe_tool (one tool's full schema, like appduct tools <name>) and
    appduct_call_tool ({ selector?, name, args?, timeoutMs? }, like appduct invoke).
    tools/list is now a fixed set of built-ins, so an app with hundreds of tools adds three
    definitions to an agent's context, not hundreds. appduct_list_tools returns 50 tools at a time
    unless given limit. selector takes a session alias or id; a call is routed by session id, so
    it fails with unknown_session rather than reaching a new device that took over a departed
    device's alias. Unknown parameters are rejected (invalid_request). timeoutMs can only
    shorten the tool's own deadline, since the app stops a tool at its declared timeout; a longer
    one, or one outside 1000–600000, is rejected rather than clamped. What goes away:

    • Calling an app tool by its own name through tools/call. It now returns tool_not_found,
      pointing at appduct_list_tools and appduct_call_tool.
    • <alias>__<name> namespacing. With several devices connected, pass selector (the
      session alias or id) instead.
    • notifications/tools/list_changed, and the listChanged capability.
    • MCP-level outputSchema enforcement and schema degradation: schemas reach the agent as
      data through appduct_describe_tool, exactly as registered, whatever their root type. The
      React Native SDK no longer warns about non-object output schemas.
    • MCP client permission rules that named individual app tools (for example
      mcp__appduct__seed_cart) no longer match anything; the client's permission now covers
      appduct_call_tool as a whole, so "always allow" there approves every app tool. To keep a
      person approving destructive calls, set policy.destructive to "prompt" (it covers tools
      annotated destructiveHint: true). "prompt"-policy
      consent itself is unchanged: it is asked per call, via elicitation.
    • The React Native SDK's input-schema warning now fires only for a root type that rules out
      an object (z.string(), z.array(...)), not for unions or intersections of objects.
    • @appduct/shared no longer exports isObjectRootedSchema.
  • Breaking (MCP): "prompt"-policy consent is elicitation-only. The Claude Code-specific
    fallback is gone: tools/list no longer emits _meta["anthropic/requiresUserInteraction"], and
    the MCP server no longer sends consent: "client". A "prompt" tool called from an MCP client
    that doesn't declare the elicitation capability is now denied with policy_denied (reason
    no_consent_channel), the same as the CLI. Current Claude Code declares elicitation, so it gets
    the elicitation prompt instead; only a client that relied on the flag without supporting
    elicitation loses access. To fix that, use a client that supports elicitation, or set the tool's
    policy to "allow" in config.json — which removes the gate for every caller, including the
    CLI — and restart the daemon (appduct daemon stop), since config.json is read once at daemon
    start. The restart disconnects every device, which then has to link again, and a running
    appduct mcp loses its daemon connection, so restart the MCP server in your client too.

    • A daemon with this change that receives consent: "client" from an older MCP server treats it
      as no consent, so the call is denied and audited as no_consent_channel.
    • @appduct/shared: ToolsCallParams.consent and the audit record's consent narrow to
      "elicitation". New audit records never carry "client"; existing audit files may.
  • config.json's wssPort accepts 0, meaning "bind an OS-assigned port". The pinned-wss
    listener takes whatever ephemeral port the OS hands it, and everything that reports or advertises
    the port — daemon.status's wssPort, a minted link's endpoint.port, and so the deep link and
    QR code composed from it — carries the bound port rather than the configured 0. This lets
    several daemons (separate state dirs) coexist on one machine without an operator hand-picking a
    port for each. Every other value must still be a port number in 1..65535; the default is
    unchanged at 8443.

  • Fixed: a zombie daemon process no longer blocks pidfile takeover. process.kill(pid, 0)
    succeeds for an exited-but-unreaped process, so a daemon that was killed after its parent CLI had
    exited could keep its pidfile looking live — in containers whose PID 1 does not reap, for the
    life of the container, leaving every later command reporting a daemon that was already dead. The
    liveness probe now also reads /proc/<pid>/status on Linux and treats State: Z as dead;
    everywhere /proc is absent or unreadable the previous behaviour is unchanged.

  • Breaking (CLI): --json output is compact by default. Every appduct <command> --json
    invocation used to pretty-print its JSON with 2-space indentation; it now prints it on a single
    line (JSON.stringify, no whitespace). Any JSON parser is unaffected. A script that greps or
    diffs the indented text directly is not — pass the new --pretty flag to restore the old
    indentation.

  • Breaking (CLI): the meta block (command, timestamp, duration_ms) is no longer emitted
    by default
    , in either human or --json output. Pass the new --verbose flag to restore it —
    the trailing Meta lines in human mode, the meta field on the --json envelope.

  • New: --pretty and --verbose global flags, alongside --json and --no-color. See the
    [appduct READMEhttps://github.com/callstackincubator/appduct/blob/main/packages/appduct/README.md) for the full description of each.

  • Breaking (CLI): appduct tools --json for a listing now returns { tools, total } instead
    of a bare array. The single-tool form (appduct tools <selector> <name>) is unchanged — it still
    returns the bare tool descriptor.

  • New: a signature-based tools listing, with --filter/--limit/--offset. The human
    listing now shows one call signature (name(params) -> result) plus a one-line description per
    tool instead of a bare name/description table, and appduct tools gains --filter <text> to
    narrow by name/description, and --limit <n>/--offset <n> to page through a large registry —
    making it cheap to read appduct tools against an app that registers hundreds of tools. See the
    [appduct READMEhttps://github.com/callstackincubator/appduct/blob/main/packages/appduct/README.md)'s "appduct tools: a signature per tool" section
    for details.

  • The appduct agent skill now defaults its example commands to plain-text output, adding
    --json only where a script (not the agent itself) will parse the result.

  • Fixed: a bad argument to tools, invoke, revoke or events (a missing <tool>, too
    many positionals, --limit 0) crashed the C...

Read more

v0.10.0

Choose a tag to compare

@V3RON V3RON released this 16 Sep 11:19
  • New: native SDKs for apps without React Native. The same Appduct core the React Native
    package uses is now published on its own:
    • iOS — AppductCore via Swift Package Manager
      (.package(url: "https://github.com/callstackincubator/appduct", from: "0.10.0")) or CocoaPods
      (pod 'AppductCore', :configurations => ['Debug']). See packages/native/ios/README.md.
    • Android — com.callstack.appduct:core for debug builds and com.callstack.appduct:core-noop
      for release builds, on Maven Central. See packages/native/android/README.md.
  • Breaking (Android): the namespace moved from com.callstackincubator.appduct to
    com.callstack.appduct.
    This covers the Kotlin package, the Android library namespace, and the
    AndroidManifest.xml meta-data keys.
    • Expo and autolinked React Native apps: nothing to do. The config plugin and autolinking pick
      up the new names on your next prebuild/build.
    • If you set the meta-data keys by hand: rename them. The old keys are no longer read, so an
      app still using them loses its configuration and falls back to the fail-closed defaults.
      • com.callstackincubator.appduct.CLI_PINS → com.callstack.appduct.CLI_PINS
      • com.callstackincubator.appduct.TRUST → com.callstack.appduct.TRUST
      • com.callstackincubator.appduct.ALLOW_PRIVATE_LAN_ONLY → com.callstack.appduct.ALLOW_PRIVATE_LAN_ONLY
    • If you import Appduct's Kotlin classes directly: update the imports to com.callstack.appduct.
  • appduct doctor recognises both the new and the pre-0.10.0 Android namespace, so a release gate
    still detects Appduct in apps built against 0.9.0 or earlier.
  • Fix: resolved Swift strict-concurrency warnings in the iOS core.
  • Fix: CLI and MCP messages say "an Appduct" instead of "a Appduct".
  • Docs: the README is scoped to React Native, with new guidance on the MCP and CLI ways to use
    Appduct with an agent.

What's Changed

  • feat: framework-free native core with plain iOS/Android entry points (#48) by @V3RON in #49
  • chore(deps): bump actions/setup-java from 6.0.0 to 6.0.1 by @dependabot[bot] in #50
  • docs: make the README easier to skim and clearer on platform support by @V3RON in #51
  • ci: stop installing the removed legacy 'tools' SDK package by @V3RON in #53
  • Rename Cordierite to Appduct by @V3RON in #52
  • docs: scope the README to React Native until native SDKs are released by @V3RON in #54
  • fix: use "an Appduct" instead of "a Appduct" by @V3RON in #56
  • docs: explain the MCP and CLI ways to use Appduct with an agent by @V3RON in #55
  • Fix the Swift strict-concurrency warnings and gate them with an iOS CI build by @V3RON in #59
  • build: CocoaPods and SwiftPM by @V3RON in #58
  • Publish core/core-noop to Maven Central as com.callstack.appduct by @V3RON in #60

Full Changelog: v0.8.0...v0.10.0

v0.8.0

Choose a tag to compare

@V3RON V3RON released this 09 Sep 02:22

What's Changed

  • fix: stop cordierite_connect and delivered deep links from silently stranding a session by @V3RON in #21
  • fix: exclude ios/CordieriteTests from npm package by @V3RON in #22
  • chore(ci): use iPhone 17 for iOS tests by @V3RON in #46
  • ci: run the iOS job on macos-latest by @V3RON in #47
  • fix(react-native): register useCordieriteTool once per mount and route calls through a ref (#28) by @V3RON in #35
  • feat(react-native): accept paired and raw JSON Schema tool schemas; dev error when no exporter (#27) by @V3RON in #40
  • fix(mcp): never emit a non-object outputSchema and guard structuredContent (#26) by @V3RON in #42
  • feat: carry per-tool timeoutMs to the daemon and size MCP/CLI transport timeouts (#25) by @V3RON in #45
  • feat(cli): detect daemon/CLI version drift and restart the daemon when safe (#30) by @V3RON in #44
  • feat(daemon): audit log retention and daemon.log rotation (#32) by @V3RON in #43
  • feat: elicitation consent channel for policy "prompt" (issue #10 channel 1) by @V3RON in #24
  • feat(cli): experimental --open ios-device delivery via xcrun devicectl (#31) by @V3RON in #36
  • feat(cli): cordierite init, scheme discovery from app.json, and --scheme/CORDIERITE_SCHEME for mcp (#29) by @V3RON in #41
  • chore(deps): bump actions/setup-java from 5.7.0 to 6.0.0 by @dependabot[bot] in #23
  • docs: five-minute READMEs, hardening moved to docs/, markdown link check (#33) by @V3RON in #37
  • chore(deps): bump gradle/actions/setup-gradle from 67621b124fd2e251c5e8a0e6e3b91318f2287669 to 9c971963bec38e04b3d30dcc455b5382be2fdbfb by @dependabot[bot] in #4

New Contributors

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@V3RON V3RON released this 19 Aug 11:28
  • Fix: a terminal daemon rejection (1008) now ends the session instead of retrying until
    grace.
    onSocketLost previously treated only close code 1000 as terminal, so an
    unretryable rejection — unknown_session after a daemon restart, invalid_resume_token, a
    session revoked or expired while offline — kept the client in reconnecting for up to
    grace_s (600s default) before its sessionChange: lost listener ever fired. Every
    daemon-side rejection of this kind closes with 1008, so 1008 is now terminal wholesale rather
    than matched by reason string; transport-level closes (1011, 1001, 1006) stay retryable. A
    failed resume's close code now travels with the rejection via
    CordieriteHandshakeClosedError so it isn't thrown away before reaching onSocketLost.
  • Fix: restoreSession() is now a first-class export, reachable without going through
    installCordieriteDeepLinkBootstrap or the cordieriteClient proxy. An app that drives
    bootstrap itself (custom deep-link routing, QR scanning, a manual connect()) had nothing
    reading the native lease, so every Metro reload dropped a session native could still have
    resumed. Exported from the root and ./noop entries and CordierePublicApi.
  • Breaking: requirePrivateIp is removed; /auto is now the only install path.
    allowPrivateLanOnly was already native build config
    (CordieriteAllowPrivateLanOnly in Info.plist / the Android manifest) enforced by native
    connect() on both platforms — the JS requirePrivateIp option could only narrow what
    native already allowed, so setting it to false without also setting the native key did
    nothing. The deep-link handler now reads allowPrivateLanOnly from the same
    getConstants() path native enforces from, failing closed when it can't be read.
    installCordieriteDeepLinkBootstrap and InstallCordieriteDeepLinkBootstrapOptions are gone;
    require("@cordierite/react-native/auto") is the only way to install the bootstrap listener
    now. See packages/react-native/README.md for the current surface.

v0.6.0

Choose a tag to compare

@V3RON V3RON released this 18 Aug 08:23
  • New: tool call cancellation (tools.cancel RPC, tool_cancel wire frame, AbortSignal in the RN handler context, SIGINT cancels an in-flight cordierite invoke).
  • New: daemon-side event retention and a pull surface (events.since), plus cordierite_events/cordierite_wait_for_event MCP tools.
  • New: minimal "prompt" policy value, gated via MCP anthropic/requiresUserInteraction (fails closed for every other caller).
  • New: cordierite/client programmatic API for test runners (connect, link/waitForSession, app.call, app.events/app.waitForEvent).

See CHANGELOG.md for full details.

v0.5.1

Choose a tag to compare

@V3RON V3RON released this 17 Aug 17:39

What's Changed

  • fix: exclude Cordierite from release builds by default by @V3RON in #5

Full Changelog: v0.5.0...v0.5.1

v0.5.0

Choose a tag to compare

@V3RON V3RON released this 17 Aug 12:33

What's Changed

  • refactor: migrate shared and react-native packages to cordierite scope by @V3RON in #1
  • Improving resilience of the tool by @V3RON in #2
  • feat!: zero-config dev mode and default-inert release builds by @V3RON in #3

New Contributors

  • @V3RON made their first contribution in #1

Full Changelog: https://github.com/callstackincubator/cordierite/commits/v0.5.0