@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,2921 @@
1
+ /**
2
+ * instrumenter.js
3
+ *
4
+ * Intelligent AST instrumentation layer for the Claude Code CLI bundle.
5
+ *
6
+ * Design goals:
7
+ * - ADDITIVE: every injection is a conditional expression that no-ops when
8
+ * the hook object is absent, so the instrumented bundle behaves identically
9
+ * to the original unless a caller installs hooks via globalThis.__elwoodHooks.
10
+ * - NON-DESTRUCTIVE: original AST nodes are wrapped, never replaced.
11
+ * - FAST: visitors make a single pass over the AST.
12
+ *
13
+ * Hook injection pattern (all hooks share this shape):
14
+ *
15
+ * if (globalThis.__elwoodHooks?.[hookName])
16
+ * globalThis.__elwoodHooks[hookName](/* metadata *\/, /* args *\/);
17
+ *
18
+ * The hook object is expected to live on `globalThis` so it survives dynamic
19
+ * import() boundaries.
20
+ */
21
+
22
+ import { traverseBundle, t, PARSER_OPTIONS } from './ast-tools.js';
23
+ import { parse } from '@babel/parser';
24
+ import _traverse from '@babel/traverse';
25
+
26
+ const traverse = _traverse.default ?? _traverse;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Constants
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Default global hook-registry variable name. */
33
+ const DEFAULT_HOOK_VAR = '__elwoodHooks';
34
+
35
+ /** Anthropic API hostname — used to identify fetch() calls to the API. */
36
+ const ANTHROPIC_API_HOST = 'api.anthropic.com';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // AST builder helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /**
43
+ * Build: `globalThis.<hookVar>?.[hookName]`
44
+ *
45
+ * @param {string} hookVar - e.g. '__elwoodHooks'
46
+ * @param {string} hookName - e.g. 'onCall'
47
+ */
48
+ function buildHookAccess(hookVar, hookName) {
49
+ // globalThis.__elwoodHooks
50
+ const registry = t.memberExpression(
51
+ t.identifier('globalThis'),
52
+ t.identifier(hookVar)
53
+ );
54
+ // globalThis.__elwoodHooks?.[hookName]
55
+ return t.optionalMemberExpression(
56
+ registry,
57
+ t.stringLiteral(hookName),
58
+ /* computed */ true,
59
+ /* optional */ true
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Build the statement:
65
+ * `if (globalThis.<hookVar>?.[hookName]) globalThis.<hookVar>[hookName](...callArgs);`
66
+ *
67
+ * @param {string} hookVar
68
+ * @param {string} hookName
69
+ * @param {import('@babel/types').Expression[]} callArgs
70
+ * @returns {import('@babel/types').IfStatement}
71
+ */
72
+ function buildHookCall(hookVar, hookName, callArgs) {
73
+ const guard = buildHookAccess(hookVar, hookName);
74
+
75
+ // globalThis.__elwoodHooks[hookName](...callArgs)
76
+ const call = t.callExpression(
77
+ t.memberExpression(
78
+ t.memberExpression(
79
+ t.identifier('globalThis'),
80
+ t.identifier(hookVar)
81
+ ),
82
+ t.stringLiteral(hookName),
83
+ /* computed */ true
84
+ ),
85
+ callArgs
86
+ );
87
+
88
+ return t.ifStatement(guard, t.expressionStatement(call));
89
+ }
90
+
91
+ /**
92
+ * Build a string literal AST node.
93
+ *
94
+ * @param {string} s
95
+ */
96
+ const str = s => t.stringLiteral(s);
97
+
98
+ /**
99
+ * Build a spread element `...expr`.
100
+ *
101
+ * @param {import('@babel/types').Expression} expr
102
+ */
103
+ const spread = expr => t.spreadElement(expr);
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // injectHooks
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * @typedef {Object} InjectHooksOptions
111
+ * @property {(string|RegExp)[]} [intercept]
112
+ * Patterns matched against function identifiers. Any named function whose
113
+ * name matches at least one pattern gets wrapped with before/after hooks.
114
+ * Default: intercept nothing (only explicit detectors run).
115
+ * @property {string} [hookVar]
116
+ * Name of the global hook registry object (default: '__elwoodHooks').
117
+ * @property {boolean} [wrapApiCalls]
118
+ * Detect and wrap fetch() calls that appear to target the Anthropic API.
119
+ * Default: true.
120
+ * @property {boolean} [wrapToolExecutions]
121
+ * Detect and wrap common tool-execution patterns.
122
+ * Default: true.
123
+ * @property {boolean} [wrapAsyncGenerators]
124
+ * Detect and wrap async generator functions (used for streaming).
125
+ * Default: true.
126
+ * @property {boolean} [wrapFunctionCalls]
127
+ * Wrap ALL named function/variable declarations matching `intercept`.
128
+ * Default: true.
129
+ */
130
+
131
+ /**
132
+ * Primary instrumentation entry point. Walks the AST and injects
133
+ * `globalThis.__elwoodHooks` call-sites at detected interesting points.
134
+ *
135
+ * @param {import('@babel/types').File} ast - parsed Babel AST (mutated in place)
136
+ * @param {InjectHooksOptions} [options]
137
+ * @returns {{ patchCount: number, hookPoints: HookPoint[] }}
138
+ */
139
+ function injectHooks(ast, options = {}) {
140
+ const {
141
+ intercept = [],
142
+ hookVar = DEFAULT_HOOK_VAR,
143
+ wrapApiCalls = true,
144
+ wrapToolExecutions = true,
145
+ wrapAsyncGenerators = true,
146
+ wrapFunctionCalls = true,
147
+ } = options;
148
+
149
+ /** @type {HookPoint[]} */
150
+ const hookPoints = [];
151
+ let patchCount = 0;
152
+
153
+ // Pre-compile intercept patterns
154
+ const patterns = intercept.map(p =>
155
+ typeof p === 'string' ? new RegExp(p) : p
156
+ );
157
+
158
+ /**
159
+ * Check if a name matches any of the user-supplied intercept patterns.
160
+ *
161
+ * @param {string | null | undefined} name
162
+ * @returns {boolean}
163
+ */
164
+ function matchesIntercept(name) {
165
+ if (!name) return false;
166
+ return patterns.some(re => re.test(name));
167
+ }
168
+
169
+ /**
170
+ * Infer a human-readable name for a call expression callee.
171
+ *
172
+ * @param {import('@babel/types').Expression} callee
173
+ * @returns {string}
174
+ */
175
+ function inferCalleeName(callee) {
176
+ if (t.isIdentifier(callee)) return callee.name;
177
+ if (t.isMemberExpression(callee)) {
178
+ const obj = t.isIdentifier(callee.object) ? callee.object.name : '(obj)';
179
+ const prop = t.isIdentifier(callee.property)
180
+ ? callee.property.name
181
+ : t.isStringLiteral(callee.property)
182
+ ? callee.property.value
183
+ : '(prop)';
184
+ return `${obj}.${prop}`;
185
+ }
186
+ return '(anonymous)';
187
+ }
188
+
189
+ /**
190
+ * Return true if a string literal inside a call expression looks like an
191
+ * Anthropic API URL.
192
+ *
193
+ * @param {import('@babel/types').CallExpression} node
194
+ * @returns {boolean}
195
+ */
196
+ function looksLikeApiCall(node) {
197
+ if (!t.isCallExpression(node)) return false;
198
+ const callee = node.callee;
199
+ // Must be `fetch(...)` or `globalThis.fetch(...)` or `window.fetch(...)`
200
+ const isNativeFetch =
201
+ (t.isIdentifier(callee) && callee.name === 'fetch') ||
202
+ (t.isMemberExpression(callee) &&
203
+ t.isIdentifier(callee.property) &&
204
+ callee.property.name === 'fetch');
205
+ if (!isNativeFetch) return false;
206
+
207
+ // Check first argument for a string URL containing the API host
208
+ const [firstArg] = node.arguments;
209
+ if (!firstArg) return false;
210
+
211
+ if (t.isStringLiteral(firstArg) && firstArg.value.includes(ANTHROPIC_API_HOST)) {
212
+ return true;
213
+ }
214
+
215
+ // Template literal: check quasi strings
216
+ if (t.isTemplateLiteral(firstArg)) {
217
+ return firstArg.quasis.some(q => q.value.cooked?.includes(ANTHROPIC_API_HOST));
218
+ }
219
+
220
+ return false;
221
+ }
222
+
223
+ /**
224
+ * Return true if a call expression looks like a tool execution.
225
+ *
226
+ * Heuristics (conservative — we prefer false negatives over false positives
227
+ * to avoid breaking callee semantics in the minified bundle):
228
+ *
229
+ * - The callee's inferred name explicitly contains known tool-dispatch
230
+ * keywords (execute, runTool, invokeTool, dispatchTool, callTool).
231
+ * - The callee is a member expression whose property is one of the above
232
+ * keywords: e.g. `obj.execute(...)`, `runner.invokeTool(...)`.
233
+ *
234
+ * We deliberately do NOT match on the `(string, ...)` signature alone
235
+ * because minified module-wrapper calls (e.g. `U(function(){…})` or
236
+ * `createRequire(import.meta.url)`) share that shape and would flood the
237
+ * hook with false positives.
238
+ *
239
+ * @param {import('@babel/types').CallExpression} node
240
+ * @returns {boolean}
241
+ */
242
+ function looksLikeToolCall(node) {
243
+ if (!t.isCallExpression(node)) return false;
244
+ const name = inferCalleeName(node.callee).toLowerCase();
245
+ // Must have an explicit keyword match — no generic (string,…) heuristic
246
+ return /\b(execute|runtool|invoketool|dispatchtool|calltool)\b/.test(name);
247
+ }
248
+
249
+ /**
250
+ * Inject a hook call statement BEFORE `path`'s node inside its parent
251
+ * block/body.
252
+ *
253
+ * @param {import('@babel/traverse').NodePath} path
254
+ * @param {string} hookName
255
+ * @param {import('@babel/types').Expression[]} args
256
+ */
257
+ function injectBefore(path, hookName, args) {
258
+ try {
259
+ const stmt = buildHookCall(hookVar, hookName, args);
260
+ path.insertBefore(stmt);
261
+ patchCount++;
262
+ } catch {
263
+ // Path may not support insertBefore (e.g. not a statement-level path)
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Replace a call expression node with a sequence that fires the hook and
269
+ * then performs the original call. Works even when the call is not at
270
+ * statement level (e.g. used as an argument or right-hand side).
271
+ *
272
+ * Pattern:
273
+ * (globalThis.__elwoodHooks?.['hookName'] &&
274
+ * globalThis.__elwoodHooks['hookName'](meta, originalArgs),
275
+ * originalCall)
276
+ *
277
+ * Uses the comma operator to stay expression-level.
278
+ *
279
+ * @param {import('@babel/traverse').NodePath} path
280
+ * @param {string} hookName
281
+ * @param {import('@babel/types').Expression[]} hookArgs
282
+ */
283
+ function wrapCallExpression(path, hookName, hookArgs) {
284
+ try {
285
+ const original = path.node;
286
+ const guard = buildHookAccess(hookVar, hookName);
287
+ const hookCall = t.callExpression(
288
+ t.memberExpression(
289
+ t.memberExpression(
290
+ t.identifier('globalThis'),
291
+ t.identifier(hookVar)
292
+ ),
293
+ t.stringLiteral(hookName),
294
+ true
295
+ ),
296
+ hookArgs
297
+ );
298
+
299
+ // (condition && hookCall, originalCall)
300
+ const logicalAndCall = t.logicalExpression('&&', guard, hookCall);
301
+ const seq = t.sequenceExpression([logicalAndCall, original]);
302
+
303
+ path.replaceWith(seq);
304
+ patchCount++;
305
+
306
+ // Skip re-traversal of the inserted node to avoid infinite loops
307
+ path.skip();
308
+ } catch {
309
+ // Bail silently — instrumentation is additive, never destructive
310
+ }
311
+ }
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Visitor
315
+ // ---------------------------------------------------------------------------
316
+
317
+ traverseBundle(ast, {
318
+ // ── Named function declarations ──────────────────────────────────────────
319
+ FunctionDeclaration(path) {
320
+ if (!wrapFunctionCalls) return;
321
+
322
+ const name = path.node.id?.name;
323
+ if (!name || !matchesIntercept(name)) return;
324
+
325
+ // Inject at the very start of the function body:
326
+ // if (globalThis.__elwoodHooks?.['onCall'])
327
+ // globalThis.__elwoodHooks['onCall']('fnName', arguments);
328
+ const body = path.node.body;
329
+ if (!t.isBlockStatement(body)) return;
330
+
331
+ const hookStmt = buildHookCall(hookVar, 'onCall', [
332
+ str(name),
333
+ t.identifier('arguments'),
334
+ ]);
335
+
336
+ body.body.unshift(hookStmt);
337
+ patchCount++;
338
+
339
+ hookPoints.push({ type: 'functionDeclaration', name, path: path.toString() });
340
+ },
341
+
342
+ // ── Variable-declared functions / arrow functions ─────────────────────────
343
+ VariableDeclarator(path) {
344
+ if (!wrapFunctionCalls) return;
345
+
346
+ const id = path.node.id;
347
+ if (!t.isIdentifier(id)) return;
348
+ const name = id.name;
349
+ if (!matchesIntercept(name)) return;
350
+
351
+ const init = path.node.init;
352
+ if (!init) return;
353
+
354
+ if (
355
+ !t.isFunctionExpression(init) &&
356
+ !t.isArrowFunctionExpression(init)
357
+ )
358
+ return;
359
+
360
+ // Arrow functions with concise body (expr => expr) can't have statements
361
+ // prepended without converting to block body first.
362
+ if (!t.isBlockStatement(init.body)) {
363
+ // Convert concise body to block body
364
+ const returnStmt = t.returnStatement(init.body);
365
+ init.body = t.blockStatement([returnStmt]);
366
+ }
367
+
368
+ const argsExpr = init.params.length > 0
369
+ ? t.arrayExpression(init.params
370
+ .filter(p => t.isIdentifier(p))
371
+ .map(p => t.identifier(p.name)))
372
+ : t.arrayExpression([]);
373
+
374
+ const hookStmt = buildHookCall(hookVar, 'onCall', [str(name), argsExpr]);
375
+ init.body.body.unshift(hookStmt);
376
+ patchCount++;
377
+
378
+ hookPoints.push({ type: 'variableFunction', name });
379
+ },
380
+
381
+ // ── fetch() calls ─────────────────────────────────────────────────────────
382
+ CallExpression(path) {
383
+ if (wrapApiCalls && looksLikeApiCall(path.node)) {
384
+ // Build a snapshot of the first argument (URL) for the hook
385
+ const [urlArg] = path.node.arguments;
386
+ const hookArgs = [
387
+ str('fetch'),
388
+ urlArg ? t.cloneNode(urlArg, true) : t.identifier('undefined'),
389
+ ];
390
+ wrapCallExpression(path, 'onApiCall', hookArgs);
391
+ hookPoints.push({ type: 'apiCall', name: 'fetch' });
392
+ return; // wrapCallExpression calls path.skip()
393
+ }
394
+
395
+ if (wrapToolExecutions && looksLikeToolCall(path.node)) {
396
+ const calleeName = inferCalleeName(path.node.callee);
397
+ const [firstArg] = path.node.arguments;
398
+ const hookArgs = [
399
+ str(calleeName),
400
+ firstArg ? t.cloneNode(firstArg, true) : t.identifier('undefined'),
401
+ ];
402
+ wrapCallExpression(path, 'onToolCall', hookArgs);
403
+ hookPoints.push({ type: 'toolCall', name: calleeName });
404
+ }
405
+ },
406
+
407
+ // ── Async generator functions (streaming) ─────────────────────────────────
408
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
409
+ if (!wrapAsyncGenerators) return;
410
+
411
+ const node = path.node;
412
+ if (!node.async || !node.generator) return;
413
+
414
+ const body = node.body;
415
+ if (!t.isBlockStatement(body)) return;
416
+
417
+ // Infer a name for the hook payload
418
+ let name = '(asyncGenerator)';
419
+ if (t.isFunctionDeclaration(node) && node.id) {
420
+ name = node.id.name;
421
+ } else if (
422
+ t.isVariableDeclarator(path.parent) &&
423
+ t.isIdentifier(path.parent.id)
424
+ ) {
425
+ name = path.parent.id.name;
426
+ }
427
+
428
+ const hookStmt = buildHookCall(hookVar, 'onStream', [str(name)]);
429
+ body.body.unshift(hookStmt);
430
+ patchCount++;
431
+
432
+ hookPoints.push({ type: 'asyncGenerator', name });
433
+ },
434
+ });
435
+
436
+ return { patchCount, hookPoints };
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // detectBundlerPattern
441
+ // ---------------------------------------------------------------------------
442
+
443
+ /**
444
+ * @typedef {Object} BundlerInfo
445
+ * @property {'webpack'|'rollup'|'esbuild'|'bun'|'unknown'} bundler
446
+ * @property {string[]} wrapperNames - detected module-wrapper function names
447
+ * @property {number} moduleCount - approximate number of bundled modules
448
+ * @property {boolean} hasLazyInits - true if tF1-style lazy initializers found
449
+ * @property {string[]} clues - human-readable detection evidence
450
+ */
451
+
452
+ /**
453
+ * Analyse the top-level AST to detect which bundler produced the bundle and
454
+ * extract structural metadata (same heuristic as generic-dependency-splitter.js).
455
+ *
456
+ * @param {import('@babel/types').File} ast
457
+ * @returns {BundlerInfo}
458
+ */
459
+ function detectBundlerPattern(ast) {
460
+ /** name → { asCallee: number, withFuncArg: number } */
461
+ const usageStats = new Map();
462
+ const clues = [];
463
+
464
+ // Check imports — esbuild/bun bundles emit a `createRequire` import
465
+ let hasCreateRequire = false;
466
+ let hasDefine = false; // webpack/UMD define()
467
+ let moduleWrapperCandidateCount = 0;
468
+
469
+ for (const node of ast.program.body) {
470
+ if (node.type === 'ImportDeclaration') {
471
+ for (const spec of node.specifiers) {
472
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
473
+ if (spec.imported.name === 'createRequire') {
474
+ hasCreateRequire = true;
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ if (node.type === 'VariableDeclaration') {
481
+ for (const decl of node.declarations) {
482
+ if (!t.isIdentifier(decl.id)) continue;
483
+ if (!decl.init || !t.isCallExpression(decl.init)) continue;
484
+
485
+ const callee = decl.init.callee;
486
+ if (!t.isIdentifier(callee)) continue;
487
+
488
+ const name = callee.name;
489
+ if (!usageStats.has(name)) {
490
+ usageStats.set(name, { asCallee: 0, withFuncArg: 0 });
491
+ }
492
+ const stats = usageStats.get(name);
493
+ stats.asCallee++;
494
+
495
+ const firstArg = decl.init.arguments[0];
496
+ if (
497
+ firstArg &&
498
+ (t.isFunctionExpression(firstArg) || t.isArrowFunctionExpression(firstArg))
499
+ ) {
500
+ stats.withFuncArg++;
501
+ }
502
+ }
503
+ }
504
+
505
+ if (
506
+ node.type === 'ExpressionStatement' &&
507
+ t.isCallExpression(node.expression)
508
+ ) {
509
+ const callee = node.expression.callee;
510
+ if (t.isIdentifier(callee) && callee.name === 'define') {
511
+ hasDefine = true;
512
+ }
513
+ }
514
+ }
515
+
516
+ // Find module-wrapper candidates
517
+ const wrapperNames = [];
518
+ for (const [name, stats] of usageStats) {
519
+ if (stats.withFuncArg >= 5) {
520
+ wrapperNames.push(name);
521
+ moduleWrapperCandidateCount += stats.withFuncArg;
522
+ }
523
+ }
524
+
525
+ // Classify bundler
526
+ let bundler = 'unknown';
527
+
528
+ if (hasCreateRequire && wrapperNames.length > 0) {
529
+ bundler = 'esbuild';
530
+ clues.push(`createRequire import detected (esbuild/bun pattern)`);
531
+ clues.push(`${wrapperNames.length} module-wrapper function(s): ${wrapperNames.slice(0, 3).join(', ')}`);
532
+ } else if (hasCreateRequire) {
533
+ bundler = 'esbuild';
534
+ clues.push(`createRequire import detected`);
535
+ } else if (wrapperNames.length > 0) {
536
+ // webpack uses __webpack_require__, rollup uses parenthesized IIFEs
537
+ const hasWebpackRequire = Array.from(usageStats.keys()).some(n =>
538
+ n.includes('webpack') || n.includes('__w')
539
+ );
540
+ bundler = hasWebpackRequire ? 'webpack' : 'rollup';
541
+ clues.push(
542
+ `${wrapperNames.length} module-wrapper(s) with ${moduleWrapperCandidateCount} total uses`
543
+ );
544
+ } else if (hasDefine) {
545
+ bundler = 'webpack';
546
+ clues.push('define() call detected (UMD/webpack)');
547
+ }
548
+
549
+ if (clues.length === 0) {
550
+ clues.push('No strong bundler signals found — treating as flat script');
551
+ }
552
+
553
+ // Detect lazy initializers: pattern `var X = tF1(() => { … })`
554
+ // tF1 is esbuild's lazy init helper name in the Claude Code bundle
555
+ const hasLazyInits = Array.from(usageStats.keys()).some(n => /^[a-z]{2,4}\d+$/.test(n));
556
+
557
+ return {
558
+ bundler,
559
+ wrapperNames,
560
+ moduleCount: moduleWrapperCandidateCount,
561
+ hasLazyInits,
562
+ clues,
563
+ };
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // extractFunctionMap
568
+ // ---------------------------------------------------------------------------
569
+
570
+ /**
571
+ * @typedef {Object} FunctionEntry
572
+ * @property {import('@babel/types').Node} node - the function AST node
573
+ * @property {string} kind - 'declaration'|'expression'|'arrow'|'method'
574
+ * @property {string|null} inferredPurpose - best-guess description
575
+ * @property {string[]} callees - names of functions called inside
576
+ * @property {string[]} memberAccesses - obj.prop strings accessed
577
+ * @property {string[]} stringLiterals - string literals found inside
578
+ */
579
+
580
+ /**
581
+ * Find ALL function definitions (named declarations, named arrow functions in
582
+ * variable declarators, class methods) in a minified bundle and return a Map
583
+ * keyed by the function's local identifier name.
584
+ *
585
+ * "inferredPurpose" is determined by:
586
+ * 1. What other functions it calls (callees)
587
+ * 2. What properties it accesses (memberAccesses)
588
+ * 3. String literals embedded in its body
589
+ *
590
+ * Implementation uses a single AST pass with a function-scope stack to
591
+ * accumulate evidence for each named function without nested traversals.
592
+ * This keeps memory usage bounded even for 9-13 MB minified bundles.
593
+ *
594
+ * @param {import('@babel/types').File} ast
595
+ * @returns {Map<string, FunctionEntry>}
596
+ */
597
+ function extractFunctionMap(ast) {
598
+ /** @type {Map<string, FunctionEntry>} */
599
+ const fnMap = new Map();
600
+
601
+ /**
602
+ * Each entry on the scope stack represents a named function we are currently
603
+ * inside. Evidence (callees / memberAccesses / stringLiterals) collected
604
+ * while inside this scope is assigned to this function.
605
+ *
606
+ * @type {Array<{ name: string, node: import('@babel/types').Node, kind: string }>}
607
+ */
608
+ const scopeStack = [];
609
+
610
+ /**
611
+ * Map from function node reference (by object identity key) to its
612
+ * accumulated evidence arrays.
613
+ *
614
+ * @type {Map<import('@babel/types').Node, { callees: string[], memberAccesses: string[], stringLiterals: string[] }>}
615
+ */
616
+ const evidence = new Map();
617
+
618
+ /**
619
+ * Heuristically guess the purpose of a function from its internal evidence.
620
+ *
621
+ * @param {string[]} callees
622
+ * @param {string[]} memberAccesses
623
+ * @param {string[]} stringLiterals
624
+ * @returns {string | null}
625
+ */
626
+ function inferPurpose(callees, memberAccesses, stringLiterals) {
627
+ const joined = [
628
+ ...callees,
629
+ ...memberAccesses,
630
+ ...stringLiterals,
631
+ ].join(' ').toLowerCase();
632
+
633
+ if (/fetch|request|http|post|get|api/.test(joined)) return 'http/api';
634
+ if (/stream|chunk|readline|asynciterator/.test(joined)) return 'streaming';
635
+ if (/tool|execute|dispatch|invoke/.test(joined)) return 'tool-execution';
636
+ if (/message|prompt|conversation|chat/.test(joined)) return 'messaging';
637
+ if (/render|jsx|react|component/.test(joined)) return 'ui/render';
638
+ if (/parse|json|deserializ/.test(joined)) return 'parsing';
639
+ if (/auth|token|key|credential/.test(joined)) return 'auth';
640
+ if (/file|read|write|fs|path/.test(joined)) return 'filesystem';
641
+ if (/error|throw|catch|reject/.test(joined)) return 'error-handling';
642
+ if (/config|setting|option|prefer/.test(joined)) return 'config';
643
+ if (/log|debug|warn|info/.test(joined)) return 'logging';
644
+ return null;
645
+ }
646
+
647
+ /**
648
+ * Ensure an evidence entry exists for `node` and return it.
649
+ *
650
+ * @param {import('@babel/types').Node} node
651
+ */
652
+ function ensureEvidence(node) {
653
+ if (!evidence.has(node)) {
654
+ evidence.set(node, { callees: [], memberAccesses: [], stringLiterals: [] });
655
+ }
656
+ return evidence.get(node);
657
+ }
658
+
659
+ /**
660
+ * Push a named function node onto the scope stack.
661
+ *
662
+ * @param {string} name
663
+ * @param {import('@babel/types').Node} node
664
+ * @param {string} kind
665
+ */
666
+ function pushScope(name, node, kind) {
667
+ ensureEvidence(node);
668
+ scopeStack.push({ name, node, kind });
669
+ }
670
+
671
+ /** Pop the innermost scope and register it in fnMap. */
672
+ function popScope() {
673
+ const frame = scopeStack.pop();
674
+ if (!frame) return;
675
+
676
+ // Only register the first definition for each name
677
+ if (fnMap.has(frame.name)) return;
678
+
679
+ const ev = evidence.get(frame.node) ?? { callees: [], memberAccesses: [], stringLiterals: [] };
680
+ const inferredPurpose = inferPurpose(ev.callees, ev.memberAccesses, ev.stringLiterals);
681
+
682
+ fnMap.set(frame.name, {
683
+ node: frame.node,
684
+ kind: frame.kind,
685
+ callees: ev.callees,
686
+ memberAccesses: ev.memberAccesses,
687
+ stringLiterals: ev.stringLiterals,
688
+ inferredPurpose,
689
+ });
690
+
691
+ // Free the evidence node — we no longer need it
692
+ evidence.delete(frame.node);
693
+ }
694
+
695
+ /**
696
+ * Add evidence to all functions currently in scope (innermost receives it,
697
+ * but we attribute to all ancestors for the purposes of inferredPurpose,
698
+ * which mirrors what a human would see by reading nested functions).
699
+ *
700
+ * For memory efficiency we only attribute to the innermost scope frame.
701
+ *
702
+ * @param {'callees'|'memberAccesses'|'stringLiterals'} kind
703
+ * @param {string} value
704
+ */
705
+ function addEvidence(kind, value) {
706
+ if (scopeStack.length === 0) return;
707
+ const frame = scopeStack[scopeStack.length - 1];
708
+ const ev = ensureEvidence(frame.node);
709
+ ev[kind].push(value);
710
+ }
711
+
712
+ // ---------------------------------------------------------------------------
713
+ // Single-pass traversal
714
+ // ---------------------------------------------------------------------------
715
+ traverseBundle(ast, {
716
+ // Enter handlers: push scope frames
717
+ FunctionDeclaration: {
718
+ enter(path) {
719
+ const name = path.node.id?.name;
720
+ if (name) pushScope(name, path.node, 'declaration');
721
+ },
722
+ exit(path) {
723
+ if (path.node.id?.name) popScope();
724
+ },
725
+ },
726
+
727
+ FunctionExpression: {
728
+ enter(path) {
729
+ // Only track if the parent is a VariableDeclarator with an Identifier id
730
+ if (
731
+ t.isVariableDeclarator(path.parent) &&
732
+ t.isIdentifier(path.parent.id)
733
+ ) {
734
+ pushScope(path.parent.id.name, path.node, 'expression');
735
+ } else if (path.node.id?.name) {
736
+ // Named function expression
737
+ pushScope(path.node.id.name, path.node, 'expression');
738
+ }
739
+ },
740
+ exit(path) {
741
+ const hasFrame =
742
+ (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) ||
743
+ path.node.id?.name;
744
+ if (hasFrame) popScope();
745
+ },
746
+ },
747
+
748
+ ArrowFunctionExpression: {
749
+ enter(path) {
750
+ if (
751
+ t.isVariableDeclarator(path.parent) &&
752
+ t.isIdentifier(path.parent.id)
753
+ ) {
754
+ pushScope(path.parent.id.name, path.node, 'arrow');
755
+ }
756
+ },
757
+ exit(path) {
758
+ if (
759
+ t.isVariableDeclarator(path.parent) &&
760
+ t.isIdentifier(path.parent.id)
761
+ ) {
762
+ popScope();
763
+ }
764
+ },
765
+ },
766
+
767
+ ClassMethod: {
768
+ enter(path) {
769
+ const key = path.node.key;
770
+ const name = t.isIdentifier(key)
771
+ ? key.name
772
+ : t.isStringLiteral(key)
773
+ ? key.value
774
+ : null;
775
+ if (name) pushScope(name, path.node, 'method');
776
+ },
777
+ exit(path) {
778
+ const key = path.node.key;
779
+ if (t.isIdentifier(key) || t.isStringLiteral(key)) popScope();
780
+ },
781
+ },
782
+
783
+ ObjectMethod: {
784
+ enter(path) {
785
+ const key = path.node.key;
786
+ const name = t.isIdentifier(key)
787
+ ? key.name
788
+ : t.isStringLiteral(key)
789
+ ? key.value
790
+ : null;
791
+ if (name) pushScope(name, path.node, 'method');
792
+ },
793
+ exit(path) {
794
+ const key = path.node.key;
795
+ if (t.isIdentifier(key) || t.isStringLiteral(key)) popScope();
796
+ },
797
+ },
798
+
799
+ // Evidence collectors (add to innermost scope only)
800
+ CallExpression(path) {
801
+ const callee = path.node.callee;
802
+ if (t.isIdentifier(callee)) {
803
+ addEvidence('callees', callee.name);
804
+ } else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
805
+ addEvidence('callees', callee.property.name);
806
+ }
807
+ },
808
+
809
+ MemberExpression(path) {
810
+ if (
811
+ t.isIdentifier(path.node.object) &&
812
+ t.isIdentifier(path.node.property) &&
813
+ !path.node.computed
814
+ ) {
815
+ addEvidence(
816
+ 'memberAccesses',
817
+ `${path.node.object.name}.${path.node.property.name}`
818
+ );
819
+ }
820
+ },
821
+
822
+ StringLiteral(path) {
823
+ const val = path.node.value;
824
+ // Keep only short, meaningful strings; discard base64, long hashes, etc.
825
+ if (
826
+ val.length > 0 &&
827
+ val.length < 120 &&
828
+ !/^[A-Za-z0-9+/]{40,}=*$/.test(val)
829
+ ) {
830
+ addEvidence('stringLiterals', val);
831
+ }
832
+ },
833
+ });
834
+
835
+ return fnMap;
836
+ }
837
+
838
+ // ---------------------------------------------------------------------------
839
+ // findCliExports
840
+ // ---------------------------------------------------------------------------
841
+
842
+ /**
843
+ * @typedef {Object} CliExportEntry
844
+ * @property {string|null} name - Minified identifier name, if discoverable
845
+ * @property {import('@babel/types').Node} node - The AST node
846
+ * @property {import('@babel/traverse').NodePath} path - The Babel path
847
+ * @property {number} confidence - 0.0–1.0 match score
848
+ * @property {string[]} [paramKeys] - (agentLoop only) matched param keys
849
+ */
850
+
851
+ /**
852
+ * @typedef {Object} CliExports
853
+ * @property {CliExportEntry} agentLoop - The main agent loop async generator
854
+ * @property {CliExportEntry} appStateFactory - The app-state initializer
855
+ * @property {{ path, node }|null} topLevelGuard - The top-level CLI invocation guard
856
+ * @property {{ name: string, count: number }|null} moduleWrapper - esbuild lazy-init wrapper
857
+ * @property {string[]} lazyInitWrappers - Names of lazy-init vars (L/p wrappers) that
858
+ * need to be called to initialize null-vars used
859
+ * by the agent loop (e.g. LRU cache class jN)
860
+ * @property {string|null} configEnabler - Name of the function that sets the config-access
861
+ * guard (e.g. h$6) — must be called before agentLoop
862
+ */
863
+
864
+ // ---------------------------------------------------------------------------
865
+ // Pass-E fingerprint helpers (utility functions)
866
+ // ---------------------------------------------------------------------------
867
+
868
+ /** Permission mode strings used as fingerprint evidence */
869
+ const PERMISSION_MODES = ['bypassPermissions', 'acceptEdits', 'plan', 'default'];
870
+
871
+ /**
872
+ * Recursively scan a node for string literals matching a target set.
873
+ * Returns the number of distinct matches found.
874
+ *
875
+ * @param {import('@babel/types').Node} node
876
+ * @param {string[]} targets
877
+ * @param {number} [maxDepth]
878
+ * @returns {number}
879
+ */
880
+ function _countDistinctStrings(node, targets, maxDepth = 10) {
881
+ const found = new Set();
882
+ function scan(n, depth) {
883
+ if (!n || typeof n !== 'object' || depth <= 0) return;
884
+ if (n.type === 'StringLiteral' && targets.includes(n.value)) found.add(n.value);
885
+ for (const key of Object.keys(n)) {
886
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
887
+ const child = n[key];
888
+ if (Array.isArray(child)) { for (const c of child) { if (c?.type) scan(c, depth - 1); } }
889
+ else if (child?.type) scan(child, depth - 1);
890
+ }
891
+ }
892
+ scan(node, maxDepth);
893
+ return found.size;
894
+ }
895
+
896
+ /**
897
+ * Recursively scan a node for any Identifier with a specific name.
898
+ *
899
+ * @param {import('@babel/types').Node} node
900
+ * @param {string} name
901
+ * @param {number} [maxDepth]
902
+ * @returns {boolean}
903
+ */
904
+ function _hasIdentifier(node, name, maxDepth = 12) {
905
+ function scan(n, depth) {
906
+ if (!n || typeof n !== 'object' || depth <= 0) return false;
907
+ if (n.type === 'Identifier' && n.name === name) return true;
908
+ for (const key of Object.keys(n)) {
909
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
910
+ const child = n[key];
911
+ if (Array.isArray(child)) { for (const c of child) { if (c?.type && scan(c, depth - 1)) return true; } }
912
+ else if (child?.type) { if (scan(child, depth - 1)) return true; }
913
+ }
914
+ return false;
915
+ }
916
+ return scan(node, maxDepth);
917
+ }
918
+
919
+ /**
920
+ * Check if a function node body contains a `switch` on PERMISSION_MODES strings.
921
+ * This is the fingerprint for setPermissionModeFn (f4A pattern).
922
+ *
923
+ * @param {import('@babel/types').Node} body
924
+ * @returns {boolean}
925
+ */
926
+ function _hasSwitchOnPermissionModes(body) {
927
+ if (!body) return false;
928
+ // Look for SwitchStatement cases with PERMISSION_MODE values
929
+ function scan(n, depth = 0) {
930
+ if (!n || typeof n !== 'object' || depth > 6) return false;
931
+ if (n.type === 'SwitchStatement') {
932
+ if (!n.cases) return false;
933
+ const caseVals = n.cases
934
+ .filter(c => c.test?.type === 'StringLiteral')
935
+ .map(c => c.test.value);
936
+ const matched = PERMISSION_MODES.filter(m => caseVals.includes(m));
937
+ if (matched.length >= 3) return true;
938
+ }
939
+ for (const key of Object.keys(n)) {
940
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
941
+ const child = n[key];
942
+ if (Array.isArray(child)) { for (const c of child) { if (c?.type && scan(c, depth + 1)) return true; } }
943
+ else if (child?.type) { if (scan(child, depth + 1)) return true; }
944
+ }
945
+ return false;
946
+ }
947
+ return scan(body);
948
+ }
949
+
950
+ /**
951
+ * Check if a function body has a switch statement that RETURNS one of the
952
+ * PERMISSION_MODE strings directly from its cases (f4A pattern — normalizer).
953
+ * Returns confidence 0-1.
954
+ *
955
+ * @param {import('@babel/types').Node} body
956
+ * @returns {number} 0 = not a match, 0.9 = strong match
957
+ */
958
+ function _permissionModeNormalizerConfidence(body) {
959
+ if (!body) return 0;
960
+ function scan(n, depth = 0) {
961
+ if (!n || typeof n !== 'object' || depth > 6) return 0;
962
+ if (n.type === 'SwitchStatement') {
963
+ // Cases should return the same string as their test value
964
+ let matchingCases = 0;
965
+ for (const c of (n.cases ?? [])) {
966
+ if (
967
+ c.test?.type === 'StringLiteral' &&
968
+ PERMISSION_MODES.includes(c.test.value) &&
969
+ c.consequent?.length >= 1 &&
970
+ c.consequent[0]?.type === 'ReturnStatement' &&
971
+ c.consequent[0].argument?.type === 'StringLiteral' &&
972
+ PERMISSION_MODES.includes(c.consequent[0].argument.value)
973
+ ) {
974
+ matchingCases++;
975
+ }
976
+ }
977
+ if (matchingCases >= 3) return 0.95;
978
+ if (matchingCases >= 2) return 0.7;
979
+ }
980
+ let best = 0;
981
+ for (const key of Object.keys(n)) {
982
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
983
+ const child = n[key];
984
+ if (Array.isArray(child)) {
985
+ for (const c of child) { if (c?.type) { const r = scan(c, depth + 1); if (r > best) best = r; } }
986
+ } else if (child?.type) { const r = scan(child, depth + 1); if (r > best) best = r; }
987
+ }
988
+ return best;
989
+ }
990
+ return scan(body);
991
+ }
992
+
993
+ /**
994
+ * Check if a function returns a string containing 'claude-' in at least one
995
+ * return statement. Returns confidence 0-1.
996
+ *
997
+ * @param {import('@babel/types').Node} body
998
+ * @returns {number}
999
+ */
1000
+ function _returnsClaudeString(body) {
1001
+ if (!body) return 0;
1002
+ // For a function body, scan for ReturnStatement → StringLiteral or template literal
1003
+ // containing 'claude-' or returns a call that resolves to such a string
1004
+ let hasDirectReturn = false;
1005
+ let hasIndirectReturn = false;
1006
+
1007
+ function scan(n, depth = 0) {
1008
+ if (!n || typeof n !== 'object' || depth > 8) return;
1009
+ if (n.type === 'ReturnStatement') {
1010
+ const arg = n.argument;
1011
+ if (!arg) return;
1012
+ if (arg.type === 'StringLiteral' && arg.value?.includes('claude-')) {
1013
+ hasDirectReturn = true; return;
1014
+ }
1015
+ // Return of a template literal with claude-
1016
+ if (arg.type === 'TemplateLiteral') {
1017
+ const has = arg.quasis?.some(q => q.value?.cooked?.includes('claude-'));
1018
+ if (has) { hasDirectReturn = true; return; }
1019
+ }
1020
+ // Return of a call expression (RZ pattern: returns Ko(), nu(), etc.)
1021
+ if (arg.type === 'CallExpression') {
1022
+ hasIndirectReturn = true;
1023
+ }
1024
+ // Return of string concat with claude-
1025
+ if (arg.type === 'BinaryExpression' && arg.operator === '+') {
1026
+ function scanBinary(bn, d2 = 0) {
1027
+ if (!bn || d2 > 3) return false;
1028
+ if (bn.type === 'StringLiteral' && bn.value?.includes('claude-')) return true;
1029
+ if (bn.type === 'BinaryExpression') return scanBinary(bn.left, d2+1) || scanBinary(bn.right, d2+1);
1030
+ return false;
1031
+ }
1032
+ if (scanBinary(arg)) hasDirectReturn = true;
1033
+ }
1034
+ }
1035
+ for (const key of Object.keys(n)) {
1036
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
1037
+ const child = n[key];
1038
+ if (Array.isArray(child)) { for (const c of child) { if (c?.type) scan(c, depth + 1); } }
1039
+ else if (child?.type) scan(child, depth + 1);
1040
+ }
1041
+ }
1042
+ scan(body);
1043
+
1044
+ if (hasDirectReturn) return 0.9;
1045
+ if (hasIndirectReturn && _countDistinctStrings(body, ['claude-3', 'claude-sonnet', 'claude-opus', 'claude-haiku'], 3) > 0) return 0.7;
1046
+ // Indirect: contains 'claude-' in body strings
1047
+ if (_countDistinctStrings(body, ['claude-3', 'claude-sonnet', 'claude-opus'], 5) > 0) return 0.5;
1048
+ return 0;
1049
+ }
1050
+
1051
+ /**
1052
+ * Keys we expect to find in the agentLoop's destructured parameter object.
1053
+ * These are the real parameter names before minification (the bundle keeps
1054
+ * them as object property keys even though the local variables are renamed).
1055
+ */
1056
+ const AGENT_LOOP_PARAM_KEYS = [
1057
+ 'prompt', 'cwd', 'tools', 'maxTurns', 'commands', 'mcpClients', 'verbose',
1058
+ 'canUseTool', 'mutableMessages', 'abortController', 'getAppState', 'setAppState',
1059
+ 'messageQueueManager', 'promptUuid', 'includePartialMessages', 'agents',
1060
+ 'replayUserMessages', 'customSystemPrompt', 'appendSystemPrompt',
1061
+ 'userSpecifiedModel', 'fallbackModel',
1062
+ ];
1063
+
1064
+ /**
1065
+ * Keys we expect in appStateFactory's return object.
1066
+ */
1067
+ const APP_STATE_RETURN_KEYS = [
1068
+ 'mainLoopModel', 'toolPermissionContext', 'mcp', 'settings', 'verbose',
1069
+ 'plugins', 'todos', 'agentDefinitions',
1070
+ ];
1071
+
1072
+ /**
1073
+ * Perform a single traversal of the AST to fingerprint and locate:
1074
+ * - agentLoop: the top-level async generator* that accepts the standard
1075
+ * agent-loop parameter bag (prompt, cwd, tools, getAppState, …)
1076
+ * - appStateFactory: the factory function that returns the app-state object
1077
+ * (contains mainLoopModel, toolPermissionContext, …)
1078
+ * - topLevelGuard: the ExpressionStatement at Program body level whose
1079
+ * expression is a bare CallExpression (the CLI auto-start guard)
1080
+ * - moduleWrapper: the most-called esbuild lazy-init wrapper function
1081
+ *
1082
+ * All fingerprinting is evidence-based: we count how many expected signals
1083
+ * are present and compute a confidence score in [0, 1].
1084
+ *
1085
+ * @param {import('@babel/types').File} ast
1086
+ * @returns {CliExports}
1087
+ */
1088
+ function findCliExports(ast) {
1089
+ /** Candidates for agentLoop: async function* with destructured param */
1090
+ const agentLoopCandidates = [];
1091
+ /** Candidates for appStateFactory: sync function that returns a large state object */
1092
+ const appStateCandidates = [];
1093
+ /** Top-level bare CallExpression (the CLI guard) */
1094
+ let topLevelGuard = null;
1095
+ /** esbuild lazy-init wrapper usage counts */
1096
+ const wrapperCounts = new Map();
1097
+
1098
+ // ── Module-wrapper pass: look at top-level VariableDeclarations ────────────
1099
+ for (const stmt of ast.program.body) {
1100
+ if (stmt.type !== 'VariableDeclaration') continue;
1101
+ for (const decl of stmt.declarations) {
1102
+ if (decl.id?.type !== 'Identifier') continue;
1103
+ if (!decl.init || decl.init.type !== 'CallExpression') continue;
1104
+ const callee = decl.init.callee;
1105
+ if (callee.type !== 'Identifier') continue;
1106
+ const firstArg = decl.init.arguments[0];
1107
+ if (!firstArg) continue;
1108
+ if (
1109
+ firstArg.type !== 'FunctionExpression' &&
1110
+ firstArg.type !== 'ArrowFunctionExpression'
1111
+ ) continue;
1112
+ wrapperCounts.set(callee.name, (wrapperCounts.get(callee.name) ?? 0) + 1);
1113
+ }
1114
+ }
1115
+
1116
+ // Find the top-level guard (last bare CallExpression at program body)
1117
+ // We look for the last ExpressionStatement whose expression is a CallExpression
1118
+ // at the very top level, using a simple loop (no traversal needed).
1119
+ for (const stmt of ast.program.body) {
1120
+ if (stmt.type !== 'ExpressionStatement') continue;
1121
+ const expr = stmt.expression;
1122
+ if (expr.type !== 'CallExpression') continue;
1123
+ const callee = expr.callee;
1124
+ // Must be a bare Identifier call (not a method call) to be the guard
1125
+ if (callee.type !== 'Identifier') continue;
1126
+ // Keep overwriting — we want the LAST one
1127
+ topLevelGuard = { path: null, node: stmt, callName: callee.name };
1128
+ }
1129
+
1130
+ // ── Pass A: agentLoop + topLevelGuard (no nested path.traverse) ─────────────
1131
+ //
1132
+ // IMPORTANT: We must NOT mix visitors that call path.traverse() internally
1133
+ // with other node-type visitors in the same traverse() call. Babel's
1134
+ // traversal state can be disturbed by nested path.traverse() calls, causing
1135
+ // some nodes to be silently skipped.
1136
+ //
1137
+ // Therefore agentLoop (pure param inspection — no nesting) and
1138
+ // appStateFactory (needs nested traversal) are kept in SEPARATE passes.
1139
+ traverse(ast, {
1140
+ // ----- Async generators → agentLoop candidates -------------------------
1141
+ FunctionDeclaration(path) {
1142
+ const node = path.node;
1143
+ if (!node.async || !node.generator) return;
1144
+ if (!node.params || node.params.length < 1) return;
1145
+
1146
+ // The agentLoop must have exactly 1 parameter that is an ObjectPattern
1147
+ const param = node.params[0];
1148
+ if (param.type !== 'ObjectPattern') return;
1149
+
1150
+ // Collect the property key names from the destructured parameter
1151
+ const presentKeys = param.properties
1152
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1153
+ .map(p => p.key.name);
1154
+
1155
+ // Count how many expected keys are present
1156
+ const matched = presentKeys.filter(k => AGENT_LOOP_PARAM_KEYS.includes(k));
1157
+
1158
+ // Need at least 4 to be a real candidate
1159
+ if (matched.length < 4) return;
1160
+
1161
+ // Infer the visible name — FunctionDeclaration always has node.id
1162
+ const name = node.id?.name ?? null;
1163
+ const confidence = matched.length / AGENT_LOOP_PARAM_KEYS.length;
1164
+
1165
+ agentLoopCandidates.push({
1166
+ name,
1167
+ node,
1168
+ path,
1169
+ confidence,
1170
+ paramKeys: presentKeys,
1171
+ matchedKeys: matched,
1172
+ });
1173
+ },
1174
+
1175
+ // ----- ExpressionStatement at Program level → top-level guard ---------
1176
+ ExpressionStatement(path) {
1177
+ if (path.parent?.type !== 'Program') return;
1178
+ const expr = path.node.expression;
1179
+ if (expr.type !== 'CallExpression') return;
1180
+ const callee = expr.callee;
1181
+ if (callee.type !== 'Identifier') return;
1182
+ // Keep overwriting — last one wins
1183
+ topLevelGuard = { path, node: path.node, callName: callee.name };
1184
+ },
1185
+ });
1186
+
1187
+ // ── Pass B: appStateFactory (separate pass to avoid nested-traverse interference)
1188
+ traverse(ast, {
1189
+ // ----- Regular functions → appStateFactory candidates -----------------
1190
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
1191
+ const node = path.node;
1192
+ // Not async generators — the factory is a simple sync function
1193
+ if (node.async && node.generator) return;
1194
+
1195
+ let returnedKeys = [];
1196
+ let hasMainLoopModel = false;
1197
+ let hasToolPermissionContext = false;
1198
+
1199
+ // Use path.traverse() for the inner scan — safe because this is now
1200
+ // in its own dedicated outer-traversal pass.
1201
+ path.traverse({
1202
+ ReturnStatement(innerPath) {
1203
+ const arg = innerPath.node.argument;
1204
+ if (arg?.type === 'ObjectExpression') {
1205
+ for (const prop of arg.properties) {
1206
+ if (prop.type === 'ObjectProperty' && prop.key?.type === 'Identifier') {
1207
+ returnedKeys.push(prop.key.name);
1208
+ }
1209
+ }
1210
+ }
1211
+ },
1212
+ Identifier(innerPath) {
1213
+ if (innerPath.node.name === 'mainLoopModel') hasMainLoopModel = true;
1214
+ if (innerPath.node.name === 'toolPermissionContext') hasToolPermissionContext = true;
1215
+ },
1216
+ });
1217
+
1218
+ if (!hasMainLoopModel) return;
1219
+
1220
+ let name = null;
1221
+ if (node.id?.name) name = node.id.name;
1222
+ else if (
1223
+ t.isVariableDeclarator(path.parent) &&
1224
+ t.isIdentifier(path.parent.id)
1225
+ ) {
1226
+ name = path.parent.id.name;
1227
+ }
1228
+
1229
+ const matchedReturnKeys = returnedKeys.filter(k => APP_STATE_RETURN_KEYS.includes(k));
1230
+ const confidence =
1231
+ (hasMainLoopModel ? 0.4 : 0) +
1232
+ (hasToolPermissionContext ? 0.3 : 0) +
1233
+ Math.min(matchedReturnKeys.length / APP_STATE_RETURN_KEYS.length, 1) * 0.3;
1234
+
1235
+ appStateCandidates.push({ name, node, path, confidence, returnedKeys });
1236
+ },
1237
+ });
1238
+
1239
+ // ── Pass C: find lazy-init wrappers (L/p calls) that initialize null vars
1240
+ // used by LRU-cache-like constructors (zW4 pattern).
1241
+ //
1242
+ // These wrappers must be called (triggered) before the agentLoop runs, to
1243
+ // ensure that classes like jN (the LRU cache implementation) are assigned.
1244
+ //
1245
+ // Strategy:
1246
+ // 1. Find all null-initialized top-level vars.
1247
+ // 2. Find all L(...)/p(...) wrappers that ASSIGN those vars to a ClassExpression.
1248
+ // 3. Return the wrapper variable names so injectCliExports can call them.
1249
+ //
1250
+ // We use a two-sub-pass approach (no nested traverse) to avoid interference.
1251
+ /** @type {Set<string>} null-initialized variable names at top level */
1252
+ const nullInitVars = new Set();
1253
+ /** @type {Map<string,string>} wrapperVarName → nullVarName it initializes */
1254
+ const lazyInitMap = new Map();
1255
+
1256
+ // Sub-pass C1: collect null-initialized vars
1257
+ for (const stmt of ast.program.body) {
1258
+ if (stmt.type !== 'VariableDeclaration') continue;
1259
+ for (const decl of stmt.declarations) {
1260
+ if (
1261
+ decl.id?.type === 'Identifier' &&
1262
+ (decl.init === null || decl.init?.type === 'NullLiteral')
1263
+ ) {
1264
+ nullInitVars.add(decl.id.name);
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ // Sub-pass C2: find L/p wrapper vars whose arrow body assigns any null-initialized var
1270
+ // to any value. This catches all module-init wrappers (not just LRU cache classes).
1271
+ //
1272
+ // We collect ALL such wrappers so we can call them all before agentLoop runs,
1273
+ // mimicking the lazy initialization that normally happens during CLI startup.
1274
+ for (const stmt of ast.program.body) {
1275
+ if (stmt.type !== 'VariableDeclaration') continue;
1276
+ for (const decl of stmt.declarations) {
1277
+ if (decl.id?.type !== 'Identifier') continue;
1278
+ const init = decl.init;
1279
+ if (!init || init.type !== 'CallExpression') continue;
1280
+ const callee = init.callee;
1281
+ if (!callee || callee.type !== 'Identifier') continue;
1282
+ // Must be an L or p wrapper call
1283
+ if (callee.name !== 'L' && callee.name !== 'p') continue;
1284
+ const arg0 = init.arguments[0];
1285
+ if (!arg0 || (arg0.type !== 'ArrowFunctionExpression' && arg0.type !== 'FunctionExpression')) continue;
1286
+
1287
+ // Scan the arg body for any assignment of a null-initialized var to ANY value
1288
+ const body = arg0.body?.body ?? (arg0.body ? [arg0.body] : []);
1289
+ let foundNullVarAssign = false;
1290
+ let assignedVarName = null;
1291
+ for (const s of body) {
1292
+ if (s.type !== 'ExpressionStatement') continue;
1293
+ const expr = s.expression;
1294
+ if (expr?.type !== 'AssignmentExpression') continue;
1295
+ const leftName = expr.left?.type === 'Identifier' ? expr.left.name : null;
1296
+ if (!leftName || !nullInitVars.has(leftName)) continue;
1297
+ // Any right-hand side type counts (ClassExpression, CallExpression, etc.)
1298
+ foundNullVarAssign = true;
1299
+ assignedVarName = leftName;
1300
+ break;
1301
+ }
1302
+
1303
+ if (foundNullVarAssign) {
1304
+ lazyInitMap.set(decl.id.name, assignedVarName);
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ const lazyInitWrappers = Array.from(lazyInitMap.keys());
1310
+
1311
+ // ── Pass E: utility function fingerprinting ──────────────────────────────────
1312
+ //
1313
+ // We look for these named functions (all by AST evidence, never by hardcoded
1314
+ // minified names):
1315
+ //
1316
+ // defaultModelFn — 0-param function that returns a string containing 'claude-'
1317
+ // AND also calls another function (not a simple string literal
1318
+ // return, which would be a version string). The body should
1319
+ // contain at least one 'claude-' string in its call-chain.
1320
+ // Fingerprint: 0 params, returnClaudeString confidence ≥ 0.5,
1321
+ // does NOT contain "claude-cli/" (that's the user-agent fn).
1322
+ //
1323
+ // supportedModelsFn — 0-param function returning array of {value, label?, description?}
1324
+ // objects where value strings contain 'null' or 'sonnet'/'opus'/
1325
+ // 'haiku' (the model picker options).
1326
+ // Fingerprint: 0 params, returns or accesses an array with
1327
+ // model option objects (has both 'value' and 'label'/'description').
1328
+ //
1329
+ // setPermissionModeFn — 1-param function with switch statement on PERMISSION_MODES.
1330
+ // The cases return the same string literals (normalization).
1331
+ // Fingerprint: 1 param, switch cases map modes → modes.
1332
+ //
1333
+ // buildPermissionCtxFn — 1-param function that uses PERMISSION_MODE strings and
1334
+ // pushes/returns a mode value. Different from setPermissionModeFn
1335
+ // in that it also reads settings (has more complex body).
1336
+ // Fingerprint: 1 param, contains PERMISSION_MODE strings,
1337
+ // has array push or conditional return of mode strings,
1338
+ // contains 'default' as fallback return.
1339
+ //
1340
+ // resumeSessionFn — async non-generator function with 1-2 params that contains
1341
+ // 'resume' and 'session' string literals and involves network/API calls.
1342
+ // Fingerprint: async, ≤2 params, contains 'resume' and 'session' strings.
1343
+ //
1344
+ // commandsArrayVar — The lazy-init var that holds the slash commands array.
1345
+ // Fingerprint: top-level var whose init is a call to a lazy-init
1346
+ // wrapper with an arrow returning a large array of command objects
1347
+ // (has objects with 'name', 'description', 'isEnabled' properties).
1348
+
1349
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1350
+ let defaultModelFn = null;
1351
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1352
+ let supportedModelsFn = null;
1353
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1354
+ let setPermissionModeFn = null;
1355
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1356
+ let buildPermissionCtxFn = null;
1357
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1358
+ let resumeSessionFn = null;
1359
+ /** @type {{ name: string|null, confidence: number }|null} */
1360
+ let commandsArrayVar = null;
1361
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1362
+ let permissionChecker = null;
1363
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1364
+ let continueSessionFn = null;
1365
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1366
+ let hookRegistrySetterFn = null;
1367
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1368
+ let hookClearFn = null;
1369
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1370
+ let allowedSettingSourcesSetterFn = null;
1371
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1372
+ let mcpConnectFn = null;
1373
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1374
+ let permissionPromptToolFn = null;
1375
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1376
+ let createLinkedTransportPairFn = null;
1377
+ /** @type {{ name: string|null, node: import('@babel/types').Node, path: import('@babel/traverse').NodePath, confidence: number }|null} */
1378
+ let getAccountInfoFn = null;
1379
+
1380
+ // Helper to check if a function body contains model-list-like content:
1381
+ // returns array with objects having {value, label} or {value, description}
1382
+ function _hasModelListContent(node) {
1383
+ // Look for ObjectExpression with 'value' + ('label' or 'description') keys
1384
+ // where value is null, a string like 'sonnet'/'opus'/'haiku', or a call
1385
+ let count = 0;
1386
+ function scan(n, depth = 0) {
1387
+ if (!n || typeof n !== 'object' || depth > 12) return;
1388
+ if (n.type === 'ObjectExpression') {
1389
+ const keys = n.properties
1390
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1391
+ .map(p => p.key.name);
1392
+ if (keys.includes('value') && (keys.includes('label') || keys.includes('description'))) {
1393
+ // Value is NullLiteral or string or call returning null
1394
+ const vp = n.properties.find(p => p.type === 'ObjectProperty' && p.key?.name === 'value');
1395
+ if (vp?.value?.type === 'NullLiteral' ||
1396
+ vp?.value?.type === 'StringLiteral' ||
1397
+ vp?.value?.type === 'CallExpression') {
1398
+ count++;
1399
+ }
1400
+ }
1401
+ }
1402
+ for (const key of Object.keys(n)) {
1403
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
1404
+ const child = n[key];
1405
+ if (Array.isArray(child)) { for (const c of child) { if (c?.type) scan(c, depth + 1); } }
1406
+ else if (child?.type) scan(child, depth + 1);
1407
+ }
1408
+ }
1409
+ scan(node);
1410
+ return count;
1411
+ }
1412
+
1413
+ // Helper: check for command array pattern in a lazy-init arrow body
1414
+ // The commands array has objects with 'name', 'description', 'isEnabled' keys
1415
+ function _isCommandArrayContent(arrayNode) {
1416
+ if (!arrayNode || arrayNode.type !== 'ArrayExpression') return 0;
1417
+ let count = 0;
1418
+ for (const el of (arrayNode.elements ?? [])) {
1419
+ if (!el) continue;
1420
+ if (el.type === 'Identifier') { count += 0.5; continue; } // spread refs count partially
1421
+ if (el.type === 'SpreadElement') { count += 0.5; continue; }
1422
+ if (el.type === 'ObjectExpression') {
1423
+ const keys = el.properties
1424
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1425
+ .map(p => p.key.name);
1426
+ if (keys.includes('name') && keys.includes('description') && (keys.includes('isEnabled') || keys.includes('type'))) {
1427
+ count++;
1428
+ }
1429
+ }
1430
+ }
1431
+ return count;
1432
+ }
1433
+
1434
+ traverse(ast, {
1435
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
1436
+ const node = path.node;
1437
+ const params = node.params ?? [];
1438
+ const paramCount = params.length;
1439
+
1440
+ // Get the name of this function
1441
+ let name = null;
1442
+ if (node.id?.name) name = node.id.name;
1443
+ else if (
1444
+ t.isVariableDeclarator(path.parent) &&
1445
+ t.isIdentifier(path.parent.id)
1446
+ ) {
1447
+ name = path.parent.id.name;
1448
+ }
1449
+
1450
+ const body = node.body;
1451
+
1452
+ // ── defaultModelFn placeholder — will be filled by sub-pass below ──
1453
+ // (defaultModelFn is found via mainLoopModel context analysis, not inline)
1454
+
1455
+ // ── supportedModelsFn (0 params, body has model-option objects) ──
1456
+ if (paramCount === 0 && !node.async && !node.generator) {
1457
+ const modelObjCount = _hasModelListContent(body);
1458
+ if (modelObjCount >= 1) {
1459
+ const conf = Math.min(0.5 + modelObjCount * 0.15, 0.95);
1460
+ if (!supportedModelsFn || conf > supportedModelsFn.confidence) {
1461
+ supportedModelsFn = { name, node, path, confidence: conf };
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+ // ── setPermissionModeFn (1 param, switch on permission modes, returns same strings) ──
1467
+ if (paramCount === 1 && !node.async && !node.generator) {
1468
+ const switchConf = _permissionModeNormalizerConfidence(body);
1469
+ if (switchConf >= 0.7) {
1470
+ if (!setPermissionModeFn || switchConf > setPermissionModeFn.confidence) {
1471
+ setPermissionModeFn = { name, node, path, confidence: switchConf };
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ // ── buildPermissionCtxFn (1 param, uses PERMISSION_MODE strings, pushes/returns mode) ──
1477
+ // P5B pattern: has 'bypassPermissions' + 'default' + .push() call on an array of modes
1478
+ // The key distinguishing feature: it calls .push() with a permission mode string,
1479
+ // meaning it's BUILDING a priority list rather than mapping/labeling.
1480
+ if (paramCount >= 1 && paramCount <= 3 && !node.async && !node.generator) {
1481
+ const modeCount = _countDistinctStrings(body, PERMISSION_MODES, 8);
1482
+ if (modeCount >= 2) {
1483
+ // Must contain 'bypassPermissions' (the most security-sensitive mode)
1484
+ const hasBypass = _countDistinctStrings(body, ['bypassPermissions'], 6) > 0;
1485
+ // Must NOT be the setPermissionModeFn (which has switch returning same strings)
1486
+ const notNormalizer = _permissionModeNormalizerConfidence(body) < 0.7;
1487
+
1488
+ // Additional distinguisher: must have a .push() call (P5B pattern)
1489
+ // or an array accumulation pattern
1490
+ function _hasPushCall(n, d = 0) {
1491
+ if (!n || typeof n !== 'object' || d > 8) return false;
1492
+ if (n.type === 'CallExpression') {
1493
+ const callee = n.callee;
1494
+ if (callee?.type === 'MemberExpression' && callee.property?.name === 'push') return true;
1495
+ }
1496
+ for (const k of Object.keys(n)) {
1497
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1498
+ const ch = n[k];
1499
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasPushCall(c, d+1)) return true; } }
1500
+ else if (ch?.type) { if (_hasPushCall(ch, d+1)) return true; }
1501
+ }
1502
+ return false;
1503
+ }
1504
+ const hasPush = _hasPushCall(body);
1505
+
1506
+ if (hasBypass && notNormalizer && hasPush) {
1507
+ const conf = Math.min(0.5 + modeCount * 0.15, 0.85);
1508
+ if (!buildPermissionCtxFn || conf > buildPermissionCtxFn.confidence) {
1509
+ buildPermissionCtxFn = { name, node, path, confidence: conf };
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+
1515
+ // ── resumeSessionFn (async, ≤2 params, contains resume-related strings) ──
1516
+ // a56 pattern: async, 2 params, loads session from disk (JSONL) or by session ID.
1517
+ // The primary discriminator vs network-based session loaders:
1518
+ // - handles `continue` (no session ID) by finding most recent session
1519
+ // - handles `resume` (with session ID) by loading JSONL from disk
1520
+ // - calls a hook ('resume') after loading
1521
+ // - returns { messages, sessionId, ... } with MANY fields
1522
+ if (node.async && !node.generator && paramCount <= 2) {
1523
+ // Use substring matching — 'resume' may appear in longer strings
1524
+ function _hasStringContaining(n, substr, maxD = 10) {
1525
+ function sc(nn, d) {
1526
+ if (!nn || typeof nn !== 'object' || d <= 0) return false;
1527
+ if (nn.type === 'StringLiteral' && typeof nn.value === 'string' && nn.value.includes(substr)) return true;
1528
+ for (const k of Object.keys(nn)) {
1529
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1530
+ const ch = nn[k];
1531
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && sc(c, d-1)) return true; } }
1532
+ else if (ch?.type) { if (sc(ch, d-1)) return true; }
1533
+ }
1534
+ return false;
1535
+ }
1536
+ return sc(n, maxD);
1537
+ }
1538
+ const hasResume = _hasStringContaining(body, 'resume', 10);
1539
+ const hasSession = _hasStringContaining(body, 'session', 10) || _hasStringContaining(body, 'Session', 10);
1540
+
1541
+ if (hasResume && hasSession) {
1542
+ // Primary fingerprint: a56 pattern — returns object with messages + sessionId
1543
+ // and handles both sessionId=undefined (continue) and sessionId=string (resume)
1544
+ // Look for: returns object with 'messages' key AND 'sessionId' key (both are disk-based)
1545
+ function _hasReturnWithKeys(n, keys, maxD = 10) {
1546
+ let found = false;
1547
+ function scan(nn, d) {
1548
+ if (!nn || typeof nn !== 'object' || d <= 0 || found) return;
1549
+ if (nn.type === 'ReturnStatement' && nn.argument?.type === 'ObjectExpression') {
1550
+ const retKeys = nn.argument.properties
1551
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1552
+ .map(p => p.key.name);
1553
+ if (keys.every(k => retKeys.includes(k))) { found = true; return; }
1554
+ }
1555
+ for (const k of Object.keys(nn)) {
1556
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1557
+ const ch = nn[k];
1558
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type) scan(c, d-1); } }
1559
+ else if (ch?.type) scan(ch, d-1);
1560
+ }
1561
+ }
1562
+ scan(n, maxD);
1563
+ return found;
1564
+ }
1565
+ const hasSessionIdReturn = _hasReturnWithKeys(body, ['messages', 'sessionId'], 12);
1566
+ const hasContinueHandling = _hasStringContaining(body, 'continue', 8);
1567
+ // Also check for forkSession (a distinctive field in the return object)
1568
+ const hasForkSession = _hasStringContaining(body, 'forkSession', 8);
1569
+
1570
+ // Old tighter fingerprint (for network-based session loaders)
1571
+ const hasAccessToken = _hasStringContaining(body, 'access_token', 8) ||
1572
+ _hasStringContaining(body, 'accessToken', 8);
1573
+ const hasTeleport = _hasStringContaining(body, 'TELEPORT', 8) ||
1574
+ _hasStringContaining(body, 'Sessions API', 8);
1575
+
1576
+ let conf = 0.4;
1577
+ if (hasSessionIdReturn) conf += 0.25;
1578
+ if (hasContinueHandling) conf += 0.15;
1579
+ if (hasForkSession) conf += 0.1;
1580
+ if (hasAccessToken) conf += 0.05;
1581
+ if (hasTeleport) conf += 0.05;
1582
+ // Prefer local-disk based (hasSessionIdReturn) over network-based
1583
+ // If no sessionId in return but has access_token, lower priority
1584
+ if (!hasSessionIdReturn && (hasAccessToken || hasTeleport)) {
1585
+ conf = Math.max(conf, 0.5); // network-based still a candidate
1586
+ }
1587
+ if (!resumeSessionFn || conf > resumeSessionFn.confidence) {
1588
+ resumeSessionFn = { name, node, path, confidence: conf };
1589
+ }
1590
+ }
1591
+ }
1592
+
1593
+ // ── permissionChecker (async, 2-4 params, checks toolPermissionContext.mode,
1594
+ // returns {behavior, updatedInput}) ──
1595
+ // FWY pattern: async function that checks permission context mode and returns
1596
+ // allow/deny/ask behavior objects.
1597
+ // Key discriminators:
1598
+ // - async (not generator)
1599
+ // - 2-4 params
1600
+ // - has 'bypassPermissions' and 'plan' string literals (mode checks)
1601
+ // - returns object with 'behavior' and 'updatedInput' keys
1602
+ // - accesses .mode property on toolPermissionContext
1603
+ if (node.async && !node.generator && paramCount >= 2 && paramCount <= 4) {
1604
+ const hasBypass = _countDistinctStrings(body, ['bypassPermissions'], 10) > 0;
1605
+ const hasPlan = _countDistinctStrings(body, ['plan'], 10) > 0;
1606
+ if (hasBypass && hasPlan) {
1607
+ // Check for {behavior, updatedInput} return
1608
+ function _hasBehaviorUpdatedInput(n, maxD = 10) {
1609
+ let found = false;
1610
+ function sc(nn, d) {
1611
+ if (!nn || typeof nn !== 'object' || d <= 0 || found) return;
1612
+ if (nn.type === 'ObjectExpression') {
1613
+ const keys = nn.properties
1614
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1615
+ .map(p => p.key.name);
1616
+ if (keys.includes('behavior') && keys.includes('updatedInput')) { found = true; return; }
1617
+ }
1618
+ for (const k of Object.keys(nn)) {
1619
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1620
+ const ch = nn[k];
1621
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type) sc(c, d-1); } }
1622
+ else if (ch?.type) sc(ch, d-1);
1623
+ }
1624
+ }
1625
+ sc(n, maxD);
1626
+ return found;
1627
+ }
1628
+ const hasBehaviorUI = _hasBehaviorUpdatedInput(body, 12);
1629
+ // Also check for .mode access (not just string literals)
1630
+ function _hasModeAccess(n, maxD = 8) {
1631
+ function sc(nn, d) {
1632
+ if (!nn || typeof nn !== 'object' || d <= 0) return false;
1633
+ if ((nn.type === 'MemberExpression' || nn.type === 'OptionalMemberExpression') &&
1634
+ nn.property?.name === 'mode') return true;
1635
+ for (const k of Object.keys(nn)) {
1636
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1637
+ const ch = nn[k];
1638
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && sc(c, d-1)) return true; } }
1639
+ else if (ch?.type) { if (sc(ch, d-1)) return true; }
1640
+ }
1641
+ return false;
1642
+ }
1643
+ return sc(n, maxD);
1644
+ }
1645
+ const hasModeAccess = _hasModeAccess(body);
1646
+ // Check for getAppState call (indicator this is the main permission checker)
1647
+ function _hasGetAppState(n, maxD = 8) {
1648
+ function sc(nn, d) {
1649
+ if (!nn || typeof nn !== 'object' || d <= 0) return false;
1650
+ if (nn.type === 'CallExpression' && nn.callee?.type === 'Identifier' &&
1651
+ nn.callee.name === 'getAppState') return true;
1652
+ // Also check _.getAppState() pattern
1653
+ if (nn.type === 'CallExpression' && nn.callee?.type === 'MemberExpression' &&
1654
+ nn.callee.property?.name === 'getAppState') return true;
1655
+ for (const k of Object.keys(nn)) {
1656
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1657
+ const ch = nn[k];
1658
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && sc(c, d-1)) return true; } }
1659
+ else if (ch?.type) { if (sc(ch, d-1)) return true; }
1660
+ }
1661
+ return false;
1662
+ }
1663
+ return sc(n, maxD);
1664
+ }
1665
+ const hasGetAppState = _hasGetAppState(body);
1666
+
1667
+ let conf = 0.4;
1668
+ if (hasBehaviorUI) conf += 0.25;
1669
+ if (hasModeAccess) conf += 0.15;
1670
+ if (hasGetAppState) conf += 0.2;
1671
+ conf = Math.min(conf, 0.95);
1672
+
1673
+ if (!permissionChecker || conf > permissionChecker.confidence) {
1674
+ permissionChecker = { name, node, path, confidence: conf };
1675
+ }
1676
+ }
1677
+ }
1678
+
1679
+ // ── continueSessionFn (async, ≤2 params, reads local JSONL session files,
1680
+ // handles both continue=true and resume=sessionId) ──
1681
+ // a56 pattern: async, 2 params, loads the most recent session when no ID given,
1682
+ // or a specific session by ID/path. Returns { messages, sessionId, ... }.
1683
+ // Key discriminators:
1684
+ // - async, not generator
1685
+ // - ≤2 params
1686
+ // - handles undefined param (continue case)
1687
+ // - returns object with 'messages' AND 'sessionId' AND 'deferredToolUse'
1688
+ // This is SEPARATE from resumeSessionFn (which may be network-based)
1689
+ if (node.async && !node.generator && paramCount <= 2) {
1690
+ // Checks both StringLiteral values AND Identifier names for a given string
1691
+ function _hascSC(n, substr, maxD = 10) {
1692
+ function sc(nn, d) {
1693
+ if (!nn || typeof nn !== 'object' || d <= 0) return false;
1694
+ if (nn.type === 'StringLiteral' && typeof nn.value === 'string' && nn.value.includes(substr)) return true;
1695
+ // Also check Identifiers (shorthand object property keys, variable names)
1696
+ if (nn.type === 'Identifier' && nn.name === substr) return true;
1697
+ for (const k of Object.keys(nn)) {
1698
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1699
+ const ch = nn[k];
1700
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && sc(c, d-1)) return true; } }
1701
+ else if (ch?.type) { if (sc(ch, d-1)) return true; }
1702
+ }
1703
+ return false;
1704
+ }
1705
+ return sc(n, maxD);
1706
+ }
1707
+ const hasDeferredToolUse = _hascSC(body, 'deferredToolUse', 10);
1708
+ const hasSessionId = _hascSC(body, 'sessionId', 10);
1709
+ const hasMessages = _hascSC(body, 'messages', 10);
1710
+ // fileHistorySnapshots is a very specific field only in a56, not in lz5
1711
+ const hasFileHistorySnapshots = _hascSC(body, 'fileHistorySnapshots', 10);
1712
+
1713
+ if (hasDeferredToolUse && hasSessionId && hasMessages) {
1714
+ // Check for return object with deferredToolUse key (strongest signal)
1715
+ function _hasReturnWithDTU(n, maxD = 10) {
1716
+ let found = false;
1717
+ function sc(nn, d) {
1718
+ if (!nn || typeof nn !== 'object' || d <= 0 || found) return;
1719
+ if (nn.type === 'ReturnStatement' && nn.argument?.type === 'ObjectExpression') {
1720
+ const retKeys = nn.argument.properties
1721
+ .filter(p => (p.type === 'ObjectProperty' && p.key?.type === 'Identifier') ||
1722
+ (p.type === 'ObjectProperty' && p.shorthand))
1723
+ .map(p => p.key?.name ?? (p.value?.type === 'Identifier' ? p.value.name : null))
1724
+ .filter(Boolean);
1725
+ if (retKeys.includes('deferredToolUse') && retKeys.includes('messages') && retKeys.includes('sessionId')) {
1726
+ found = true; return;
1727
+ }
1728
+ }
1729
+ for (const k of Object.keys(nn)) {
1730
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1731
+ const ch = nn[k];
1732
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type) sc(c, d-1); } }
1733
+ else if (ch?.type) sc(ch, d-1);
1734
+ }
1735
+ }
1736
+ sc(n, maxD);
1737
+ return found;
1738
+ }
1739
+ const hasDTUReturn = _hasReturnWithDTU(body, 12);
1740
+
1741
+ let conf = 0.3;
1742
+ if (hasDTUReturn) conf += 0.25;
1743
+ // fileHistorySnapshots is THE strongest discriminator for a56 vs lz5
1744
+ if (hasFileHistorySnapshots) conf += 0.35;
1745
+ conf = Math.min(conf, 0.95);
1746
+
1747
+ if (!continueSessionFn || conf > continueSessionFn.confidence) {
1748
+ continueSessionFn = { name, node, path, confidence: conf };
1749
+ }
1750
+ }
1751
+ }
1752
+
1753
+ // ── hookRegistrySetterFn ($66 pattern) ───────────────────────────────────
1754
+ // Push-based hook registry adder. Fingerprint:
1755
+ // - 1 param, not async, not generator
1756
+ // - body contains a for...of loop
1757
+ // - body contains Object.entries() call
1758
+ // - body contains .push(...) call (spreading into array)
1759
+ // - body assigns to a .registeredHooks member expression
1760
+ // This is $66() in v2.1.x: pushes hook matchers into v8.registeredHooks.
1761
+ if (!node.async && !node.generator && paramCount === 1) {
1762
+ const bodyStmts = (body?.type === 'BlockStatement') ? body.body : null;
1763
+ if (bodyStmts && bodyStmts.length >= 1) {
1764
+ // Check for for...of loop with Object.entries()
1765
+ function _hasForOfObjectEntries(n, maxD = 6) {
1766
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
1767
+ if (n.type === 'ForOfStatement') {
1768
+ // Right side should contain Object.entries(...)
1769
+ const right = n.right;
1770
+ if (right?.type === 'CallExpression') {
1771
+ const callee = right.callee;
1772
+ if (callee?.type === 'MemberExpression' &&
1773
+ callee.object?.type === 'Identifier' && callee.object.name === 'Object' &&
1774
+ callee.property?.type === 'Identifier' && callee.property.name === 'entries') {
1775
+ return true;
1776
+ }
1777
+ }
1778
+ }
1779
+ for (const k of Object.keys(n)) {
1780
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1781
+ const ch = n[k];
1782
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasForOfObjectEntries(c, maxD-1)) return true; } }
1783
+ else if (ch?.type) { if (_hasForOfObjectEntries(ch, maxD-1)) return true; }
1784
+ }
1785
+ return false;
1786
+ }
1787
+ // Check for .push(...spread) call
1788
+ function _hasPushSpread(n, maxD = 6) {
1789
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
1790
+ if (n.type === 'CallExpression') {
1791
+ const callee = n.callee;
1792
+ if (callee?.type === 'MemberExpression' && callee.property?.name === 'push') {
1793
+ // Check at least one argument is a SpreadElement
1794
+ if ((n.arguments ?? []).some(a => a.type === 'SpreadElement')) return true;
1795
+ }
1796
+ }
1797
+ for (const k of Object.keys(n)) {
1798
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1799
+ const ch = n[k];
1800
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasPushSpread(c, maxD-1)) return true; } }
1801
+ else if (ch?.type) { if (_hasPushSpread(ch, maxD-1)) return true; }
1802
+ }
1803
+ return false;
1804
+ }
1805
+ // Check for .registeredHooks member assignment
1806
+ function _hasRegisteredHooksAssign(n, maxD = 6) {
1807
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
1808
+ if (n.type === 'AssignmentExpression' && n.left?.type === 'MemberExpression' &&
1809
+ n.left.property?.name === 'registeredHooks') return true;
1810
+ // Also check simple LogicalExpression like: x.registeredHooks = x.registeredHooks || {}
1811
+ if (n.type === 'MemberExpression' && n.property?.name === 'registeredHooks') return true;
1812
+ for (const k of Object.keys(n)) {
1813
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1814
+ const ch = n[k];
1815
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasRegisteredHooksAssign(c, maxD-1)) return true; } }
1816
+ else if (ch?.type) { if (_hasRegisteredHooksAssign(ch, maxD-1)) return true; }
1817
+ }
1818
+ return false;
1819
+ }
1820
+
1821
+ const hasForOf = _hasForOfObjectEntries(body, 6);
1822
+ const hasPushSpread = _hasPushSpread(body, 6);
1823
+ const hasRegHooks = _hasRegisteredHooksAssign(body, 6);
1824
+
1825
+ if (hasForOf && hasPushSpread && hasRegHooks) {
1826
+ const conf = 0.95;
1827
+ if (!hookRegistrySetterFn || conf > hookRegistrySetterFn.confidence) {
1828
+ hookRegistrySetterFn = { name, node, path, confidence: conf };
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+
1834
+ // ── allowedSettingSourcesSetterFn (Ga0 pattern) ───────────────────────────
1835
+ // 1-param function that assigns to X.allowedSettingSources = param.
1836
+ // Fingerprint: 1 param, body has single AssignmentExpression with
1837
+ // left = MemberExpression where property.name === 'allowedSettingSources'
1838
+ if (!node.async && !node.generator && paramCount === 1) {
1839
+ const bodyStmts2 = (body?.type === 'BlockStatement') ? body.body : null;
1840
+ if (bodyStmts2?.length === 1) {
1841
+ const stmt2 = bodyStmts2[0];
1842
+ if (
1843
+ stmt2?.type === 'ExpressionStatement' &&
1844
+ stmt2.expression?.type === 'AssignmentExpression' &&
1845
+ stmt2.expression.operator === '=' &&
1846
+ stmt2.expression.left?.type === 'MemberExpression' &&
1847
+ stmt2.expression.left.property?.type === 'Identifier' &&
1848
+ stmt2.expression.left.property.name === 'allowedSettingSources'
1849
+ ) {
1850
+ const conf = 0.9;
1851
+ if (!allowedSettingSourcesSetterFn || conf > allowedSettingSourcesSetterFn.confidence) {
1852
+ allowedSettingSourcesSetterFn = { name, node, path, confidence: conf };
1853
+ }
1854
+ }
1855
+ }
1856
+ }
1857
+
1858
+ // ── getAccountInfoFn (hT6 pattern) ─────────────────────────────────────────
1859
+ // 0-param sync function that returns an object with auth account info.
1860
+ // Fingerprint:
1861
+ // - 0 params, not async, not generator
1862
+ // - body returns objects with keys from: tokenSource, apiKeySource,
1863
+ // organization, email, subscription
1864
+ // - body contains 'firstParty' string (early return guard)
1865
+ // - body accesses .emailAddress and .organizationName properties
1866
+ if (paramCount === 0 && !node.async && !node.generator) {
1867
+ const hasFirstParty = _countDistinctStrings(body, ['firstParty'], 4) > 0;
1868
+ if (hasFirstParty) {
1869
+ // Check for properties that are unique to getAccountInformation:
1870
+ // returns object with tokenSource, apiKeySource, email, organization
1871
+ function _hasAuthInfoReturn(n, maxD = 8) {
1872
+ let found = false;
1873
+ function sc(nn, d) {
1874
+ if (!nn || typeof nn !== 'object' || d <= 0 || found) return;
1875
+ if (nn.type === 'ObjectExpression') {
1876
+ const keys = nn.properties
1877
+ .filter(p => p.type === 'ObjectProperty' && p.key?.type === 'Identifier')
1878
+ .map(p => p.key.name);
1879
+ // Must have at least 2 of: tokenSource, apiKeySource, organization, email, subscription
1880
+ const authKeys = ['tokenSource', 'apiKeySource', 'organization', 'email', 'subscription'];
1881
+ const matched = authKeys.filter(k => keys.includes(k));
1882
+ if (matched.length >= 2) { found = true; return; }
1883
+ }
1884
+ for (const k of Object.keys(nn)) {
1885
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1886
+ const ch = nn[k];
1887
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type) sc(c, d-1); } }
1888
+ else if (ch?.type) sc(ch, d-1);
1889
+ }
1890
+ }
1891
+ sc(n, maxD);
1892
+ return found;
1893
+ }
1894
+ // Check for .emailAddress and .organizationName member access
1895
+ function _hasAuthMemberAccess(n, maxD = 8) {
1896
+ let hasEmail = false, hasOrg = false;
1897
+ function sc(nn, d) {
1898
+ if (!nn || typeof nn !== 'object' || d <= 0) return;
1899
+ if ((nn.type === 'MemberExpression' || nn.type === 'OptionalMemberExpression') &&
1900
+ nn.property?.type === 'Identifier') {
1901
+ if (nn.property.name === 'emailAddress') hasEmail = true;
1902
+ if (nn.property.name === 'organizationName') hasOrg = true;
1903
+ }
1904
+ for (const k of Object.keys(nn)) {
1905
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1906
+ const ch = nn[k];
1907
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type) sc(c, d-1); } }
1908
+ else if (ch?.type) sc(ch, d-1);
1909
+ }
1910
+ }
1911
+ sc(n, maxD);
1912
+ return hasEmail && hasOrg;
1913
+ }
1914
+
1915
+ const hasAuthReturn = _hasAuthInfoReturn(body);
1916
+ const hasMemberAccess = _hasAuthMemberAccess(body);
1917
+
1918
+ if (hasAuthReturn || hasMemberAccess) {
1919
+ let conf = 0.5;
1920
+ if (hasAuthReturn) conf += 0.25;
1921
+ if (hasMemberAccess) conf += 0.2;
1922
+ conf = Math.min(conf, 0.95);
1923
+ if (!getAccountInfoFn || conf > getAccountInfoFn.confidence) {
1924
+ getAccountInfoFn = { name, node, path, confidence: conf };
1925
+ }
1926
+ }
1927
+ }
1928
+ }
1929
+
1930
+ // ── permissionPromptToolFn (Be5/Qz5 pattern) ─────────────────────────────────
1931
+ // 2-4 param sync function containing 'stdio' string and '--permission-prompt-tool'
1932
+ // in an error message (template literal), and .createCanUseTool() call.
1933
+ // Fingerprint: 2-4 params, contains 'stdio' literal, contains
1934
+ // '--permission-prompt-tool' in StringLiteral or TemplateLiteral quasi,
1935
+ // contains 'createCanUseTool' property access.
1936
+ // NOTE: In older cli.js versions this was 2-3 params (Be5), newer versions
1937
+ // have 4 params (Qz5). Accept 2-4 to cover both.
1938
+ if (!node.async && !node.generator && paramCount >= 2 && paramCount <= 4) {
1939
+ const hasStdio = _countDistinctStrings(body, ['stdio'], 4) > 0;
1940
+ // '--permission-prompt-tool' may appear in a template literal quasi, not just StringLiteral
1941
+ // Note: it can be deeply nested (depth ~9 in Be5), so use maxD=12
1942
+ function _hasPermPromptToolStr(n, maxD = 12) {
1943
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
1944
+ if (n.type === 'StringLiteral' && n.value?.includes('--permission-prompt-tool')) return true;
1945
+ if (n.type === 'TemplateElement' && n.value?.raw?.includes('--permission-prompt-tool')) return true;
1946
+ for (const k of Object.keys(n)) {
1947
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1948
+ const ch = n[k];
1949
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasPermPromptToolStr(c, maxD-1)) return true; } }
1950
+ else if (ch?.type) { if (_hasPermPromptToolStr(ch, maxD-1)) return true; }
1951
+ }
1952
+ return false;
1953
+ }
1954
+ const hasPermTool = _hasPermPromptToolStr(body);
1955
+ function _hasCreateCanUseTool(n, maxD = 8) {
1956
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
1957
+ if (n.type === 'MemberExpression' && n.property?.name === 'createCanUseTool') return true;
1958
+ for (const k of Object.keys(n)) {
1959
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
1960
+ const ch = n[k];
1961
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasCreateCanUseTool(c, maxD-1)) return true; } }
1962
+ else if (ch?.type) { if (_hasCreateCanUseTool(ch, maxD-1)) return true; }
1963
+ }
1964
+ return false;
1965
+ }
1966
+ const hasCreateCanUseTool = _hasCreateCanUseTool(body);
1967
+ if (hasStdio && hasPermTool && hasCreateCanUseTool) {
1968
+ const conf = 0.9;
1969
+ if (!permissionPromptToolFn || conf > permissionPromptToolFn.confidence) {
1970
+ permissionPromptToolFn = { name, node, path, confidence: conf };
1971
+ }
1972
+ }
1973
+ }
1974
+ },
1975
+ });
1976
+
1977
+ // ── mcpConnectFn (be/vx pattern) ──────────────────────────────────────────────
1978
+ //
1979
+ // Pattern A (older cli.js): top-level VariableDeclaration where init is a
1980
+ // direct wrapper call with an async function as the first argument:
1981
+ // var be = t0(async(A,B,Q) => { ... 'SDK servers should be handled' ... })
1982
+ //
1983
+ // Pattern B (newer cli.js): top-level lazy module wrapper where an assignment
1984
+ // inside the wrapper body assigns the result of a wrapper call with async fn:
1985
+ // var Tf = L(() => { ... vx = Y1(async(A,B,Q) => { ... SDK string ... }) })
1986
+ //
1987
+ // Discriminating strings in the async function body (both patterns):
1988
+ // - 'SDK servers should be handled in print.ts'
1989
+ // - 'sse-ide' AND 'ws-ide' type checks
1990
+ //
1991
+ // Strategy: first try Pattern A (fast, no traverse); if not found, use traverse
1992
+ // to locate the AssignmentExpression containing an async fn with the SDK string.
1993
+ //
1994
+ // Note: _scanForString is defined below (hoisted function declaration).
1995
+
1996
+ // Pattern A: top-level VariableDeclaration with direct async arg
1997
+ for (const stmt of ast.program.body) {
1998
+ if (stmt.type !== 'VariableDeclaration') continue;
1999
+ for (const decl of stmt.declarations) {
2000
+ if (decl.id?.type !== 'Identifier') continue;
2001
+ const init = decl.init;
2002
+ if (!init || init.type !== 'CallExpression') continue;
2003
+ const arg0 = init.arguments?.[0];
2004
+ if (!arg0 || (!arg0.async)) continue;
2005
+ const fn = arg0;
2006
+ const fnBody = fn.body;
2007
+ if (!fnBody) continue;
2008
+ // Check for the discriminating strings
2009
+ const hasSdkHandled = _scanForString(fnBody, 'SDK servers should be handled in print.ts', 12);
2010
+ if (!hasSdkHandled) continue;
2011
+ const hasSseIde = _countDistinctStrings(fnBody, ['sse-ide'], 8) > 0;
2012
+ const hasWsIde = _countDistinctStrings(fnBody, ['ws-ide'], 8) > 0;
2013
+ const conf = 0.5 + (hasSseIde ? 0.2 : 0) + (hasWsIde ? 0.2 : 0);
2014
+ if (!mcpConnectFn || conf > mcpConnectFn.confidence) {
2015
+ mcpConnectFn = { name: decl.id.name, node: fn, path: null, confidence: conf };
2016
+ }
2017
+ }
2018
+ }
2019
+
2020
+ // Pattern B: traverse looking for AssignmentExpression with async fn arg (new v2.x pattern)
2021
+ if (!mcpConnectFn) {
2022
+ traverse(ast, {
2023
+ AssignmentExpression(path) {
2024
+ const node = path.node;
2025
+ if (node.operator !== '=') return;
2026
+ const left = node.left;
2027
+ const right = node.right;
2028
+ // right must be a CallExpression whose first arg is async
2029
+ if (!right || right.type !== 'CallExpression') return;
2030
+ const arg0 = right.arguments?.[0];
2031
+ if (!arg0 || !arg0.async) return;
2032
+ const fnBody = arg0.body;
2033
+ if (!fnBody) return;
2034
+ // Check discriminating strings
2035
+ const hasSdkHandled = _scanForString(fnBody, 'SDK servers should be handled in print.ts', 15);
2036
+ if (!hasSdkHandled) return;
2037
+ const hasSseIde = _countDistinctStrings(fnBody, ['sse-ide'], 10) > 0;
2038
+ const hasWsIde = _countDistinctStrings(fnBody, ['ws-ide'], 10) > 0;
2039
+ const conf = 0.5 + (hasSseIde ? 0.2 : 0) + (hasWsIde ? 0.2 : 0);
2040
+ // Get the variable name being assigned
2041
+ let name = null;
2042
+ if (left.type === 'Identifier') name = left.name;
2043
+ if (!name) return;
2044
+ if (!mcpConnectFn || conf > mcpConnectFn.confidence) {
2045
+ mcpConnectFn = { name, node: arg0, path: null, confidence: conf };
2046
+ }
2047
+ }
2048
+ });
2049
+ }
2050
+
2051
+ // ── hookClearFn (Jj5 pattern) ────────────────────────────────────────────────
2052
+ //
2053
+ // The hook registry clearer: 0-param function that sets v8.registeredHooks = null.
2054
+ // Fingerprint:
2055
+ // - 0 params, not async, not generator
2056
+ // - single statement: assignment of null to <obj>.registeredHooks
2057
+ // - Only top-level FunctionDeclarations (scan program body directly)
2058
+ //
2059
+ // This is Jj5() in v2.1.x.
2060
+ for (const stmt of ast.program.body) {
2061
+ if (stmt.type !== 'FunctionDeclaration') continue;
2062
+ if (!stmt.id?.name) continue;
2063
+ if ((stmt.params ?? []).length !== 0) continue;
2064
+ const bodyStmts = stmt.body?.body ?? [];
2065
+ if (bodyStmts.length !== 1) continue;
2066
+ const s = bodyStmts[0];
2067
+ if (
2068
+ s?.type === 'ExpressionStatement' &&
2069
+ s.expression?.type === 'AssignmentExpression' &&
2070
+ s.expression.operator === '=' &&
2071
+ s.expression.left?.type === 'MemberExpression' &&
2072
+ s.expression.left.property?.name === 'registeredHooks' &&
2073
+ s.expression.right?.type === 'NullLiteral'
2074
+ ) {
2075
+ const conf = 0.95;
2076
+ if (!hookClearFn || conf > hookClearFn.confidence) {
2077
+ hookClearFn = { name: stmt.id.name, node: stmt, path: null, confidence: conf };
2078
+ }
2079
+ }
2080
+ }
2081
+
2082
+ // ── createLinkedTransportPair (gfz pattern) ──────────────────────────────────
2083
+ //
2084
+ // 0-param function that creates two linked $c1 transport instances and returns them
2085
+ // as a [client, server] pair. Used for in-process MCP server connections.
2086
+ //
2087
+ // Fingerprint:
2088
+ // - 0 params, not async, not generator
2089
+ // - body has exactly 2-3 statements
2090
+ // - creates two instances with `new <class>()` using the same constructor
2091
+ // - calls ._setPeer() on each with the other
2092
+ // - returns [q, K] (array of two identifiers)
2093
+ //
2094
+ // Also look for the module export object: { createLinkedTransportPair: () => gfz }
2095
+ // Strategy: scan program body for FunctionDeclaration matching the fingerprint.
2096
+ //
2097
+ // This is gfz() in v2.1.x.
2098
+ for (const stmt of ast.program.body) {
2099
+ if (stmt.type !== 'FunctionDeclaration') continue;
2100
+ if (!stmt.id?.name) continue;
2101
+ if ((stmt.params ?? []).length !== 0) continue;
2102
+ const bodyStmts = stmt.body?.body ?? [];
2103
+ if (bodyStmts.length < 1 || bodyStmts.length > 4) continue;
2104
+
2105
+ // Must have a return statement that returns an ArrayExpression with 2 identifiers.
2106
+ // The return may be:
2107
+ // - Direct: return [q, K]
2108
+ // - Sequence: return q._setPeer(K), K._setPeer(q), [q, K]
2109
+ // In both cases, we find the ArrayExpression.
2110
+ const retStmt = bodyStmts.find(s => s.type === 'ReturnStatement');
2111
+ if (!retStmt) continue;
2112
+ const retArg = retStmt.argument;
2113
+ let retArray = null;
2114
+ if (retArg?.type === 'ArrayExpression') {
2115
+ retArray = retArg;
2116
+ } else if (retArg?.type === 'SequenceExpression') {
2117
+ const exprs = retArg.expressions ?? [];
2118
+ const last = exprs[exprs.length - 1];
2119
+ if (last?.type === 'ArrayExpression') retArray = last;
2120
+ }
2121
+ if (!retArray || retArray.elements?.length !== 2) continue;
2122
+ if (!retArray.elements.every(e => e?.type === 'Identifier')) continue;
2123
+
2124
+ // Must contain a ._setPeer() call
2125
+ let hasSSetPeer = false;
2126
+ function _hasSetPeer(n, maxD = 4) {
2127
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
2128
+ if (n.type === 'CallExpression') {
2129
+ const callee = n.callee;
2130
+ if ((callee?.type === 'MemberExpression' || callee?.type === 'OptionalMemberExpression') &&
2131
+ callee.property?.name === '_setPeer') return true;
2132
+ }
2133
+ for (const k of Object.keys(n)) {
2134
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
2135
+ const ch = n[k];
2136
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasSetPeer(c, maxD-1)) return true; } }
2137
+ else if (ch?.type) { if (_hasSetPeer(ch, maxD-1)) return true; }
2138
+ }
2139
+ return false;
2140
+ }
2141
+ hasSSetPeer = _hasSetPeer(stmt.body, 5);
2142
+ if (!hasSSetPeer) continue;
2143
+
2144
+ // Must use `new` expression (creates transport instances)
2145
+ let hasNew = false;
2146
+ function _hasNewExpr(n, maxD = 4) {
2147
+ if (!n || typeof n !== 'object' || maxD <= 0) return false;
2148
+ if (n.type === 'NewExpression') return true;
2149
+ for (const k of Object.keys(n)) {
2150
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
2151
+ const ch = n[k];
2152
+ if (Array.isArray(ch)) { for (const c of ch) { if (c?.type && _hasNewExpr(c, maxD-1)) return true; } }
2153
+ else if (ch?.type) { if (_hasNewExpr(ch, maxD-1)) return true; }
2154
+ }
2155
+ return false;
2156
+ }
2157
+ hasNew = _hasNewExpr(stmt.body, 5);
2158
+ if (!hasNew) continue;
2159
+
2160
+ const conf = 0.92;
2161
+ if (!createLinkedTransportPairFn || conf > createLinkedTransportPairFn.confidence) {
2162
+ createLinkedTransportPairFn = { name: stmt.id.name, node: stmt, path: null, confidence: conf };
2163
+ }
2164
+ }
2165
+
2166
+ // ── defaultModelFn sub-pass: find the function called as mainLoopModel: fn() ──
2167
+ //
2168
+ // Strategy: scan the appStateFactory's body (and any other function that
2169
+ // contains `mainLoopModel`) for ObjectProperty nodes whose key is
2170
+ // 'mainLoopModel' and whose value is a CallExpression. The callee of that
2171
+ // call is the defaultModelFn candidate.
2172
+ //
2173
+ // We also look for zero-arg functions that:
2174
+ // 1. Have exactly 3 statements in their body
2175
+ // 2. Declare a variable via a call
2176
+ // 3. Do an if-null-check returning another call
2177
+ // 4. Have a final fallback return call
2178
+ // This matches the RZ pattern precisely.
2179
+
2180
+ traverse(ast, {
2181
+ ObjectProperty(path) {
2182
+ const keyNode = path.node.key;
2183
+ if (keyNode?.type !== 'Identifier' || keyNode.name !== 'mainLoopModel') return;
2184
+ const valNode = path.node.value;
2185
+ if (valNode?.type !== 'CallExpression') return;
2186
+ // The callee should be a zero-arg function call
2187
+ const callee = valNode.callee;
2188
+ if ((callee?.type !== 'Identifier') || valNode.arguments.length !== 0) return;
2189
+ const fnName = callee.name;
2190
+ if (!fnName) return;
2191
+ // Set this as the defaultModelFn with high confidence
2192
+ if (!defaultModelFn || 0.9 > defaultModelFn.confidence) {
2193
+ // We store just the name for now — we'll look up the node separately
2194
+ defaultModelFn = { name: fnName, node: null, path: null, confidence: 0.9 };
2195
+ }
2196
+ },
2197
+ });
2198
+
2199
+ // If we found a name via the ObjectProperty scan, look up the actual node
2200
+ if (defaultModelFn?.name && !defaultModelFn.node) {
2201
+ const targetName = defaultModelFn.name;
2202
+ traverse(ast, {
2203
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
2204
+ const node = path.node;
2205
+ let name = null;
2206
+ if (node.id?.name) name = node.id.name;
2207
+ else if (
2208
+ t.isVariableDeclarator(path.parent) &&
2209
+ t.isIdentifier(path.parent.id)
2210
+ ) name = path.parent.id.name;
2211
+ if (name !== targetName) return;
2212
+ defaultModelFn = { name, node, path, confidence: 0.9 };
2213
+ path.stop();
2214
+ },
2215
+ });
2216
+ }
2217
+
2218
+ // ── commandsArrayVar: find top-level lazy-init var whose arrow returns a large
2219
+ // array of command objects (v2.0.x pattern: rTB = t0(() => [...cmd refs...]))
2220
+ // OR an async commands-aggregator function (v2.1.x pattern: Q0(cwd))
2221
+ // ────────────────────────────────────────────────────────────────────────────
2222
+
2223
+ // Pattern A (v2.0.x): top-level var = lazyWrapper(() => [identifiers...])
2224
+ for (const stmt of ast.program.body) {
2225
+ if (stmt.type !== 'VariableDeclaration') continue;
2226
+ for (const decl of stmt.declarations) {
2227
+ if (decl.id?.type !== 'Identifier') continue;
2228
+ const init = decl.init;
2229
+ if (!init || init.type !== 'CallExpression') continue;
2230
+ const arg0 = init.arguments?.[0];
2231
+ if (!arg0 || arg0.type !== 'ArrowFunctionExpression') continue;
2232
+ // The arrow's body should be an array expression (possibly parenthesized)
2233
+ const arrowBody = arg0.body;
2234
+ if (arrowBody?.type !== 'ArrayExpression') continue;
2235
+ const cmdCount = _isCommandArrayContent(arrowBody);
2236
+ if (cmdCount >= 5) {
2237
+ const conf = Math.min(0.5 + cmdCount * 0.04, 0.95);
2238
+ if (!commandsArrayVar || conf > commandsArrayVar.confidence) {
2239
+ commandsArrayVar = { name: decl.id.name, confidence: conf };
2240
+ }
2241
+ }
2242
+ }
2243
+ }
2244
+
2245
+ // Pattern B (v2.1.x): async named function(cwd) that aggregates built-in + custom commands
2246
+ // Fingerprint: async, 1 named param, body is a BlockStatement that:
2247
+ // 1. Has an await call (calls the base commands loader async)
2248
+ // 2. Has a ReturnStatement returning an ArrayExpression (or spread array)
2249
+ // 3. Has a CallExpression with callee.property.name === 'findIndex' somewhere
2250
+ // 4. Has a CallExpression with callee.property.name === 'filter' somewhere
2251
+ // This matches the Q0 pattern: returns deduped merge of built-in and custom commands.
2252
+ if (!commandsArrayVar) {
2253
+ /**
2254
+ * Recursively check if a node contains a MemberExpression callee with given property.
2255
+ */
2256
+ function _hasMethodCall(node, methodName, maxD = 8) {
2257
+ if (!node || typeof node !== 'object' || maxD <= 0) return false;
2258
+ if (node.type === 'CallExpression') {
2259
+ const callee = node.callee;
2260
+ if (callee?.type === 'MemberExpression' && callee.property?.name === methodName) return true;
2261
+ if (callee?.type === 'OptionalMemberExpression' && callee.property?.name === methodName) return true;
2262
+ }
2263
+ for (const k of Object.keys(node)) {
2264
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
2265
+ const ch = node[k];
2266
+ if (Array.isArray(ch)) {
2267
+ for (const c of ch) { if (c?.type && _hasMethodCall(c, methodName, maxD-1)) return true; }
2268
+ } else if (ch?.type) {
2269
+ if (_hasMethodCall(ch, methodName, maxD-1)) return true;
2270
+ }
2271
+ }
2272
+ return false;
2273
+ }
2274
+
2275
+ /**
2276
+ * Check if a body has an await expression anywhere.
2277
+ */
2278
+ function _hasAwait(node, maxD = 6) {
2279
+ if (!node || typeof node !== 'object' || maxD <= 0) return false;
2280
+ if (node.type === 'AwaitExpression') return true;
2281
+ for (const k of Object.keys(node)) {
2282
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'type') continue;
2283
+ const ch = node[k];
2284
+ if (Array.isArray(ch)) {
2285
+ for (const c of ch) { if (c?.type && _hasAwait(c, maxD-1)) return true; }
2286
+ } else if (ch?.type) {
2287
+ if (_hasAwait(ch, maxD-1)) return true;
2288
+ }
2289
+ }
2290
+ return false;
2291
+ }
2292
+
2293
+ /**
2294
+ * Check if a body has a ReturnStatement returning a spread/concat array.
2295
+ */
2296
+ function _hasReturnArray(body) {
2297
+ if (body?.type !== 'BlockStatement') return false;
2298
+ for (const stmt of body.body ?? []) {
2299
+ if (stmt.type !== 'ReturnStatement') continue;
2300
+ const arg = stmt.argument;
2301
+ if (!arg) continue;
2302
+ // return [...a, ...b] or return [...a, ...b, ...c]
2303
+ if (arg.type === 'ArrayExpression') return true;
2304
+ // return z (a variable holding an array) — less certain but acceptable
2305
+ if (arg.type === 'Identifier') return true;
2306
+ }
2307
+ return false;
2308
+ }
2309
+
2310
+ traverse(ast, {
2311
+ 'FunctionDeclaration|FunctionExpression'(path) {
2312
+ const node = path.node;
2313
+ if (!node.async || node.generator) return;
2314
+ if ((node.params ?? []).length !== 1) return;
2315
+ let name = node.id?.name ?? null;
2316
+ if (!name) return;
2317
+ const body = node.body;
2318
+ if (body?.type !== 'BlockStatement') return;
2319
+ // Guard: body should be small (commands aggregator is compact, not a mega function)
2320
+ if ((body.body ?? []).length > 20) return;
2321
+
2322
+ const hasFindIndex = _hasMethodCall(body, 'findIndex');
2323
+ const hasFilter = _hasMethodCall(body, 'filter');
2324
+ const hasAwait = _hasAwait(body);
2325
+ const hasRetArr = _hasReturnArray(body);
2326
+
2327
+ if (hasFindIndex && hasFilter && hasAwait && hasRetArr) {
2328
+ const conf = 0.85;
2329
+ if (!commandsArrayVar || conf > commandsArrayVar.confidence) {
2330
+ commandsArrayVar = { name, confidence: conf };
2331
+ }
2332
+ path.stop();
2333
+ }
2334
+ },
2335
+ });
2336
+ }
2337
+
2338
+ // ── Pass D: find the config enabler function (h$6 pattern) ─────────────────
2339
+ //
2340
+ // The config enabler is a FunctionDeclaration that:
2341
+ // 1. Contains the string literal "enable_configs_started"
2342
+ // 2. Sets a boolean guard to true
2343
+ //
2344
+ // Finding it allows us to call it before agentLoop, ensuring the config
2345
+ // guard is lifted even in SDK mode.
2346
+ let configEnabler = null;
2347
+
2348
+ // Use a simple loop over program body — we only care about FunctionDeclarations
2349
+ // that contain the fingerprint string. We avoid traverse() here to keep this
2350
+ // pass self-contained and fast.
2351
+ function _scanForString(node, target, maxDepth = 8) {
2352
+ if (!node || typeof node !== 'object' || maxDepth <= 0) return false;
2353
+ if (node.type === 'StringLiteral' && node.value === target) return true;
2354
+ for (const key of Object.keys(node)) {
2355
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') continue;
2356
+ const child = node[key];
2357
+ if (Array.isArray(child)) {
2358
+ for (const c of child) { if (c?.type && _scanForString(c, target, maxDepth - 1)) return true; }
2359
+ } else if (child?.type) {
2360
+ if (_scanForString(child, target, maxDepth - 1)) return true;
2361
+ }
2362
+ }
2363
+ return false;
2364
+ }
2365
+
2366
+ for (const stmt of ast.program.body) {
2367
+ if (stmt.type !== 'FunctionDeclaration') continue;
2368
+ if (!stmt.id?.name) continue;
2369
+ // The function must contain "enable_configs_started" string
2370
+ if (_scanForString(stmt.body, 'enable_configs_started')) {
2371
+ configEnabler = stmt.id.name;
2372
+ break;
2373
+ }
2374
+ }
2375
+
2376
+ // ── Pick best candidates ────────────────────────────────────────────────────
2377
+
2378
+ // Sort by confidence (desc), pick the best
2379
+ agentLoopCandidates.sort((a, b) => b.confidence - a.confidence);
2380
+ appStateCandidates.sort((a, b) => b.confidence - a.confidence);
2381
+
2382
+ const bestAgentLoop = agentLoopCandidates[0] ?? null;
2383
+ const bestAppState = appStateCandidates[0] ?? null;
2384
+
2385
+ // Module wrapper: highest-count name
2386
+ let moduleWrapper = null;
2387
+ let maxCount = 0;
2388
+ for (const [name, count] of wrapperCounts) {
2389
+ if (count > maxCount) {
2390
+ maxCount = count;
2391
+ moduleWrapper = { name, count };
2392
+ }
2393
+ }
2394
+
2395
+ // ── Confidence gate ────────────────────────────────────────────────────────
2396
+ if (!bestAgentLoop || bestAgentLoop.confidence < 0.5) {
2397
+ const allCandidateInfo = agentLoopCandidates.slice(0, 5).map(c =>
2398
+ ` ${c.name ?? '(anon)'}: confidence=${c.confidence.toFixed(2)}, ` +
2399
+ `matchedKeys=[${c.matchedKeys?.join(', ')}]`
2400
+ ).join('\n');
2401
+
2402
+ throw new Error(
2403
+ `elwood: agentLoop not found with sufficient confidence.\n` +
2404
+ `Required confidence ≥ 0.5, best found: ${bestAgentLoop ? bestAgentLoop.confidence.toFixed(2) : 'none'}\n` +
2405
+ `\nTop candidates:\n${allCandidateInfo || ' (none)'}\n\n` +
2406
+ `Run scripts/investigate-ast.js for diagnostic output.`
2407
+ );
2408
+ }
2409
+
2410
+ return {
2411
+ agentLoop: {
2412
+ name: bestAgentLoop.name,
2413
+ node: bestAgentLoop.node,
2414
+ path: bestAgentLoop.path,
2415
+ confidence: bestAgentLoop.confidence,
2416
+ paramKeys: bestAgentLoop.paramKeys,
2417
+ },
2418
+ appStateFactory: bestAppState
2419
+ ? {
2420
+ name: bestAppState.name,
2421
+ node: bestAppState.node,
2422
+ path: bestAppState.path,
2423
+ confidence: bestAppState.confidence,
2424
+ }
2425
+ : null,
2426
+ topLevelGuard,
2427
+ moduleWrapper,
2428
+ lazyInitWrappers,
2429
+ configEnabler,
2430
+ // ── Pass E results ────────────────────────────────────────────────────────
2431
+ defaultModelFn: defaultModelFn ? { name: defaultModelFn.name, node: defaultModelFn.node, path: defaultModelFn.path, confidence: defaultModelFn.confidence } : null,
2432
+ supportedModelsFn: supportedModelsFn ? { name: supportedModelsFn.name, node: supportedModelsFn.node, path: supportedModelsFn.path, confidence: supportedModelsFn.confidence } : null,
2433
+ setPermissionModeFn: setPermissionModeFn? { name: setPermissionModeFn.name, node: setPermissionModeFn.node,path: setPermissionModeFn.path, confidence: setPermissionModeFn.confidence } : null,
2434
+ buildPermissionCtxFn:buildPermissionCtxFn?{ name: buildPermissionCtxFn.name,node: buildPermissionCtxFn.node,path: buildPermissionCtxFn.path,confidence: buildPermissionCtxFn.confidence}: null,
2435
+ resumeSessionFn: resumeSessionFn ? { name: resumeSessionFn.name, node: resumeSessionFn.node, path: resumeSessionFn.path, confidence: resumeSessionFn.confidence } : null,
2436
+ commandsArrayVar: commandsArrayVar ? { name: commandsArrayVar.name, confidence: commandsArrayVar.confidence } : null,
2437
+ mcpStateShape: { accessPatterns: ['mcp.clients'] },
2438
+ // New Pass F results
2439
+ permissionChecker: permissionChecker ? { name: permissionChecker.name, node: permissionChecker.node, path: permissionChecker.path, confidence: permissionChecker.confidence } : null,
2440
+ continueSessionFn: continueSessionFn ? { name: continueSessionFn.name, node: continueSessionFn.node, path: continueSessionFn.path, confidence: continueSessionFn.confidence } : null,
2441
+ // New Pass G results (gap fixes)
2442
+ hookRegistrySetterFn: hookRegistrySetterFn ? { name: hookRegistrySetterFn.name, confidence: hookRegistrySetterFn.confidence } : null,
2443
+ hookClearFn: hookClearFn ? { name: hookClearFn.name, confidence: hookClearFn.confidence } : null,
2444
+ allowedSettingSourcesSetterFn: allowedSettingSourcesSetterFn ? { name: allowedSettingSourcesSetterFn.name, confidence: allowedSettingSourcesSetterFn.confidence } : null,
2445
+ mcpConnectFn: mcpConnectFn ? { name: mcpConnectFn.name, confidence: mcpConnectFn.confidence } : null,
2446
+ permissionPromptToolFn: permissionPromptToolFn ? { name: permissionPromptToolFn.name, confidence: permissionPromptToolFn.confidence } : null,
2447
+ createLinkedTransportPairFn: createLinkedTransportPairFn ? { name: createLinkedTransportPairFn.name, confidence: createLinkedTransportPairFn.confidence } : null,
2448
+ getAccountInfoFn: getAccountInfoFn ? { name: getAccountInfoFn.name, confidence: getAccountInfoFn.confidence } : null,
2449
+ };
2450
+ }
2451
+
2452
+ // ---------------------------------------------------------------------------
2453
+ // injectCliExports
2454
+ // ---------------------------------------------------------------------------
2455
+
2456
+ /**
2457
+ * Mutate `ast` in-place to:
2458
+ *
2459
+ * A. Wrap the top-level guard statement (CLI auto-start) so it only runs
2460
+ * when `globalThis.__elwoodImporting` is falsy:
2461
+ *
2462
+ * if (!globalThis.__elwoodImporting) { <original statement> }
2463
+ *
2464
+ * B. Append a register call at the END of the program body:
2465
+ *
2466
+ * if (typeof globalThis.__elwoodRegister === 'function') {
2467
+ * globalThis.__elwoodRegister({
2468
+ * agentLoop: typeof <name> !== 'undefined' ? <name> : undefined,
2469
+ * appStateFactory: typeof <name> !== 'undefined' ? <name> : undefined,
2470
+ * });
2471
+ * }
2472
+ *
2473
+ * Uses `@babel/types` builders throughout — no string templates.
2474
+ *
2475
+ * @param {import('@babel/types').File} ast
2476
+ * @param {CliExports} found - result of findCliExports()
2477
+ */
2478
+ function injectCliExports(ast, found) {
2479
+ // ── A. Suppress auto-execution ────────────────────────────────────────────
2480
+ if (found.topLevelGuard?.path) {
2481
+ const guardPath = found.topLevelGuard.path;
2482
+ const originalNode = found.topLevelGuard.node;
2483
+
2484
+ // Build: !globalThis.__elwoodImporting
2485
+ const notImporting = t.unaryExpression(
2486
+ '!',
2487
+ t.memberExpression(
2488
+ t.identifier('globalThis'),
2489
+ t.identifier('__elwoodImporting')
2490
+ )
2491
+ );
2492
+
2493
+ // Build: if (!globalThis.__elwoodImporting) { <original statement> }
2494
+ const wrapped = t.ifStatement(
2495
+ notImporting,
2496
+ t.blockStatement([t.cloneNode(originalNode, true)])
2497
+ );
2498
+
2499
+ try {
2500
+ guardPath.replaceWith(wrapped);
2501
+ } catch {
2502
+ // Fall back: try removing and re-inserting if replaceWith fails
2503
+ try {
2504
+ guardPath.remove();
2505
+ ast.program.body.push(wrapped);
2506
+ } catch {
2507
+ // Best-effort — if both fail, append to program body
2508
+ ast.program.body.push(wrapped);
2509
+ }
2510
+ }
2511
+ } else if (found.topLevelGuard?.node) {
2512
+ // We have the node but not the path (found via raw loop) — locate it in
2513
+ // program body and wrap it manually
2514
+ const idx = ast.program.body.indexOf(found.topLevelGuard.node);
2515
+ if (idx !== -1) {
2516
+ const notImporting = t.unaryExpression(
2517
+ '!',
2518
+ t.memberExpression(
2519
+ t.identifier('globalThis'),
2520
+ t.identifier('__elwoodImporting')
2521
+ )
2522
+ );
2523
+ const wrapped = t.ifStatement(
2524
+ notImporting,
2525
+ t.blockStatement([t.cloneNode(found.topLevelGuard.node, true)])
2526
+ );
2527
+ ast.program.body[idx] = wrapped;
2528
+ }
2529
+ }
2530
+
2531
+ // ── B. Inject register call at end of program body ────────────────────────
2532
+ //
2533
+ // if (typeof globalThis.__elwoodRegister === 'function') {
2534
+ // globalThis.__elwoodRegister({
2535
+ // agentLoop: typeof <name> !== 'undefined' ? <name> : undefined,
2536
+ // appStateFactory: typeof <name> !== 'undefined' ? <name> : undefined,
2537
+ // });
2538
+ // }
2539
+
2540
+ /**
2541
+ * Build: `typeof <name> !== 'undefined' ? <name> : undefined`
2542
+ * If name is null, just build `undefined`.
2543
+ */
2544
+ function safeRef(name) {
2545
+ if (!name) return t.identifier('undefined');
2546
+ return t.conditionalExpression(
2547
+ t.binaryExpression(
2548
+ '!==',
2549
+ t.unaryExpression('typeof', t.identifier(name)),
2550
+ t.stringLiteral('undefined')
2551
+ ),
2552
+ t.identifier(name),
2553
+ t.identifier('undefined')
2554
+ );
2555
+ }
2556
+
2557
+ const agentLoopName = found.agentLoop?.name ?? null;
2558
+ const appStateFactoryName = found.appStateFactory?.name ?? null;
2559
+ const defaultModelFnName = found.defaultModelFn?.name ?? null;
2560
+ const supportedModelsFnName = found.supportedModelsFn?.name ?? null;
2561
+ const setPermModeFnName = found.setPermissionModeFn?.name ?? null;
2562
+ const buildPermCtxFnName = found.buildPermissionCtxFn?.name ?? null;
2563
+ const resumeSessionFnName = found.resumeSessionFn?.name ?? null;
2564
+ const commandsArrayVarName = found.commandsArrayVar?.name ?? null;
2565
+ const permissionCheckerName = found.permissionChecker?.name ?? null;
2566
+ const continueSessionFnName = found.continueSessionFn?.name ?? null;
2567
+ const hookRegistrySetterFnName = found.hookRegistrySetterFn?.name ?? null;
2568
+ const hookClearFnName = found.hookClearFn?.name ?? null;
2569
+ const allowedSettingSourcesSetterName = found.allowedSettingSourcesSetterFn?.name ?? null;
2570
+ const mcpConnectFnName = found.mcpConnectFn?.name ?? null;
2571
+ const permissionPromptToolFnName = found.permissionPromptToolFn?.name ?? null;
2572
+ const createLinkedTransportPairFnName = found.createLinkedTransportPairFn?.name ?? null;
2573
+ const getAccountInfoFnName = found.getAccountInfoFn?.name ?? null;
2574
+
2575
+ /**
2576
+ * Build the register object properties list, including all discovered symbols.
2577
+ * Each property uses safeRef() so that missing symbols degrade to `undefined`.
2578
+ *
2579
+ * @param {string|null} sessionClassName - filled in during the second pass
2580
+ * @returns {import('@babel/types').ObjectProperty[]}
2581
+ */
2582
+ function buildRegisterProps(sessionClassName) {
2583
+ const props = [
2584
+ t.objectProperty(t.identifier('agentLoop'), safeRef(agentLoopName)),
2585
+ t.objectProperty(t.identifier('appStateFactory'), safeRef(appStateFactoryName)),
2586
+ t.objectProperty(t.identifier('SessionClass'), safeRef(sessionClassName ?? null)),
2587
+ t.objectProperty(t.identifier('defaultModelFn'), safeRef(defaultModelFnName)),
2588
+ t.objectProperty(t.identifier('supportedModelsFn'), safeRef(supportedModelsFnName)),
2589
+ t.objectProperty(t.identifier('setPermissionModeFn'), safeRef(setPermModeFnName)),
2590
+ t.objectProperty(t.identifier('buildPermissionCtxFn'), safeRef(buildPermCtxFnName)),
2591
+ t.objectProperty(t.identifier('resumeSessionFn'), safeRef(resumeSessionFnName)),
2592
+ t.objectProperty(t.identifier('permissionChecker'), safeRef(permissionCheckerName)),
2593
+ t.objectProperty(t.identifier('continueSessionFn'), safeRef(continueSessionFnName)),
2594
+ // Gap-fix exports
2595
+ t.objectProperty(t.identifier('hookRegistrySetterFn'), safeRef(hookRegistrySetterFnName)),
2596
+ t.objectProperty(t.identifier('hookClearFn'), safeRef(hookClearFnName)),
2597
+ t.objectProperty(t.identifier('allowedSettingSourcesSetterFn'), safeRef(allowedSettingSourcesSetterName)),
2598
+ t.objectProperty(t.identifier('mcpConnectFn'), safeRef(mcpConnectFnName)),
2599
+ t.objectProperty(t.identifier('permissionPromptToolFn'), safeRef(permissionPromptToolFnName)),
2600
+ t.objectProperty(t.identifier('createLinkedTransportPair'), safeRef(createLinkedTransportPairFnName)),
2601
+ t.objectProperty(t.identifier('getAccountInfoFn'), safeRef(getAccountInfoFnName)),
2602
+ ];
2603
+ // commandsArrayVar is a lazy-init wrapper — expose as commandsFn so callers
2604
+ // can call it to get the array.
2605
+ props.push(t.objectProperty(t.identifier('commandsFn'), safeRef(commandsArrayVarName)));
2606
+ return props;
2607
+ }
2608
+
2609
+ // Build the object to pass to __elwoodRegister (first pass — no SessionClass yet)
2610
+ const registerArg = t.objectExpression(buildRegisterProps(null));
2611
+
2612
+ // Build: globalThis.__elwoodRegister({ ... })
2613
+ const registerCall = t.callExpression(
2614
+ t.memberExpression(
2615
+ t.identifier('globalThis'),
2616
+ t.identifier('__elwoodRegister')
2617
+ ),
2618
+ [registerArg]
2619
+ );
2620
+
2621
+ // Build: typeof globalThis.__elwoodRegister === 'function'
2622
+ const registerGuard = t.binaryExpression(
2623
+ '===',
2624
+ t.unaryExpression(
2625
+ 'typeof',
2626
+ t.memberExpression(
2627
+ t.identifier('globalThis'),
2628
+ t.identifier('__elwoodRegister')
2629
+ )
2630
+ ),
2631
+ t.stringLiteral('function')
2632
+ );
2633
+
2634
+ // ── B-pre. Inject initialization calls BEFORE the register call ──────────
2635
+ //
2636
+ // 1. Config enabler: call the function that sets the config-access guard
2637
+ // (e.g. h$6()) so that config can be read during agentLoop. This is
2638
+ // safe to call even when no config file exists — the function handles
2639
+ // ENOENT gracefully and uses built-in defaults.
2640
+ //
2641
+ // 2. Lazy-init wrappers: some null-initialized vars (like jN, the LRU
2642
+ // cache node class) are only assigned inside L(...) lazy-init wrappers.
2643
+ // Calling them ensures those classes are initialized before agentLoop runs.
2644
+ //
2645
+ // All calls are guarded with `typeof <name> === 'function'` for safety.
2646
+
2647
+ /**
2648
+ * Build: `if (typeof <name> === 'function') { try { <name>(); } catch {} }`
2649
+ * The try/catch prevents one failing init from aborting the whole chain.
2650
+ */
2651
+ function buildSafeCall(name) {
2652
+ const guard = t.binaryExpression(
2653
+ '===',
2654
+ t.unaryExpression('typeof', t.identifier(name)),
2655
+ t.stringLiteral('function')
2656
+ );
2657
+ const call = t.callExpression(t.identifier(name), []);
2658
+ // Wrap in try/catch so one failure doesn't abort all inits
2659
+ const tryCatch = t.tryStatement(
2660
+ t.blockStatement([t.expressionStatement(call)]),
2661
+ t.catchClause(t.identifier('_elwood_e'), t.blockStatement([]))
2662
+ );
2663
+ return t.ifStatement(guard, t.blockStatement([tryCatch]));
2664
+ }
2665
+
2666
+ for (const wrapperName of found.lazyInitWrappers ?? []) {
2667
+ ast.program.body.push(buildSafeCall(wrapperName));
2668
+ }
2669
+
2670
+ // Call the config enabler AFTER all lazy-init wrappers so that dependencies
2671
+ // like YW (set inside one of the L wrappers) are available.
2672
+ if (found.configEnabler) {
2673
+ ast.program.body.push(buildSafeCall(found.configEnabler));
2674
+ }
2675
+
2676
+ // ── B-mid. Patch be() (mcpConnectFn) to handle type="sdk" in-process ────────
2677
+ //
2678
+ // The be() function throws for type="sdk":
2679
+ // else if (K.type === "sdk") throw new Error("SDK servers should be handled in print.ts");
2680
+ //
2681
+ // We replace this with a call to a globally-registered handler:
2682
+ // else if (K.type === "sdk") {
2683
+ // if (typeof globalThis.__elwoodHandleSdkMcp === 'function') {
2684
+ // O = await globalThis.__elwoodHandleSdkMcp(q, K);
2685
+ // } else {
2686
+ // throw new Error("SDK servers should be handled in print.ts");
2687
+ // }
2688
+ // }
2689
+ //
2690
+ // Variable names are extracted DYNAMICALLY from the AST:
2691
+ // - configVarName: from the IfStatement test (K.type === "sdk" → K)
2692
+ // - serverNameVarName: first param of the enclosing async function (q)
2693
+ // - transportVarName: the first VariableDeclarator in the enclosing try block
2694
+ // whose init is absent (let O, A=...) → O
2695
+ //
2696
+ // __elwoodHandleSdkMcp(name, config) is installed by query.js before agentLoop.
2697
+ // It must return the CLIENT-SIDE transport object and also connect the server side.
2698
+ try {
2699
+ traverse(ast, {
2700
+ StringLiteral(path) {
2701
+ if (path.node.value !== 'SDK servers should be handled in print.ts') return;
2702
+ // Walk up to the IfStatement that contains this throw
2703
+ let ifPath = path;
2704
+ while (ifPath && ifPath.node.type !== 'IfStatement') {
2705
+ ifPath = ifPath.parentPath;
2706
+ }
2707
+ if (!ifPath || ifPath.node.type !== 'IfStatement') return;
2708
+
2709
+ // ── Extract configVarName from the IfStatement test ──────────────────
2710
+ // Test shape: K.type === "sdk"
2711
+ // The test is a BinaryExpression with left = MemberExpression(K, "type")
2712
+ let configVarName = 'K'; // safe default
2713
+ const testNode = ifPath.node.test;
2714
+ if (
2715
+ testNode?.type === 'BinaryExpression' &&
2716
+ (testNode.operator === '===' || testNode.operator === '==') &&
2717
+ testNode.left?.type === 'MemberExpression' &&
2718
+ testNode.left.property?.name === 'type' &&
2719
+ testNode.left.object?.type === 'Identifier'
2720
+ ) {
2721
+ configVarName = testNode.left.object.name;
2722
+ }
2723
+
2724
+ // ── Extract serverNameVarName from enclosing function's first param ──
2725
+ // Walk up to enclosing async function (ArrowFunctionExpression or FunctionExpression)
2726
+ let serverNameVarName = 'q'; // safe default
2727
+ let fnPath = ifPath;
2728
+ while (fnPath && !['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration'].includes(fnPath.node.type)) {
2729
+ fnPath = fnPath.parentPath;
2730
+ }
2731
+ if (fnPath) {
2732
+ const firstParam = fnPath.node.params?.[0];
2733
+ if (firstParam?.type === 'Identifier') {
2734
+ serverNameVarName = firstParam.name;
2735
+ }
2736
+ }
2737
+
2738
+ // ── Extract transportVarName from the try block ───────────────────────
2739
+ // Look for a TryStatement ancestor; the first statement in its block should
2740
+ // be a VariableDeclaration with the first declarator having no init (e.g. let O, A=...)
2741
+ let transportVarName = 'O'; // safe default
2742
+ let tryPath = ifPath;
2743
+ while (tryPath && tryPath.node.type !== 'TryStatement') {
2744
+ tryPath = tryPath.parentPath;
2745
+ }
2746
+ if (tryPath) {
2747
+ const tryBodyStmts = tryPath.node.block?.body ?? [];
2748
+ for (const stmt of tryBodyStmts) {
2749
+ if (stmt.type === 'VariableDeclaration') {
2750
+ // First declarator with no initializer (e.g. `let O, A = MP()`)
2751
+ const noInitDecl = stmt.declarations.find(d => !d.init && d.id?.type === 'Identifier');
2752
+ if (noInitDecl) {
2753
+ transportVarName = noInitDecl.id.name;
2754
+ break;
2755
+ }
2756
+ }
2757
+ }
2758
+ }
2759
+
2760
+ // Build the handler guard:
2761
+ // if (typeof globalThis.__elwoodHandleSdkMcp === 'function') {
2762
+ // <transport> = await globalThis.__elwoodHandleSdkMcp(<serverName>, <config>);
2763
+ // } else {
2764
+ // throw new Error("SDK servers should be handled in print.ts");
2765
+ // }
2766
+ const handlerGuard = t.binaryExpression(
2767
+ '===',
2768
+ t.unaryExpression('typeof', t.memberExpression(
2769
+ t.identifier('globalThis'), t.identifier('__elwoodHandleSdkMcp')
2770
+ )),
2771
+ t.stringLiteral('function')
2772
+ );
2773
+ const handlerCall = t.awaitExpression(t.callExpression(
2774
+ t.memberExpression(t.identifier('globalThis'), t.identifier('__elwoodHandleSdkMcp')),
2775
+ [t.identifier(serverNameVarName), t.identifier(configVarName)]
2776
+ ));
2777
+ const assignTransport = t.expressionStatement(
2778
+ t.assignmentExpression('=', t.identifier(transportVarName), handlerCall)
2779
+ );
2780
+ const fallbackThrow = t.throwStatement(t.newExpression(
2781
+ t.identifier('Error'),
2782
+ [t.stringLiteral('SDK servers should be handled in print.ts')]
2783
+ ));
2784
+
2785
+ const newConsequent = t.blockStatement([
2786
+ t.ifStatement(
2787
+ handlerGuard,
2788
+ t.blockStatement([assignTransport]),
2789
+ t.blockStatement([fallbackThrow])
2790
+ )
2791
+ ]);
2792
+
2793
+ ifPath.node.consequent = newConsequent;
2794
+ path.stop();
2795
+ }
2796
+ });
2797
+ } catch (patchErr) {
2798
+ // Non-fatal: be() patch failed — sdk type MCP servers will still throw,
2799
+ // but all other functionality remains intact.
2800
+ }
2801
+
2802
+ const registerIfStmt = t.ifStatement(
2803
+ registerGuard,
2804
+ t.blockStatement([t.expressionStatement(registerCall)])
2805
+ );
2806
+
2807
+ ast.program.body.push(registerIfStmt);
2808
+
2809
+ // ── C. Also inject Zz5 (session class) if we can find it ─────────────────
2810
+ // We look for the ClassDeclaration that has a submitMessage async generator
2811
+ // method and yield types that include 'result', 'system', 'assistant'.
2812
+ // We inject a second register call that includes the SessionClass reference.
2813
+ //
2814
+ // Strategy: traverse the AST to find the class, then add another register
2815
+ // call that fills in SessionClass.
2816
+
2817
+ let sessionClassName = null;
2818
+
2819
+ traverse(ast, {
2820
+ ClassDeclaration(path) {
2821
+ const cls = path.node;
2822
+ if (!cls.id?.name) return;
2823
+
2824
+ // Look for submitMessage as an async generator method
2825
+ let hasSubmitMessage = false;
2826
+ let hasYieldResult = false;
2827
+
2828
+ for (const member of cls.body.body) {
2829
+ if (
2830
+ member.type === 'ClassMethod' &&
2831
+ member.key?.name === 'submitMessage' &&
2832
+ member.async && member.generator
2833
+ ) {
2834
+ hasSubmitMessage = true;
2835
+
2836
+ // Quick scan of submitMessage for 'result' yield type
2837
+ let found = false;
2838
+ const scan = (node) => {
2839
+ if (!node || typeof node !== 'object' || found) return;
2840
+ if (node.type === 'YieldExpression' && node.argument?.type === 'ObjectExpression') {
2841
+ for (const prop of node.argument.properties) {
2842
+ if (
2843
+ prop.type === 'ObjectProperty' &&
2844
+ prop.key?.name === 'type' &&
2845
+ prop.value?.type === 'StringLiteral' &&
2846
+ (prop.value.value === 'result' || prop.value.value === 'assistant' || prop.value.value === 'system')
2847
+ ) {
2848
+ found = true;
2849
+ }
2850
+ }
2851
+ }
2852
+ for (const key of Object.keys(node)) {
2853
+ if (key === 'type' || key === 'loc' || key === 'start' || key === 'end') continue;
2854
+ const child = node[key];
2855
+ if (Array.isArray(child)) child.forEach(c => { if (c?.type) scan(c); });
2856
+ else if (child?.type) scan(child);
2857
+ }
2858
+ };
2859
+ scan(member.body);
2860
+ if (found) hasYieldResult = true;
2861
+ }
2862
+ }
2863
+
2864
+ if (hasSubmitMessage && hasYieldResult) {
2865
+ sessionClassName = cls.id.name;
2866
+ path.stop();
2867
+ }
2868
+ },
2869
+ });
2870
+
2871
+ if (sessionClassName) {
2872
+ // Build a second register call with SessionClass filled in
2873
+ const registerArg2 = t.objectExpression(buildRegisterProps(sessionClassName));
2874
+
2875
+ const registerCall2 = t.callExpression(
2876
+ t.memberExpression(
2877
+ t.identifier('globalThis'),
2878
+ t.identifier('__elwoodRegister')
2879
+ ),
2880
+ [registerArg2]
2881
+ );
2882
+
2883
+ const registerIfStmt2 = t.ifStatement(
2884
+ t.binaryExpression(
2885
+ '===',
2886
+ t.unaryExpression(
2887
+ 'typeof',
2888
+ t.memberExpression(
2889
+ t.identifier('globalThis'),
2890
+ t.identifier('__elwoodRegister')
2891
+ )
2892
+ ),
2893
+ t.stringLiteral('function')
2894
+ ),
2895
+ t.blockStatement([t.expressionStatement(registerCall2)])
2896
+ );
2897
+
2898
+ // Replace the first register stmt with this more complete one
2899
+ const lastIdx = ast.program.body.length - 1;
2900
+ if (ast.program.body[lastIdx] === registerIfStmt) {
2901
+ ast.program.body[lastIdx] = registerIfStmt2;
2902
+ } else {
2903
+ ast.program.body.push(registerIfStmt2);
2904
+ }
2905
+ }
2906
+ }
2907
+
2908
+ // ---------------------------------------------------------------------------
2909
+ // Exports
2910
+ // ---------------------------------------------------------------------------
2911
+
2912
+ export {
2913
+ injectHooks,
2914
+ detectBundlerPattern,
2915
+ extractFunctionMap,
2916
+ findCliExports,
2917
+ injectCliExports,
2918
+ // Re-export constants for callers
2919
+ DEFAULT_HOOK_VAR,
2920
+ ANTHROPIC_API_HOST,
2921
+ };