Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/bump-claude-agent-sdk-0.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@herdctl/core": minor
---

Bump `@anthropic-ai/claude-agent-sdk` from `^0.1.0` (resolved 0.1.77) to
`^0.3.0` (resolves 0.3.205).

The pinned `^0.1.0` range could not cross a `0.x` minor, freezing herdctl on a
stale SDK line whose bundled JS harness lacks the current agentic toolset
(`ScheduleWakeup`, `ToolSearch`, `Cron*`, `Monitor`, …). `0.3.x` drops the
bundled harness and instead extracts/runs the native Claude Code binary, which
carries those tools — unblocking cross-turn autonomy (e.g. a persistent
`openSession()` agent scheduling `ScheduleWakeup` and being re-invoked when it
fires).

Adapts the SDK adapter surface to the 0.3.x types:

- `Query.interrupt()` now resolves to an optional interrupt-receipt object
instead of `void`; the streaming `RuntimeSession.interrupt()` awaits and
discards it to keep its fire-and-forget `Promise<void>` contract.
- The SDK's `tool()` handler return type now uses a literal-typed MCP
`CallToolResult`; the injected-MCP adapter casts `InjectedMcpToolDef.handler`
at that boundary so the transport-agnostic tool definition stays SDK-free.

No `@herdctl/core` public API changes. `query()`, `createSdkMcpServer()`,
`tool()`, streaming-input `AsyncIterable<SDKUserMessage>`, and the `Query`
control methods (`interrupt`, `supportedCommands`, `setModel`) all remain
compatible. Note: the SDK peer-depends on `zod@^4`, while core stays on
`zod@^3`; the schemas passed to `tool()` remain structurally compatible and
typecheck/build/tests are green.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"lint": "biome check src/"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
"@anthropic-ai/claude-agent-sdk": "^0.3.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the SDK declares zod as a peer dependency or regular dependency.
# Also check core's zod version.

# Core's zod version
echo "=== Core zod dependency ==="
cat packages/core/package.json | jq '{dependencies: .dependencies.zod, devDependencies: .devDependencies.zod, peerDependencies: .peerDependencies.zod} | del(..|nulls)'

# SDK's zod declaration (if installed)
SDK_PKG="node_modules/@anthropic-ai/claude-agent-sdk/package.json"
if [ -f "$SDK_PKG" ]; then
  echo "=== SDK zod declarations ==="
  cat "$SDK_PKG" | jq '{peerDependencies: .peerDependencies, dependencies: .dependencies} | del(..|nulls)'
else
  echo "=== SDK package.json not found in node_modules — checking lockfile ==="
  if [ -f "package-lock.json" ]; then
    jq '.packages | to_entries[] | select(.key | contains("`@anthropic-ai/claude-agent-sdk`")) | {key: .key, version: .value.version, peerDependencies: .value.peerDependencies, dependencies: .value.dependencies}' package-lock.json 2>/dev/null | head -40
  elif [ -f "pnpm-lock.yaml" ]; then
    echo "pnpm lockfile found — check manually for SDK zod peer deps"
  else
    echo "No lockfile found"
  fi
fi

# Check for multiple zod versions in the dependency tree
echo "=== zod versions in lockfile ==="
if [ -f "package-lock.json" ]; then
  jq -r '.packages | to_entries[] | select(.key | test("zod")) | "\(.key) -> \(.value.version)"' package-lock.json 2>/dev/null | head -20
fi

Repository: edspencer/herdctl

Length of output: 378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo files of interest ==="
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|changeset|CHANGELOG|README\.md)$|claude-agent-sdk|zod' || true

echo
echo "=== references to `@anthropic-ai/claude-agent-sdk` ==="
rg -n '`@anthropic-ai/claude-agent-sdk`|zod@\^4|zod@\^3|peerDependencies|dependencies' packages pnpm-lock.yaml package.json .changeset -g '!**/node_modules/**' || true

