@secemp/elwood 0.1.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/examples/01-basic-query.js +38 -0
  4. package/examples/02-bug-fixer.js +57 -0
  5. package/examples/03-custom-system-prompt.js +41 -0
  6. package/examples/04-read-only-agent.js +38 -0
  7. package/examples/05-find-todos.js +37 -0
  8. package/examples/06-session-resume.js +70 -0
  9. package/examples/07-hooks-pretooluse.js +74 -0
  10. package/examples/08-hooks-posttooluse-audit.js +66 -0
  11. package/examples/09-hooks-block-etc.js +68 -0
  12. package/examples/10-hooks-redirect-sandbox.js +70 -0
  13. package/examples/11-subagents.js +54 -0
  14. package/examples/12-mcp-stdio.js +48 -0
  15. package/examples/13-mcp-http.js +54 -0
  16. package/examples/14-custom-tool.js +84 -0
  17. package/examples/15-custom-tool-unit-converter.js +132 -0
  18. package/examples/16-mcp-github.js +71 -0
  19. package/examples/17-session-store-postgres.js +78 -0
  20. package/examples/18-session-store-redis.js +65 -0
  21. package/examples/19-session-store-s3.js +67 -0
  22. package/examples/20-session-list.js +72 -0
  23. package/examples/21-hooks-notification-slack.js +78 -0
  24. package/examples/22-hooks-webhook-posttooluse.js +78 -0
  25. package/examples/23-hooks-subagent-tracker.js +59 -0
  26. package/examples/24-v2-session-api.js +62 -0
  27. package/examples/README.md +95 -0
  28. package/examples/basic.js +240 -0
  29. package/examples/smoke-test.js +296 -0
  30. package/package.json +52 -0
  31. package/src/ast-tools.js +182 -0
  32. package/src/index.js +70 -0
  33. package/src/instrumenter.js +2921 -0
  34. package/src/loader.js +306 -0
  35. package/src/locate.js +296 -0
  36. package/src/query.js +2168 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * examples/basic.js
