@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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/examples/01-basic-query.js +38 -0
- package/examples/02-bug-fixer.js +57 -0
- package/examples/03-custom-system-prompt.js +41 -0
- package/examples/04-read-only-agent.js +38 -0
- package/examples/05-find-todos.js +37 -0
- package/examples/06-session-resume.js +70 -0
- package/examples/07-hooks-pretooluse.js +74 -0
- package/examples/08-hooks-posttooluse-audit.js +66 -0
- package/examples/09-hooks-block-etc.js +68 -0
- package/examples/10-hooks-redirect-sandbox.js +70 -0
- package/examples/11-subagents.js +54 -0
- package/examples/12-mcp-stdio.js +48 -0
- package/examples/13-mcp-http.js +54 -0
- package/examples/14-custom-tool.js +84 -0
- package/examples/15-custom-tool-unit-converter.js +132 -0
- package/examples/16-mcp-github.js +71 -0
- package/examples/17-session-store-postgres.js +78 -0
- package/examples/18-session-store-redis.js +65 -0
- package/examples/19-session-store-s3.js +67 -0
- package/examples/20-session-list.js +72 -0
- package/examples/21-hooks-notification-slack.js +78 -0
- package/examples/22-hooks-webhook-posttooluse.js +78 -0
- package/examples/23-hooks-subagent-tracker.js +59 -0
- package/examples/24-v2-session-api.js +62 -0
- package/examples/README.md +95 -0
- package/examples/basic.js +240 -0
- package/examples/smoke-test.js +296 -0
- package/package.json +52 -0
- package/src/ast-tools.js +182 -0
- package/src/index.js +70 -0
- package/src/instrumenter.js +2921 -0
- package/src/loader.js +306 -0
- package/src/locate.js +296 -0
- 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
|
+
};
|