echo
echo "=== package manifests mentioning zod ==="
for f in packages/core/package.json package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    jq '{name, dependencies: .dependencies.zod, devDependencies: .devDependencies.zod, peerDependencies: .peerDependencies.zod, packageManager}' "$f" 2>/dev/null || cat "$f"
  fi
done

echo
echo "=== lockfile entries for claude-agent-sdk ==="
python3 - <<'PY'
import yaml, json, os, sys
path = "pnpm-lock.yaml"
if not os.path.exists(path):
    print("pnpm-lock.yaml not found")
    raise SystemExit(0)
try:
    with open(path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
except Exception as e:
    print(f"failed to parse pnpm-lock.yaml: {e}")
    raise SystemExit(0)

for section in ("packages", "snapshots", "importers"):
    obj = data.get(section)
    if isinstance(obj, dict):
        print(f"--- {section} matches ---")
        for k, v in obj.items():
            if "claude-agent-sdk" in str(k) or (isinstance(v, dict) and any("claude-agent-sdk" in str(x) for x in v.keys())):
                print(json.dumps({str(k): v}, indent=2)[:4000])
PY

Repository: edspencer/herdctl

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== pnpm-lock.yaml stanza for claude-agent-sdk ==="
sed -n '352,375p' pnpm-lock.yaml

echo
echo "=== packages/core/package.json ==="
cat -n packages/core/package.json | sed -n '1,80p'

echo
echo "=== packages/core/src/sdk-runtime.ts around tool() ==="
cat -n packages/core/src/sdk-runtime.ts | sed -n '70,120p'

echo
echo "=== changeset note ==="
cat -n .changeset/bump-claude-agent-sdk-0.3.md | sed -n '20,40p'

Repository: edspencer/herdctl

Length of output: 2852


Align zod with the SDK peer range packages/core/package.json:21-28
@anthropic-ai/claude-agent-sdk@0.3.205 requires zod@^4.0.0, but core still provides zod@^3.22.0. This leaves the package outside the SDK’s declared peer range and will trigger peer-dependency warnings; update core to zod 4 or use an SDK release that accepts zod 3.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/package.json` at line 21, The core package is still pinning zod
3 while `@anthropic-ai/claude-agent-sdk` expects zod ^4.0.0, so update the
dependency in packages/core/package.json to a zod 4-compatible version or switch
the SDK version to one that supports zod 3. Make sure the package.json entry for
zod matches the peer range required by the Claude agent SDK and keep the
existing `@anthropic-ai/claude-agent-sdk` dependency aligned with that choice.

"chokidar": "^5",
"cron-parser": "^4.9.0",
"dockerode": "^4.0.9",
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/runner/runtime/sdk-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ function defToSdkMcpServer(def: InjectedMcpServerDef) {
zodShape[key] = jsonPropertyToZod(prop, requiredFields.includes(key));
}

return tool(toolDef.name, toolDef.description, zodShape, toolDef.handler);
// herdctl's McpToolCallResult is structurally an MCP CallToolResult (text
// content), but the SDK types the content `type` as a literal union. Cast the
// handler at this adapter boundary rather than leaking the SDK's MCP types
// into the transport-agnostic InjectedMcpToolDef.
const handler = toolDef.handler as unknown as Parameters<typeof tool>[3];
return tool(toolDef.name, toolDef.description, zodShape, handler);
});

return createSdkMcpServer({
Expand Down Expand Up @@ -177,7 +182,12 @@ export class SDKRuntime implements RuntimeInterface {
send: async (text: string) => {
input.push(toUserMessage(text));
},
interrupt: () => q.interrupt(),
interrupt: async () => {
// The SDK's interrupt() resolves to an optional interrupt-receipt object
// (still-queued message uuids); the RuntimeSession contract is fire-and-
// forget, so discard it to satisfy the Promise<void> return type.
await q.interrupt();
},
listCommands: () => q.supportedCommands(),
setModel: (model?: string) => q.setModel(model),
close: async () => {
Expand Down
Loading
Loading