llms-ink 0.6.0 → 0.7.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
@@ -301,3 +301,32 @@ Requests are limited to 128 KiB after JSON encoding and responses to 16 KiB. The
301
301
  For JSON, serialize it yourself and pass the resulting string to `inspect()`. The SDK sends `format: "text"`, so its digest covers those exact bytes; it does not promise cross-language JSON canonicalization. See [pilot limits and privacy](https://llms.ink/inspect/).
302
302
 
303
303
  JavaScript uses a 65-second total request deadline; pass `{ signal }` to cancel earlier.
304
+
305
+ ## Claude conversation guard (experimental, Node / POSIX)
306
+
307
+ `llms-ink/guard/claude` binds one Claude Agent SDK conversation to one authenticated local Guard session. Install the optional peer separately:
308
+
309
+ ```sh
310
+ npm install llms-ink@0.7.0 @anthropic-ai/claude-agent-sdk@0.3.319
311
+ ```
312
+
313
+ First [start the Python 0.10.0 local service](https://llms.ink/guard/service/) with a reviewed gateway configuration and existing Claude authentication. Then try the bundled terminal host:
314
+
315
+ ```sh
316
+ node node_modules/llms-ink/examples/guard-claude.mjs \
317
+ --owner-file /private/guard/owner.json \
318
+ --python /absolute/path/to/.venv/bin/python \
319
+ --cwd /absolute/path/to/workspace
320
+ ```
321
+
322
+ It requires an interactive terminal and displays the complete review for each permitted action. Type `approve` to send that call once; Enter declines. `/exit`, terminal close and handled termination revoke the session. The example escapes terminal control characters rather than interpreting them. `--model` and `--claude-executable` are optional.
323
+
324
+ For application hosts, import `createGuardedClaude` and pass absolute `ownerFile`, `python` and `cwd` paths plus `review: async ({message, signal}) => boolean`. Render `message` completely as plain text; abort the UI on `signal`. Only a fresh literal `true` accepts. The returned object exposes the non-secret `sessionId`, sequential `run(prompt, onMessage?)` and `close()`. Always await close in a finally block. Create a new instance for each independent conversation; there is no resume/fork interface. The owner file never enters the SDK's MCP configuration.
325
+
326
+ The default fixed session TTL is 900 seconds (`ttl`, maximum 86400), with no renewal. Each turn has a 120000 ms deadline (`turnTimeoutMs`, maximum 600000). Reviews stop after 55 seconds. The adapter configures no native tools, other MCP connections, plugins or local hooks. SDK `maxTurns` defaults to 12 and `maxBudgetUsd` to 1; SDK cost estimates are not a hard billing limit. Optional `model` and `claudeExecutable` select the host model/binary.
327
+
328
+ `GuardHostError` reports incomplete cleanup with `sessionId` and `credentialDirectory`. If revocation cannot be confirmed, the private credential file remains for recovery; retry `close()` or revoke through the owner CLI. A host process killed without cleanup can leave authority until the service's TTL. The owner process, SDK and approval UI are trusted; private files do not isolate malicious same-user code. This is not a process sandbox, exposure ledger or automatic desktop plugin. Compaction does not establish clean context.
329
+
330
+ Allowed results go to Claude using the host's authentication, after local inspection. The adapter does not call hosted Inspect. Other SDK entry points do not import the optional Claude dependency. [Guide](https://llms.ink/guard/claude/) · [Runnable example](examples/guard-claude.mjs).
331
+
332
+ Lifecycle tests use real service sockets and subprocess effects with a simulated SDK loop. Run them with `LLMS_INK_GUARD_TEST_PYTHON=/absolute/python npm run test:guard` after installing Python `llms-ink==0.10.0`. The separately opted-in `test:guard:live` requires `LLMS_INK_CLAUDE_LIVE=1`, existing Claude authentication and the optional local model when `LLMS_INK_HOST_REAL_MODEL=1`. Scripted fixture approvals are interoperability evidence, not independent human review or proof of attack prevention.
@@ -0,0 +1,44 @@
1
+ import { type SDKMessage, type SDKResultMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ export interface GuardReview {
3
+ /** Complete guard-authored review, including command, arguments and policy/definition hashes. Render as plain text. */
4
+ message: string;
5
+ /** Stop displaying the form when this signal aborts. Never reuse a decision. */
6
+ signal: AbortSignal;
7
+ }
8
+ export interface GuardedClaudeOptions {
9
+ /** Private owner credential. This path is used by the control process, never sent to Claude. */
10
+ ownerFile: string;
11
+ /** Absolute Python interpreter with llms-ink[guard] 0.10.0 installed. */
12
+ python: string;
13
+ /** Called for each fresh approval. Only literal true accepts. Errors, cancellation and timeout decline. */
14
+ review: (request: GuardReview) => Promise<boolean>;
15
+ cwd: string;
16
+ model?: string;
17
+ /** Optional absolute path to a supported Claude Code executable. */
18
+ claudeExecutable?: string;
19
+ /** Fixed lifetime, 1–86400 seconds. Default 900. No automatic renewal. */
20
+ ttl?: number;
21
+ /** Per-turn deadline, including reviews. Default 120000, maximum 600000. */
22
+ turnTimeoutMs?: number;
23
+ maxTurns?: number;
24
+ maxBudgetUsd?: number;
25
+ }
26
+ export interface GuardedClaudeConversation {
27
+ /** Non-secret guard session ID, distinct from Claude's conversation UUID. */
28
+ readonly sessionId: string;
29
+ /** Run one turn. Calls must be sequential; callbacks receive host output, never owner credentials. */
30
+ run(prompt: string, onMessage?: (message: SDKMessage) => void | Promise<void>): Promise<SDKResultMessage>;
31
+ /** Revoke authority and close the host. Idempotent after confirmed cleanup; retry on cleanup errors. */
32
+ close(): Promise<void>;
33
+ }
34
+ export declare class GuardHostError extends Error {
35
+ readonly sessionId: string | undefined;
36
+ readonly credentialDirectory: string | undefined;
37
+ constructor(message: string, sessionId?: string, credentialDirectory?: string);
38
+ }
39
+ /**
40
+ * Create one non-resumable, tool-scoped conversation. New conversations require new instances.
41
+ * The owner process, SDK and review callback are trusted. This is not an OS sandbox.
42
+ * A process crash leaves authority bounded by the fixed TTL; there is no crash-proof lease or exposure ledger.
43
+ */
44
+ export declare function createGuardedClaude(options: GuardedClaudeOptions): Promise<GuardedClaudeConversation>;
@@ -0,0 +1,280 @@
1
+ /** Node/POSIX host integration for explicitly scoped Claude Agent SDK conversations. */
2
+ import { execFile } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { mkdtemp, chmod, lstat, readFile, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { isAbsolute, join } from 'node:path';
7
+ import { query } from '@anthropic-ai/claude-agent-sdk';
8
+ export class GuardHostError extends Error {
9
+ sessionId;
10
+ credentialDirectory;
11
+ constructor(message, sessionId, credentialDirectory) {
12
+ super(message);
13
+ this.name = 'GuardHostError';
14
+ this.sessionId = sessionId;
15
+ this.credentialDirectory = credentialDirectory;
16
+ }
17
+ }
18
+ const idPattern = /^[a-f0-9]{32}$/;
19
+ const reviewSchema = { type: 'object', properties: { approve: { type: 'boolean', title: 'Approve this call once', default: false } }, required: ['approve'] };
20
+ function sameShape(a, b) {
21
+ if (a === b)
22
+ return true;
23
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b))
24
+ return false;
25
+ const ak = Object.keys(a), bk = Object.keys(b);
26
+ return ak.length === bk.length && ak.every(k => Object.hasOwn(b, k) && sameShape(a[k], b[k]));
27
+ }
28
+ function control(python, args) {
29
+ return new Promise((resolve, reject) => execFile(python, ['-m', 'llms_ink.guard', ...args], { timeout: 30000, maxBuffer: 16384, encoding: 'utf8' }, (error, stdout) => {
30
+ if (error) {
31
+ reject(new GuardHostError('Guard control did not complete.'));
32
+ return;
33
+ }
34
+ try {
35
+ const value = JSON.parse(stdout);
36
+ if (!value || typeof value !== 'object' || Array.isArray(value))
37
+ throw new Error();
38
+ resolve(value);
39
+ }
40
+ catch {
41
+ reject(new GuardHostError('Guard control returned an invalid response.'));
42
+ }
43
+ }));
44
+ }
45
+ class Input {
46
+ pending;
47
+ wake;
48
+ ended = false;
49
+ send(prompt, session) {
50
+ if (this.ended || this.pending)
51
+ throw new GuardHostError('Conversation input is unavailable.');
52
+ this.pending = { type: 'user', session_id: session, message: { role: 'user', content: prompt }, parent_tool_use_id: null };
53
+ this.wake?.();
54
+ }
55
+ close() { this.ended = true; this.wake?.(); }
56
+ async *[Symbol.asyncIterator]() {
57
+ while (!this.ended) {
58
+ if (!this.pending)
59
+ await new Promise(resolve => { this.wake = resolve; });
60
+ this.wake = undefined;
61
+ if (this.ended)
62
+ return;
63
+ const next = this.pending;
64
+ this.pending = undefined;
65
+ if (next)
66
+ yield next;
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Create one non-resumable, tool-scoped conversation. New conversations require new instances.
72
+ * The owner process, SDK and review callback are trusted. This is not an OS sandbox.
73
+ * A process crash leaves authority bounded by the fixed TTL; there is no crash-proof lease or exposure ledger.
74
+ */
75
+ export async function createGuardedClaude(options) {
76
+ options = { ...options };
77
+ if (process.platform === 'win32')
78
+ throw new GuardHostError('Guard sessions require macOS or Linux.');
79
+ const { python, ownerFile, review, cwd, claudeExecutable } = options;
80
+ const ttl = options.ttl ?? 900, timeout = options.turnTimeoutMs ?? 120000;
81
+ if (!isAbsolute(python) || !isAbsolute(ownerFile) || !isAbsolute(cwd) || (claudeExecutable && !isAbsolute(claudeExecutable)) || typeof review !== 'function'
82
+ || !Number.isInteger(ttl) || ttl < 1 || ttl > 86400 || !Number.isInteger(timeout) || timeout < 1 || timeout > 600000
83
+ || (options.maxTurns !== undefined && (!Number.isInteger(options.maxTurns) || options.maxTurns < 1 || options.maxTurns > 100))
84
+ || (options.maxBudgetUsd !== undefined && (!Number.isFinite(options.maxBudgetUsd) || options.maxBudgetUsd <= 0)))
85
+ throw new GuardHostError('Absolute paths, a review callback and bounded lifetimes are required.');
86
+ const directory = await mkdtemp(join(tmpdir(), 'ink-host-'));
87
+ await chmod(directory, 0o700);
88
+ const file = join(directory, 'session.json');
89
+ let sessionId;
90
+ async function revoke() {
91
+ if (!sessionId)
92
+ return;
93
+ try {
94
+ const result = await control(python, ['session-revoke', '--owner-file', ownerFile, '--session-id', sessionId]);
95
+ if (result.ok !== true)
96
+ throw new Error();
97
+ }
98
+ catch {
99
+ try {
100
+ const state = await control(python, ['service-status', '--owner-file', ownerFile]);
101
+ if (state.ok !== true || !Array.isArray(state.sessions) || state.sessions.some(s => !s || typeof s !== 'object' || typeof s.sessionId !== 'string' || (s.sessionId === sessionId && s.active !== false)))
102
+ throw new Error();
103
+ }
104
+ catch {
105
+ throw new GuardHostError('Session revocation could not be confirmed. Keep the credential directory and retry close or revoke it with the owner CLI.', sessionId, directory);
106
+ }
107
+ }
108
+ }
109
+ try {
110
+ const result = await control(python, ['session-create', '--owner-file', ownerFile, '--output', file, '--ttl', String(ttl)]);
111
+ if (typeof result.sessionId !== 'string' || !idPattern.test(result.sessionId))
112
+ throw new Error();
113
+ sessionId = result.sessionId;
114
+ const info = await lstat(file);
115
+ if (!info.isFile() || info.isSymbolicLink() || (info.mode & 0o077) !== 0 || info.uid !== process.getuid?.())
116
+ throw new Error();
117
+ }
118
+ catch {
119
+ // A terminated CLI may have written a capability before printing its ID.
120
+ if (!sessionId) {
121
+ try {
122
+ const saved = JSON.parse(await readFile(file, 'utf8'));
123
+ if (typeof saved.sessionId === 'string' && idPattern.test(saved.sessionId))
124
+ sessionId = saved.sessionId;
125
+ }
126
+ catch { /* no recoverable credential */ }
127
+ }
128
+ await revoke();
129
+ await rm(directory, { recursive: true, force: true });
130
+ throw new GuardHostError('Could not create a guarded conversation.');
131
+ }
132
+ const guardId = sessionId;
133
+ const hostId = randomUUID(), input = new Input(), abort = new AbortController();
134
+ let closing = false, closed = false, busy = false, initialized = false, closeWork;
135
+ let host;
136
+ let expiresTimer;
137
+ async function whileOpen(work) {
138
+ if (abort.signal.aborted)
139
+ throw new GuardHostError('Conversation closed.');
140
+ let cancel;
141
+ try {
142
+ return await Promise.race([work, new Promise((_, reject) => {
143
+ cancel = () => reject(new GuardHostError('Conversation closed.'));
144
+ abort.signal.addEventListener('abort', cancel, { once: true });
145
+ })]);
146
+ }
147
+ finally {
148
+ if (cancel)
149
+ abort.signal.removeEventListener('abort', cancel);
150
+ }
151
+ }
152
+ async function close() {
153
+ if (closed)
154
+ return;
155
+ if (closeWork)
156
+ return closeWork;
157
+ closing = true;
158
+ input.close();
159
+ abort.abort();
160
+ clearTimeout(expiresTimer);
161
+ closeWork = (async () => {
162
+ let failure;
163
+ try {
164
+ await revoke();
165
+ }
166
+ catch (error) {
167
+ failure = error;
168
+ }
169
+ try {
170
+ host?.close();
171
+ }
172
+ catch {
173
+ failure ??= new GuardHostError('Claude host cleanup did not complete. Retry close.', guardId, directory);
174
+ }
175
+ if (failure)
176
+ throw failure;
177
+ await rm(directory, { recursive: true, force: true });
178
+ closed = true;
179
+ })();
180
+ try {
181
+ await closeWork;
182
+ }
183
+ finally {
184
+ closeWork = undefined;
185
+ }
186
+ }
187
+ async function elicit(request, context) {
188
+ if (closing || !busy || context.signal.aborted || request.serverName !== 'guarded' || (request.mode ?? 'form') !== 'form'
189
+ || typeof request.message !== 'string' || Buffer.byteLength(request.message) > 8192 || !sameShape(request.requestedSchema, reviewSchema))
190
+ return { action: 'decline' };
191
+ const lifetime = new AbortController();
192
+ const cancel = () => lifetime.abort();
193
+ abort.signal.addEventListener('abort', cancel, { once: true });
194
+ context.signal.addEventListener('abort', cancel, { once: true });
195
+ const timer = setTimeout(cancel, 55000);
196
+ try {
197
+ const answer = await Promise.race([
198
+ Promise.resolve().then(() => review(Object.freeze({ message: request.message, signal: lifetime.signal }))),
199
+ new Promise(resolve => lifetime.signal.addEventListener('abort', () => resolve(false), { once: true })),
200
+ ]);
201
+ return answer === true && !lifetime.signal.aborted && !closing ? { action: 'accept', content: { approve: true } } : { action: 'decline' };
202
+ }
203
+ catch {
204
+ return { action: 'decline' };
205
+ }
206
+ finally {
207
+ clearTimeout(timer);
208
+ cancel();
209
+ abort.signal.removeEventListener('abort', cancel);
210
+ context.signal.removeEventListener('abort', cancel);
211
+ }
212
+ }
213
+ try {
214
+ const sdkOptions = {
215
+ abortController: abort, sessionId: hostId, persistSession: false, settingSources: [], plugins: [], tools: [],
216
+ strictMcpConfig: true, mcpServers: { guarded: { command: python, args: ['-m', 'llms_ink.guard', 'connect', '--session-file', file] } },
217
+ canUseTool: async (name) => name.startsWith('mcp__guarded__') && initialized && busy && !closing
218
+ ? { behavior: 'allow' } : { behavior: 'deny', message: 'Only guarded tools are available.' },
219
+ settings: { disableAllHooks: true, disableClaudeAiConnectors: true, disabledMcpServers: ['claude-in-chrome'] },
220
+ extraArgs: { 'disable-slash-commands': null, 'no-chrome': null },
221
+ cwd, model: options.model, pathToClaudeCodeExecutable: claudeExecutable,
222
+ maxTurns: options.maxTurns ?? 12, maxBudgetUsd: options.maxBudgetUsd ?? 1, onElicitation: elicit,
223
+ };
224
+ host = query({ prompt: input, options: sdkOptions });
225
+ // Revocation on idle expiry does not wait for another prompt. Failures remain
226
+ // observable through close(); the service independently enforces its TTL.
227
+ expiresTimer = setTimeout(() => { void close().catch(() => { }); }, ttl * 1000);
228
+ expiresTimer.unref();
229
+ }
230
+ catch {
231
+ await close();
232
+ throw new GuardHostError('Claude host startup failed.');
233
+ }
234
+ return Object.freeze({ sessionId: guardId,
235
+ async run(prompt, onMessage) {
236
+ if (closing || busy)
237
+ throw new GuardHostError('The conversation is closed or another turn is running.');
238
+ if (typeof prompt !== 'string' || !prompt.trim() || prompt.trimStart().startsWith('/') || Buffer.byteLength(prompt) > 131072)
239
+ throw new GuardHostError('Provide a text prompt up to 128 KiB; host commands require a new conversation.');
240
+ busy = true;
241
+ const timer = setTimeout(() => { void close().catch(() => { }); }, timeout);
242
+ try {
243
+ const state = await control(python, ['service-status', '--owner-file', ownerFile]);
244
+ if (closing || state.ok !== true || !Array.isArray(state.sessions) || !state.sessions.some(s => s?.sessionId === guardId && s.active === true))
245
+ throw new Error();
246
+ input.send(prompt, hostId);
247
+ while (true) {
248
+ const next = await whileOpen(host.next());
249
+ if (next.done || closing)
250
+ throw new Error();
251
+ const message = next.value;
252
+ if ('session_id' in message && message.session_id !== hostId)
253
+ throw new Error();
254
+ if (message.type === 'system' && message.subtype === 'init') {
255
+ if (message.mcp_servers.length !== 1 || message.mcp_servers[0].name !== 'guarded' || message.mcp_servers[0].status !== 'connected'
256
+ || !message.tools.length || message.tools.some(t => !t.startsWith('mcp__guarded__')))
257
+ throw new Error();
258
+ initialized = true;
259
+ }
260
+ if (!initialized && message.type !== 'system')
261
+ throw new Error();
262
+ if (onMessage)
263
+ await whileOpen(Promise.resolve().then(() => onMessage(message)));
264
+ if (message.type === 'result') {
265
+ if (message.is_error)
266
+ throw new Error();
267
+ return message;
268
+ }
269
+ }
270
+ }
271
+ catch {
272
+ await close();
273
+ throw new GuardHostError('Guarded turn did not complete; the conversation was closed.');
274
+ }
275
+ finally {
276
+ clearTimeout(timer);
277
+ busy = false;
278
+ }
279
+ }, close, });
280
+ }
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { type InspectionReceipt } from './inspect.ts';
17
17
  export type { InspectionReceipt } from './inspect.ts';
18
- export declare const VERSION = "0.6.0";
18
+ export declare const VERSION = "0.7.0";
19
19
  export declare const DEFAULT_BASE_URL = "https://llms.ink";
20
20
  /** `llms` for the index (llms.txt), `full` for llms-full.txt. */
21
21
  export type Mode = 'llms' | 'full';
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { prepareInspection, inspectionJson, inspectionReceipt } from "./inspect.js";
17
17
  import { env } from "./env.js";
18
- export const VERSION = '0.6.0';
18
+ export const VERSION = '0.7.0';
19
19
  export const DEFAULT_BASE_URL = 'https://llms.ink';
20
20
  /**
21
21
  * An API error, or llms.ink couldn't be reached (then `status` and `code` are undefined).
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // Run from an installed llms-ink package with the optional Claude SDK peer installed.
3
+ import {parseArgs} from 'node:util';
4
+ import {createInterface} from 'node:readline/promises';
5
+ import {createGuardedClaude} from '../dist/guard-claude.js';
6
+ const {values}=parseArgs({options:{'owner-file':{type:'string'},python:{type:'string'},cwd:{type:'string'},model:{type:'string'},'claude-executable':{type:'string'}}});
7
+ if(!process.stdin.isTTY || !process.stdout.isTTY) throw Error('This example requires an interactive terminal for each approval.');
8
+ if(!values['owner-file'] || !values.python || !values.cwd) throw Error('Pass --owner-file, --python and --cwd with absolute paths.');
9
+ const terminal=createInterface({input:process.stdin,output:process.stdout});
10
+ // Preserve complete text while making terminal controls and bidi controls visible.
11
+ const plain=text=>text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0'));
12
+ let conversation,stopping=false;
13
+ const stop=()=>{stopping=true;terminal.close();void conversation?.close().catch(()=>{});};
14
+ terminal.on('SIGINT',stop);terminal.on('close',()=>{stopping=true;void conversation?.close().catch(()=>{});});process.on('SIGTERM',stop);
15
+ try{
16
+ conversation=await createGuardedClaude({ownerFile:values['owner-file'],python:values.python,cwd:values.cwd,model:values.model,claudeExecutable:values['claude-executable'],
17
+ review:async({message,signal})=>{
18
+ process.stdout.write('\n'+plain(message)+'\n\n');
19
+ try{return(await terminal.question('Type approve to send this call once; Enter declines: ',{signal}))==='approve';}catch{return false;}
20
+ }});
21
+ console.log('Guarded conversation. /exit closes and revokes this session.');
22
+ while(!stopping){
23
+ const prompt=await terminal.question('\nYou: ');
24
+ if(prompt.trim()==='/exit')break;
25
+ if(!prompt.trim())continue;
26
+ await conversation.run(prompt,message=>{
27
+ if(message.type==='assistant')for(const part of message.message.content)if(part.type==='text')process.stdout.write(plain(part.text)+'\n');
28
+ });
29
+ }
30
+ }catch(error){if(!stopping){console.error(error.message);process.exitCode=1;}}
31
+ finally{
32
+ terminal.close();process.removeListener('SIGTERM',stop);
33
+ try{await conversation?.close();}catch(error){console.error(error.message);if(error.sessionId)console.error('Session ID:',error.sessionId);if(error.credentialDirectory)console.error('Credential directory:',error.credentialDirectory);process.exitCode=1;}
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llms-ink",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Turn any link — site, docs section, page, GitHub repo, PDF or video — into llms.txt and llms-full.txt. Typed client for llms.ink, plus a Vite plugin, an Astro integration and middleware that serve the files from your site, and Vercel AI SDK tools. No dependencies.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,24 +23,55 @@
23
23
  "./ai": {
24
24
  "types": "./dist/ai.d.ts",
25
25
  "default": "./dist/ai.js"
26
+ },
27
+ "./guard/claude": {
28
+ "types": "./dist/guard-claude.d.ts",
29
+ "default": "./dist/guard-claude.js"
26
30
  }
27
31
  },
28
32
  "main": "./dist/index.js",
29
33
  "types": "./dist/index.d.ts",
30
- "files": ["dist"],
34
+ "files": [
35
+ "dist",
36
+ "examples/guard-claude.mjs"
37
+ ],
31
38
  "sideEffects": false,
32
- "repository": { "type": "git", "url": "git+https://github.com/lukax/llms-ink-js.git" },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/lukax/llms-ink-js.git"
42
+ },
33
43
  "bugs": "https://github.com/lukax/llms-ink-js/issues",
34
- "engines": { "node": ">=18" },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
35
47
  "scripts": {
36
48
  "build": "rm -rf dist && tsc -p tsconfig.json",
37
49
  "prepublishOnly": "npm run build && npm test",
38
50
  "test": "npm run build && tsc -p test/tsconfig.json && node --test test/client.test.mjs test/plugins.test.mjs test/middleware.test.mjs test/traffic.test.mjs test/inspect.test.mjs test/ai.test.mjs",
39
51
  "test:live": "npm run build && node --test test/live.test.mjs",
40
52
  "test:bun": "npm run build && bun test ./test/client.test.mjs ./test/plugins.test.mjs ./test/middleware.test.mjs ./test/traffic.test.mjs ./test/inspect.test.mjs ./test/ai.test.mjs",
41
- "test:bun:live": "npm run build && bun test --timeout 300000 ./test/live.test.mjs"
53
+ "test:bun:live": "npm run build && bun test --timeout 300000 ./test/live.test.mjs",
54
+ "test:guard": "node --experimental-test-module-mocks --test test/guard-claude.test.mjs",
55
+ "test:guard:live": "node --test test/guard-claude-live.test.mjs"
42
56
  },
43
- "keywords": ["llms.txt", "llms-full.txt", "llm", "ai", "markdown", "docs", "context", "rag", "sdk", "llms-ink", "vite-plugin", "astro-integration", "middleware", "ai-sdk", "vercel-ai-sdk", "tools"],
57
+ "keywords": [
58
+ "llms.txt",
59
+ "llms-full.txt",
60
+ "llm",
61
+ "ai",
62
+ "markdown",
63
+ "docs",
64
+ "context",
65
+ "rag",
66
+ "sdk",
67
+ "llms-ink",
68
+ "vite-plugin",
69
+ "astro-integration",
70
+ "middleware",
71
+ "ai-sdk",
72
+ "vercel-ai-sdk",
73
+ "tools"
74
+ ],
44
75
  "homepage": "https://llms.ink/docs/",
45
76
  "author": "Lucas Espindola (https://lucasespindola.com)",
46
77
  "license": "MIT",
@@ -48,15 +79,28 @@
48
79
  "ai": ">=5",
49
80
  "astro": ">=4",
50
81
  "vite": ">=5",
51
- "zod": "^3.25.76 || ^4.1.8"
82
+ "zod": "^3.25.76 || ^4.1.8",
83
+ "@anthropic-ai/claude-agent-sdk": "^0.3.319"
52
84
  },
53
85
  "peerDependenciesMeta": {
54
- "ai": { "optional": true },
55
- "astro": { "optional": true },
56
- "vite": { "optional": true },
57
- "zod": { "optional": true }
86
+ "ai": {
87
+ "optional": true
88
+ },
89
+ "astro": {
90
+ "optional": true
91
+ },
92
+ "vite": {
93
+ "optional": true
94
+ },
95
+ "zod": {
96
+ "optional": true
97
+ },
98
+ "@anthropic-ai/claude-agent-sdk": {
99
+ "optional": true
100
+ }
58
101
  },
59
102
  "devDependencies": {
103
+ "@anthropic-ai/claude-agent-sdk": "0.3.319",
60
104
  "@types/node": "^22.20.5",
61
105
  "ai": "^7.0.164",
62
106
  "astro": "^7.4.7",