@keystrokehq/cli 0.0.136 → 0.0.138
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/{discovery-XqwkB9H_-C2XdYbG2.mjs → discovery-DV7FkTRA-BFmtZG9u.mjs} +432 -6
- package/dist/discovery-DV7FkTRA-BFmtZG9u.mjs.map +1 -0
- package/dist/dist-CmCvzhNH.mjs +676 -0
- package/dist/dist-CmCvzhNH.mjs.map +1 -0
- package/dist/dist-CqVk9JsS.mjs +1569 -0
- package/dist/dist-CqVk9JsS.mjs.map +1 -0
- package/dist/{dist-uZuvgIVp.mjs → dist-QMg0gSzo.mjs} +25 -29
- package/dist/dist-QMg0gSzo.mjs.map +1 -0
- package/dist/index.mjs +180 -1764
- package/dist/index.mjs.map +1 -1
- package/dist/walk-project-171B4cvO-DJy3qZKd.mjs +102 -0
- package/dist/walk-project-171B4cvO-DJy3qZKd.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/discovery-XqwkB9H_-C2XdYbG2.mjs.map +0 -1
- package/dist/dist-uZuvgIVp.mjs.map +0 -1
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { _ as string, c as _function, d as custom, f as discriminatedUnion, h as object, i as walkTypeScriptFiles, l as array, n as discoverModuleFileEntries, o as ZodType, p as literal, r as entryIdFromFile, v as union, x as toJSONSchema } from "./discovery-DV7FkTRA-BFmtZG9u.mjs";
|
|
3
|
+
import { K as PromptInputSchema, X as ROUTE_MANIFEST_REL_PATH, bt as normalizeCredentialList, q as PromptResponseSchema } from "./dist-CqVk9JsS.mjs";
|
|
4
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
//#region ../../packages/manifest/dist/index.mjs
|
|
8
|
+
function isManifestAgent(value) {
|
|
9
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10
|
+
const agent = value;
|
|
11
|
+
return typeof agent.slug === "string" && agent.slug.trim().length > 0 && typeof agent.buildRuntime === "function" && typeof agent.model === "string" && typeof agent.systemPrompt === "string";
|
|
12
|
+
}
|
|
13
|
+
function validateManifestAgent(value, filePath) {
|
|
14
|
+
if (!isManifestAgent(value)) throw new Error(`${filePath} must default-export defineAgent(...)`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function normalizeWebhookEndpoint(endpoint) {
|
|
18
|
+
return endpoint.replace(/^\/+|\/+$/g, "").replace(/^triggers\/?/, "");
|
|
19
|
+
}
|
|
20
|
+
function agentRouteFromKey(key) {
|
|
21
|
+
return `/agents/${key}`;
|
|
22
|
+
}
|
|
23
|
+
function agentSessionsListPath(route) {
|
|
24
|
+
return `${route}/sessions`;
|
|
25
|
+
}
|
|
26
|
+
function agentSessionDetailPath(route) {
|
|
27
|
+
return `${route}/sessions/:sessionId`;
|
|
28
|
+
}
|
|
29
|
+
function workflowRouteFromKey(key) {
|
|
30
|
+
return `/workflows/${key}`;
|
|
31
|
+
}
|
|
32
|
+
function workflowRunsListPath(route) {
|
|
33
|
+
return `${route}/runs`;
|
|
34
|
+
}
|
|
35
|
+
function workflowRunDetailPath(route) {
|
|
36
|
+
return `${route}/runs/:runId`;
|
|
37
|
+
}
|
|
38
|
+
function triggerRunsListPath(attachmentKey) {
|
|
39
|
+
return `/triggers/${attachmentKey}/runs`;
|
|
40
|
+
}
|
|
41
|
+
function triggerRunDetailPath(attachmentKey) {
|
|
42
|
+
return `/triggers/${attachmentKey}/runs/:runId`;
|
|
43
|
+
}
|
|
44
|
+
function pollRouteFromKey(attachmentKey) {
|
|
45
|
+
return `/triggers/${attachmentKey}/poll`;
|
|
46
|
+
}
|
|
47
|
+
function pollGroupRouteFromId(pollId) {
|
|
48
|
+
return `/triggers/polls/${pollId}/run`;
|
|
49
|
+
}
|
|
50
|
+
function webhookRouteFromEndpoint(endpoint) {
|
|
51
|
+
const normalized = normalizeWebhookEndpoint(endpoint);
|
|
52
|
+
if (!normalized) throw new Error("Webhook endpoint must not be empty");
|
|
53
|
+
return `/triggers/${normalized}`;
|
|
54
|
+
}
|
|
55
|
+
async function discoverAgentEntries(agentsDir) {
|
|
56
|
+
const files = await discoverModuleFileEntries(agentsDir, {
|
|
57
|
+
nestedEntry: "agent",
|
|
58
|
+
duplicateLabel: "agent module file"
|
|
59
|
+
});
|
|
60
|
+
const entries = [];
|
|
61
|
+
for (const { filePath, moduleFile } of files) {
|
|
62
|
+
const agent = await importAgentDefinition(filePath);
|
|
63
|
+
entries.push({
|
|
64
|
+
key: agent.slug,
|
|
65
|
+
route: agentRouteFromKey(agent.slug),
|
|
66
|
+
filePath,
|
|
67
|
+
moduleFile
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
async function importAgentDefinition(filePath) {
|
|
73
|
+
return validateManifestAgent((await import(pathToFileURL(filePath).href)).default, filePath);
|
|
74
|
+
}
|
|
75
|
+
const ACTION = Symbol.for("keystroke.action");
|
|
76
|
+
function isManifestAction(value) {
|
|
77
|
+
if (typeof value !== "object" || value === null) return false;
|
|
78
|
+
return ACTION in value && value[ACTION] === true;
|
|
79
|
+
}
|
|
80
|
+
function getManifestActionCredentialRequirements(action) {
|
|
81
|
+
return Array.isArray(action.credentials) ? action.credentials : void 0;
|
|
82
|
+
}
|
|
83
|
+
function countAgentCredentials(agent) {
|
|
84
|
+
const keys = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const tool of agent.tools ?? []) {
|
|
86
|
+
const record = tool;
|
|
87
|
+
const requirements = isManifestAction(tool) ? getManifestActionCredentialRequirements(tool) : "credentials" in record && Array.isArray(record.credentials) ? record.credentials : void 0;
|
|
88
|
+
if (!requirements?.length) continue;
|
|
89
|
+
for (const requirement of normalizeCredentialList(requirements)) keys.add(requirement.key);
|
|
90
|
+
}
|
|
91
|
+
return keys.size;
|
|
92
|
+
}
|
|
93
|
+
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
94
|
+
function toPosix$1(path) {
|
|
95
|
+
return path.split(sep).join("/");
|
|
96
|
+
}
|
|
97
|
+
function parseSkillFrontmatter(raw) {
|
|
98
|
+
const match = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
99
|
+
if (!match) return {};
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const line of match[1]?.split("\n") ?? []) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
104
|
+
const colon = trimmed.indexOf(":");
|
|
105
|
+
if (colon < 0) continue;
|
|
106
|
+
const key = trimmed.slice(0, colon).trim();
|
|
107
|
+
let value = trimmed.slice(colon + 1).trim();
|
|
108
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
109
|
+
if (key === "name") out.name = value;
|
|
110
|
+
else if (key === "description") out.description = value;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function walkSkillFiles(root, dir, out) {
|
|
115
|
+
for (const name of readdirSync(dir).sort()) {
|
|
116
|
+
const absolute = join(dir, name);
|
|
117
|
+
const stats = statSync(absolute);
|
|
118
|
+
if (stats.isDirectory()) {
|
|
119
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
120
|
+
walkSkillFiles(root, absolute, out);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!stats.isFile() || name !== "SKILL.md") continue;
|
|
124
|
+
const moduleFile = toPosix$1(relative(root, absolute));
|
|
125
|
+
const slug = toPosix$1(relative(join(root, "src", "skills"), absolute)).replace(/\/?SKILL\.md$/, "");
|
|
126
|
+
if (!slug) continue;
|
|
127
|
+
const frontmatter = parseSkillFrontmatter(readFileSync(absolute, "utf8"));
|
|
128
|
+
out.push({
|
|
129
|
+
slug,
|
|
130
|
+
name: frontmatter.name,
|
|
131
|
+
description: frontmatter.description,
|
|
132
|
+
moduleFile
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Discover skill metadata from src/skills SKILL.md files for the route manifest. */
|
|
137
|
+
function discoverSkillManifestEntries(projectRoot) {
|
|
138
|
+
const skillsDir = join(projectRoot, "src", "skills");
|
|
139
|
+
if (!statSync(skillsDir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
140
|
+
const entries = [];
|
|
141
|
+
walkSkillFiles(projectRoot, skillsDir, entries);
|
|
142
|
+
return entries;
|
|
143
|
+
}
|
|
144
|
+
function schemaToJson(schema) {
|
|
145
|
+
return toJSONSchema(schema, { target: "openapi-3.0" });
|
|
146
|
+
}
|
|
147
|
+
function collectIntegrationKeys(integrations) {
|
|
148
|
+
return integrations.map((integration) => integration.key);
|
|
149
|
+
}
|
|
150
|
+
function serializeRouteManifest(manifest) {
|
|
151
|
+
return manifest.map((entry) => {
|
|
152
|
+
switch (entry.kind) {
|
|
153
|
+
case "health": return entry;
|
|
154
|
+
case "agent": return {
|
|
155
|
+
kind: entry.kind,
|
|
156
|
+
method: entry.method,
|
|
157
|
+
path: entry.path,
|
|
158
|
+
agentSlug: entry.agentSlug,
|
|
159
|
+
moduleFile: entry.moduleFile,
|
|
160
|
+
name: entry.name,
|
|
161
|
+
description: entry.description,
|
|
162
|
+
model: entry.model,
|
|
163
|
+
systemPrompt: entry.systemPrompt,
|
|
164
|
+
toolCount: entry.toolCount,
|
|
165
|
+
credentialCount: entry.credentialCount,
|
|
166
|
+
requestSchema: schemaToJson(entry.request),
|
|
167
|
+
responseSchema: schemaToJson(entry.response)
|
|
168
|
+
};
|
|
169
|
+
case "workflow": return {
|
|
170
|
+
kind: entry.kind,
|
|
171
|
+
method: entry.method,
|
|
172
|
+
path: entry.path,
|
|
173
|
+
workflowSlug: entry.workflowSlug,
|
|
174
|
+
workflowName: entry.workflowName,
|
|
175
|
+
description: entry.description,
|
|
176
|
+
subscribable: entry.subscribable,
|
|
177
|
+
moduleFile: entry.moduleFile,
|
|
178
|
+
requestSchema: schemaToJson(entry.request),
|
|
179
|
+
responseSchema: schemaToJson(entry.response)
|
|
180
|
+
};
|
|
181
|
+
case "trigger-webhook": return {
|
|
182
|
+
kind: entry.kind,
|
|
183
|
+
method: entry.method,
|
|
184
|
+
path: entry.path,
|
|
185
|
+
attachmentIds: entry.attachmentIds,
|
|
186
|
+
moduleFile: entry.moduleFile,
|
|
187
|
+
requestSchema: schemaToJson(entry.request),
|
|
188
|
+
attachmentSchemas: Object.fromEntries(Object.entries(entry.attachmentSchemas).map(([attachmentKey, schemas]) => [attachmentKey, {
|
|
189
|
+
requestSchema: schemaToJson(schemas.request),
|
|
190
|
+
...schemas.filter ? { filterSchema: schemaToJson(schemas.filter) } : {}
|
|
191
|
+
}])),
|
|
192
|
+
responseSchema: schemaToJson(entry.response)
|
|
193
|
+
};
|
|
194
|
+
case "trigger-poll": return {
|
|
195
|
+
kind: entry.kind,
|
|
196
|
+
method: entry.method,
|
|
197
|
+
path: entry.path,
|
|
198
|
+
attachmentId: entry.attachmentId,
|
|
199
|
+
moduleFile: entry.moduleFile,
|
|
200
|
+
schedule: entry.schedule,
|
|
201
|
+
responseSchema: schemaToJson(entry.response)
|
|
202
|
+
};
|
|
203
|
+
case "trigger-poll-group": return {
|
|
204
|
+
kind: entry.kind,
|
|
205
|
+
method: entry.method,
|
|
206
|
+
path: entry.path,
|
|
207
|
+
pollId: entry.pollId,
|
|
208
|
+
attachmentIds: entry.attachmentIds,
|
|
209
|
+
moduleFile: entry.moduleFile,
|
|
210
|
+
schedule: entry.schedule,
|
|
211
|
+
responseSchema: schemaToJson(entry.response)
|
|
212
|
+
};
|
|
213
|
+
case "plugin": return {
|
|
214
|
+
kind: entry.kind,
|
|
215
|
+
method: entry.method,
|
|
216
|
+
path: entry.path,
|
|
217
|
+
plugin: entry.plugin
|
|
218
|
+
};
|
|
219
|
+
default: return entry;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function toStoredRouteManifest(manifest, options) {
|
|
224
|
+
return {
|
|
225
|
+
version: 1,
|
|
226
|
+
entries: serializeRouteManifest(manifest),
|
|
227
|
+
skills: options?.skills ?? [],
|
|
228
|
+
integrations: options?.integrations
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function buildStoredRouteManifestFromContext(ctx) {
|
|
232
|
+
const integrations = ctx.options?.integrations ?? ctx.options?.plugins?.map((plugin) => ({
|
|
233
|
+
key: plugin.name,
|
|
234
|
+
mount: plugin.register
|
|
235
|
+
})) ?? [];
|
|
236
|
+
return toStoredRouteManifest(ctx.manifest, {
|
|
237
|
+
integrations: collectIntegrationKeys(integrations),
|
|
238
|
+
skills: ctx.skills ?? (ctx.projectRoot ? discoverSkillManifestEntries(ctx.projectRoot) : void 0) ?? []
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function persistStoredRouteManifest(projectRoot, manifest) {
|
|
242
|
+
const path = join(projectRoot, ROUTE_MANIFEST_REL_PATH);
|
|
243
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
244
|
+
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
245
|
+
}
|
|
246
|
+
const WORKFLOW = Symbol.for("keystroke.workflow");
|
|
247
|
+
function isManifestWorkflow(value) {
|
|
248
|
+
if (typeof value !== "object" || value === null) return false;
|
|
249
|
+
if (!(WORKFLOW in value) || value[WORKFLOW] !== true) return false;
|
|
250
|
+
const workflow = value;
|
|
251
|
+
return typeof workflow.slug === "string" && workflow.slug.trim().length > 0 && workflow.input instanceof Object && "safeParse" in workflow.input && workflow.output instanceof Object && "safeParse" in workflow.output;
|
|
252
|
+
}
|
|
253
|
+
function validateManifestWorkflow(value, filePath) {
|
|
254
|
+
if (!isManifestWorkflow(value)) throw new Error(`${filePath} must default-export defineWorkflow(...)`);
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
const TRIGGER_ATTACHMENT = Symbol.for("keystroke.triggerAttachment");
|
|
258
|
+
const zodSchema = custom((value) => value instanceof ZodType, "must be a Zod schema");
|
|
259
|
+
const cronScheduleSchema = string().trim().min(1, "cron schedule must be a non-empty string");
|
|
260
|
+
const workflowSchema = custom((value) => isManifestWorkflow(value), "must be defineWorkflow(...)");
|
|
261
|
+
const agentSchema = custom((value) => typeof value === "object" && value !== null && typeof value.slug === "string" && value.slug.trim().length > 0 && typeof value.prompt === "function", "must be defineAgent(...)");
|
|
262
|
+
const promptSchema = union([string(), _function()]);
|
|
263
|
+
const baseSourceShape = {
|
|
264
|
+
key: string().trim().min(1),
|
|
265
|
+
attach: _function()
|
|
266
|
+
};
|
|
267
|
+
const triggerSourceSchema = discriminatedUnion("kind", [
|
|
268
|
+
object({
|
|
269
|
+
kind: literal("webhook"),
|
|
270
|
+
...baseSourceShape,
|
|
271
|
+
endpoint: string().min(1),
|
|
272
|
+
request: zodSchema,
|
|
273
|
+
filter: zodSchema.optional(),
|
|
274
|
+
passes: _function()
|
|
275
|
+
}),
|
|
276
|
+
object({
|
|
277
|
+
kind: literal("cron"),
|
|
278
|
+
...baseSourceShape,
|
|
279
|
+
schedule: cronScheduleSchema
|
|
280
|
+
}),
|
|
281
|
+
object({
|
|
282
|
+
kind: literal("poll"),
|
|
283
|
+
...baseSourceShape,
|
|
284
|
+
id: string().optional(),
|
|
285
|
+
schedule: cronScheduleSchema,
|
|
286
|
+
run: _function(),
|
|
287
|
+
filters: array(_function()),
|
|
288
|
+
passes: _function()
|
|
289
|
+
})
|
|
290
|
+
]);
|
|
291
|
+
const triggerAttachmentCoreSchema = discriminatedUnion("target", [object({
|
|
292
|
+
key: string().trim().min(1),
|
|
293
|
+
source: triggerSourceSchema,
|
|
294
|
+
target: literal("workflow"),
|
|
295
|
+
workflow: workflowSchema,
|
|
296
|
+
transform: _function().optional()
|
|
297
|
+
}), object({
|
|
298
|
+
key: string().trim().min(1),
|
|
299
|
+
source: triggerSourceSchema,
|
|
300
|
+
target: literal("agent"),
|
|
301
|
+
agent: agentSchema,
|
|
302
|
+
prompt: promptSchema
|
|
303
|
+
})]);
|
|
304
|
+
function isManifestTriggerAttachment(value) {
|
|
305
|
+
if (typeof value !== "object" || value === null) return false;
|
|
306
|
+
if (!(TRIGGER_ATTACHMENT in value) || value[TRIGGER_ATTACHMENT] !== true) return false;
|
|
307
|
+
return triggerAttachmentCoreSchema.safeParse(value).success;
|
|
308
|
+
}
|
|
309
|
+
function validateManifestTriggerAttachment(value, filePath) {
|
|
310
|
+
if (!isManifestTriggerAttachment(value)) throw new Error(`${filePath} must default-export a trigger attachment from .attach({ workflow }) or .attach({ agent, prompt })`);
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
function shouldDiscoverTriggerFile(triggersDir, filePath) {
|
|
314
|
+
return !relative(triggersDir, filePath).split(sep).includes("sources");
|
|
315
|
+
}
|
|
316
|
+
async function discoverTriggerAttachments(triggersDir) {
|
|
317
|
+
const files = await discoverModuleFileEntries(triggersDir, {
|
|
318
|
+
nestedEntry: "trigger",
|
|
319
|
+
duplicateLabel: "trigger module file",
|
|
320
|
+
shouldDiscoverFile: (filePath) => shouldDiscoverTriggerFile(triggersDir, filePath)
|
|
321
|
+
});
|
|
322
|
+
const attachments = [];
|
|
323
|
+
for (const { filePath, moduleFile } of files) {
|
|
324
|
+
const attachment = await importTriggerAttachment(filePath);
|
|
325
|
+
attachments.push({
|
|
326
|
+
key: attachment.key,
|
|
327
|
+
filePath,
|
|
328
|
+
moduleFile,
|
|
329
|
+
attachment
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return attachments;
|
|
333
|
+
}
|
|
334
|
+
function validateImportedTriggerAttachment(def, filePath) {
|
|
335
|
+
return validateManifestTriggerAttachment(def, filePath);
|
|
336
|
+
}
|
|
337
|
+
async function importTriggerAttachment(filePath) {
|
|
338
|
+
return validateImportedTriggerAttachment((await import(pathToFileURL(filePath).href)).default, filePath);
|
|
339
|
+
}
|
|
340
|
+
function pollGroupId(discovered) {
|
|
341
|
+
const source = discovered.attachment.source;
|
|
342
|
+
if (source.kind !== "poll") throw new Error(`Attachment "${discovered.key}" is not a poll trigger`);
|
|
343
|
+
return source.id ?? source.key;
|
|
344
|
+
}
|
|
345
|
+
function buildPollGroups(attachments) {
|
|
346
|
+
const byId = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const discovered of attachments) {
|
|
348
|
+
if (discovered.attachment.source.kind !== "poll") continue;
|
|
349
|
+
const id = pollGroupId(discovered);
|
|
350
|
+
const group = byId.get(id) ?? [];
|
|
351
|
+
group.push(discovered);
|
|
352
|
+
byId.set(id, group);
|
|
353
|
+
}
|
|
354
|
+
return [...byId.entries()].map(([id, groupAttachments]) => ({
|
|
355
|
+
id,
|
|
356
|
+
attachments: groupAttachments
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
function buildWebhookBindingsByRoute(attachments, handlerOptionsFor) {
|
|
360
|
+
const webhookBindingsByRoute = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const discovered of attachments) {
|
|
362
|
+
const source = discovered.attachment.source;
|
|
363
|
+
if (source.kind !== "webhook") continue;
|
|
364
|
+
const route = webhookRouteFromEndpoint(source.endpoint);
|
|
365
|
+
const bindings = webhookBindingsByRoute.get(route) ?? [];
|
|
366
|
+
bindings.push({
|
|
367
|
+
discovered,
|
|
368
|
+
options: handlerOptionsFor(discovered.key)
|
|
369
|
+
});
|
|
370
|
+
webhookBindingsByRoute.set(route, bindings);
|
|
371
|
+
}
|
|
372
|
+
return webhookBindingsByRoute;
|
|
373
|
+
}
|
|
374
|
+
function webhookSchemaEntriesFromBindings(bindings) {
|
|
375
|
+
return bindings.flatMap((binding) => {
|
|
376
|
+
const source = binding.discovered.attachment.source;
|
|
377
|
+
if (source.kind !== "webhook") return [];
|
|
378
|
+
return [{
|
|
379
|
+
attachmentKey: binding.discovered.key,
|
|
380
|
+
request: source.request,
|
|
381
|
+
...source.filter ? { filter: source.filter } : {}
|
|
382
|
+
}];
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function webhookMatchSchemaForBindings(bindings) {
|
|
386
|
+
const schemas = bindings.flatMap((binding) => {
|
|
387
|
+
const source = binding.discovered.attachment.source;
|
|
388
|
+
return source.kind === "webhook" ? [source.request] : [];
|
|
389
|
+
});
|
|
390
|
+
if (schemas.length === 0) throw new Error("Webhook bindings require at least one webhook source");
|
|
391
|
+
if (schemas.length === 1) return schemas[0];
|
|
392
|
+
return union(schemas);
|
|
393
|
+
}
|
|
394
|
+
function webhookManifestAttachmentSchemasFromBindings(bindings) {
|
|
395
|
+
return Object.fromEntries(webhookSchemaEntriesFromBindings(bindings).map((entry) => [entry.attachmentKey, {
|
|
396
|
+
request: entry.request,
|
|
397
|
+
...entry.filter ? { filter: entry.filter } : {}
|
|
398
|
+
}]));
|
|
399
|
+
}
|
|
400
|
+
async function discoverWorkflowEntries(workflowsDir) {
|
|
401
|
+
const files = await discoverModuleFileEntries(workflowsDir, {
|
|
402
|
+
nestedEntry: "workflow",
|
|
403
|
+
duplicateLabel: "workflow module file"
|
|
404
|
+
});
|
|
405
|
+
const entries = [];
|
|
406
|
+
for (const { filePath, moduleFile } of files) {
|
|
407
|
+
const definition = await importWorkflowDefinition(filePath);
|
|
408
|
+
entries.push({
|
|
409
|
+
key: definition.slug,
|
|
410
|
+
route: workflowRouteFromKey(definition.slug),
|
|
411
|
+
filePath,
|
|
412
|
+
moduleFile
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return entries;
|
|
416
|
+
}
|
|
417
|
+
function validateImportedWorkflowDefinition(def, filePath) {
|
|
418
|
+
return validateManifestWorkflow(def, filePath);
|
|
419
|
+
}
|
|
420
|
+
async function importWorkflowDefinition(filePath) {
|
|
421
|
+
return validateImportedWorkflowDefinition((await import(pathToFileURL(filePath).href)).default, filePath);
|
|
422
|
+
}
|
|
423
|
+
async function discoverWorkflows(workflowsDir) {
|
|
424
|
+
const entries = await discoverWorkflowEntries(workflowsDir);
|
|
425
|
+
const workflows = [];
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
const definition = await importWorkflowDefinition(entry.filePath);
|
|
428
|
+
workflows.push({
|
|
429
|
+
key: definition.slug,
|
|
430
|
+
route: workflowRouteFromKey(definition.slug),
|
|
431
|
+
filePath: entry.filePath,
|
|
432
|
+
definition
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return workflows;
|
|
436
|
+
}
|
|
437
|
+
function resolveDistModuleDirs(projectRoot) {
|
|
438
|
+
const distBase = join(projectRoot, "dist");
|
|
439
|
+
if (!existsSync(distBase)) throw new Error(`Build output missing at ${distBase}. Run keystroke build before emitting the route manifest.`);
|
|
440
|
+
return {
|
|
441
|
+
agentsDir: join(distBase, "agents"),
|
|
442
|
+
workflowsDir: join(distBase, "workflows"),
|
|
443
|
+
triggersDir: join(distBase, "triggers")
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function toPosix(path) {
|
|
447
|
+
return path.split(sep).join("/");
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve manifest moduleFile values to project-root-relative source paths.
|
|
451
|
+
*
|
|
452
|
+
* Discovery runs over compiled `dist/` modules, so the raw moduleFile is a
|
|
453
|
+
* dist-relative `.mjs` path. Dist entries are named by their source entry id
|
|
454
|
+
* (`team/escalation.mjs` ← `src/agents/team/escalation/agent.ts`), so walking
|
|
455
|
+
* the matching source dir by the same id rules recovers the source path. Falls
|
|
456
|
+
* back to the dist-relative value when no source file matches (e.g. building
|
|
457
|
+
* a dist-only artifact with no `src/`).
|
|
458
|
+
*/
|
|
459
|
+
var SourceModuleFileResolver = class {
|
|
460
|
+
projectRoot;
|
|
461
|
+
mapsByKind = /* @__PURE__ */ new Map();
|
|
462
|
+
constructor(projectRoot) {
|
|
463
|
+
this.projectRoot = projectRoot;
|
|
464
|
+
}
|
|
465
|
+
async sourceMapFor(kindDir, nestedEntry) {
|
|
466
|
+
const existing = this.mapsByKind.get(kindDir);
|
|
467
|
+
if (existing) return existing;
|
|
468
|
+
const map = /* @__PURE__ */ new Map();
|
|
469
|
+
const sourceDir = join(this.projectRoot, "src", kindDir);
|
|
470
|
+
for (const filePath of await walkTypeScriptFiles(sourceDir)) {
|
|
471
|
+
const id = entryIdFromFile(sourceDir, filePath, { nestedEntry });
|
|
472
|
+
if (id) map.set(id, toPosix(relative(this.projectRoot, filePath)));
|
|
473
|
+
}
|
|
474
|
+
this.mapsByKind.set(kindDir, map);
|
|
475
|
+
return map;
|
|
476
|
+
}
|
|
477
|
+
async resolve(kindDir, nestedEntry, distDir, distFilePath) {
|
|
478
|
+
const fallback = toPosix(relative(distDir, distFilePath));
|
|
479
|
+
const id = entryIdFromFile(distDir, distFilePath, { nestedEntry });
|
|
480
|
+
if (!id) return fallback;
|
|
481
|
+
return (await this.sourceMapFor(kindDir, nestedEntry)).get(id) ?? fallback;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
/** Build a stored route manifest from compiled dist/ modules without starting a server. */
|
|
485
|
+
async function buildStoredRouteManifestForProject(projectRoot) {
|
|
486
|
+
const previousRoot = process.env.KEYSTROKE_ROOT;
|
|
487
|
+
process.env.KEYSTROKE_ROOT = projectRoot;
|
|
488
|
+
try {
|
|
489
|
+
const dirs = resolveDistModuleDirs(projectRoot);
|
|
490
|
+
const sourcePaths = new SourceModuleFileResolver(projectRoot);
|
|
491
|
+
const manifest = [{
|
|
492
|
+
kind: "health",
|
|
493
|
+
method: "GET",
|
|
494
|
+
path: "/health"
|
|
495
|
+
}];
|
|
496
|
+
const agentEntries = await discoverAgentEntries(dirs.agentsDir);
|
|
497
|
+
for (const entry of agentEntries) {
|
|
498
|
+
const agent = await importAgentDefinition(entry.filePath);
|
|
499
|
+
const moduleFile = await sourcePaths.resolve("agents", "agent", dirs.agentsDir, entry.filePath);
|
|
500
|
+
manifest.push({
|
|
501
|
+
kind: "agent",
|
|
502
|
+
method: "POST",
|
|
503
|
+
path: entry.route,
|
|
504
|
+
agentSlug: agent.slug,
|
|
505
|
+
moduleFile,
|
|
506
|
+
name: agent.name,
|
|
507
|
+
description: agent.description,
|
|
508
|
+
model: agent.model,
|
|
509
|
+
systemPrompt: agent.systemPrompt,
|
|
510
|
+
toolCount: agent.tools?.length ?? 0,
|
|
511
|
+
credentialCount: countAgentCredentials(agent),
|
|
512
|
+
request: PromptInputSchema,
|
|
513
|
+
response: PromptResponseSchema
|
|
514
|
+
});
|
|
515
|
+
manifest.push({
|
|
516
|
+
kind: "agent-sessions-list",
|
|
517
|
+
method: "GET",
|
|
518
|
+
path: agentSessionsListPath(entry.route),
|
|
519
|
+
agentSlug: entry.key,
|
|
520
|
+
moduleFile
|
|
521
|
+
});
|
|
522
|
+
manifest.push({
|
|
523
|
+
kind: "agent-session-detail",
|
|
524
|
+
method: "GET",
|
|
525
|
+
path: agentSessionDetailPath(entry.route),
|
|
526
|
+
agentSlug: entry.key,
|
|
527
|
+
moduleFile
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const workflows = await discoverWorkflows(dirs.workflowsDir);
|
|
531
|
+
for (const workflow of workflows) {
|
|
532
|
+
const route = workflowRouteFromKey(workflow.key);
|
|
533
|
+
const moduleFile = await sourcePaths.resolve("workflows", "workflow", dirs.workflowsDir, workflow.filePath);
|
|
534
|
+
manifest.push({
|
|
535
|
+
kind: "workflow",
|
|
536
|
+
method: "POST",
|
|
537
|
+
path: route,
|
|
538
|
+
workflowSlug: workflow.definition.slug,
|
|
539
|
+
workflowName: workflow.definition.name,
|
|
540
|
+
description: workflow.definition.description,
|
|
541
|
+
subscribable: workflow.definition.subscription?.mode === "subscribable",
|
|
542
|
+
moduleFile,
|
|
543
|
+
request: workflow.definition.input,
|
|
544
|
+
response: workflow.definition.output
|
|
545
|
+
});
|
|
546
|
+
manifest.push({
|
|
547
|
+
kind: "workflow-runs-list",
|
|
548
|
+
method: "GET",
|
|
549
|
+
path: workflowRunsListPath(route),
|
|
550
|
+
workflowSlug: workflow.definition.slug,
|
|
551
|
+
workflowName: workflow.definition.name,
|
|
552
|
+
moduleFile
|
|
553
|
+
});
|
|
554
|
+
manifest.push({
|
|
555
|
+
kind: "workflow-run-detail",
|
|
556
|
+
method: "GET",
|
|
557
|
+
path: workflowRunDetailPath(route),
|
|
558
|
+
workflowSlug: workflow.definition.slug,
|
|
559
|
+
workflowName: workflow.definition.name,
|
|
560
|
+
moduleFile
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const attachments = await discoverTriggerAttachments(dirs.triggersDir);
|
|
564
|
+
const discoveredByKey = new Map(attachments.map((attachment) => [attachment.key, attachment]));
|
|
565
|
+
const pollGroups = buildPollGroups(attachments);
|
|
566
|
+
for (const discovered of discoveredByKey.values()) {
|
|
567
|
+
const source = discovered.attachment.source;
|
|
568
|
+
const moduleFile = await sourcePaths.resolve("triggers", "trigger", dirs.triggersDir, discovered.filePath);
|
|
569
|
+
if (source.kind === "cron") {
|
|
570
|
+
manifest.push({
|
|
571
|
+
kind: "cron-schedule",
|
|
572
|
+
attachmentId: discovered.key,
|
|
573
|
+
moduleFile,
|
|
574
|
+
schedule: source.schedule
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (source.kind === "poll") {
|
|
579
|
+
const route = pollRouteFromKey(source.key);
|
|
580
|
+
manifest.push({
|
|
581
|
+
kind: "trigger-poll",
|
|
582
|
+
method: "POST",
|
|
583
|
+
path: route,
|
|
584
|
+
attachmentId: discovered.key,
|
|
585
|
+
moduleFile,
|
|
586
|
+
schedule: source.schedule,
|
|
587
|
+
response: PromptResponseSchema
|
|
588
|
+
});
|
|
589
|
+
manifest.push({
|
|
590
|
+
kind: "trigger-runs-list",
|
|
591
|
+
method: "GET",
|
|
592
|
+
path: triggerRunsListPath(discovered.key),
|
|
593
|
+
attachmentId: discovered.key,
|
|
594
|
+
moduleFile
|
|
595
|
+
});
|
|
596
|
+
manifest.push({
|
|
597
|
+
kind: "trigger-run-detail",
|
|
598
|
+
method: "GET",
|
|
599
|
+
path: triggerRunDetailPath(discovered.key),
|
|
600
|
+
attachmentId: discovered.key,
|
|
601
|
+
moduleFile
|
|
602
|
+
});
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (source.kind === "webhook") {
|
|
606
|
+
const route = webhookRouteFromEndpoint(source.endpoint);
|
|
607
|
+
const bindings = buildWebhookBindingsByRoute(discoveredByKey.values(), () => ({
|
|
608
|
+
execution: { attachmentKey: discovered.key },
|
|
609
|
+
attachmentKey: discovered.key
|
|
610
|
+
})).get(route) ?? [{
|
|
611
|
+
discovered,
|
|
612
|
+
options: { attachmentKey: discovered.key }
|
|
613
|
+
}];
|
|
614
|
+
manifest.push({
|
|
615
|
+
kind: "trigger-webhook",
|
|
616
|
+
method: "POST",
|
|
617
|
+
path: route,
|
|
618
|
+
attachmentIds: bindings.map(({ discovered: row }) => row.key),
|
|
619
|
+
moduleFile,
|
|
620
|
+
request: webhookMatchSchemaForBindings(bindings),
|
|
621
|
+
attachmentSchemas: webhookManifestAttachmentSchemasFromBindings(bindings),
|
|
622
|
+
response: PromptResponseSchema
|
|
623
|
+
});
|
|
624
|
+
for (const { discovered: row } of bindings) {
|
|
625
|
+
const rowModuleFile = await sourcePaths.resolve("triggers", "trigger", dirs.triggersDir, row.filePath);
|
|
626
|
+
manifest.push({
|
|
627
|
+
kind: "trigger-runs-list",
|
|
628
|
+
method: "GET",
|
|
629
|
+
path: triggerRunsListPath(row.key),
|
|
630
|
+
attachmentId: row.key,
|
|
631
|
+
moduleFile: rowModuleFile
|
|
632
|
+
});
|
|
633
|
+
manifest.push({
|
|
634
|
+
kind: "trigger-run-detail",
|
|
635
|
+
method: "GET",
|
|
636
|
+
path: triggerRunDetailPath(row.key),
|
|
637
|
+
attachmentId: row.key,
|
|
638
|
+
moduleFile: rowModuleFile
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
for (const group of pollGroups) {
|
|
644
|
+
if (group.attachments.length <= 1) continue;
|
|
645
|
+
const first = group.attachments[0];
|
|
646
|
+
const source = first.attachment.source;
|
|
647
|
+
manifest.push({
|
|
648
|
+
kind: "trigger-poll-group",
|
|
649
|
+
method: "POST",
|
|
650
|
+
path: pollGroupRouteFromId(group.id),
|
|
651
|
+
pollId: group.id,
|
|
652
|
+
attachmentIds: group.attachments.map((attachment) => attachment.key),
|
|
653
|
+
moduleFile: await sourcePaths.resolve("triggers", "trigger", dirs.triggersDir, first.filePath),
|
|
654
|
+
schedule: source.kind === "poll" ? source.schedule : "",
|
|
655
|
+
response: PromptResponseSchema
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return buildStoredRouteManifestFromContext({
|
|
659
|
+
manifest,
|
|
660
|
+
options: {},
|
|
661
|
+
projectRoot,
|
|
662
|
+
skills: discoverSkillManifestEntries(projectRoot)
|
|
663
|
+
});
|
|
664
|
+
} finally {
|
|
665
|
+
if (previousRoot === void 0) delete process.env.KEYSTROKE_ROOT;
|
|
666
|
+
else process.env.KEYSTROKE_ROOT = previousRoot;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Write `dist/.keystroke/route-manifest.json` for the project. */
|
|
670
|
+
async function emitStoredRouteManifestForProject(projectRoot) {
|
|
671
|
+
persistStoredRouteManifest(projectRoot, await buildStoredRouteManifestForProject(projectRoot));
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
674
|
+
export { emitStoredRouteManifestForProject };
|
|
675
|
+
|
|
676
|
+
//# sourceMappingURL=dist-CmCvzhNH.mjs.map
|