@convilyn/sdk-author 0.7.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.cjs ADDED
@@ -0,0 +1,1391 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var promises = require('fs/promises');
6
+ var path = require('path');
7
+ var url = require('url');
8
+ var crypto = require('crypto');
9
+ var http = require('http');
10
+
11
+ // src/version.ts
12
+ var VERSION = "0.7.0";
13
+
14
+ // src/errors.ts
15
+ var ConvilynAuthorError = class extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "ConvilynAuthorError";
19
+ }
20
+ };
21
+ var InvalidSignatureError = class extends ConvilynAuthorError {
22
+ reason;
23
+ constructor(reason, message) {
24
+ super(message);
25
+ this.name = "InvalidSignatureError";
26
+ this.reason = reason;
27
+ }
28
+ };
29
+ var ConvilynStartupError = class extends ConvilynAuthorError {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "ConvilynStartupError";
33
+ }
34
+ };
35
+ var ConvilynApiError = class extends ConvilynAuthorError {
36
+ status;
37
+ body;
38
+ constructor(status, message, body) {
39
+ super(message);
40
+ this.name = "ConvilynApiError";
41
+ this.status = status;
42
+ this.body = body;
43
+ }
44
+ };
45
+
46
+ // src/types.ts
47
+ function allChecksPassed(report) {
48
+ return report.results.every((result) => result.passed);
49
+ }
50
+
51
+ // src/cli/print.ts
52
+ var consolePrinter = {
53
+ out: (line) => {
54
+ process.stdout.write(`${line}
55
+ `);
56
+ },
57
+ err: (line) => {
58
+ process.stderr.write(`${line}
59
+ `);
60
+ }
61
+ };
62
+ var NAME_RE = /^[a-zA-Z0-9_-]+$/;
63
+ function isValidProjectName(name) {
64
+ return NAME_RE.test(name);
65
+ }
66
+ function serverTs(name) {
67
+ return `import { defineTool, ToolServer, ToolResult } from '@convilyn/sdk-author'
68
+ import { z } from 'zod'
69
+
70
+ const echo = defineTool({
71
+ name: 'echo',
72
+ description: 'Echo the input text back.',
73
+ input: z.object({ text: z.string().min(1) }),
74
+ idempotent: true,
75
+ handler: (args) => ToolResult.ok({ echoed: args.text }, \`Echoed \${args.text.length} chars\`),
76
+ })
77
+
78
+ export const server = new ToolServer(
79
+ { name: '${name}', version: '0.1.0', description: 'A Convilyn tool server.' },
80
+ { tools: [echo] }
81
+ )
82
+
83
+ export default server
84
+ `;
85
+ }
86
+ function packageJson(name) {
87
+ return `${JSON.stringify(
88
+ {
89
+ name,
90
+ version: "0.1.0",
91
+ private: true,
92
+ type: "module",
93
+ scripts: {
94
+ build: "tsc",
95
+ synth: "convilyn-author synth --file dist/server.js",
96
+ dev: "convilyn-author dev --file dist/server.js",
97
+ test: "convilyn-author test --file dist/server.js"
98
+ },
99
+ dependencies: {
100
+ "@convilyn/sdk-author": `^${VERSION}`,
101
+ zod: "^3.23.0"
102
+ },
103
+ devDependencies: {
104
+ typescript: "^5.7.2"
105
+ }
106
+ },
107
+ null,
108
+ 2
109
+ )}
110
+ `;
111
+ }
112
+ var TSCONFIG = `${JSON.stringify(
113
+ {
114
+ compilerOptions: {
115
+ target: "ES2022",
116
+ module: "NodeNext",
117
+ moduleResolution: "NodeNext",
118
+ outDir: "dist",
119
+ strict: true,
120
+ skipLibCheck: true,
121
+ declaration: false
122
+ },
123
+ include: ["server.ts"]
124
+ },
125
+ null,
126
+ 2
127
+ )}
128
+ `;
129
+ var GITIGNORE = `node_modules/
130
+ dist/
131
+ convilyn.manifest.json
132
+ .env
133
+ `;
134
+ var ENV_EXAMPLE = `# Inbound HMAC secret (enables POST /mcp signature verification)
135
+ CONVILYN_HMAC_SECRET=
136
+ # Local dev server port
137
+ CONVILYN_PORT=8080
138
+ # Platform API (used by push/deploy once those land)
139
+ CONVILYN_PLATFORM_URL=https://api.convilyn.corenovus.com
140
+ CONVILYN_API_KEY=
141
+ `;
142
+ function readme(name) {
143
+ return `# ${name}
144
+
145
+ A Convilyn tool server, scaffolded by \`convilyn-author init\`.
146
+
147
+ ## Develop
148
+
149
+ \`\`\`bash
150
+ npm install
151
+ npm run build # server.ts -> dist/server.js
152
+ npx convilyn-author dev # run the JSON-RPC /mcp server locally
153
+ npx convilyn-author synth # write convilyn.manifest.json
154
+ npx convilyn-author test # local compliance checks
155
+ \`\`\`
156
+
157
+ Edit \`server.ts\` to add tools with \`defineTool\`.
158
+ `;
159
+ }
160
+ function projectFiles(name) {
161
+ return {
162
+ "server.ts": serverTs(name),
163
+ "package.json": packageJson(name),
164
+ "tsconfig.json": TSCONFIG,
165
+ ".gitignore": GITIGNORE,
166
+ ".env.example": ENV_EXAMPLE,
167
+ "README.md": readme(name)
168
+ };
169
+ }
170
+ async function isNonEmptyDir(dir) {
171
+ try {
172
+ const entries = await promises.readdir(dir);
173
+ return entries.length > 0;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+ async function scaffoldProject(options) {
179
+ const { name } = options;
180
+ if (!isValidProjectName(name)) {
181
+ throw new Error(
182
+ `Invalid project name "${name}": use letters, digits, hyphens, underscores only.`
183
+ );
184
+ }
185
+ const path$1 = path.resolve(options.dir ?? `./${name}`);
186
+ if (await isNonEmptyDir(path$1)) {
187
+ throw new Error(`Target directory "${path$1}" is not empty \u2014 refusing to overwrite.`);
188
+ }
189
+ await promises.mkdir(path$1, { recursive: true });
190
+ const files = projectFiles(name);
191
+ const written = [];
192
+ for (const [rel, content] of Object.entries(files)) {
193
+ await promises.writeFile(path.join(path$1, rel), content, "utf8");
194
+ written.push(rel);
195
+ }
196
+ return { path: path$1, files: written.sort() };
197
+ }
198
+ function isToolServerLike(value) {
199
+ if (typeof value !== "object" || value === null) {
200
+ return false;
201
+ }
202
+ const candidate = value;
203
+ return typeof candidate.manifest === "function" && typeof candidate.dispatch === "function" && typeof candidate.server === "object" && candidate.server !== null;
204
+ }
205
+ function pickServer(mod) {
206
+ if (isToolServerLike(mod.default)) {
207
+ return mod.default;
208
+ }
209
+ if (isToolServerLike(mod.server)) {
210
+ return mod.server;
211
+ }
212
+ for (const value of Object.values(mod)) {
213
+ if (isToolServerLike(value)) {
214
+ return value;
215
+ }
216
+ }
217
+ return void 0;
218
+ }
219
+ async function loadToolServer(file) {
220
+ const absolute = path.resolve(file);
221
+ let mod;
222
+ try {
223
+ mod = await import(url.pathToFileURL(absolute).href);
224
+ } catch (err) {
225
+ throw new Error(
226
+ `Could not import server module "${file}": ${err.message}. Point --file at a built .js module (e.g. "npm run build" then --file dist/server.js).`
227
+ );
228
+ }
229
+ const server = pickServer(mod);
230
+ if (server === void 0) {
231
+ throw new Error(
232
+ `No ToolServer found in "${file}". Export it as the default export or as a named "server" export.`
233
+ );
234
+ }
235
+ return server;
236
+ }
237
+ var PUBLIC_SCHEMA_VERSION = "1";
238
+ var DEFAULT_SCHEMA_VERSION = "1.0";
239
+ var MAX_NAME = 80;
240
+ var MAX_DESCRIPTION = 500;
241
+ var MAX_SYSTEM_PROMPT = 8e3;
242
+ var MAX_TOOLS = 20;
243
+ var MAX_TOOL_NAME = 128;
244
+ var MAX_NOTE = 200;
245
+ var MAX_TAGS = 8;
246
+ var MAX_TAG_LEN = 24;
247
+ var ORIGIN = { x: 0, y: 0 };
248
+ var FORBIDDEN_TOOL = "request_user_input";
249
+ var DEFAULT_VERSION = "1.0.0";
250
+ var MAX_SPEC_ID = 128;
251
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
252
+ function validateName(name) {
253
+ if (name.length < 1 || name.length > MAX_NAME) {
254
+ throw new ConvilynAuthorError(`name must be 1\u2013${MAX_NAME} characters (got ${name.length})`);
255
+ }
256
+ }
257
+ function validateSpecId(specId) {
258
+ if (specId.length < 1 || specId.length > MAX_SPEC_ID || /\s/.test(specId)) {
259
+ throw new ConvilynAuthorError(
260
+ `specId must be 1\u2013${MAX_SPEC_ID} characters with no whitespace (the portal expects the "dev_<developer-id>.<name>" namespace)`
261
+ );
262
+ }
263
+ }
264
+ function validateVersion(version) {
265
+ if (!SEMVER_RE.test(version)) {
266
+ throw new ConvilynAuthorError(`version must be valid semver (e.g. "1.0.0"; got "${version}")`);
267
+ }
268
+ }
269
+ function validateDescription(description) {
270
+ if (description.length > MAX_DESCRIPTION) {
271
+ throw new ConvilynAuthorError(`description must be \u2264 ${MAX_DESCRIPTION} characters`);
272
+ }
273
+ }
274
+ function validateSystemPrompt(systemPrompt) {
275
+ if (systemPrompt.length > MAX_SYSTEM_PROMPT) {
276
+ throw new ConvilynAuthorError(`systemPrompt must be \u2264 ${MAX_SYSTEM_PROMPT} characters`);
277
+ }
278
+ }
279
+ function validateTag(tag) {
280
+ if (tag.length < 1 || tag.length > MAX_TAG_LEN) {
281
+ throw new ConvilynAuthorError(`tag "${tag}" must be 1\u2013${MAX_TAG_LEN} characters`);
282
+ }
283
+ }
284
+ function validateTags(tags) {
285
+ if (tags.length > MAX_TAGS) {
286
+ throw new ConvilynAuthorError(`at most ${MAX_TAGS} tags are allowed (got ${tags.length})`);
287
+ }
288
+ tags.forEach(validateTag);
289
+ }
290
+ function toolKey(toolName) {
291
+ const parts = toolName.split(":");
292
+ return parts[parts.length - 1] ?? toolName;
293
+ }
294
+ function validateToolName(toolName) {
295
+ if (toolName.length < 1 || toolName.length > MAX_TOOL_NAME) {
296
+ throw new ConvilynAuthorError(`toolName must be 1\u2013${MAX_TOOL_NAME} characters`);
297
+ }
298
+ if (toolKey(toolName) === FORBIDDEN_TOOL) {
299
+ throw new ConvilynAuthorError(
300
+ `tool "${FORBIDDEN_TOOL}" is not permitted in a workflow palette (the agent cannot pause mid-execution)`
301
+ );
302
+ }
303
+ }
304
+ var WorkflowSpec = class _WorkflowSpec {
305
+ #state;
306
+ constructor(name, options = {}) {
307
+ validateName(name);
308
+ if (options.specId != null) {
309
+ validateSpecId(options.specId);
310
+ }
311
+ const version = options.version ?? DEFAULT_VERSION;
312
+ validateVersion(version);
313
+ this.#state = {
314
+ name,
315
+ systemPrompt: "",
316
+ schemaVersion: DEFAULT_SCHEMA_VERSION,
317
+ specId: options.specId,
318
+ version,
319
+ tags: [],
320
+ toolPalette: []
321
+ };
322
+ }
323
+ #cloneWith(patch) {
324
+ const next = new _WorkflowSpec(this.#state.name);
325
+ next.#state = {
326
+ ...this.#state,
327
+ ...patch,
328
+ tags: [...patch.tags ?? this.#state.tags],
329
+ toolPalette: [...patch.toolPalette ?? this.#state.toolPalette]
330
+ };
331
+ return next;
332
+ }
333
+ // ── read-only accessors (return copies to preserve immutability) ──────────
334
+ get name() {
335
+ return this.#state.name;
336
+ }
337
+ get description() {
338
+ return this.#state.description;
339
+ }
340
+ get systemPrompt() {
341
+ return this.#state.systemPrompt;
342
+ }
343
+ get schemaVersion() {
344
+ return this.#state.schemaVersion;
345
+ }
346
+ get specId() {
347
+ return this.#state.specId;
348
+ }
349
+ get version() {
350
+ return this.#state.version;
351
+ }
352
+ get tags() {
353
+ return [...this.#state.tags];
354
+ }
355
+ get toolPalette() {
356
+ return this.#state.toolPalette.map((item) => ({ ...item, position: { ...item.position } }));
357
+ }
358
+ get canvasLayout() {
359
+ return this.#state.canvasLayout;
360
+ }
361
+ // ── builder methods ───────────────────────────────────────────────────────
362
+ withName(name) {
363
+ validateName(name);
364
+ return this.#cloneWith({ name });
365
+ }
366
+ withDescription(description) {
367
+ validateDescription(description);
368
+ return this.#cloneWith({ description });
369
+ }
370
+ withSystemPrompt(systemPrompt) {
371
+ validateSystemPrompt(systemPrompt);
372
+ return this.#cloneWith({ systemPrompt });
373
+ }
374
+ withSchemaVersion(schemaVersion) {
375
+ return this.#cloneWith({ schemaVersion });
376
+ }
377
+ /** Set the portal spec id (`dev_<developer-id>.<name>` namespace). */
378
+ withSpecId(specId) {
379
+ validateSpecId(specId);
380
+ return this.#cloneWith({ specId });
381
+ }
382
+ /** Set the blueprint semver version (default `1.0.0`). */
383
+ withVersion(version) {
384
+ validateVersion(version);
385
+ return this.#cloneWith({ version });
386
+ }
387
+ /** Replace the tag set. */
388
+ withTags(tags) {
389
+ validateTags(tags);
390
+ return this.#cloneWith({ tags: [...tags] });
391
+ }
392
+ /** Append a tag (no-op if already present). */
393
+ addTag(tag) {
394
+ validateTag(tag);
395
+ if (this.#state.tags.includes(tag)) {
396
+ return this.#cloneWith({});
397
+ }
398
+ const tags = [...this.#state.tags, tag];
399
+ validateTags(tags);
400
+ return this.#cloneWith({ tags });
401
+ }
402
+ /**
403
+ * Add (or replace, by `toolName`) one tool on the palette. Rejects the
404
+ * `request_user_input` tool (invariant I3) and palettes larger than
405
+ * {@link MAX_TOOLS}.
406
+ */
407
+ addTool(toolName, options = {}) {
408
+ validateToolName(toolName);
409
+ if (options.note != null && options.note.length > MAX_NOTE) {
410
+ throw new ConvilynAuthorError(`tool note must be \u2264 ${MAX_NOTE} characters`);
411
+ }
412
+ const item = {
413
+ toolName,
414
+ position: options.position ? { ...options.position } : { ...ORIGIN },
415
+ ...options.note != null ? { note: options.note } : {}
416
+ };
417
+ const existing = this.#state.toolPalette.findIndex((entry) => entry.toolName === toolName);
418
+ const toolPalette = existing >= 0 ? this.#state.toolPalette.map((entry, index) => index === existing ? item : entry) : [...this.#state.toolPalette, item];
419
+ if (toolPalette.length > MAX_TOOLS) {
420
+ throw new ConvilynAuthorError(`at most ${MAX_TOOLS} tools are allowed`);
421
+ }
422
+ return this.#cloneWith({ toolPalette });
423
+ }
424
+ /** Add several tools by name (default canvas position), de-duplicated. */
425
+ useTools(...toolNames) {
426
+ return toolNames.reduce((spec, toolName) => spec.addTool(toolName), this);
427
+ }
428
+ withCanvasLayout(canvasLayout) {
429
+ return this.#cloneWith({ canvasLayout });
430
+ }
431
+ // ── compile / serialize ───────────────────────────────────────────────────
432
+ /**
433
+ * The PUBLIC publish blueprint (snake_case; Python-SDK parity). This is the
434
+ * shape `submitWorkflow` / `convilyn-author push` send to the Developer
435
+ * Portal. Requires a `specId` (constructor option or {@link withSpecId}).
436
+ *
437
+ * Mirrors the Python SDK's `exclude_none` semantics: `agent_config` is
438
+ * emitted only when a systemPrompt is set, `mcp_config` only when the
439
+ * palette is non-empty. UI-only metadata (positions, notes, canvasLayout)
440
+ * is deliberately NOT emitted — the portal's blueprint schema rejects
441
+ * unknown fields; that data belongs to {@link toExportPayload}.
442
+ */
443
+ compile() {
444
+ const specId = this.#state.specId;
445
+ if (specId == null) {
446
+ throw new ConvilynAuthorError(
447
+ 'compile() requires a specId \u2014 pass it to the constructor (new WorkflowSpec(name, { specId })) or call withSpecId(). The Developer Portal expects the "dev_<developer-id>.<name>" namespace.'
448
+ );
449
+ }
450
+ const blueprint = {
451
+ public_schema_version: PUBLIC_SCHEMA_VERSION,
452
+ spec_id: specId,
453
+ version: this.#state.version,
454
+ name: this.#state.name,
455
+ keywords: [...this.#state.tags]
456
+ };
457
+ if (this.#state.description != null) {
458
+ blueprint.description = this.#state.description;
459
+ }
460
+ if (this.#state.systemPrompt !== "") {
461
+ blueprint.agent_config = { system_prompt: this.#state.systemPrompt };
462
+ }
463
+ if (this.#state.toolPalette.length > 0) {
464
+ const tools = this.#state.toolPalette.map((item) => item.toolName);
465
+ const servers = [
466
+ ...new Set(
467
+ tools.filter((toolName) => toolName.includes(":")).map((toolName) => toolName.split(":")[0])
468
+ )
469
+ ];
470
+ blueprint.mcp_config = { mcp_servers: servers, tools };
471
+ }
472
+ return blueprint;
473
+ }
474
+ /** The compiled blueprint serialised as 2-space-indented JSON. */
475
+ compileJson() {
476
+ return JSON.stringify(this.compile(), null, 2);
477
+ }
478
+ /** Write the compiled blueprint to a JSON file (default `workflow.spec.json`). */
479
+ async save(path = "workflow.spec.json") {
480
+ await promises.writeFile(path, this.compileJson(), "utf8");
481
+ return path;
482
+ }
483
+ /**
484
+ * The camelCase user_workflows `ExportPayload` (canvas/authoring contract).
485
+ * Carries the UI-only metadata (positions, notes, canvasLayout) that the
486
+ * publish blueprint omits.
487
+ */
488
+ toExportPayload() {
489
+ const payload = {
490
+ schemaVersion: this.#state.schemaVersion,
491
+ name: this.#state.name,
492
+ systemPrompt: this.#state.systemPrompt,
493
+ toolPalette: this.#state.toolPalette.map((item) => ({
494
+ toolName: item.toolName,
495
+ position: { ...item.position },
496
+ ...item.note != null ? { note: item.note } : {}
497
+ })),
498
+ tags: [...this.#state.tags]
499
+ };
500
+ if (this.#state.description != null) {
501
+ payload.description = this.#state.description;
502
+ }
503
+ if (this.#state.canvasLayout != null) {
504
+ payload.canvasLayout = this.#state.canvasLayout;
505
+ }
506
+ return payload;
507
+ }
508
+ /** Rebuild a {@link WorkflowSpec} from a user_workflows `ExportPayload`. */
509
+ static fromExportPayload(payload) {
510
+ let spec = new _WorkflowSpec(payload.name).withSystemPrompt(payload.systemPrompt ?? "").withSchemaVersion(payload.schemaVersion ?? DEFAULT_SCHEMA_VERSION).withTags(payload.tags ?? []);
511
+ if (payload.description != null) {
512
+ spec = spec.withDescription(payload.description);
513
+ }
514
+ if (payload.canvasLayout != null) {
515
+ spec = spec.withCanvasLayout(payload.canvasLayout);
516
+ }
517
+ for (const item of payload.toolPalette ?? []) {
518
+ spec = spec.addTool(item.toolName, {
519
+ position: item.position,
520
+ ...item.note != null ? { note: item.note } : {}
521
+ });
522
+ }
523
+ return spec;
524
+ }
525
+ /**
526
+ * Rebuild a {@link WorkflowSpec} from a publish blueprint. Lossy for canvas
527
+ * metadata by design — that data lives in the `ExportPayload` contract.
528
+ */
529
+ static fromBlueprint(blueprint) {
530
+ let spec = new _WorkflowSpec(blueprint.name, {
531
+ specId: blueprint.spec_id,
532
+ ...blueprint.version != null ? { version: blueprint.version } : {}
533
+ }).withSystemPrompt(blueprint.agent_config?.system_prompt ?? "").withTags(blueprint.keywords ?? []);
534
+ if (blueprint.description != null) {
535
+ spec = spec.withDescription(blueprint.description);
536
+ }
537
+ for (const toolName of blueprint.mcp_config?.tools ?? []) {
538
+ spec = spec.addTool(toolName);
539
+ }
540
+ return spec;
541
+ }
542
+ /**
543
+ * Parse a spec from a JSON string. Shape-sniffs: objects carrying `spec_id`
544
+ * or `public_schema_version` parse as a blueprint; anything else as an
545
+ * `ExportPayload` — so `push --workflow-file` accepts both file formats.
546
+ */
547
+ static fromJson(data) {
548
+ const parsed = JSON.parse(data);
549
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
550
+ throw new ConvilynAuthorError("workflow spec JSON must be an object");
551
+ }
552
+ if ("spec_id" in parsed || "public_schema_version" in parsed) {
553
+ return _WorkflowSpec.fromBlueprint(parsed);
554
+ }
555
+ return _WorkflowSpec.fromExportPayload(parsed);
556
+ }
557
+ /** Read a saved spec from a JSON file (default `workflow.spec.json`). */
558
+ static async load(path = "workflow.spec.json") {
559
+ return _WorkflowSpec.fromJson(await promises.readFile(path, "utf8"));
560
+ }
561
+ };
562
+
563
+ // src/cli/load-workflow.ts
564
+ function isWorkflowSpecLike(value) {
565
+ if (typeof value !== "object" || value === null) {
566
+ return false;
567
+ }
568
+ const candidate = value;
569
+ return typeof candidate.compile === "function" && typeof candidate.name === "string";
570
+ }
571
+ function pickWorkflow(mod) {
572
+ if (isWorkflowSpecLike(mod.default)) {
573
+ return mod.default;
574
+ }
575
+ if (isWorkflowSpecLike(mod.workflow)) {
576
+ return mod.workflow;
577
+ }
578
+ for (const value of Object.values(mod)) {
579
+ if (isWorkflowSpecLike(value)) {
580
+ return value;
581
+ }
582
+ }
583
+ return void 0;
584
+ }
585
+ async function loadWorkflowSpec(file) {
586
+ const absolute = path.resolve(file);
587
+ if (path.extname(absolute).toLowerCase() === ".json") {
588
+ try {
589
+ return await WorkflowSpec.load(absolute);
590
+ } catch (err) {
591
+ throw new Error(
592
+ `Could not load workflow spec "${file}": ${err.message}. Point --workflow-file at a compiled spec JSON (from WorkflowSpec.save()) or a built .js module that exports a WorkflowSpec.`
593
+ );
594
+ }
595
+ }
596
+ let mod;
597
+ try {
598
+ mod = await import(url.pathToFileURL(absolute).href);
599
+ } catch (err) {
600
+ throw new Error(
601
+ `Could not import workflow module "${file}": ${err.message}. Point --workflow-file at a built .js module (e.g. "npm run build" then --workflow-file dist/workflow.js) or a compiled .spec.json.`
602
+ );
603
+ }
604
+ const workflow = pickWorkflow(mod);
605
+ if (workflow === void 0) {
606
+ throw new Error(
607
+ `No WorkflowSpec found in "${file}". Export it as the default export or as a named "workflow" export.`
608
+ );
609
+ }
610
+ return workflow;
611
+ }
612
+ var SIGNATURE_HEADER = "x-convilyn-signature";
613
+ var TIMESTAMP_HEADER = "x-convilyn-timestamp";
614
+ var DEFAULT_HMAC_TOLERANCE_SECONDS = 300;
615
+ function expectedSignature(secret, timestamp, body) {
616
+ return crypto.createHmac("sha256", secret).update(timestamp).update(".").update(body).digest("hex");
617
+ }
618
+ function constantTimeEqual(a, b) {
619
+ const aBuf = Buffer.from(a);
620
+ const bBuf = Buffer.from(b);
621
+ if (aBuf.length !== bBuf.length) {
622
+ return false;
623
+ }
624
+ return crypto.timingSafeEqual(aBuf, bBuf);
625
+ }
626
+ function verifySignature(secret, body, headers, options = {}) {
627
+ const tolerance = options.toleranceSeconds != null && options.toleranceSeconds > 0 ? options.toleranceSeconds : DEFAULT_HMAC_TOLERANCE_SECONDS;
628
+ const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
629
+ if (!secret) {
630
+ throw new InvalidSignatureError("missing_secret", "HMAC secret is not configured on the server");
631
+ }
632
+ const signature = headers[SIGNATURE_HEADER];
633
+ const timestamp = headers[TIMESTAMP_HEADER];
634
+ if (!signature || !timestamp) {
635
+ throw new InvalidSignatureError(
636
+ "missing_header",
637
+ "Missing X-Convilyn-Signature or X-Convilyn-Timestamp"
638
+ );
639
+ }
640
+ if (!/^[+-]?\d+$/.test(timestamp)) {
641
+ throw new InvalidSignatureError(
642
+ "invalid_timestamp",
643
+ "X-Convilyn-Timestamp must be an integer Unix-seconds value"
644
+ );
645
+ }
646
+ const tsValue = Number(timestamp);
647
+ if (Math.abs(now() - tsValue) > tolerance) {
648
+ throw new InvalidSignatureError(
649
+ "timestamp_out_of_range",
650
+ "Request timestamp outside acceptable tolerance window"
651
+ );
652
+ }
653
+ const expected = expectedSignature(secret, timestamp, body);
654
+ if (!constantTimeEqual(expected, signature)) {
655
+ throw new InvalidSignatureError(
656
+ "signature_mismatch",
657
+ "Signature did not match expected HMAC-SHA256 digest"
658
+ );
659
+ }
660
+ }
661
+
662
+ // src/config.ts
663
+ function envOr(env, key, fallback) {
664
+ const value = env[key];
665
+ return value !== void 0 && value !== "" ? value : fallback;
666
+ }
667
+ function envIntOr(env, key, fallback) {
668
+ const value = env[key];
669
+ if (value !== void 0 && value !== "") {
670
+ const parsed = Number(value);
671
+ if (Number.isInteger(parsed)) {
672
+ return parsed;
673
+ }
674
+ }
675
+ return fallback;
676
+ }
677
+ var SDKConfig = class _SDKConfig {
678
+ serverHost;
679
+ serverPort;
680
+ logLevel;
681
+ awsRegion;
682
+ dynamodbEndpoint;
683
+ toolDataTable;
684
+ environment;
685
+ apiKey;
686
+ platformUrl;
687
+ hmacSecret;
688
+ hmacToleranceSeconds;
689
+ toolConfirmationSecret;
690
+ #lambda;
691
+ constructor(options = {}) {
692
+ this.serverHost = options.serverHost ?? "127.0.0.1";
693
+ this.serverPort = options.serverPort ?? 8080;
694
+ this.logLevel = options.logLevel ?? "INFO";
695
+ this.awsRegion = options.awsRegion ?? "ap-northeast-1";
696
+ this.dynamodbEndpoint = options.dynamodbEndpoint ?? "";
697
+ this.toolDataTable = options.toolDataTable ?? "tool-data";
698
+ this.environment = options.environment ?? "local";
699
+ this.apiKey = options.apiKey ?? "";
700
+ this.platformUrl = options.platformUrl ?? "https://api.convilyn.corenovus.com";
701
+ this.hmacSecret = options.hmacSecret ?? "";
702
+ this.hmacToleranceSeconds = options.hmacToleranceSeconds ?? DEFAULT_HMAC_TOLERANCE_SECONDS;
703
+ this.toolConfirmationSecret = options.toolConfirmationSecret ?? "";
704
+ this.#lambda = options.lambda ?? false;
705
+ }
706
+ /** True when running in AWS Lambda. */
707
+ get isLambda() {
708
+ return this.#lambda;
709
+ }
710
+ /** True for a local/dev environment (relaxes HMAC). */
711
+ get isLocal() {
712
+ if (this.environment === "local") {
713
+ return true;
714
+ }
715
+ return !this.#lambda && (this.serverHost === "127.0.0.1" || this.serverHost === "localhost");
716
+ }
717
+ /** Load configuration from environment variables, falling back to defaults. */
718
+ static fromEnv(env = typeof process !== "undefined" ? process.env : {}) {
719
+ return new _SDKConfig({
720
+ serverHost: envOr(env, "CONVILYN_HOST", "127.0.0.1"),
721
+ serverPort: envIntOr(env, "CONVILYN_PORT", 8080),
722
+ logLevel: envOr(env, "CONVILYN_LOG_LEVEL", "INFO"),
723
+ awsRegion: envOr(env, "AWS_REGION", "ap-northeast-1"),
724
+ dynamodbEndpoint: envOr(env, "DYNAMODB_ENDPOINT", ""),
725
+ toolDataTable: envOr(env, "TOOL_DATA_TABLE", "tool-data"),
726
+ environment: envOr(env, "CONVILYN_ENVIRONMENT", "local"),
727
+ apiKey: envOr(env, "CONVILYN_API_KEY", ""),
728
+ platformUrl: envOr(env, "CONVILYN_PLATFORM_URL", "https://api.convilyn.corenovus.com"),
729
+ hmacSecret: envOr(env, "CONVILYN_HMAC_SECRET", ""),
730
+ hmacToleranceSeconds: envIntOr(
731
+ env,
732
+ "CONVILYN_HMAC_TOLERANCE_SECONDS",
733
+ DEFAULT_HMAC_TOLERANCE_SECONDS
734
+ ),
735
+ toolConfirmationSecret: envOr(env, "CONVILYN_TOOL_CONFIRMATION_SECRET", ""),
736
+ lambda: Boolean(env["AWS_LAMBDA_FUNCTION_NAME"])
737
+ });
738
+ }
739
+ };
740
+
741
+ // src/client.ts
742
+ function withApiV1(platformUrl) {
743
+ const trimmed = platformUrl.replace(/\/+$/, "");
744
+ return trimmed.endsWith("/api/v1") ? trimmed : `${trimmed}/api/v1`;
745
+ }
746
+ var CONSUMER_KEY_PREFIX = "ck_";
747
+ function isConsumerKey(key) {
748
+ return key.startsWith(CONSUMER_KEY_PREFIX);
749
+ }
750
+ function maskKey(key) {
751
+ return key.length <= 8 ? "***" : `${key.slice(0, 4)}\u2026${key.slice(-2)}`;
752
+ }
753
+ var ConvilynClient = class {
754
+ baseUrl;
755
+ #apiKey;
756
+ #fetch;
757
+ constructor(options = {}) {
758
+ const config = options.config ?? SDKConfig.fromEnv();
759
+ this.baseUrl = options.baseUrl ?? withApiV1(config.platformUrl);
760
+ this.#apiKey = options.apiKey ?? config.apiKey;
761
+ if (this.#apiKey && isConsumerKey(this.#apiKey)) {
762
+ throw new ConvilynAuthorError(
763
+ `${maskKey(this.#apiKey)} looks like a Convilyn consumer API key ("${CONSUMER_KEY_PREFIX}"), not an Author SDK / developer-portal token. The Author SDK authenticates with a cvl_ developer key \u2014 call register() to mint one, or set CONVILYN_API_KEY to your cvl_ key. (Consumer keys call the data-plane API; they do not publish workflows / tools.)`
764
+ );
765
+ }
766
+ this.#fetch = options.fetch ?? globalThis.fetch;
767
+ }
768
+ /** The `cvl_` developer key currently in use (empty until set or registered). */
769
+ get apiKey() {
770
+ return this.#apiKey;
771
+ }
772
+ #headers(auth) {
773
+ const headers = { "content-type": "application/json" };
774
+ if (auth) {
775
+ if (!this.#apiKey) {
776
+ throw new ConvilynAuthorError(
777
+ "No developer API key set. Provide `apiKey`, set CONVILYN_API_KEY to a cvl_ developer key, or call register() first."
778
+ );
779
+ }
780
+ headers.authorization = `Bearer ${this.#apiKey}`;
781
+ }
782
+ return headers;
783
+ }
784
+ async #request(method, path, body, auth = true) {
785
+ const res = await this.#fetch(`${this.baseUrl}${path}`, {
786
+ method,
787
+ headers: this.#headers(auth),
788
+ body: body !== void 0 ? JSON.stringify(body) : void 0
789
+ });
790
+ if (!res.ok) {
791
+ let parsed;
792
+ try {
793
+ parsed = await res.json();
794
+ } catch {
795
+ parsed = void 0;
796
+ }
797
+ throw new ConvilynApiError(
798
+ res.status,
799
+ `${method} ${path} failed (HTTP ${res.status})`,
800
+ parsed
801
+ );
802
+ }
803
+ if (res.status === 204) {
804
+ return void 0;
805
+ }
806
+ return await res.json();
807
+ }
808
+ // ── Developer registration ──────────────────────────────────────
809
+ /**
810
+ * `POST /developers/register` — create a developer account and mint a `cvl_`
811
+ * key. No auth required. The returned key is captured on this client for
812
+ * subsequent calls (and returned so the caller can persist it — it is shown
813
+ * only once).
814
+ */
815
+ async register(request) {
816
+ const result = await this.#request(
817
+ "POST",
818
+ "/developers/register",
819
+ request,
820
+ false
821
+ );
822
+ if (result.api_key && !this.#apiKey) {
823
+ this.#apiKey = result.api_key;
824
+ }
825
+ return result;
826
+ }
827
+ // ── Server operations ───────────────────────────────────────────
828
+ /** `POST /developers/servers` — submit a tool-server manifest for verification. */
829
+ async submitServer(request) {
830
+ return this.#request("POST", "/developers/servers", request);
831
+ }
832
+ /** `GET /developers/servers` — list this developer's servers. */
833
+ async listServers() {
834
+ return this.#request("GET", "/developers/servers");
835
+ }
836
+ /** `GET /developers/servers/{id}/status` — verification status of a server. */
837
+ async serverStatus(serverId) {
838
+ return this.#request(
839
+ "GET",
840
+ `/developers/servers/${encodeURIComponent(serverId)}/status`
841
+ );
842
+ }
843
+ /** `POST /developers/servers/{id}/test` — trigger a sandbox test run. */
844
+ async testServer(serverId, slots = {}) {
845
+ return this.#request(
846
+ "POST",
847
+ `/developers/servers/${encodeURIComponent(serverId)}/test`,
848
+ { slots }
849
+ );
850
+ }
851
+ /** `DELETE /developers/servers/{id}` — deactivate a server. */
852
+ async deactivateServer(serverId) {
853
+ await this.#request("DELETE", `/developers/servers/${encodeURIComponent(serverId)}`);
854
+ }
855
+ // ── Workflow operations ─────────────────────────────────────────
856
+ /** `POST /developers/workflows` — submit a compiled workflow spec + its server ids. */
857
+ async submitWorkflow(request) {
858
+ return this.#request("POST", "/developers/workflows", request);
859
+ }
860
+ /** `GET /developers/workflows` — list this developer's workflows. */
861
+ async listWorkflows() {
862
+ return this.#request("GET", "/developers/workflows");
863
+ }
864
+ /** `GET /developers/workflows/{id}/status` — verification status of a workflow. */
865
+ async workflowStatus(workflowId) {
866
+ return this.#request(
867
+ "GET",
868
+ `/developers/workflows/${encodeURIComponent(workflowId)}/status`
869
+ );
870
+ }
871
+ /** `POST /developers/workflows/{id}/test` — sandbox-test a submitted workflow. */
872
+ async testWorkflow(workflowId, testInput = {}) {
873
+ return this.#request(
874
+ "POST",
875
+ `/developers/workflows/${encodeURIComponent(workflowId)}/test`,
876
+ { test_input: testInput }
877
+ );
878
+ }
879
+ /** `DELETE /developers/workflows/{id}` — deactivate a workflow. */
880
+ async deactivateWorkflow(workflowId) {
881
+ await this.#request("DELETE", `/developers/workflows/${encodeURIComponent(workflowId)}`);
882
+ }
883
+ // ── Convilyn-Hosted Author Runtime (port of the Python client) ─────────────
884
+ /**
885
+ * `POST /developers/runtimes/hosted` — deploy a tool server into the
886
+ * Convilyn-Hosted Author Runtime. Counterpart to {@link submitServer}:
887
+ * instead of registering a caller-deployed endpoint, the platform
888
+ * provisions a sandboxed Lambda and returns its public endpoint URL.
889
+ *
890
+ * Backends without the hosted stack answer 503 `HOSTED_NOT_AVAILABLE`
891
+ * (older builds: 501); environments with the router unmounted answer 404.
892
+ * All surface as {@link ConvilynApiError} — catch and fall back to
893
+ * `push --endpoint-url` (BYO) when a hard guarantee is needed.
894
+ */
895
+ async deployHostedRuntime(manifest, options) {
896
+ const body = { manifest, region: options.region };
897
+ if (options.workflowSpec !== void 0) {
898
+ body.workflow_spec = options.workflowSpec;
899
+ }
900
+ return this.#request("POST", "/developers/runtimes/hosted", body);
901
+ }
902
+ /**
903
+ * `POST /developers/runtimes/{id}/rollback` — atomically flip the runtime
904
+ * back to its previous active version.
905
+ */
906
+ async rollbackHostedRuntime(runtimeId) {
907
+ return this.#request(
908
+ "POST",
909
+ `/developers/runtimes/${encodeURIComponent(runtimeId)}/rollback`
910
+ );
911
+ }
912
+ /**
913
+ * `GET /developers/runtimes/{id}/logs` — recent runtime log lines.
914
+ * `since` is an ISO-8601 timestamp or relative shorthand (`"5m"`, `"1h"`)
915
+ * interpreted server-side; `limit` defaults to 100 (server caps ~1000).
916
+ * Accepts either a bare list or an `{entries: [...]}` envelope (mirror of
917
+ * the Python client) so the SDK survives the wire shape stabilising.
918
+ */
919
+ async getHostedRuntimeLogs(runtimeId, options = {}) {
920
+ const limit = Math.trunc(options.limit ?? 100);
921
+ let path = `/developers/runtimes/${encodeURIComponent(runtimeId)}/logs?limit=${limit}`;
922
+ if (options.since) {
923
+ path = `${path}&since=${encodeURIComponent(options.since)}`;
924
+ }
925
+ const result = await this.#request("GET", path);
926
+ if (Array.isArray(result)) {
927
+ return result;
928
+ }
929
+ if (typeof result === "object" && result !== null && Array.isArray(result.entries)) {
930
+ return result.entries;
931
+ }
932
+ return [];
933
+ }
934
+ };
935
+
936
+ // src/jsonrpc.ts
937
+ var JSONRPC_VERSION = "2.0";
938
+ var TOOLS_CALL = "tools/call";
939
+ function isRecord(value) {
940
+ return typeof value === "object" && value !== null && !Array.isArray(value);
941
+ }
942
+ function asRpcRequest(parsed) {
943
+ if (!isRecord(parsed) || typeof parsed.method !== "string") {
944
+ return null;
945
+ }
946
+ const id = typeof parsed.id === "string" || typeof parsed.id === "number" ? parsed.id : null;
947
+ return {
948
+ jsonrpc: typeof parsed.jsonrpc === "string" ? parsed.jsonrpc : JSONRPC_VERSION,
949
+ method: parsed.method,
950
+ params: isRecord(parsed.params) ? parsed.params : {},
951
+ id
952
+ };
953
+ }
954
+
955
+ // src/http-runtime.ts
956
+ var ENV_DEV_INSECURE = "CONVILYN_DEV_INSECURE";
957
+ var DEV_INSECURE_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
958
+ function devInsecureRequested(env = process.env) {
959
+ return DEV_INSECURE_TRUTHY.has((env[ENV_DEV_INSECURE] ?? "").trim().toLowerCase());
960
+ }
961
+ var JSON_HEADERS = { "content-type": "application/json" };
962
+ function sendJson(res, statusCode, body) {
963
+ res.writeHead(statusCode, JSON_HEADERS);
964
+ res.end(JSON.stringify(body));
965
+ }
966
+ var MAX_BODY_BYTES = 10 * 1024 * 1024;
967
+ var BodyTooLargeError = class extends Error {
968
+ constructor() {
969
+ super(`Request body exceeds ${MAX_BODY_BYTES} bytes`);
970
+ this.name = "BodyTooLargeError";
971
+ }
972
+ };
973
+ function readBody(req, maxBytes = MAX_BODY_BYTES) {
974
+ return new Promise((resolve4, reject) => {
975
+ const chunks = [];
976
+ let total = 0;
977
+ req.on("data", (chunk) => {
978
+ total += chunk.length;
979
+ if (total > maxBytes) {
980
+ chunks.length = 0;
981
+ reject(new BodyTooLargeError());
982
+ return;
983
+ }
984
+ chunks.push(chunk);
985
+ });
986
+ req.on("end", () => resolve4(Buffer.concat(chunks)));
987
+ req.on("error", reject);
988
+ });
989
+ }
990
+ async function handleMcp(server, req, res, secret, toleranceSeconds, allowInsecure) {
991
+ let body;
992
+ try {
993
+ body = await readBody(req);
994
+ } catch (err) {
995
+ if (err instanceof BodyTooLargeError) {
996
+ res.once("finish", () => req.destroy());
997
+ sendJson(res, 413, {
998
+ code: "PAYLOAD_TOO_LARGE",
999
+ message: "Request body too large"
1000
+ });
1001
+ return;
1002
+ }
1003
+ throw err;
1004
+ }
1005
+ if (secret === "" && allowInsecure) {
1006
+ await dispatchParsed(server, res, body);
1007
+ return;
1008
+ }
1009
+ try {
1010
+ verifySignature(secret, body, req.headers, {
1011
+ toleranceSeconds
1012
+ });
1013
+ } catch (err) {
1014
+ if (err instanceof InvalidSignatureError) {
1015
+ sendJson(res, 401, {
1016
+ code: "INVALID_SIGNATURE",
1017
+ message: "Request signature could not be verified"
1018
+ });
1019
+ return;
1020
+ }
1021
+ throw err;
1022
+ }
1023
+ await dispatchParsed(server, res, body);
1024
+ }
1025
+ async function dispatchParsed(server, res, body) {
1026
+ let parsed;
1027
+ try {
1028
+ parsed = JSON.parse(body.toString("utf8"));
1029
+ } catch {
1030
+ sendJson(res, 400, { error: "Invalid JSON" });
1031
+ return;
1032
+ }
1033
+ const request = asRpcRequest(parsed);
1034
+ if (request === null) {
1035
+ sendJson(res, 400, { error: "Invalid JSON" });
1036
+ return;
1037
+ }
1038
+ const response = await server.dispatch(request);
1039
+ sendJson(res, 200, response);
1040
+ }
1041
+ function serve(toolServer, options = {}) {
1042
+ const config = options.config ?? SDKConfig.fromEnv();
1043
+ const host = options.host ?? config.serverHost;
1044
+ const port = options.port ?? config.serverPort;
1045
+ const secret = options.hmacSecret ?? config.hmacSecret;
1046
+ const toleranceSeconds = options.toleranceSeconds ?? config.hmacToleranceSeconds;
1047
+ const allowInsecure = options.allowInsecure ?? devInsecureRequested();
1048
+ if (secret === "" && !allowInsecure) {
1049
+ throw new ConvilynStartupError(
1050
+ `CONVILYN_HMAC_SECRET is required to serve inbound /mcp traffic. Refusing to start without signature verification. For local development use \`convilyn-author dev\` or set ${ENV_DEV_INSECURE}=1 (INSECURE \u2014 local only).`
1051
+ );
1052
+ }
1053
+ if (secret === "") {
1054
+ console.warn(
1055
+ "convilyn-author: CONVILYN_HMAC_SECRET not set; serving /mcp WITHOUT signature verification (insecure local dev only \u2014 do NOT deploy this configuration)"
1056
+ );
1057
+ }
1058
+ const httpServer = http.createServer((req, res) => {
1059
+ void route(toolServer, req, res, secret, toleranceSeconds, allowInsecure).catch(() => {
1060
+ if (!res.headersSent) {
1061
+ sendJson(res, 500, { error: "Internal server error" });
1062
+ }
1063
+ });
1064
+ });
1065
+ httpServer.listen(port, host);
1066
+ return httpServer;
1067
+ }
1068
+ async function route(toolServer, req, res, secret, toleranceSeconds, allowInsecure) {
1069
+ const path = (req.url ?? "").split("?")[0];
1070
+ const method = req.method ?? "GET";
1071
+ if (method === "GET" && path === "/health") {
1072
+ sendJson(res, 200, {
1073
+ status: "healthy",
1074
+ server: toolServer.server.name,
1075
+ version: VERSION,
1076
+ tool_count: toolServer.toolCount
1077
+ });
1078
+ return;
1079
+ }
1080
+ if (method === "GET" && path === "/manifest") {
1081
+ sendJson(res, 200, toolServer.manifest().toWire());
1082
+ return;
1083
+ }
1084
+ if (method === "POST" && path === "/mcp") {
1085
+ await handleMcp(toolServer, req, res, secret, toleranceSeconds, allowInsecure);
1086
+ return;
1087
+ }
1088
+ sendJson(res, 404, { error: "Not found" });
1089
+ }
1090
+
1091
+ // src/tool-server.ts
1092
+ var GET_TOOL_DATA = "get_tool_data";
1093
+
1094
+ // src/compliance.ts
1095
+ function isRecord2(value) {
1096
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1097
+ }
1098
+ function pass(checkName, message) {
1099
+ return { passed: true, checkName, message };
1100
+ }
1101
+ function fail(checkName, message) {
1102
+ return { passed: false, checkName, message };
1103
+ }
1104
+ function checkServerMetadata(server) {
1105
+ const { name, version, description } = server.server;
1106
+ if (!name || !version || !description) {
1107
+ return fail("server_metadata", "server name, version, and description must all be non-empty");
1108
+ }
1109
+ return pass("server_metadata", "server metadata is complete");
1110
+ }
1111
+ function checkToolsExist(server) {
1112
+ return server.toolCount > 0 ? pass("tools_exist", `server has ${server.toolCount} tool(s)`) : fail("tools_exist", "server has no registered tools");
1113
+ }
1114
+ function checkToolSchemas(server) {
1115
+ for (const tool of server.manifest().tools) {
1116
+ if (!tool.name || !tool.description) {
1117
+ return fail("tool_schemas", `tool "${tool.name}" is missing a name or description`);
1118
+ }
1119
+ if (!isRecord2(tool.inputSchema) || tool.inputSchema.type !== "object") {
1120
+ return fail("tool_schemas", `tool "${tool.name}" input_schema is not a JSON-Schema object`);
1121
+ }
1122
+ }
1123
+ return pass("tool_schemas", "all tool schemas are valid");
1124
+ }
1125
+ function checkManifestSynth(server) {
1126
+ try {
1127
+ const wire = server.manifest().toWire();
1128
+ return isRecord2(wire) ? pass("manifest_synth", "manifest compiles to a wire object") : fail("manifest_synth", "manifest did not compile to an object");
1129
+ } catch (err) {
1130
+ return fail("manifest_synth", `manifest synth threw: ${err.message}`);
1131
+ }
1132
+ }
1133
+ async function checkRefIdPattern(server) {
1134
+ const refId = await server.dataStore.store({ __compliance_probe__: true });
1135
+ return /^td_[0-9a-f]{12}$/.test(refId) ? pass("ref_id_pattern", "data store returns a td_<12-hex> ref_id") : fail("ref_id_pattern", `ref_id "${refId}" does not match td_<12-hex>`);
1136
+ }
1137
+ async function checkGetToolData(server) {
1138
+ const refId = await server.dataStore.store({ probe: 1 });
1139
+ const response = await server.dispatch({
1140
+ jsonrpc: "2.0",
1141
+ method: TOOLS_CALL,
1142
+ params: { name: GET_TOOL_DATA, arguments: { ref_id: refId } },
1143
+ id: "compliance"
1144
+ });
1145
+ const result = response.result;
1146
+ if (isRecord2(result) && result.status === "ok") {
1147
+ return pass("get_tool_data", "built-in get_tool_data resolves a stored ref_id");
1148
+ }
1149
+ return fail("get_tool_data", "built-in get_tool_data did not resolve a stored ref_id");
1150
+ }
1151
+ async function runComplianceChecks(server) {
1152
+ const results = [
1153
+ checkServerMetadata(server),
1154
+ checkToolsExist(server),
1155
+ checkToolSchemas(server),
1156
+ checkManifestSynth(server),
1157
+ await checkRefIdPattern(server),
1158
+ await checkGetToolData(server)
1159
+ ];
1160
+ return { results };
1161
+ }
1162
+
1163
+ // src/cli/commands.ts
1164
+ var DEFAULT_SERVER_FILE = "server.js";
1165
+ var DEFAULT_WORKFLOW_FILE = "workflow.spec.json";
1166
+ var DEFAULT_MANIFEST_OUTPUT = "convilyn.manifest.json";
1167
+ async function runSynth(options) {
1168
+ const server = await loadToolServer(options.file ?? DEFAULT_SERVER_FILE);
1169
+ const manifest = server.manifest();
1170
+ const path = await manifest.save(options.output ?? DEFAULT_MANIFEST_OUTPUT);
1171
+ return { path, toolCount: manifest.tools.length, toolNames: manifest.tools.map((t) => t.name) };
1172
+ }
1173
+ async function runDev(options) {
1174
+ const server = await loadToolServer(options.file ?? DEFAULT_SERVER_FILE);
1175
+ return serve(server, {
1176
+ host: options.host,
1177
+ port: options.port,
1178
+ hmacSecret: options.hmacSecret,
1179
+ // `dev` IS the explicit insecure-local opt-in (mirror of the Python
1180
+ // CLI's run(dev=True)). A configured secret still wins — the opt-in
1181
+ // never bypasses verification.
1182
+ allowInsecure: true
1183
+ });
1184
+ }
1185
+ async function runTest(options) {
1186
+ const server = await loadToolServer(options.file ?? DEFAULT_SERVER_FILE);
1187
+ return runComplianceChecks(server);
1188
+ }
1189
+ async function runPush(options) {
1190
+ if (!options.endpointUrl) {
1191
+ throw new Error(
1192
+ "push requires --endpoint-url <https://...> \u2014 the HTTPS URL where your tool server is deployed. Use `deploy --hosted` for a Convilyn-hosted server."
1193
+ );
1194
+ }
1195
+ const server = await loadToolServer(options.file ?? DEFAULT_SERVER_FILE);
1196
+ const workflow = await loadWorkflowSpec(options.workflowFile ?? DEFAULT_WORKFLOW_FILE);
1197
+ const client = options.client ?? new ConvilynClient();
1198
+ const manifest = server.manifest().toWire();
1199
+ const serverResult = await client.submitServer({
1200
+ manifest,
1201
+ endpoint_url: options.endpointUrl
1202
+ });
1203
+ const workflowResult = await client.submitWorkflow({
1204
+ // CompilableWorkflow.compile() is typed `unknown` (the file loader duck-types
1205
+ // both a built WorkflowSpec and a pre-compiled JSON blueprint).
1206
+ workflow_spec: workflow.compile(),
1207
+ server_ids: [serverResult.server_id]
1208
+ });
1209
+ return {
1210
+ serverId: serverResult.server_id,
1211
+ serverName: serverResult.server_name,
1212
+ serverStatus: serverResult.status,
1213
+ workflowId: workflowResult.workflow_id,
1214
+ workflowSpecId: workflowResult.spec_id,
1215
+ workflowStatus: workflowResult.status
1216
+ };
1217
+ }
1218
+ async function runDeploy(options) {
1219
+ if (!options.hosted) {
1220
+ throw new Error(
1221
+ "deploy currently requires --hosted. Use `convilyn-author push --endpoint-url <https://...>` for caller-deployed (BYO) servers."
1222
+ );
1223
+ }
1224
+ const server = await loadToolServer(options.file ?? DEFAULT_SERVER_FILE);
1225
+ const manifest = server.manifest().toWire();
1226
+ const workflowFile = options.workflowFile ?? DEFAULT_WORKFLOW_FILE;
1227
+ let workflowSpec;
1228
+ if (await promises.stat(workflowFile).then(
1229
+ () => true,
1230
+ () => false
1231
+ )) {
1232
+ const workflow = await loadWorkflowSpec(workflowFile);
1233
+ workflowSpec = workflow.compile();
1234
+ }
1235
+ const client = options.client ?? new ConvilynClient();
1236
+ const result = await client.deployHostedRuntime(manifest, {
1237
+ region: options.region ?? "us-east-1",
1238
+ ...workflowSpec !== void 0 ? { workflowSpec } : {}
1239
+ });
1240
+ return {
1241
+ runtimeId: result.runtime_id,
1242
+ endpointUrl: result.endpoint_url ?? null,
1243
+ status: result.status,
1244
+ region: result.region,
1245
+ version: result.version,
1246
+ ...result.workflow_id != null ? { workflowId: result.workflow_id } : {},
1247
+ workflowIncluded: workflowSpec !== void 0
1248
+ };
1249
+ }
1250
+ async function runRollback(options) {
1251
+ const client = options.client ?? new ConvilynClient();
1252
+ return client.rollbackHostedRuntime(options.runtimeId);
1253
+ }
1254
+ async function runLogs(options) {
1255
+ const client = options.client ?? new ConvilynClient();
1256
+ return client.getHostedRuntimeLogs(options.runtimeId, {
1257
+ ...options.since !== void 0 ? { since: options.since } : {},
1258
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
1259
+ });
1260
+ }
1261
+
1262
+ // src/cli/program.ts
1263
+ function fail2(printer, err) {
1264
+ printer.err(`error: ${err.message}`);
1265
+ process.exitCode = 1;
1266
+ }
1267
+ function failHosted(printer, err) {
1268
+ if (err instanceof ConvilynApiError && (err.status === 501 || err.status === 503)) {
1269
+ printer.err(
1270
+ `error: hosted runtime not yet available on this platform (${err.message}). Use \`convilyn-author push --endpoint-url ...\` as a BYO fallback.`
1271
+ );
1272
+ process.exitCode = 1;
1273
+ return;
1274
+ }
1275
+ fail2(printer, err);
1276
+ }
1277
+ function buildProgram(printer = consolePrinter) {
1278
+ const program = new commander.Command();
1279
+ program.name("convilyn-author").description("Author and run Convilyn tool servers.").version(VERSION);
1280
+ program.command("init").description("Scaffold a new tool-server project.").argument("<name>", "project name (letters, digits, hyphens, underscores)").option("--dir <dir>", "target directory (default ./<name>)").action(async (name, opts) => {
1281
+ try {
1282
+ const result = await scaffoldProject({ name, dir: opts.dir });
1283
+ printer.out(`Project created: ${result.path}`);
1284
+ printer.out(` ${result.files.join(", ")}`);
1285
+ printer.out(` cd ${name} && npm install && npm run build && npx convilyn-author dev`);
1286
+ } catch (err) {
1287
+ fail2(printer, err);
1288
+ }
1289
+ });
1290
+ program.command("synth").description("Compile the server manifest to JSON.").option("--file <file>", "built server module (default server.js)").option("--output <output>", "manifest output path (default convilyn.manifest.json)").action(async (opts) => {
1291
+ try {
1292
+ const result = await runSynth(opts);
1293
+ printer.out(`Found ${result.toolCount} tool(s): ${result.toolNames.join(", ")}`);
1294
+ printer.out(`Manifest written: ${result.path}`);
1295
+ } catch (err) {
1296
+ fail2(printer, err);
1297
+ }
1298
+ });
1299
+ program.command("dev").description("Run the JSON-RPC /mcp server locally.").option("--file <file>", "built server module (default server.js)").option("--host <host>", "bind host (default from CONVILYN_HOST)").option("--port <port>", "bind port (default from CONVILYN_PORT)", (v) => parseInt(v, 10)).action(async (opts) => {
1300
+ try {
1301
+ await runDev(opts);
1302
+ printer.out("convilyn-author dev: serving /mcp (Ctrl+C to stop)");
1303
+ } catch (err) {
1304
+ fail2(printer, err);
1305
+ }
1306
+ });
1307
+ program.command("test").description("Run local compliance checks (no platform calls).").option("--file <file>", "built server module (default server.js)").action(async (opts) => {
1308
+ try {
1309
+ const report = await runTest(opts);
1310
+ for (const r of report.results) {
1311
+ printer.out(` [${r.passed ? "PASS" : "FAIL"}] ${r.checkName}: ${r.message}`);
1312
+ }
1313
+ if (allChecksPassed(report)) {
1314
+ printer.out(`All ${report.results.length} checks passed`);
1315
+ } else {
1316
+ const failed = report.results.filter((r) => !r.passed).length;
1317
+ printer.err(`${failed} of ${report.results.length} checks failed`);
1318
+ process.exitCode = 1;
1319
+ }
1320
+ } catch (err) {
1321
+ fail2(printer, err);
1322
+ }
1323
+ });
1324
+ program.command("push").description("Submit a server + workflow to the Developer Portal.").option("--file <file>", "built server module (default server.js)").option(
1325
+ "--workflow-file <file>",
1326
+ "compiled workflow spec or built module (default workflow.spec.json)"
1327
+ ).option("--endpoint-url <url>", "HTTPS URL of your deployed server (required)").action(async (opts) => {
1328
+ try {
1329
+ const result = await runPush(opts);
1330
+ printer.out(`Server submitted: ${result.serverId} (${result.serverStatus})`);
1331
+ printer.out(`Workflow submitted: ${result.workflowId} (${result.workflowStatus})`);
1332
+ printer.out("Check verification with the ConvilynClient serverStatus() / workflowStatus().");
1333
+ } catch (err) {
1334
+ fail2(printer, err);
1335
+ }
1336
+ });
1337
+ program.command("deploy").description("Deploy a tool server to the Convilyn-Hosted Author Runtime.").option("--file <file>", "built server module (default server.js)").option(
1338
+ "--workflow-file <file>",
1339
+ "compiled workflow spec (default workflow.spec.json; optional \u2014 skipped when absent)"
1340
+ ).option("--hosted", "use the managed runtime (required \u2014 BYO uses push)").option("--region <region>", "AWS region the hosted Lambda is provisioned in", "us-east-1").action(
1341
+ async (opts) => {
1342
+ try {
1343
+ printer.out(`Deploying to Convilyn-Hosted Runtime in ${opts.region ?? "us-east-1"}...`);
1344
+ const result = await runDeploy(opts);
1345
+ printer.out(`Runtime provisioned: ${result.runtimeId} (${result.status})`);
1346
+ printer.out(`Endpoint URL: ${result.endpointUrl ?? "pending (provisioning)"}`);
1347
+ if (result.workflowId != null) {
1348
+ printer.out(`Workflow registered: ${result.workflowId}`);
1349
+ }
1350
+ printer.out(
1351
+ `Use 'convilyn-author logs ${result.runtimeId}' to follow runtime logs, 'convilyn-author rollback ${result.runtimeId}' to revert.`
1352
+ );
1353
+ } catch (err) {
1354
+ failHosted(printer, err);
1355
+ }
1356
+ }
1357
+ );
1358
+ program.command("rollback").description("Roll a hosted runtime back to its previous active version.").argument("<runtimeId>", "runtime id returned by deploy --hosted").action(async (runtimeId) => {
1359
+ try {
1360
+ printer.out(`Rolling back ${runtimeId}...`);
1361
+ const result = await runRollback({ runtimeId });
1362
+ printer.out(`Rollback complete: now-active version ${result.version} (${result.status})`);
1363
+ } catch (err) {
1364
+ fail2(printer, err);
1365
+ }
1366
+ });
1367
+ program.command("logs").description("Fetch recent logs for a hosted runtime.").argument("<runtimeId>", "runtime id returned by deploy --hosted").option("--since <since>", "relative window (e.g. '5m', '1h') or ISO-8601 timestamp").option("--limit <limit>", "maximum log entries to fetch", (v) => parseInt(v, 10)).action(async (runtimeId, opts) => {
1368
+ try {
1369
+ const entries = await runLogs({ runtimeId, ...opts });
1370
+ if (entries.length === 0) {
1371
+ printer.out(`No log entries for ${runtimeId} in the requested window.`);
1372
+ return;
1373
+ }
1374
+ for (const entry of entries) {
1375
+ printer.out(`[${entry.timestamp}] ${entry.level} ${entry.message}`);
1376
+ }
1377
+ } catch (err) {
1378
+ fail2(printer, err);
1379
+ }
1380
+ });
1381
+ return program;
1382
+ }
1383
+
1384
+ // src/cli.ts
1385
+ buildProgram().parseAsync(process.argv).catch((err) => {
1386
+ process.stderr.write(`error: ${err.message}
1387
+ `);
1388
+ process.exitCode = 1;
1389
+ });
1390
+ //# sourceMappingURL=cli.cjs.map
1391
+ //# sourceMappingURL=cli.cjs.map