@odingard/guardian-hook 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,33 @@ Guardian sets modes **per connection**, not per tool. So the hook uses two recor
78
78
 
79
79
  `npx @odingard/guardian-hook setup` prints all of this. To see where a command would be routed, run `npx @odingard/guardian-hook classify "terraform apply"`, which prints `shell_deploy`.
80
80
 
81
+ ## Claude Agent SDK agents (0.2.0)
82
+
83
+ Agents built on Anthropic's Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) can be governed by Guardian in the same way:
84
+
85
+ ```ts
86
+ import { query } from "@anthropic-ai/claude-agent-sdk";
87
+ import { guardianHookMatcher } from "@odingard/guardian-hook/agent-sdk";
88
+
89
+ for await (const message of query({
90
+ prompt: "Run the tests, then deploy staging",
91
+ options: { hooks: { PreToolUse: [guardianHookMatcher()] } },
92
+ })) {
93
+ if (message.type === "result") console.log(message.subtype);
94
+ }
95
+ ```
96
+
97
+ It uses the same environment variables, the same two connections (the console's "Set up for Claude Code" creates them) and the same decisions as the Claude Code hook. Approvers see the agent as "Claude agent" unless you pass `agentName`.
98
+
99
+ `guardianHookMatcher()` registers the hook with:
100
+
101
+ - `onFailure: "block"`, so a crash blocks the call instead of letting it through (Agent SDK 0.3.317 or later);
102
+ - a timeout longer than Guardian's 5-minute approval window.
103
+
104
+ A `PreToolUse` hook runs for every tool call, even calls an allow rule or `bypassPermissions` would approve, and its deny holds in every permission mode.
105
+
106
+ A runnable example is in `examples/claude-agent-sdk.mjs`.
107
+
81
108
  ## Limits (read these)
82
109
 
83
110
  - **Decision-gated.** The agent performs the effect after Guardian authorizes and redeems the call. The receipt proves Guardian's authorization of that exact call, not the command's own outcome.
package/agent-sdk.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ // Types for "@odingard/guardian-hook/agent-sdk" (see src/agent-sdk.ts).
2
+ type Env = Readonly<Record<string, string | undefined>>;
3
+
4
+ export type GuardianHookInput = Readonly<{
5
+ hook_event_name?: string;
6
+ tool_name?: unknown;
7
+ tool_input?: unknown;
8
+ cwd?: unknown;
9
+ }>;
10
+
11
+ export type GuardianHookOutput =
12
+ | Record<string, never>
13
+ | {
14
+ hookSpecificOutput: {
15
+ hookEventName: "PreToolUse";
16
+ permissionDecision: "allow" | "deny";
17
+ permissionDecisionReason: string;
18
+ };
19
+ };
20
+
21
+ export type GuardianPreToolUseOptions = Readonly<{
22
+ env?: Env;
23
+ agentName?: string;
24
+ log?: (line: string) => void;
25
+ }>;
26
+
27
+ export declare function guardianPreToolUse(
28
+ options?: GuardianPreToolUseOptions,
29
+ ): (input: GuardianHookInput) => Promise<GuardianHookOutput>;
30
+
31
+ export declare const GUARDIAN_HOOK_TIMEOUT_SECONDS: number;
32
+
33
+ export declare function guardianHookMatcher(
34
+ options?: GuardianPreToolUseOptions,
35
+ ): {
36
+ hooks: ((input: GuardianHookInput) => Promise<GuardianHookOutput>)[];
37
+ timeout: number;
38
+ onFailure: "block";
39
+ };
@@ -0,0 +1,369 @@
1
+ // src/run.ts
2
+ import { createGuardianClient } from "@odingard/guardian-sdk";
3
+
4
+ // src/classify.ts
5
+ import { createHash } from "node:crypto";
6
+ var HOOK_TOOLS = [
7
+ {
8
+ name: "shell",
9
+ suggestedMode: "allow",
10
+ description: "Ordinary shell commands (build, test, read-only inspection)."
11
+ },
12
+ {
13
+ name: "shell_deploy",
14
+ suggestedMode: "ask",
15
+ description: "Infrastructure and deploy commands: terraform/tofu apply or destroy, pulumi up, kubectl apply/delete, helm install/upgrade, cdk/serverless/fly/vercel deploys, cloud deploy CLIs."
16
+ },
17
+ {
18
+ name: "shell_destructive",
19
+ suggestedMode: "ask",
20
+ description: "Destructive commands: rm -rf, git reset --hard, git clean, force pushes, dropping databases, disk formatting."
21
+ },
22
+ {
23
+ name: "git_push",
24
+ suggestedMode: "ask",
25
+ description: "Pushing commits to a remote."
26
+ },
27
+ {
28
+ name: "file_write",
29
+ suggestedMode: "allow",
30
+ description: "Creating or editing files in the workspace."
31
+ },
32
+ {
33
+ name: "web_fetch",
34
+ suggestedMode: "allow",
35
+ description: "Fetching a URL."
36
+ }
37
+ ];
38
+ var MAX_COMMAND = 2e3;
39
+ var DEPLOY = [
40
+ /\b(terraform|tofu)\b.*\s(apply|destroy|import|state\s+(rm|mv|push))\b/,
41
+ /\bpulumi\s+(up|destroy|update)\b/,
42
+ /\bkubectl\b.*\s(apply|delete|create|replace|patch|scale|rollout|drain|cordon)\b/,
43
+ /\bhelm\s+(install|upgrade|uninstall|rollback|delete)\b/,
44
+ /\b(cdk|serverless|sls|sam)\s+(deploy|destroy|remove)\b/,
45
+ /\bfly(ctl)?\s+(deploy|destroy|apps\s+destroy|scale)\b/,
46
+ /\bvercel\b.*(--prod\b|\bdeploy\b|\bpromote\b|\bremove\b)/,
47
+ /\bnetlify\s+deploy\b.*--prod\b/,
48
+ /\bgcloud\s+.*\b(deploy|delete)\b/,
49
+ /\baws\s+.*\b(deploy|delete-stack|create-stack|update-stack|terminate-instances|delete-db-instance)\b/,
50
+ /\baz\s+.*\b(deploy|delete|create)\b/,
51
+ /\bwrangler\s+(deploy|publish|delete)\b/,
52
+ /\bdocker\s+(push|compose\s+.*\bup\b.*--remote)\b/,
53
+ /\b(npm|pnpm|yarn)\s+publish\b/,
54
+ /\btwine\s+upload\b/,
55
+ /\bgh\s+(release\s+create|workflow\s+run|pr\s+merge)\b/
56
+ ];
57
+ var DESTRUCTIVE = [
58
+ /\brm\s+(?:-\S*[rR]\S*\s+(?:-\S+\s+)*|-\S*f\S*\s+-\S*[rR])/,
59
+ /\bgit\s+reset\s+.*--hard\b/,
60
+ /\bgit\s+clean\s+-\S*f/,
61
+ /\bgit\s+push\b.*(\s--force\b|\s-f\b|--force-with-lease|\s\+\S)/,
62
+ /\bgit\s+(branch\s+-D|checkout\s+--\s|restore\s+--source)\b/,
63
+ /\b(drop|truncate)\s+(table|database|schema)\b/i,
64
+ /\bdelete\s+from\s+\w+\s*(;|$)/i,
65
+ /\b(mkfs|dd\s+if=|shred|wipefs)\b/,
66
+ /\bchmod\s+-R\s+777\b/,
67
+ /\bfind\b.*\s-delete\b/
68
+ ];
69
+ var GIT_PUSH = /\bgit\s+push\b/;
70
+ function classifyCommand(command) {
71
+ if (DESTRUCTIVE.some((re) => re.test(command))) return "shell_destructive";
72
+ if (DEPLOY.some((re) => re.test(command))) return "shell_deploy";
73
+ if (GIT_PUSH.test(command)) return "git_push";
74
+ return "shell";
75
+ }
76
+ function mcpToolName(server, tool) {
77
+ const part = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^(\d)/, "x$1") || "x";
78
+ return `mcp.${part(server)}.${part(tool)}`.slice(0, 64).replace(/[._-]+$/, "");
79
+ }
80
+ function bounded(text, max = MAX_COMMAND) {
81
+ return text.length <= max ? text : `${text.slice(0, max)}\u2026 [${text.length - max} more chars]`;
82
+ }
83
+ function digest(text) {
84
+ return `sha256:${createHash("sha256").update(text).digest("hex")}`;
85
+ }
86
+ function str(value) {
87
+ return typeof value === "string" ? value : "";
88
+ }
89
+ function governedCall(call) {
90
+ const { tool, input } = call;
91
+ const cwd = call.cwd ? { cwd: call.cwd } : {};
92
+ if (tool === "Bash" || tool === "shell") {
93
+ const command = str(input.command);
94
+ const toolName = classifyCommand(command);
95
+ return {
96
+ toolName,
97
+ arguments: { command: bounded(command), ...cwd },
98
+ summary: `Run: ${bounded(command, 160)}`
99
+ };
100
+ }
101
+ if (["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(tool)) {
102
+ const path = str(input.file_path) || str(input.notebook_path);
103
+ const content = str(input.content) || str(input.new_string) || str(input.new_source) || (Array.isArray(input.edits) ? JSON.stringify(input.edits) : "");
104
+ return {
105
+ toolName: "file_write",
106
+ arguments: {
107
+ operation: tool,
108
+ path,
109
+ contentBytes: Buffer.byteLength(content),
110
+ contentDigest: digest(content),
111
+ ...cwd
112
+ },
113
+ summary: `${tool === "Write" ? "Write" : "Edit"} ${path}`
114
+ };
115
+ }
116
+ if (tool === "WebFetch") {
117
+ const url = str(input.url);
118
+ return {
119
+ toolName: "web_fetch",
120
+ arguments: { url: bounded(url, 500) },
121
+ summary: `Fetch ${bounded(url, 160)}`
122
+ };
123
+ }
124
+ const mcp = /^mcp__(.+?)__(.+)$/.exec(tool);
125
+ if (mcp) {
126
+ const [, server, name] = mcp;
127
+ const json = JSON.stringify(input);
128
+ return {
129
+ toolName: mcpToolName(server, name),
130
+ arguments: json.length <= MAX_COMMAND ? { server, tool: name, input } : {
131
+ server,
132
+ tool: name,
133
+ inputBytes: json.length,
134
+ inputDigest: digest(json)
135
+ },
136
+ summary: `MCP ${server} \u2192 ${name}`
137
+ };
138
+ }
139
+ return null;
140
+ }
141
+ function tierOf(toolName) {
142
+ const known = HOOK_TOOLS.find((t) => t.name === toolName);
143
+ return known ? known.suggestedMode : "ask";
144
+ }
145
+
146
+ // src/decide.ts
147
+ var PLAIN = {
148
+ NOT_AUTHORIZED: "this agent is not authorized for it",
149
+ APPROVAL_DENIED: "the approver declined it",
150
+ APPROVAL_NOT_SATISFIED: "the approver declined it",
151
+ APPROVAL_EXPIRED: "the approval expired",
152
+ UNAUTHENTICATED: "the Guardian agent credential was rejected",
153
+ INVALID_REQUEST: `this tool is not allowed for the agent yet. In the Guardian console: Agents \u2192 this agent \u2192 the connection's card \u2192 "Allow a tool by name"`,
154
+ SERVICE_UNAVAILABLE: "Guardian could not be reached",
155
+ REQUEST_CONFLICT: "Guardian saw a conflicting request"
156
+ };
157
+ function plain(code) {
158
+ return PLAIN[code] ?? code;
159
+ }
160
+ function denied(call, code, extra = {}) {
161
+ return {
162
+ decision: "deny",
163
+ reason: `Guardian blocked ${call.toolName} (${code}): ${plain(code)}. Do not retry or work around this; ask the developer to review it in the Guardian console.`,
164
+ ...extra
165
+ };
166
+ }
167
+ var isPending = (r) => !r.ok && (r.reasonCode === "APPROVAL_REQUIRED" || r.reasonCode === "APPROVAL_PENDING") || r.ok && r.decision === "ASK";
168
+ async function decide(call, options) {
169
+ const wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
170
+ const now = options.now ?? Date.now;
171
+ const log = options.log ?? (() => {
172
+ });
173
+ let result;
174
+ try {
175
+ result = await options.client.request({
176
+ action: "tool.invoke",
177
+ connectionId: options.connectionId,
178
+ toolName: call.toolName,
179
+ arguments: call.arguments,
180
+ purpose: `${options.agentName}: ${call.summary}`,
181
+ ...options.requestTtlMs !== void 0 ? { ttlMs: options.requestTtlMs } : {}
182
+ });
183
+ } catch {
184
+ return denied(call, "SERVICE_UNAVAILABLE");
185
+ }
186
+ const requestId = result.requestId;
187
+ let approvedByHuman = false;
188
+ if (isPending(result)) {
189
+ if (!requestId) return denied(call, "MISSING_REQUEST_ID");
190
+ log(
191
+ `Guardian: "${call.summary}" is held for approval (request ${requestId}). Approve it on your phone or in the console.`
192
+ );
193
+ const deadline = now() + options.approvalTimeoutMs;
194
+ const maxInterval = Math.max(
195
+ options.maxPollIntervalMs ?? 1e4,
196
+ options.pollIntervalMs
197
+ );
198
+ let interval = options.pollIntervalMs;
199
+ for (; ; ) {
200
+ await wait(interval);
201
+ interval = Math.min(Math.ceil(interval * 1.5), maxInterval);
202
+ let claimed;
203
+ try {
204
+ claimed = await options.client.claim(requestId);
205
+ } catch {
206
+ claimed = { ok: false, reasonCode: "APPROVAL_REQUIRED", requestId };
207
+ }
208
+ if (!isPending(claimed)) {
209
+ result = claimed;
210
+ approvedByHuman = claimed.ok && claimed.decision === "ALLOW";
211
+ break;
212
+ }
213
+ if (now() >= deadline) {
214
+ return {
215
+ decision: "deny",
216
+ reason: `Guardian is still waiting for a human to approve ${call.toolName} (request ${requestId}). Nothing ran. Tell the developer it needs approval in the Guardian app, then try again.`,
217
+ requestId,
218
+ guardianDecision: "ASK"
219
+ };
220
+ }
221
+ }
222
+ }
223
+ if (!result.ok || result.decision !== "ALLOW" || !result.capabilityToken) {
224
+ const code = result.ok ? result.reasonCode ?? (result.decision === "DENY" ? "DENIED" : "MISSING_CAPABILITY") : result.reasonCode;
225
+ return denied(call, code, {
226
+ ...requestId ? { requestId } : {},
227
+ guardianDecision: "DENY"
228
+ });
229
+ }
230
+ let executed;
231
+ try {
232
+ executed = await options.client.execute({
233
+ requestId: result.requestId,
234
+ capabilityToken: result.capabilityToken
235
+ });
236
+ } catch {
237
+ executed = { ok: false, reasonCode: "SERVICE_UNAVAILABLE" };
238
+ }
239
+ if (!executed.ok) {
240
+ return denied(call, executed.reasonCode, {
241
+ requestId: result.requestId,
242
+ guardianDecision: approvedByHuman ? "ASK" : "ALLOW",
243
+ approvedByHuman
244
+ });
245
+ }
246
+ return {
247
+ decision: "allow",
248
+ reason: approvedByHuman ? `Approved by a human in Guardian (request ${result.requestId}).` : `Allowed by Guardian policy (request ${result.requestId}).`,
249
+ requestId: result.requestId,
250
+ guardianDecision: approvedByHuman ? "ASK" : "ALLOW",
251
+ approvedByHuman
252
+ };
253
+ }
254
+
255
+ // src/run.ts
256
+ var DEFAULT_BASE_URL = "https://guardian-api.odingard.com";
257
+ var REQUEST_TTL_MS = 3e5;
258
+ var MAX_WAIT_MS = REQUEST_TTL_MS;
259
+ function claudeCodeOutput(verdict) {
260
+ return JSON.stringify({
261
+ hookSpecificOutput: {
262
+ hookEventName: "PreToolUse",
263
+ permissionDecision: verdict.decision,
264
+ permissionDecisionReason: verdict.reason
265
+ }
266
+ });
267
+ }
268
+ function connectionFor(toolName, env) {
269
+ const allow = env.GUARDIAN_CONNECTION_ID?.trim() || void 0;
270
+ if (tierOf(toolName) === "allow") return allow;
271
+ return env.GUARDIAN_HOOK_ASK_CONNECTION_ID?.trim() || allow;
272
+ }
273
+ async function runHook(stdin, env, deps = {}) {
274
+ const failClosed = (reason) => claudeCodeOutput({ decision: "deny", reason: `Guardian hook: ${reason}` });
275
+ let input;
276
+ try {
277
+ input = JSON.parse(stdin);
278
+ } catch {
279
+ return failClosed(
280
+ "could not read the tool call from the agent. Nothing ran."
281
+ );
282
+ }
283
+ if (typeof input.tool_name !== "string") {
284
+ return failClosed("the tool call had no tool name. Nothing ran.");
285
+ }
286
+ const call = governedCall({
287
+ tool: input.tool_name,
288
+ input: input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {},
289
+ ...typeof input.cwd === "string" ? { cwd: input.cwd } : {}
290
+ });
291
+ if (call === null) return "";
292
+ const connectionId = connectionFor(call.toolName, env);
293
+ const credential = env.GUARDIAN_AGENT_CREDENTIAL?.trim();
294
+ const baseUrl = (env.GUARDIAN_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(
295
+ /\/+$/,
296
+ ""
297
+ );
298
+ if (!deps.client && (!credential || !connectionId)) {
299
+ return failClosed(
300
+ "GUARDIAN_AGENT_CREDENTIAL and GUARDIAN_CONNECTION_ID must be set (run `npx @odingard/guardian-hook setup`). Nothing ran."
301
+ );
302
+ }
303
+ if (!deps.client && !baseUrl.startsWith("https://")) {
304
+ return failClosed("GUARDIAN_BASE_URL must be https. Nothing ran.");
305
+ }
306
+ const requested = Number(
307
+ env.GUARDIAN_HOOK_APPROVAL_TIMEOUT_MS ?? MAX_WAIT_MS
308
+ );
309
+ const approvalTimeoutMs = Number.isFinite(requested) ? Math.min(Math.max(requested, 0), MAX_WAIT_MS) : MAX_WAIT_MS;
310
+ const verdict = await decide(call, {
311
+ client: deps.client ?? createGuardianClient({
312
+ baseUrl,
313
+ credential,
314
+ ...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {}
315
+ }),
316
+ connectionId: connectionId ?? "",
317
+ agentName: env.GUARDIAN_HOOK_AGENT_NAME?.trim() || "Claude Code",
318
+ approvalTimeoutMs,
319
+ requestTtlMs: REQUEST_TTL_MS,
320
+ pollIntervalMs: 2e3,
321
+ ...deps.wait ? { wait: deps.wait } : {},
322
+ ...deps.now ? { now: deps.now } : {},
323
+ ...deps.log ? { log: deps.log } : {}
324
+ });
325
+ return claudeCodeOutput(verdict);
326
+ }
327
+
328
+ // src/agent-sdk.ts
329
+ function guardianPreToolUse(options = {}) {
330
+ return async (input) => {
331
+ if (input.hook_event_name && input.hook_event_name !== "PreToolUse") {
332
+ return {};
333
+ }
334
+ const base = options.env ?? process.env;
335
+ const env = {
336
+ ...base,
337
+ GUARDIAN_HOOK_AGENT_NAME: options.agentName ?? base.GUARDIAN_HOOK_AGENT_NAME ?? "Claude agent"
338
+ };
339
+ const output = await runHook(
340
+ JSON.stringify({
341
+ tool_name: input.tool_name,
342
+ tool_input: input.tool_input,
343
+ cwd: input.cwd
344
+ }),
345
+ env,
346
+ {
347
+ log: options.log ?? ((line) => process.stderr.write(`${line}
348
+ `)),
349
+ ...options.client ? { client: options.client } : {},
350
+ ...options.wait ? { wait: options.wait } : {},
351
+ ...options.now ? { now: options.now } : {}
352
+ }
353
+ );
354
+ return output ? JSON.parse(output) : {};
355
+ };
356
+ }
357
+ var GUARDIAN_HOOK_TIMEOUT_SECONDS = Math.ceil(MAX_WAIT_MS / 1e3) + 30;
358
+ function guardianHookMatcher(options = {}) {
359
+ return {
360
+ hooks: [guardianPreToolUse(options)],
361
+ timeout: GUARDIAN_HOOK_TIMEOUT_SECONDS,
362
+ onFailure: "block"
363
+ };
364
+ }
365
+ export {
366
+ GUARDIAN_HOOK_TIMEOUT_SECONDS,
367
+ guardianHookMatcher,
368
+ guardianPreToolUse
369
+ };
package/dist/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import { createGuardianClient } from "@odingard/guardian-sdk";
5
-
6
3
  // src/classify.ts
7
4
  import { createHash } from "node:crypto";
8
5
  var HOOK_TOOLS = [
@@ -145,6 +142,9 @@ function tierOf(toolName) {
145
142
  return known ? known.suggestedMode : "ask";
146
143
  }
147
144
 
145
+ // src/run.ts
146
+ import { createGuardianClient } from "@odingard/guardian-sdk";
147
+
148
148
  // src/decide.ts
149
149
  var PLAIN = {
150
150
  NOT_AUTHORIZED: "this agent is not authorized for it",
@@ -254,7 +254,7 @@ async function decide(call, options) {
254
254
  };
255
255
  }
256
256
 
257
- // src/index.ts
257
+ // src/run.ts
258
258
  var DEFAULT_BASE_URL = "https://guardian-api.odingard.com";
259
259
  var REQUEST_TTL_MS = 3e5;
260
260
  var MAX_WAIT_MS = REQUEST_TTL_MS;
@@ -370,6 +370,8 @@ async function runHook(stdin, env, deps = {}) {
370
370
  });
371
371
  return claudeCodeOutput(verdict);
372
372
  }
373
+
374
+ // src/index.ts
373
375
  async function readStdin() {
374
376
  const chunks = [];
375
377
  for await (const chunk of process.stdin) chunks.push(chunk);
package/package.json CHANGED
@@ -1,22 +1,31 @@
1
1
  {
2
2
  "name": "@odingard/guardian-hook",
3
- "version": "0.1.1",
4
- "description": "Guardian for coding agents: a Claude Code PreToolUse hook that sends shell commands, file writes and MCP calls to Guardian for a deterministic ALLOW/ASK/DENY decision. Deploys and destructive commands wait for a human approval on your phone; every allowed call carries a verifiable receipt.",
3
+ "version": "0.2.0",
4
+ "description": "Guardian for coding agents: a Claude Code PreToolUse hook (and a PreToolUse hook for Claude Agent SDK agents) that sends shell commands, file writes and MCP calls to Guardian for a deterministic ALLOW/ASK/DENY decision. Deploys and destructive commands wait for a human approval on your phone; every allowed call carries a verifiable receipt.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./agent-sdk": {
11
+ "types": "./agent-sdk.d.ts",
12
+ "default": "./dist/agent-sdk.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
8
16
  "bin": {
9
17
  "guardian-hook": "dist/index.js"
10
18
  },
11
19
  "files": [
12
20
  "dist",
21
+ "agent-sdk.d.ts",
13
22
  "README.md"
14
23
  ],
15
24
  "engines": {
16
25
  "node": ">=22"
17
26
  },
18
27
  "scripts": {
19
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --external:@odingard/guardian-sdk --outfile=dist/index.js",
28
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --external:@odingard/guardian-sdk --outfile=dist/index.js && esbuild src/agent-sdk.ts --bundle --platform=node --format=esm --target=node22 --external:@odingard/guardian-sdk --outfile=dist/agent-sdk.js",
20
29
  "test": "vitest run",
21
30
  "typecheck": "tsc -p tsconfig.json",
22
31
  "prepublishOnly": "npm run build && npm test"