3
+ *
4
+ * Minimal demonstration of the `elwood` library.
5
+ *
6
+ * Run:
7
+ * node examples/basic.js
8
+ *
9
+ * What it does:
10
+ * 1. Locates the installed Claude Code CLI (cli.js) automatically.
11
+ * 2. Installs hook callbacks that log every intercepted event.
12
+ * 3. Parses + instruments the CLI bundle via Babel AST.
13
+ * 4. Reports statistics: how many hook sites were injected, which bundler
14
+ * pattern was detected, and a sample of the function map.
15
+ *
16
+ * The example deliberately does NOT fully import the instrumented bundle
17
+ * (step 4 of load()) because executing the CLI's top-level side effects
18
+ * would start the actual Claude Code process. If you do want to load it,
19
+ * uncomment the `load()` section at the bottom.
20
+ */
21
+
22
+ import {
23
+ locate,
24
+ instrument,
25
+ parseBundle,
26
+ detectBundlerPattern,
27
+ extractFunctionMap,
28
+ } from '../src/index.js';
29
+
30
+ // Increase heap for large bundles when running this example directly:
31
+ // node --max-old-space-size=4096 examples/basic.js
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // 0. Install global hooks (must happen before the bundle executes)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ globalThis.__elwoodHooks = {
38
+ /**
39
+ * Fires at the top of any function whose name matched the `intercept` list.
40
+ *
41
+ * @param {string} fnName - the function's identifier name
42
+ * @param {IArguments | any[]} args - the call-time arguments
43
+ */
44
+ onCall(fnName, args) {
45
+ console.log(` [hook:onCall] ${fnName}() called with ${Array.isArray(args) ? args.length : '?'} arg(s)`);
46
+ },
47
+
48
+ /**
49
+ * Fires when a fetch() call is detected that targets an Anthropic API URL.
50
+ *
51
+ * @param {string} label - always 'fetch'
52
+ * @param {string} url - the URL passed to fetch()
53
+ */
54
+ onApiCall(label, url) {
55
+ console.log(` [hook:onApiCall] ${label} → ${url}`);
56
+ },
57
+
58
+ /**
59
+ * Fires when a tool-execution call pattern is detected.
60
+ *
61
+ * @param {string} callee - the function/method name
62
+ * @param {string} firstArg - the first argument (typically the tool name)
63
+ */
64
+ onToolCall(callee, firstArg) {
65
+ console.log(` [hook:onToolCall] ${callee}(${JSON.stringify(firstArg)}, …)`);
66
+ },
67
+
68
+ /**
69
+ * Fires at the top of any async generator function (streaming pattern).
70
+ *
71
+ * @param {string} generatorName - inferred name of the generator
72
+ */
73
+ onStream(generatorName) {
74
+ console.log(` [hook:onStream] async generator ${generatorName} started`);
75
+ },
76
+ };
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 1. Locate the CLI
80
+ // ---------------------------------------------------------------------------
81
+
82
+ console.log('='.repeat(60));
83
+ console.log('elwood — basic example');
84
+ console.log('='.repeat(60));
85
+
86
+ console.log('\n[1] Locating Claude Code CLI...');
87
+
88
+ // You can override the auto-detection by passing an explicit cliPath below.
89
+ // Recommended: use the v22 9MB bundle — the v18 13MB bundle requires more heap.
90
+ // Hint: node --max-old-space-size=8192 examples/basic.js
91
+ let location;
92
+ try {
93
+ location = await locate();
94
+ } catch (err) {
95
+ console.error('\nFailed to locate cli.js:', err.message);
96
+ process.exit(1);
97
+ }
98
+
99
+ // If locate() found the larger v18 bundle and a smaller v22 one exists,
100
+ // silently prefer the v22 bundle for this example to avoid OOM.
101
+ {
102
+ const { existsSync } = await import('node:fs');
103
+ const v22cli =
104
+ (process.env.HOME ?? '') +
105
+ '/.nvm/versions/node/v22.0.0/lib/node_modules/@anthropic-ai/claude-code/cli.js';
106
+ if (existsSync(v22cli) && v22cli !== location.cliPath) {
107
+ console.log(` [note] preferring v22 bundle (${v22cli}) for memory efficiency`);
108
+ location = {
109
+ cliPath: v22cli,
110
+ sdkPath: null,
111
+ packageDir: v22cli.replace('/cli.js', ''),
112
+ version: '2.0.0',
113
+ };
114
+ }
115
+ }
116
+
117
+ const { cliPath, sdkPath, packageDir, version } = location;
118
+
119
+ console.log(` cliPath : ${cliPath}`);
120
+ console.log(` sdkPath : ${sdkPath ?? '(not found)'}`);
121
+ console.log(` packageDir : ${packageDir}`);
122
+ console.log(` version : ${version}`);
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // 2. Parse the bundle and detect its structure
126
+ // ---------------------------------------------------------------------------
127
+
128
+ console.log('\n[2] Parsing the CLI bundle (this may take a few seconds)...');
129
+ console.time(' parse');
130
+
131
+ let ast;
132
+ try {
133
+ ast = parseBundle(cliPath);
134
+ } catch (err) {
135
+ console.error('\nFailed to parse cli.js:', err.message);
136
+ process.exit(1);
137
+ }
138
+
139
+ console.timeEnd(' parse');
140
+
141
+ // Bundler detection
142
+ console.log('\n[3] Detecting bundler pattern...');
143
+ const bundlerInfo = detectBundlerPattern(ast);
144
+ console.log(` bundler : ${bundlerInfo.bundler}`);
145
+ console.log(` moduleCount : ${bundlerInfo.moduleCount}`);
146
+ console.log(` wrappers : ${bundlerInfo.wrapperNames.slice(0, 5).join(', ') || '(none)'}`);
147
+ console.log(` hasLazyInits : ${bundlerInfo.hasLazyInits}`);
148
+ console.log(' clues :');
149
+ for (const clue of bundlerInfo.clues) {
150
+ console.log(` • ${clue}`);
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // 3. Build the function map (sample first 10)
155
+ // ---------------------------------------------------------------------------
156
+
157
+ console.log('\n[4] Extracting function map (sample)...');
158
+ console.time(' extractFunctionMap');
159
+ const fnMap = extractFunctionMap(ast);
160
+ console.timeEnd(' extractFunctionMap');
161
+
162
+ console.log(` Total named functions found: ${fnMap.size}`);
163
+ console.log(' Sample (first 10):');
164
+
165
+ let i = 0;
166
+ for (const [name, entry] of fnMap) {
167
+ if (i++ >= 10) break;
168
+ const purpose = entry.inferredPurpose ?? '(unknown)';
169
+ console.log(` ${name.padEnd(25)} [${entry.kind.padEnd(11)}] purpose: ${purpose}`);
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // 4. Run the instrumentation pipeline (WITHOUT loading)
174
+ // ---------------------------------------------------------------------------
175
+
176
+ console.log('\n[5] Running instrumentation pipeline...');
177
+ console.time(' instrument');
178
+
179
+ let result;
180
+ try {
181
+ result = await instrument({
182
+ cliPath,
183
+ // Intercept functions whose names match these patterns — purely illustrative,
184
+ // since a minified bundle uses short names. Swap for real patterns if
185
+ // you reverse-engineer specific function names via fnMap above.
186
+ intercept: [],
187
+ wrapApiCalls: true,
188
+ wrapToolExecutions: true,
189
+ wrapAsyncGenerators: true,
190
+ wrapFunctionCalls: false, // keep fast — no generic function wrapping
191
+ verbose: false,
192
+ });
193
+ } catch (err) {
194
+ console.error('\nInstrumentation failed:', err.message);
195
+ process.exit(1);
196
+ }
197
+
198
+ console.timeEnd(' instrument');
199
+
200
+ console.log(` patchCount : ${result.patchCount}`);
201
+ console.log(` hookPoints : ${result.hookPoints.length} site(s)`);
202
+ console.log(` codeSize : ${(result.code.length / 1024 / 1024).toFixed(2)} MB`);
203
+
204
+ if (result.hookPoints.length > 0) {
205
+ console.log(' Sample hook points:');
206
+ for (const hp of result.hookPoints.slice(0, 5)) {
207
+ console.log(` • [${hp.type.padEnd(16)}] ${hp.name ?? '(anonymous)'}`);
208
+ }
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // 5. (Optional) Full load — uncomment to actually import the instrumented bundle
213
+ // ---------------------------------------------------------------------------
214
+
215
+ /*
216
+ import { load } from '../src/index.js';
217
+
218
+ console.log('\n[6] Loading the instrumented bundle via dynamic import...');
219
+ console.log(' WARNING: this will execute the CLI bundle top-level code.');
220
+ console.time(' load');
221
+
222
+ const loaded = await load({
223
+ cliPath,
224
+ instrument: true,
225
+ wrapApiCalls: true,
226
+ wrapToolExecutions: true,
227
+ verbose: true,
228
+ keepTemp: false,
229
+ });
230
+
231
+ console.timeEnd(' load');
232
+ console.log(` exports: ${Object.keys(loaded.exports).join(', ')}`);
233
+
234
+ // Clean up the temp file
235
+ loaded.teardown();
236
+ */
237
+
238
+ console.log('\n' + '='.repeat(60));
239
+ console.log('Done.');
240
+ console.log('='.repeat(60));
@@ -0,0 +1,296 @@
1
+ /**
2
+ * smoke-test.js
3
+ *
4
+ * Verifies that the elwood SDK loads correctly and all public APIs
5
+ * have the expected shape. Does NOT make any API calls (no ANTHROPIC_API_KEY needed).
6
+ *
7
+ * Checks:
8
+ * 1. preload() completes (Babel AST pipeline works)
9
+ * 2. Version, patch count, hook points count are printed
10
+ * 3. HOOK_EVENTS contains the expected event names
11
+ * 4. EXIT_REASONS contains the expected reason strings
12
+ * 5. tool() returns an object with { name, description, inputSchema, handler }
13
+ * 6. createSdkMcpServer() returns something (Promise or object)
14
+ * 7. query is a function
15
+ * 8. Session utilities (listSessions, getSessionMessages, getSessionInfo) are functions
16
+ * 9. V2 session APIs are functions
17
+ * 10. AbortError is a proper Error subclass
18
+ *
19
+ * Run:
20
+ * node examples/smoke-test.js
21
+ */
22
+
23
+ import {
24
+ // Core query
25
+ query,
26
+ AbortError,
27
+
28
+ // Tool / MCP helpers
29
+ tool,
30
+ createSdkMcpServer,
31
+ preload,
32
+
33
+ // Constants
34
+ HOOK_EVENTS,
35
+ EXIT_REASONS,
36
+
37
+ // Session utilities
38
+ listSessions,
39
+ getSessionMessages,
40
+ getSessionInfo,
41
+
42
+ // V2 Session API
43
+ unstable_v2_createSession,
44
+ unstable_v2_resumeSession,
45
+ unstable_v2_prompt,
46
+
47
+ // AST tools (for completeness)
48
+ locate,
49
+ load,
50
+ instrument,
51
+ parseBundle,
52
+ detectBundlerPattern,
53
+ extractFunctionMap,
54
+ injectHooks,
55
+ } from '../src/index.js';
56
+
57
+ let passed = 0;
58
+ let failed = 0;
59
+
60
+ function assert(condition, label) {
61
+ if (condition) {
62
+ console.log(` [PASS] ${label}`);
63
+ passed++;
64
+ } else {
65
+ console.error(` [FAIL] ${label}`);
66
+ failed++;
67
+ }
68
+ }
69
+
70
+ console.log('='.repeat(60));
71
+ console.log('elwood smoke test');
72
+ console.log('='.repeat(60));
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // 1. Verify exports exist and have the right types
76
+ // ---------------------------------------------------------------------------
77
+ console.log('\n[1] Checking exported functions and constants...');
78
+
79
+ assert(typeof query === 'function', 'query is a function');
80
+ assert(typeof tool === 'function', 'tool is a function');
81
+ assert(typeof createSdkMcpServer === 'function', 'createSdkMcpServer is a function');
82
+ assert(typeof preload === 'function', 'preload is a function');
83
+ assert(typeof locate === 'function', 'locate is a function');
84
+ assert(typeof load === 'function', 'load is a function');
85
+ assert(typeof instrument === 'function', 'instrument is a function');
86
+ assert(typeof parseBundle === 'function', 'parseBundle is a function');
87
+ assert(typeof detectBundlerPattern === 'function', 'detectBundlerPattern is a function');
88
+ assert(typeof extractFunctionMap === 'function', 'extractFunctionMap is a function');
89
+ assert(typeof injectHooks === 'function', 'injectHooks is a function');
90
+
91
+ // Session utilities
92
+ assert(typeof listSessions === 'function', 'listSessions is a function');
93
+ assert(typeof getSessionMessages === 'function', 'getSessionMessages is a function');
94
+ assert(typeof getSessionInfo === 'function', 'getSessionInfo is a function');
95
+
96
+ // V2 session API
97
+ assert(typeof unstable_v2_createSession === 'function', 'unstable_v2_createSession is a function');
98
+ assert(typeof unstable_v2_resumeSession === 'function', 'unstable_v2_resumeSession is a function');
99
+ assert(typeof unstable_v2_prompt === 'function', 'unstable_v2_prompt is a function');
100
+
101
+ // AbortError
102
+ assert(typeof AbortError === 'function', 'AbortError is a function (class)');
103
+ const abortErr = new AbortError('test');
104
+ assert(abortErr instanceof Error, 'AbortError instance is an Error');
105
+ assert(abortErr.name === 'AbortError', 'AbortError.name is "AbortError"');
106
+ assert(abortErr.message === 'test', 'AbortError.message is "test"');
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // 2. HOOK_EVENTS verification
110
+ // ---------------------------------------------------------------------------
111
+ console.log('\n[2] Verifying HOOK_EVENTS...');
112
+
113
+ assert(Array.isArray(HOOK_EVENTS), 'HOOK_EVENTS is an array');
114
+ assert(Object.isFrozen(HOOK_EVENTS), 'HOOK_EVENTS is frozen');
115
+ assert(HOOK_EVENTS.length >= 10, `HOOK_EVENTS has ${HOOK_EVENTS.length} entries (>= 10)`);
116
+
117
+ const expectedHookEvents = [
118
+ 'PreToolUse',
119
+ 'PostToolUse',
120
+ 'PostToolUseFailure',
121
+ 'Notification',
122
+ 'UserPromptSubmit',
123
+ 'SessionStart',
124
+ 'SessionEnd',
125
+ 'Stop',
126
+ 'SubagentStart',
127
+ 'SubagentStop',
128
+ 'PreCompact',
129
+ 'PermissionRequest',
130
+ ];
131
+ for (const event of expectedHookEvents) {
132
+ assert(HOOK_EVENTS.includes(event), `HOOK_EVENTS contains "${event}"`);
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // 3. EXIT_REASONS verification
137
+ // ---------------------------------------------------------------------------
138
+ console.log('\n[3] Verifying EXIT_REASONS...');
139
+
140
+ assert(Array.isArray(EXIT_REASONS), 'EXIT_REASONS is an array');
141
+ assert(Object.isFrozen(EXIT_REASONS), 'EXIT_REASONS is frozen');
142
+
143
+ const expectedExitReasons = [
144
+ 'clear',
145
+ 'logout',
146
+ 'prompt_input_exit',
147
+ 'other',
148
+ 'bypass_permissions_disabled',
149
+ ];
150
+ for (const reason of expectedExitReasons) {
151
+ assert(EXIT_REASONS.includes(reason), `EXIT_REASONS contains "${reason}"`);
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // 4. tool() returns the right shape
156
+ // ---------------------------------------------------------------------------
157
+ console.log('\n[4] Verifying tool() return shape...');
158
+
159
+ const testTool = tool(
160
+ 'test_tool',
161
+ 'A test tool for smoke testing',
162
+ {
163
+ type: 'object',
164
+ properties: { input: { type: 'string' } },
165
+ required: ['input'],
166
+ },
167
+ async (args) => ({ content: [{ type: 'text', text: `Echo: ${args.input}` }] })
168
+ );
169
+
170
+ assert(typeof testTool === 'object' && testTool !== null, 'tool() returns an object');
171
+ assert(testTool.name === 'test_tool', 'tool().name is "test_tool"');
172
+ assert(testTool.description === 'A test tool for smoke testing', 'tool().description is correct');
173
+ assert(typeof testTool.inputSchema === 'object', 'tool().inputSchema is an object');
174
+ assert(typeof testTool.handler === 'function', 'tool().handler is a function');
175
+
176
+ // Verify handler works
177
+ const handlerResult = await testTool.handler({ input: 'hello' });
178
+ assert(
179
+ handlerResult.content[0].text === 'Echo: hello',
180
+ 'tool().handler returns correct content'
181
+ );
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // 5. createSdkMcpServer() returns something
185
+ // ---------------------------------------------------------------------------
186
+ console.log('\n[5] Verifying createSdkMcpServer()...');
187
+
188
+ const mcpResult = createSdkMcpServer({
189
+ name: 'smoke-test-server',
190
+ version: '0.0.1',
191
+ tools: [testTool],
192
+ });
193
+
194
+ // createSdkMcpServer returns either a sync object or a Promise
195
+ const isPromise = mcpResult && typeof mcpResult.then === 'function';
196
+ const isObject = typeof mcpResult === 'object' && mcpResult !== null;
197
+ assert(isPromise || isObject, `createSdkMcpServer() returns a ${isPromise ? 'Promise' : 'sync object'}`);
198
+
199
+ if (isPromise) {
200
+ // If it's a promise, verify it resolves (or fails gracefully)
201
+ try {
202
+ const resolved = await mcpResult;
203
+ assert(
204
+ resolved && typeof resolved === 'object',
205
+ 'createSdkMcpServer() promise resolves to an object'
206
+ );
207
+ assert(resolved.type === 'sdk', 'resolved.type is "sdk"');
208
+ assert(resolved.name === 'smoke-test-server', 'resolved.name is "smoke-test-server"');
209
+ } catch (err) {
210
+ // It's acceptable for createSdkMcpServer to fail if no McpServer class is available
211
+ console.log(` [INFO] createSdkMcpServer() promise rejected: ${err.message?.slice(0, 80)}`);
212
+ console.log(' [INFO] This is expected if @modelcontextprotocol/sdk is not installed');
213
+ }
214
+ } else {
215
+ assert(mcpResult.type === 'sdk', 'result.type is "sdk"');
216
+ assert(mcpResult.name === 'smoke-test-server', 'result.name is "smoke-test-server"');
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // 6. query() is callable (but we don't actually run it)
221
+ // ---------------------------------------------------------------------------
222
+ console.log('\n[6] Verifying query() is callable...');
223
+
224
+ assert(typeof query === 'function', 'query is a function');
225
+
226
+ // Verify it returns an async generator when called
227
+ const gen = query({ prompt: 'test', options: {} });
228
+ assert(
229
+ gen !== null && typeof gen === 'object',
230
+ 'query() returns a non-null object'
231
+ );
232
+ assert(typeof gen[Symbol.asyncIterator] === 'function', 'query() returns an async iterable');
233
+ assert(typeof gen.interrupt === 'function', 'query() result has interrupt() method');
234
+ assert(typeof gen.setModel === 'function', 'query() result has setModel() method');
235
+ assert(typeof gen.setPermissionMode === 'function', 'query() result has setPermissionMode() method');
236
+ assert(typeof gen.supportedModels === 'function', 'query() result has supportedModels() method');
237
+ assert(typeof gen.supportedCommands === 'function', 'query() result has supportedCommands() method');
238
+ assert(typeof gen.mcpServerStatus === 'function', 'query() result has mcpServerStatus() method');
239
+ assert(typeof gen.close === 'function', 'query() result has close() method');
240
+
241
+ // Clean up: close the generator without iterating (avoids loading cli.js)
242
+ try {
243
+ gen.close();
244
+ } catch {
245
+ // ignore
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // 7. listSessions works (reads from filesystem, no API key needed)
250
+ // ---------------------------------------------------------------------------
251
+ console.log('\n[7] Verifying session utilities...');
252
+
253
+ const sessions = await listSessions();
254
+ assert(Array.isArray(sessions), 'listSessions() returns an array');
255
+ console.log(` [INFO] Found ${sessions.length} session(s) in ~/.claude/projects/`);
256
+
257
+ if (sessions.length > 0) {
258
+ const first = sessions[0];
259
+ assert(typeof first.sessionId === 'string', 'session has sessionId');
260
+ assert(typeof first.projectPath === 'string', 'session has projectPath');
261
+ assert(first.mtime instanceof Date, 'session has mtime as Date');
262
+
263
+ const info = await getSessionInfo(first.sessionId);
264
+ if (info) {
265
+ assert(typeof info.messageCount === 'number', 'getSessionInfo returns messageCount');
266
+ }
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // 8. Locate CLI (verifies the AST pipeline can find cli.js)
271
+ // ---------------------------------------------------------------------------
272
+ console.log('\n[8] Locating Claude Code CLI...');
273
+
274
+ try {
275
+ const loc = await locate();
276
+ assert(typeof loc.cliPath === 'string', `CLI found at: ${loc.cliPath}`);
277
+ assert(typeof loc.version === 'string', `CLI version: ${loc.version}`);
278
+ console.log(` [INFO] packageDir: ${loc.packageDir}`);
279
+ } catch (err) {
280
+ console.log(` [WARN] locate() failed: ${err.message?.slice(0, 80)}`);
281
+ console.log(' [WARN] This is expected if Claude Code CLI is not installed');
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Summary
286
+ // ---------------------------------------------------------------------------
287
+ console.log('\n' + '='.repeat(60));
288
+ console.log(`Results: ${passed} passed, ${failed} failed`);
289
+ console.log('='.repeat(60));
290
+
291
+ if (failed > 0) {
292
+ console.error('\nSmoke test FAILED');
293
+ process.exit(1);
294
+ } else {
295
+ console.log('\nSmoke test passed!');
296
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@secemp/elwood",
3
+ "version": "0.1.0",
4
+ "description": "Programmatically import and instrument the locally installed Claude Code CLI via Babel AST",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "files": [
11
+ "src/",
12
+ "examples/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/secemp9/elwood.git"
22
+ },
23
+ "homepage": "https://github.com/secemp9/elwood",
24
+ "bugs": {
25
+ "url": "https://github.com/secemp9/elwood/issues"
26
+ },
27
+ "dependencies": {
28
+ "@babel/generator": "^7.28.0",
29
+ "@babel/parser": "^7.28.0",
30
+ "@babel/traverse": "^7.28.0",
31
+ "@babel/types": "^7.28.0",
32
+ "which": "^4.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "node-gyp": "^10.0.0"
36
+ },
37
+ "scripts": {
38
+ "test": "node scripts/test-query-full.js && node scripts/test-gap-fixes.js && node scripts/test-hooks-behavioral.js && node scripts/test-mcp-behavioral.js",
39
+ "test:examples": "node scripts/test-examples.js",
40
+ "smoke": "node examples/smoke-test.js",
41
+ "example": "node examples/01-basic-query.js"
42
+ },
43
+ "keywords": [
44
+ "claude",
45
+ "anthropic",
46
+ "babel",
47
+ "ast",
48
+ "instrumentation",
49
+ "cli"
50
+ ],
51
+ "license": "MIT"
52
+ }