agent-manifest 3.2.0 → 3.3.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/dist/cli.js +58 -7
- package/dist/discovery/service.js +37 -12
- package/dist/index.js +22 -1
- package/dist/parser/intent-classifier.js +1 -1
- package/dist/parser/openapi-parser.js +156 -0
- package/dist/parser/prisma-parser.js +198 -0
- package/dist/parser/remix-parser.js +158 -0
- package/dist/parser/socketio-parser.js +169 -0
- package/dist/parser/sse-parser.js +141 -0
- package/dist/parser/trpc-parser.js +192 -0
- package/dist/parser/websocket-parser.js +168 -0
- package/package.json +1 -1
- package/schema/agent.schema.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RemixParser = void 0;
|
|
37
|
+
exports.looksLikeRemixRouteFile = looksLikeRemixRouteFile;
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
const intent_classifier_1 = require("./intent-classifier");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Parses Remix route files and extracts `action` and `loader` exports.
|
|
43
|
+
*
|
|
44
|
+
* Remix v2 file-based routing:
|
|
45
|
+
* app/routes/users.tsx → /users
|
|
46
|
+
* app/routes/users.$id.tsx → /users/:id
|
|
47
|
+
* app/routes/_index.tsx → / (layout route — leading _ stripped)
|
|
48
|
+
* app/routes/users.$id.edit.tsx → /users/:id/edit
|
|
49
|
+
*
|
|
50
|
+
* - `loader` export → GET (read)
|
|
51
|
+
* - `action` export → POST (write)
|
|
52
|
+
*/
|
|
53
|
+
class RemixParser {
|
|
54
|
+
project;
|
|
55
|
+
constructor(sharedProject) {
|
|
56
|
+
this.project = sharedProject ?? new ts_morph_1.Project({
|
|
57
|
+
compilerOptions: { allowJs: true, checkJs: false },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async parseFile(filePath, projectPath) {
|
|
61
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
62
|
+
const routePath = this.filePathToRoute(filePath, projectPath);
|
|
63
|
+
const actions = [];
|
|
64
|
+
// Look for: export async function action / export async function loader
|
|
65
|
+
// or: export const action = async ({ request }) => { ... }
|
|
66
|
+
const exportedNames = this.getExportedNames(sourceFile);
|
|
67
|
+
for (const exportName of exportedNames) {
|
|
68
|
+
if (exportName !== 'action' && exportName !== 'loader')
|
|
69
|
+
continue;
|
|
70
|
+
const httpMethod = exportName === 'loader' ? 'GET' : 'POST';
|
|
71
|
+
const actionName = this.routeToActionName(routePath, exportName);
|
|
72
|
+
const safety = (0, intent_classifier_1.classifySafety)({ name: actionName, httpMethod, type: 'api' });
|
|
73
|
+
actions.push({
|
|
74
|
+
name: actionName,
|
|
75
|
+
description: `Remix ${exportName}: ${httpMethod} ${routePath}`,
|
|
76
|
+
intent: (0, intent_classifier_1.inferIntent)(actionName),
|
|
77
|
+
type: 'api',
|
|
78
|
+
location: routePath,
|
|
79
|
+
method: httpMethod,
|
|
80
|
+
safety,
|
|
81
|
+
agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
|
|
82
|
+
requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod, type: 'api' }),
|
|
83
|
+
inputs: this.extractRouteParams(routePath),
|
|
84
|
+
outputs: { type: 'object' },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return actions;
|
|
88
|
+
}
|
|
89
|
+
getExportedNames(sourceFile) {
|
|
90
|
+
const names = [];
|
|
91
|
+
// export function name / export async function name
|
|
92
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
93
|
+
if (fn.isExported())
|
|
94
|
+
names.push(fn.getName() ?? '');
|
|
95
|
+
}
|
|
96
|
+
// export const name = ...
|
|
97
|
+
for (const varStatement of sourceFile.getVariableStatements()) {
|
|
98
|
+
if (!varStatement.isExported())
|
|
99
|
+
continue;
|
|
100
|
+
for (const decl of varStatement.getDeclarations()) {
|
|
101
|
+
names.push(decl.getName());
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return names.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Convert a Remix v2 file path to a URL route path.
|
|
108
|
+
*
|
|
109
|
+
* Rules:
|
|
110
|
+
* - Strip leading `app/routes/` prefix
|
|
111
|
+
* - Strip file extension
|
|
112
|
+
* - Split on `.` → each segment becomes a path segment
|
|
113
|
+
* - `$param` → `:param`
|
|
114
|
+
* - Segments starting with `_` are layout markers — skip (unless it's the whole segment)
|
|
115
|
+
* - `_index` → empty (index route)
|
|
116
|
+
*/
|
|
117
|
+
filePathToRoute(filePath, projectPath) {
|
|
118
|
+
const rel = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
119
|
+
// Strip known prefix variations: app/routes/, routes/
|
|
120
|
+
const withoutPrefix = rel
|
|
121
|
+
.replace(/^.*?(?:app\/routes|routes)\//, '')
|
|
122
|
+
.replace(/\.[jt]sx?$/, '');
|
|
123
|
+
const segments = withoutPrefix.split('.');
|
|
124
|
+
const routeSegments = [];
|
|
125
|
+
for (const seg of segments) {
|
|
126
|
+
if (seg === '_index')
|
|
127
|
+
continue; // index route
|
|
128
|
+
if (seg.startsWith('_'))
|
|
129
|
+
continue; // layout route (e.g. _auth, _app)
|
|
130
|
+
routeSegments.push(seg.replace(/^\$/, ':')); // $id → :id
|
|
131
|
+
}
|
|
132
|
+
return '/' + routeSegments.join('/');
|
|
133
|
+
}
|
|
134
|
+
routeToActionName(routePath, exportName) {
|
|
135
|
+
const slug = routePath
|
|
136
|
+
.replace(/^\//, '')
|
|
137
|
+
.replace(/\/:([^/]+)/g, '_$1')
|
|
138
|
+
.replace(/\//g, '_')
|
|
139
|
+
.replace(/[^a-zA-Z0-9_]/g, '');
|
|
140
|
+
return slug ? `${slug}_${exportName}` : `root_${exportName}`;
|
|
141
|
+
}
|
|
142
|
+
extractRouteParams(routePath) {
|
|
143
|
+
const params = {};
|
|
144
|
+
for (const match of routePath.matchAll(/:([a-zA-Z][a-zA-Z0-9_]*)/g)) {
|
|
145
|
+
params[match[1]] = { type: 'string', description: `Path parameter: ${match[1]}`, required: true };
|
|
146
|
+
}
|
|
147
|
+
return params;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.RemixParser = RemixParser;
|
|
151
|
+
function looksLikeRemixRouteFile(content) {
|
|
152
|
+
return ((content.includes('export') && content.includes('function loader')) ||
|
|
153
|
+
(content.includes('export') && content.includes('function action')) ||
|
|
154
|
+
content.includes('LoaderFunctionArgs') ||
|
|
155
|
+
content.includes('ActionFunctionArgs') ||
|
|
156
|
+
content.includes('LoaderFunction') ||
|
|
157
|
+
content.includes('ActionFunction'));
|
|
158
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SocketIOParser = void 0;
|
|
37
|
+
exports.looksLikeSocketIOFile = looksLikeSocketIOFile;
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
const intent_classifier_1 = require("./intent-classifier");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Parses Socket.IO server files and extracts socket event handlers as agent actions.
|
|
43
|
+
*
|
|
44
|
+
* Detects patterns like:
|
|
45
|
+
* socket.on("create-room", ({ playerName, fid }: { playerName: string; fid?: number }) => { ... })
|
|
46
|
+
* socket.on("make-move", async ({ col }: { col: number }) => { ... })
|
|
47
|
+
*
|
|
48
|
+
* Infrastructure events (connection, disconnect, error, etc.) are skipped.
|
|
49
|
+
*/
|
|
50
|
+
const SKIP_EVENTS = new Set([
|
|
51
|
+
'connection', 'disconnect', 'disconnecting', 'connect',
|
|
52
|
+
'connect_error', 'reconnect', 'reconnect_attempt', 'reconnect_error',
|
|
53
|
+
'reconnect_failed', 'error', 'ping', 'pong', 'close', 'open',
|
|
54
|
+
]);
|
|
55
|
+
class SocketIOParser {
|
|
56
|
+
project;
|
|
57
|
+
constructor(sharedProject) {
|
|
58
|
+
this.project = sharedProject ?? new ts_morph_1.Project({
|
|
59
|
+
compilerOptions: { allowJs: true, checkJs: false },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async parseFile(filePath, projectPath) {
|
|
63
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
64
|
+
const relativePath = './' + path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
65
|
+
const actions = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
sourceFile.forEachDescendant(node => {
|
|
68
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
69
|
+
return;
|
|
70
|
+
const expr = node.getExpression();
|
|
71
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
72
|
+
return;
|
|
73
|
+
if (expr.getName() !== 'on')
|
|
74
|
+
return;
|
|
75
|
+
const args = node.getArguments();
|
|
76
|
+
if (args.length < 2)
|
|
77
|
+
return;
|
|
78
|
+
const eventArg = args[0];
|
|
79
|
+
if (!ts_morph_1.Node.isStringLiteral(eventArg))
|
|
80
|
+
return;
|
|
81
|
+
const eventName = eventArg.getLiteralValue();
|
|
82
|
+
if (SKIP_EVENTS.has(eventName))
|
|
83
|
+
return;
|
|
84
|
+
if (seen.has(eventName))
|
|
85
|
+
return;
|
|
86
|
+
seen.add(eventName);
|
|
87
|
+
const handler = args[1];
|
|
88
|
+
if (!ts_morph_1.Node.isArrowFunction(handler) && !ts_morph_1.Node.isFunctionExpression(handler))
|
|
89
|
+
return;
|
|
90
|
+
// Extract description from JSDoc on the handler
|
|
91
|
+
let description = `Socket event: ${eventName}`;
|
|
92
|
+
const jsDocs = handler.getJsDocs?.() ?? [];
|
|
93
|
+
if (jsDocs.length > 0) {
|
|
94
|
+
const text = jsDocs[0].getDescription().trim();
|
|
95
|
+
if (text)
|
|
96
|
+
description = text;
|
|
97
|
+
}
|
|
98
|
+
const inputs = this.extractInputs(handler);
|
|
99
|
+
// Normalize kebab-case event names to camelCase for intent/safety classification
|
|
100
|
+
const camelName = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
101
|
+
const safety = (0, intent_classifier_1.classifySafety)({ name: camelName, type: 'socket' });
|
|
102
|
+
const intent = (0, intent_classifier_1.inferIntent)(camelName);
|
|
103
|
+
actions.push({
|
|
104
|
+
name: eventName,
|
|
105
|
+
description,
|
|
106
|
+
intent,
|
|
107
|
+
type: 'socket',
|
|
108
|
+
location: relativePath,
|
|
109
|
+
socketEvent: eventName,
|
|
110
|
+
safety,
|
|
111
|
+
agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
|
|
112
|
+
requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, type: 'socket' }),
|
|
113
|
+
inputs,
|
|
114
|
+
outputs: { type: 'object' },
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return actions;
|
|
118
|
+
}
|
|
119
|
+
extractInputs(handler) {
|
|
120
|
+
const params = handler.getParameters?.() ?? [];
|
|
121
|
+
if (params.length === 0)
|
|
122
|
+
return {};
|
|
123
|
+
const firstParam = params[0];
|
|
124
|
+
const bindingPattern = firstParam.getNameNode?.();
|
|
125
|
+
if (!bindingPattern || !ts_morph_1.Node.isObjectBindingPattern(bindingPattern))
|
|
126
|
+
return {};
|
|
127
|
+
// Prefer explicit type annotation on the parameter: ({ col }: { col: number })
|
|
128
|
+
const typeNode = firstParam.getTypeNode?.();
|
|
129
|
+
if (typeNode && ts_morph_1.Node.isTypeLiteral(typeNode)) {
|
|
130
|
+
const inputs = {};
|
|
131
|
+
for (const member of typeNode.getMembers()) {
|
|
132
|
+
if (!ts_morph_1.Node.isPropertySignature(member))
|
|
133
|
+
continue;
|
|
134
|
+
const name = member.getName();
|
|
135
|
+
const optional = member.hasQuestionToken();
|
|
136
|
+
const tsType = member.getTypeNode()?.getText() ?? 'any';
|
|
137
|
+
inputs[name] = {
|
|
138
|
+
type: this.tsTypeToJsonType(tsType),
|
|
139
|
+
required: !optional,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return inputs;
|
|
143
|
+
}
|
|
144
|
+
// Fallback: read names from the destructuring pattern only (no type info)
|
|
145
|
+
const inputs = {};
|
|
146
|
+
for (const element of bindingPattern.getElements()) {
|
|
147
|
+
if (ts_morph_1.Node.isBindingElement(element)) {
|
|
148
|
+
inputs[element.getName()] = { type: 'any', required: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return inputs;
|
|
152
|
+
}
|
|
153
|
+
tsTypeToJsonType(ts) {
|
|
154
|
+
const t = ts.trim();
|
|
155
|
+
if (t === 'string')
|
|
156
|
+
return 'string';
|
|
157
|
+
if (t === 'number' || t === 'bigint')
|
|
158
|
+
return 'number';
|
|
159
|
+
if (t === 'boolean')
|
|
160
|
+
return 'boolean';
|
|
161
|
+
return 'object';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.SocketIOParser = SocketIOParser;
|
|
165
|
+
/** Quick check: does this file look like it registers Socket.IO event handlers? */
|
|
166
|
+
function looksLikeSocketIOFile(content) {
|
|
167
|
+
return (/socket\.on\s*\(\s*['"`]/.test(content) ||
|
|
168
|
+
/io\.on\s*\(\s*['"`]connection['"`]/.test(content));
|
|
169
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SSEParser = void 0;
|
|
37
|
+
exports.looksLikeSSEFile = looksLikeSSEFile;
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
const intent_classifier_1 = require("./intent-classifier");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Detects Server-Sent Event (SSE) streaming endpoints.
|
|
43
|
+
*
|
|
44
|
+
* Patterns detected:
|
|
45
|
+
* // Express
|
|
46
|
+
* res.setHeader('Content-Type', 'text/event-stream')
|
|
47
|
+
* res.writeHead(200, { 'Content-Type': 'text/event-stream' })
|
|
48
|
+
*
|
|
49
|
+
* // Next.js App Router (route handler)
|
|
50
|
+
* return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } })
|
|
51
|
+
* return new StreamingTextResponse(stream)
|
|
52
|
+
*
|
|
53
|
+
* // AI SDK
|
|
54
|
+
* return result.toDataStreamResponse()
|
|
55
|
+
* return result.toTextStreamResponse()
|
|
56
|
+
*
|
|
57
|
+
* SSE endpoints are classified as 'read' safety + streaming output.
|
|
58
|
+
*/
|
|
59
|
+
class SSEParser {
|
|
60
|
+
project;
|
|
61
|
+
constructor(sharedProject) {
|
|
62
|
+
this.project = sharedProject ?? new ts_morph_1.Project({
|
|
63
|
+
compilerOptions: { allowJs: true, checkJs: false },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async parseFile(filePath, projectPath) {
|
|
67
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
68
|
+
const relativePath = './' + path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
69
|
+
const actions = [];
|
|
70
|
+
// Collect functions/arrow-fns that contain SSE signals
|
|
71
|
+
const sseFunctions = new Set();
|
|
72
|
+
sourceFile.forEachDescendant(node => {
|
|
73
|
+
if (!this.isSSESignal(node))
|
|
74
|
+
return;
|
|
75
|
+
// Walk up to the nearest function/arrow/method to name the action
|
|
76
|
+
const fn = node.getFirstAncestorByKind(213 /* ArrowFunction */) ??
|
|
77
|
+
node.getFirstAncestorByKind(259 /* FunctionDeclaration */) ??
|
|
78
|
+
node.getFirstAncestorByKind(171 /* MethodDeclaration */) ??
|
|
79
|
+
node.getFirstAncestorByKind(215 /* FunctionExpression */);
|
|
80
|
+
if (fn && !sseFunctions.has(fn)) {
|
|
81
|
+
sseFunctions.add(fn);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
for (const fn of sseFunctions) {
|
|
85
|
+
const name = this.resolveFunctionName(fn, filePath);
|
|
86
|
+
const safety = 'read';
|
|
87
|
+
actions.push({
|
|
88
|
+
name,
|
|
89
|
+
description: `Streaming SSE endpoint — ${path.basename(filePath)}`,
|
|
90
|
+
intent: (0, intent_classifier_1.inferIntent)(name),
|
|
91
|
+
type: 'api',
|
|
92
|
+
location: relativePath,
|
|
93
|
+
method: 'GET',
|
|
94
|
+
safety,
|
|
95
|
+
agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
|
|
96
|
+
requiredAuth: (0, intent_classifier_1.inferActionAuth)({ safety, httpMethod: 'GET', type: 'api' }),
|
|
97
|
+
inputs: {},
|
|
98
|
+
outputs: { type: 'stream', description: 'Server-Sent Events stream' },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return actions;
|
|
102
|
+
}
|
|
103
|
+
isSSESignal(node) {
|
|
104
|
+
if (ts_morph_1.Node.isStringLiteral(node)) {
|
|
105
|
+
return node.getLiteralValue() === 'text/event-stream';
|
|
106
|
+
}
|
|
107
|
+
if (ts_morph_1.Node.isCallExpression(node)) {
|
|
108
|
+
const text = node.getText();
|
|
109
|
+
return (text.includes('toDataStreamResponse') ||
|
|
110
|
+
text.includes('toTextStreamResponse') ||
|
|
111
|
+
text.includes('toUIMessageStreamResponse') ||
|
|
112
|
+
text.includes('StreamingTextResponse'));
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
resolveFunctionName(fn, filePath) {
|
|
117
|
+
// Named function declaration
|
|
118
|
+
const decl = fn.getName?.();
|
|
119
|
+
if (decl)
|
|
120
|
+
return decl;
|
|
121
|
+
// Arrow function assigned to a variable
|
|
122
|
+
const varDecl = fn.getFirstAncestorByKind(249 /* VariableDeclaration */);
|
|
123
|
+
if (varDecl) {
|
|
124
|
+
const varName = varDecl.getName?.();
|
|
125
|
+
if (varName)
|
|
126
|
+
return varName;
|
|
127
|
+
}
|
|
128
|
+
// Fallback: derive from filename
|
|
129
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
130
|
+
return `${base}_stream`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.SSEParser = SSEParser;
|
|
134
|
+
function looksLikeSSEFile(content) {
|
|
135
|
+
return (content.includes('text/event-stream') ||
|
|
136
|
+
content.includes('toDataStreamResponse') ||
|
|
137
|
+
content.includes('toTextStreamResponse') ||
|
|
138
|
+
content.includes('toUIMessageStreamResponse') ||
|
|
139
|
+
content.includes('StreamingTextResponse') ||
|
|
140
|
+
content.includes('event-stream'));
|
|
141
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TRPCParser = void 0;
|
|
37
|
+
exports.looksLikeTRPCFile = looksLikeTRPCFile;
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
const intent_classifier_1 = require("./intent-classifier");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Parses tRPC router files and extracts procedures as agent actions.
|
|
43
|
+
*
|
|
44
|
+
* Detects patterns like:
|
|
45
|
+
* export const userRouter = createTRPCRouter({
|
|
46
|
+
* list: publicProcedure.query(({ ctx }) => { ... }),
|
|
47
|
+
* create: protectedProcedure.input(z.object({ name: z.string() })).mutation(({ ctx, input }) => { ... }),
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
class TRPCParser {
|
|
51
|
+
project;
|
|
52
|
+
constructor(sharedProject) {
|
|
53
|
+
this.project = sharedProject ?? new ts_morph_1.Project({
|
|
54
|
+
compilerOptions: { allowJs: true, checkJs: false },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async parseFile(filePath, projectPath) {
|
|
58
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
59
|
+
const relativePath = './' + path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
60
|
+
const actions = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
sourceFile.forEachDescendant(node => {
|
|
63
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
64
|
+
return;
|
|
65
|
+
const expr = node.getExpression();
|
|
66
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
67
|
+
return;
|
|
68
|
+
const methodName = expr.getName();
|
|
69
|
+
if (methodName !== 'query' && methodName !== 'mutation' && methodName !== 'subscription')
|
|
70
|
+
return;
|
|
71
|
+
// Walk up to find the PropertyAssignment that gives us the procedure name.
|
|
72
|
+
// Structure: createTRPCRouter({ procedureName: procedure.input(...).query(handler) })
|
|
73
|
+
const propAssignment = node.getFirstAncestorByKind(147 /* SyntaxKind.PropertyAssignment */);
|
|
74
|
+
if (!propAssignment)
|
|
75
|
+
return;
|
|
76
|
+
const procedureName = propAssignment.getName?.()?.replace(/['"]/g, '') ?? '';
|
|
77
|
+
if (!procedureName || seen.has(procedureName))
|
|
78
|
+
return;
|
|
79
|
+
seen.add(procedureName);
|
|
80
|
+
// Determine if protected (auth required) by scanning the chain text
|
|
81
|
+
const chainText = expr.getExpression().getText();
|
|
82
|
+
const isProtected = /protected|authed|private|admin|requireAuth/i.test(chainText);
|
|
83
|
+
// Description from JSDoc on the PropertyAssignment
|
|
84
|
+
let description = `tRPC ${methodName}: ${procedureName}`;
|
|
85
|
+
const jsDocs = propAssignment.getJsDocs?.() ?? [];
|
|
86
|
+
if (jsDocs.length > 0) {
|
|
87
|
+
const text = jsDocs[0].getDescription?.().trim();
|
|
88
|
+
if (text)
|
|
89
|
+
description = text;
|
|
90
|
+
}
|
|
91
|
+
// Extract inputs from .input(z.object({...})) in the chain
|
|
92
|
+
const inputs = this.extractInputsFromChain(expr.getExpression());
|
|
93
|
+
// query → GET semantics, mutation → POST, subscription → streaming
|
|
94
|
+
const httpLike = methodName === 'query' ? 'GET' : 'POST';
|
|
95
|
+
const safety = (0, intent_classifier_1.classifySafety)({ name: procedureName, httpMethod: httpLike, type: 'function' });
|
|
96
|
+
actions.push({
|
|
97
|
+
name: procedureName,
|
|
98
|
+
description,
|
|
99
|
+
intent: (0, intent_classifier_1.inferIntent)(procedureName),
|
|
100
|
+
type: 'function',
|
|
101
|
+
location: relativePath,
|
|
102
|
+
method: methodName,
|
|
103
|
+
safety,
|
|
104
|
+
agentSafe: (0, intent_classifier_1.deriveAgentSafe)(safety),
|
|
105
|
+
requiredAuth: (0, intent_classifier_1.inferActionAuth)({
|
|
106
|
+
safety,
|
|
107
|
+
httpMethod: httpLike,
|
|
108
|
+
appAuthType: isProtected ? 'bearer' : undefined,
|
|
109
|
+
type: 'function',
|
|
110
|
+
}),
|
|
111
|
+
inputs,
|
|
112
|
+
outputs: { type: 'object' },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return actions;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Walk the call expression chain (e.g. procedure.use(mid).input(schema).query(handler))
|
|
119
|
+
* looking for an .input(zodObject) call and extract its fields.
|
|
120
|
+
*/
|
|
121
|
+
extractInputsFromChain(chainExpr) {
|
|
122
|
+
// Traverse the chain: each link is a PropertyAccessExpression whose object
|
|
123
|
+
// is another CallExpression. We're looking for the one named "input".
|
|
124
|
+
let current = chainExpr;
|
|
125
|
+
while (current) {
|
|
126
|
+
if (ts_morph_1.Node.isCallExpression(current)) {
|
|
127
|
+
const callExpr = current;
|
|
128
|
+
const callCallee = callExpr.getExpression?.();
|
|
129
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(callCallee) && callCallee.getName() === 'input') {
|
|
130
|
+
const inputArg = callExpr.getArguments?.()[0];
|
|
131
|
+
if (inputArg)
|
|
132
|
+
return this.extractZodObjectFields(inputArg);
|
|
133
|
+
}
|
|
134
|
+
current = callExpr.getExpression?.();
|
|
135
|
+
}
|
|
136
|
+
else if (ts_morph_1.Node.isPropertyAccessExpression(current)) {
|
|
137
|
+
current = current.getExpression?.();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
/** Extract field names/types from a z.object({ ... }) argument node. */
|
|
146
|
+
extractZodObjectFields(node) {
|
|
147
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
148
|
+
return {};
|
|
149
|
+
const callee = node.getExpression();
|
|
150
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callee) || callee.getName() !== 'object')
|
|
151
|
+
return {};
|
|
152
|
+
const arg = node.getArguments()[0];
|
|
153
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
|
|
154
|
+
return {};
|
|
155
|
+
const inputs = {};
|
|
156
|
+
for (const prop of arg.getProperties()) {
|
|
157
|
+
if (!ts_morph_1.Node.isPropertyAssignment(prop))
|
|
158
|
+
continue;
|
|
159
|
+
const name = prop.getName().replace(/['"]/g, '');
|
|
160
|
+
const valText = prop.getInitializer()?.getText() ?? '';
|
|
161
|
+
const optional = valText.includes('.optional()') || valText.includes('.nullish()');
|
|
162
|
+
inputs[name] = {
|
|
163
|
+
type: this.inferZodType(valText),
|
|
164
|
+
required: !optional,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return inputs;
|
|
168
|
+
}
|
|
169
|
+
inferZodType(zodText) {
|
|
170
|
+
if (/z\.string/i.test(zodText))
|
|
171
|
+
return 'string';
|
|
172
|
+
if (/z\.number|z\.int/i.test(zodText))
|
|
173
|
+
return 'number';
|
|
174
|
+
if (/z\.boolean/i.test(zodText))
|
|
175
|
+
return 'boolean';
|
|
176
|
+
if (/z\.array/i.test(zodText))
|
|
177
|
+
return 'array';
|
|
178
|
+
if (/z\.object/i.test(zodText))
|
|
179
|
+
return 'object';
|
|
180
|
+
if (/z\.enum/i.test(zodText))
|
|
181
|
+
return 'string';
|
|
182
|
+
return 'any';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.TRPCParser = TRPCParser;
|
|
186
|
+
function looksLikeTRPCFile(content) {
|
|
187
|
+
return (content.includes('createTRPCRouter') ||
|
|
188
|
+
content.includes('initTRPC') ||
|
|
189
|
+
content.includes('publicProcedure') ||
|
|
190
|
+
content.includes('protectedProcedure') ||
|
|
191
|
+
(content.includes('.query(') && content.includes('procedure')));
|
|
192
|
+
}
|