@tuongaz/seeflow 0.1.61 → 0.1.63
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/README.md +3 -3
- package/dist/web/assets/{index-BXYHeBKM.js → index-DAP_yx-l.js} +354 -354
- package/dist/web/assets/{index.es-BzG6d4Ro.js → index.es-2bA-nRVD.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-CcOxqEhi.js → jspdf.es.min-C7u0-VKd.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/{.seeflow/flow.json → flow.json} +3 -25
- package/examples/ecommerce-platform/{.seeflow/scripts → scripts}/play.ts +1 -1
- package/examples/order-pipeline/{.seeflow/flow.json → flow.json} +1 -10
- package/package.json +1 -1
- package/src/api.ts +65 -55
- package/src/cli-helpers.ts +6 -5
- package/src/cli-manifest.ts +103 -15
- package/src/cli.ts +85 -13
- package/src/diagram.ts +0 -1
- package/src/file-ref.ts +16 -15
- package/src/mcp.ts +58 -16
- package/src/merge.ts +0 -1
- package/src/node-files.ts +5 -5
- package/src/operations.ts +40 -101
- package/src/paths.ts +16 -0
- package/src/proxy.ts +13 -13
- package/src/schema-catalog.ts +3 -9
- package/src/schema.ts +36 -96
- package/src/server.ts +0 -4
- package/src/short-id.ts +24 -0
- package/src/status-runner.ts +3 -3
- package/src/watcher.ts +15 -27
- package/src/sdk-template.ts +0 -37
- package/src/sdk-writer.ts +0 -37
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-3zFtHg6ENc/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-5F424NWbEu/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-CbwYqb7NfB/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-XwygzfKPZ5/view.html +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-fkptXw7uvs/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-kwBY8YPmYM/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-mPqan8rFYN/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-yKrg9DV5fJ/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/style.json → style.json} +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-GXTKUcE3ye/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-XKIyds0TDg/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-YOYiHJpY0i/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-zUIH7WFnhK/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/scripts → scripts}/play.ts +0 -0
- /package/examples/order-pipeline/{.seeflow/style.json → style.json} +0 -0
package/src/cli-manifest.ts
CHANGED
|
@@ -134,7 +134,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
134
134
|
synopsis: 'seeflow register [--path <dir>] [--flow <file>]',
|
|
135
135
|
description:
|
|
136
136
|
'Register a demo repo with the studio. Reads <repoPath>/<flow> (defaulting ' +
|
|
137
|
-
'to
|
|
137
|
+
'to ./flow.json), validates the schema, and writes an entry to ' +
|
|
138
138
|
'~/.seeflow/registry.json. Alias of flows:register.',
|
|
139
139
|
category: 'flows',
|
|
140
140
|
args: [],
|
|
@@ -143,12 +143,12 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
143
143
|
{
|
|
144
144
|
name: 'flow',
|
|
145
145
|
valuePlaceholder: '<file>',
|
|
146
|
-
description: 'Path to flow.json relative to repo root (default:
|
|
146
|
+
description: 'Path to flow.json relative to repo root (default: flow.json)',
|
|
147
147
|
},
|
|
148
148
|
],
|
|
149
149
|
outputs: {
|
|
150
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
151
|
-
errorKinds: ['fileNotFound', 'badJson', 'badSchema'
|
|
150
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
151
|
+
errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
|
|
152
152
|
},
|
|
153
153
|
requiresStudio: false,
|
|
154
154
|
examples: ['seeflow register', 'seeflow register --path ./my-app'],
|
|
@@ -164,13 +164,13 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
164
164
|
{
|
|
165
165
|
name: 'flow',
|
|
166
166
|
valuePlaceholder: '<file>',
|
|
167
|
-
description: 'Path to flow.json relative to repo root (default:
|
|
167
|
+
description: 'Path to flow.json relative to repo root (default: flow.json)',
|
|
168
168
|
},
|
|
169
169
|
],
|
|
170
170
|
body: { schemaRef: 'RegisterBody' },
|
|
171
171
|
outputs: {
|
|
172
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
173
|
-
errorKinds: ['fileNotFound', 'badJson', 'badSchema'
|
|
172
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
173
|
+
errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
|
|
174
174
|
},
|
|
175
175
|
requiresStudio: false,
|
|
176
176
|
examples: ['seeflow flows:register --path ./my-app'],
|
|
@@ -303,22 +303,36 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
303
303
|
// ---- project -----------------------------------------------------------
|
|
304
304
|
{
|
|
305
305
|
name: 'projects:create',
|
|
306
|
-
synopsis: 'seeflow projects:create --name <name>',
|
|
306
|
+
synopsis: 'seeflow projects:create --path <dir> --name <name> [--description <text>]',
|
|
307
307
|
description:
|
|
308
|
-
'Scaffold a new project
|
|
309
|
-
'
|
|
308
|
+
'Scaffold a new project at <path> with an empty flow.json and register it. ' +
|
|
309
|
+
'Errors if <path>/flow.json already exists — use flows:register for ' +
|
|
310
|
+
'an existing project.',
|
|
310
311
|
category: 'project',
|
|
311
312
|
args: [],
|
|
312
313
|
flags: [
|
|
314
|
+
{
|
|
315
|
+
name: 'path',
|
|
316
|
+
valuePlaceholder: '<dir>',
|
|
317
|
+
description: 'Project folder (created if it does not exist)',
|
|
318
|
+
required: true,
|
|
319
|
+
},
|
|
313
320
|
{ name: 'name', valuePlaceholder: '<name>', description: 'Project name', required: true },
|
|
321
|
+
{
|
|
322
|
+
name: 'description',
|
|
323
|
+
valuePlaceholder: '<text>',
|
|
324
|
+
description: 'Optional human description, written into flow.json',
|
|
325
|
+
},
|
|
314
326
|
],
|
|
315
327
|
body: { schemaRef: 'CreateProjectBody' },
|
|
316
328
|
outputs: {
|
|
317
|
-
okExample: { id: 'abc12345', slug: 'checkout'
|
|
318
|
-
errorKinds: ['scaffoldFailed'],
|
|
329
|
+
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
330
|
+
errorKinds: ['alreadyExists', 'scaffoldFailed'],
|
|
319
331
|
},
|
|
320
332
|
requiresStudio: false,
|
|
321
|
-
examples: [
|
|
333
|
+
examples: [
|
|
334
|
+
'seeflow projects:create --path ./checkout --name "Checkout" --description "Cart + payments flow"',
|
|
335
|
+
],
|
|
322
336
|
},
|
|
323
337
|
// ---- nodes -------------------------------------------------------------
|
|
324
338
|
{
|
|
@@ -331,7 +345,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
331
345
|
body: {
|
|
332
346
|
example: {
|
|
333
347
|
type: 'stateNode',
|
|
334
|
-
data: { name: 'hello',
|
|
348
|
+
data: { name: 'hello', stateSource: { kind: 'request' } },
|
|
335
349
|
},
|
|
336
350
|
},
|
|
337
351
|
outputs: {
|
|
@@ -514,7 +528,38 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
514
528
|
],
|
|
515
529
|
outputs: { okExample: { ok: true } },
|
|
516
530
|
requiresStudio: false,
|
|
517
|
-
examples: ['seeflow validate --file
|
|
531
|
+
examples: ['seeflow validate --file flow.json'],
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'ids',
|
|
535
|
+
synopsis: 'seeflow ids <type> <count>',
|
|
536
|
+
description:
|
|
537
|
+
'Generate <count> canonical short ids of the given <type>, one per line ' +
|
|
538
|
+
'on stdout. <type> must be `node` (emits `node-<10 base62 chars>`) or ' +
|
|
539
|
+
'`connector` (emits `conn-<10 base62 chars>`). <count> must be an ' +
|
|
540
|
+
'integer in [1, 100]. Uses the same alphabet, length, and rejection-' +
|
|
541
|
+
'sampling logic as the canvas / server / upload regex, so skill-minted ' +
|
|
542
|
+
'ids match every other id producer in the studio. Pure compute — no ' +
|
|
543
|
+
'studio required. Call once per type (i.e. one call for nodes, one for ' +
|
|
544
|
+
'connectors) when seeding a flow.json.',
|
|
545
|
+
category: 'meta',
|
|
546
|
+
args: [
|
|
547
|
+
{
|
|
548
|
+
name: 'type',
|
|
549
|
+
required: true,
|
|
550
|
+
description: "Id kind to generate: 'node' (→ `node-…`) or 'connector' (→ `conn-…`)",
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'count',
|
|
554
|
+
required: true,
|
|
555
|
+
description: 'How many ids to print (integer, 1..100)',
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
flags: [],
|
|
559
|
+
outputKind: 'text',
|
|
560
|
+
outputs: {},
|
|
561
|
+
requiresStudio: false,
|
|
562
|
+
examples: ['seeflow ids node 10', 'seeflow ids connector 5'],
|
|
518
563
|
},
|
|
519
564
|
{
|
|
520
565
|
name: 'schema',
|
|
@@ -562,6 +607,49 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
562
607
|
requiresStudio: true,
|
|
563
608
|
examples: ['seeflow e2e abc12345'],
|
|
564
609
|
},
|
|
610
|
+
{
|
|
611
|
+
name: 'emit',
|
|
612
|
+
synopsis:
|
|
613
|
+
'seeflow emit <flowId> <nodeId> <status> [--run-id <id>] [--payload <json>] [--studio-url <url>]',
|
|
614
|
+
description:
|
|
615
|
+
'Broadcast a node-status event to the studio (status: running | done | error). ' +
|
|
616
|
+
'User apps shell out to this command instead of importing an in-repo helper; ' +
|
|
617
|
+
"the studio re-broadcasts the event on the flow's SSE stream.",
|
|
618
|
+
category: 'live',
|
|
619
|
+
args: [
|
|
620
|
+
{ name: 'flowId', required: true, description: 'Flow id or slug' },
|
|
621
|
+
{ name: 'nodeId', required: true, description: 'Node id in the flow' },
|
|
622
|
+
{ name: 'status', required: true, description: 'Status: running | done | error' },
|
|
623
|
+
],
|
|
624
|
+
flags: [
|
|
625
|
+
{
|
|
626
|
+
name: 'run-id',
|
|
627
|
+
valuePlaceholder: '<id>',
|
|
628
|
+
description: 'Correlate events emitted within a single run',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'payload',
|
|
632
|
+
valuePlaceholder: '<json>',
|
|
633
|
+
description: 'JSON string merged into the event payload',
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: 'studio-url',
|
|
637
|
+
valuePlaceholder: '<url>',
|
|
638
|
+
description: 'Override studio base URL (skips auto-start; targets the URL as-is)',
|
|
639
|
+
},
|
|
640
|
+
{ name: 'no-start', description: 'Fail if the studio is not already running' },
|
|
641
|
+
],
|
|
642
|
+
outputs: {
|
|
643
|
+
okExample: { ok: true },
|
|
644
|
+
errorKinds: [],
|
|
645
|
+
},
|
|
646
|
+
requiresStudio: true,
|
|
647
|
+
examples: [
|
|
648
|
+
'seeflow emit abc12345 api-charge done',
|
|
649
|
+
'seeflow emit abc12345 api-charge error --payload \'{"code":402}\'',
|
|
650
|
+
'seeflow emit abc12345 api-charge running --run-id run-9b3 --studio-url http://localhost:4321',
|
|
651
|
+
],
|
|
652
|
+
},
|
|
565
653
|
];
|
|
566
654
|
|
|
567
655
|
function resolveSchemaRef(ref: string): unknown {
|
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
NodePatchBodySchema,
|
|
13
13
|
ReorderBodySchema,
|
|
14
14
|
} from './operations.ts';
|
|
15
|
-
import { seeflowHome } from './paths.ts';
|
|
15
|
+
import { PROJECT_FLOW_FILENAME, seeflowHome } from './paths.ts';
|
|
16
16
|
import { defaultProcessSpawner } from './process-spawner.ts';
|
|
17
17
|
import { type Registry, createRegistry } from './registry.ts';
|
|
18
18
|
import {
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
} from './runtime.ts';
|
|
29
29
|
import { FlowSchema } from './schema.ts';
|
|
30
30
|
import { serve } from './server.ts';
|
|
31
|
+
import { MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
31
32
|
import { createStatusRunner } from './status-runner.ts';
|
|
32
33
|
|
|
33
|
-
const DEFAULT_FLOW_PATH =
|
|
34
|
+
const DEFAULT_FLOW_PATH = PROJECT_FLOW_FILENAME;
|
|
34
35
|
const HEALTH_TIMEOUT_MS = 10_000;
|
|
35
36
|
const HEALTH_POLL_INTERVAL_MS = 150;
|
|
36
37
|
const STOP_TIMEOUT_MS = 5_000;
|
|
@@ -159,8 +160,12 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
159
160
|
await runValidate();
|
|
160
161
|
} else if (sub === 'schema') {
|
|
161
162
|
await runSchema();
|
|
163
|
+
} else if (sub === 'ids') {
|
|
164
|
+
await runIds();
|
|
162
165
|
} else if (sub === 'e2e') {
|
|
163
166
|
await runE2e();
|
|
167
|
+
} else if (sub === 'emit') {
|
|
168
|
+
await runEmit();
|
|
164
169
|
} else {
|
|
165
170
|
console.error(`Unknown subcommand: ${sub}`);
|
|
166
171
|
printHelp();
|
|
@@ -180,7 +185,7 @@ Commands (work without a running studio):
|
|
|
180
185
|
stop Stop a background studio instance
|
|
181
186
|
register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
|
|
182
187
|
flows:register Register a demo repo
|
|
183
|
-
projects:create Create a new project (--name <name>)
|
|
188
|
+
projects:create Create a new project (--path <dir> --name <name> [--description <text>])
|
|
184
189
|
flows:list List registered flows
|
|
185
190
|
flows:summary List registered flows (id + name + description only)
|
|
186
191
|
flows:get <id> Get flow details
|
|
@@ -200,9 +205,16 @@ Commands (work without a running studio):
|
|
|
200
205
|
validate Schema-validate a flow.json (--file <file> [--style <file>])
|
|
201
206
|
schema [<category>] Get the flow.json schema. No arg → category index;
|
|
202
207
|
category arg → full JSON Schema(s) for that category
|
|
208
|
+
ids <type> <count> Print <count> short ids of the given <type>, one per
|
|
209
|
+
line. <type> is 'node' (-> 'node-...') or 'connector'
|
|
210
|
+
(-> 'conn-...'). <count> is 1..100. Call once per type
|
|
211
|
+
when seeding a flow.json (e.g. 'ids node 10', then
|
|
212
|
+
'ids connector 12').
|
|
203
213
|
|
|
204
214
|
Commands (require a running studio):
|
|
205
215
|
flows:play <id> <n> Trigger a play on node <n>
|
|
216
|
+
emit <id> <n> <st> Broadcast a status event for node <n> (st: running|done|error)
|
|
217
|
+
[--run-id <id>] [--payload <json>] [--studio-url <url>]
|
|
206
218
|
e2e <id> End-to-end validate a registered flow (--skip-nodes a,b)
|
|
207
219
|
|
|
208
220
|
Meta:
|
|
@@ -227,14 +239,14 @@ Options (start):
|
|
|
227
239
|
Options (register):
|
|
228
240
|
--path <dir> Path to repo root (default: current directory)
|
|
229
241
|
--flow <file> Path to flow JSON, relative to repo root
|
|
230
|
-
(default:
|
|
242
|
+
(default: flow.json)
|
|
231
243
|
|
|
232
244
|
Examples:
|
|
233
245
|
npx -y @tuongaz/seeflow@latest
|
|
234
246
|
npx -y @tuongaz/seeflow@latest --port 8080
|
|
235
247
|
npx -y @tuongaz/seeflow@latest start --foreground
|
|
236
248
|
npx -y @tuongaz/seeflow@latest register --path ./my-app
|
|
237
|
-
npx -y @tuongaz/seeflow@latest projects:create --name "Checkout"
|
|
249
|
+
npx -y @tuongaz/seeflow@latest projects:create --path ./checkout --name "Checkout"
|
|
238
250
|
npx -y @tuongaz/seeflow@latest flows:list
|
|
239
251
|
npx -y @tuongaz/seeflow@latest stop
|
|
240
252
|
`.trim(),
|
|
@@ -321,7 +333,7 @@ async function seedExamples(registry: Registry) {
|
|
|
321
333
|
|
|
322
334
|
async function seedExample(registry: Registry, exampleName: string) {
|
|
323
335
|
const destDir = join(seeflowHome(), exampleName);
|
|
324
|
-
const flowPath =
|
|
336
|
+
const flowPath = PROJECT_FLOW_FILENAME;
|
|
325
337
|
|
|
326
338
|
// Always sync from source so that schema changes and example updates are
|
|
327
339
|
// reflected on every startup, even when the dest directory already exists.
|
|
@@ -415,6 +427,25 @@ function reportDaemonFailure(logPath: string | undefined) {
|
|
|
415
427
|
console.error(tail || '(log is empty — daemon exited before writing anything)');
|
|
416
428
|
}
|
|
417
429
|
|
|
430
|
+
async function runIds() {
|
|
431
|
+
const typeArg = argv[1];
|
|
432
|
+
if (!typeArg || typeArg.startsWith('--')) {
|
|
433
|
+
printError("Missing required positional argument: type (expected 'node' or 'connector')");
|
|
434
|
+
}
|
|
435
|
+
if (!isIdType(typeArg)) {
|
|
436
|
+
printError(`Invalid type: ${typeArg} (expected 'node' or 'connector')`);
|
|
437
|
+
}
|
|
438
|
+
const rawCount = argv[2];
|
|
439
|
+
if (!rawCount || rawCount.startsWith('--')) {
|
|
440
|
+
printError(`Missing required positional argument: count (integer 1..${MAX_ID_COUNT})`);
|
|
441
|
+
}
|
|
442
|
+
const count = Number.parseInt(rawCount as string, 10);
|
|
443
|
+
if (!Number.isFinite(count) || count < 1 || count > MAX_ID_COUNT) {
|
|
444
|
+
printError(`Invalid count: ${rawCount} (expected an integer in [1, ${MAX_ID_COUNT}])`);
|
|
445
|
+
}
|
|
446
|
+
for (const id of generateIds(typeArg, count)) process.stdout.write(`${id}\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
418
449
|
async function printVersion() {
|
|
419
450
|
const pkgPath = join(import.meta.dir, '../package.json');
|
|
420
451
|
const pkg = (await Bun.file(pkgPath).json()) as { version?: string };
|
|
@@ -529,12 +560,6 @@ async function runRegister() {
|
|
|
529
560
|
} else {
|
|
530
561
|
console.log(`Registered "${parsed.data.name}" (slug: ${body.slug})`);
|
|
531
562
|
}
|
|
532
|
-
|
|
533
|
-
if (body.sdk?.outcome === 'written') {
|
|
534
|
-
console.log(`Wrote ${body.sdk.filePath} (event-bound state node detected)`);
|
|
535
|
-
} else if (body.sdk?.outcome === 'present') {
|
|
536
|
-
console.log(`SDK helper already present at ${body.sdk.filePath} (skipped)`);
|
|
537
|
-
}
|
|
538
563
|
}
|
|
539
564
|
|
|
540
565
|
async function ensureStudioRunning(url: string, port: number, noStart: boolean) {
|
|
@@ -586,10 +611,18 @@ async function waitForHealth(url: string, timeoutMs: number): Promise<boolean> {
|
|
|
586
611
|
// ---- HTTP-passthrough subcommands ----------------------------------------
|
|
587
612
|
|
|
588
613
|
async function runProjectsCreate() {
|
|
614
|
+
const rawPath = flagValue('path');
|
|
615
|
+
if (!rawPath) printError('Missing required flag: --path');
|
|
589
616
|
const name = flagValue('name');
|
|
590
617
|
if (!name) printError('Missing required flag: --name');
|
|
618
|
+
const description = flagValue('description');
|
|
619
|
+
|
|
591
620
|
const ops = createCliOperations();
|
|
592
|
-
const result = await ops.createProject({
|
|
621
|
+
const result = await ops.createProject({
|
|
622
|
+
path: resolve(rawPath as string),
|
|
623
|
+
name: name as string,
|
|
624
|
+
...(description !== undefined ? { description } : {}),
|
|
625
|
+
});
|
|
593
626
|
printOutcome(result);
|
|
594
627
|
}
|
|
595
628
|
|
|
@@ -651,6 +684,45 @@ async function runFlowsPlay() {
|
|
|
651
684
|
printOk(out);
|
|
652
685
|
}
|
|
653
686
|
|
|
687
|
+
async function runEmit() {
|
|
688
|
+
const flowId = requireArg(1, '<flowId>');
|
|
689
|
+
const nodeId = requireArg(2, '<nodeId>');
|
|
690
|
+
const status = requireArg(3, '<status>');
|
|
691
|
+
if (status !== 'running' && status !== 'done' && status !== 'error') {
|
|
692
|
+
printError(`Invalid status: ${status} (expected running | done | error)`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const runId = flagValue('run-id');
|
|
696
|
+
const rawPayload = flagValue('payload');
|
|
697
|
+
let payload: unknown;
|
|
698
|
+
if (rawPayload !== undefined) {
|
|
699
|
+
try {
|
|
700
|
+
payload = JSON.parse(rawPayload);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
703
|
+
printError(`--payload must be valid JSON: ${message}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Explicit --studio-url targets a specific instance; skip auto-start since
|
|
708
|
+
// the caller is asserting where the studio lives.
|
|
709
|
+
const studioUrlFlag = flagValue('studio-url');
|
|
710
|
+
let url: string;
|
|
711
|
+
if (studioUrlFlag) {
|
|
712
|
+
url = studioUrlFlag.replace(/\/+$/, '');
|
|
713
|
+
} else {
|
|
714
|
+
({ url } = await studioUrlOrDie(hasFlag('no-start')));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const body: Record<string, unknown> = { flowId, nodeId, status };
|
|
718
|
+
if (runId !== undefined) body.runId = runId;
|
|
719
|
+
if (payload !== undefined) body.payload = payload;
|
|
720
|
+
|
|
721
|
+
const res = await postJson(`${url}/api/emit`, body);
|
|
722
|
+
const out = (await handleResponse(res)) as object;
|
|
723
|
+
printOk(out);
|
|
724
|
+
}
|
|
725
|
+
|
|
654
726
|
async function runNodesAdd() {
|
|
655
727
|
const flowId = requireArg(1, '<flowId>');
|
|
656
728
|
const body = await bodyFromFlags();
|
package/src/diagram.ts
CHANGED
package/src/file-ref.ts
CHANGED
|
@@ -19,24 +19,25 @@ const looksLikeFlowNode = (obj: Record<string, unknown>): obj is { id: string; d
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve every `file://<relative-path>` string in `raw` by reading the file
|
|
22
|
-
* under `<
|
|
22
|
+
* under `<projectRoot>/nodes/<nodeId>/` (node-relative) and substituting its
|
|
23
23
|
* UTF-8 content. Strings outside any enclosing flow node are treated as
|
|
24
24
|
* invalid — every supported file:// ref currently lives inside `node.data`.
|
|
25
25
|
*
|
|
26
|
-
* Returns the mutated tree plus the sorted, de-duplicated list of
|
|
27
|
-
* paths that resolved cleanly (the watcher tracks these for
|
|
28
|
-
* external contract uses `nodes/<id>/<file>` even though
|
|
26
|
+
* Returns the mutated tree plus the sorted, de-duplicated list of project-
|
|
27
|
+
* root-relative paths that resolved cleanly (the watcher tracks these for
|
|
28
|
+
* live reload, so the external contract uses `nodes/<id>/<file>` even though
|
|
29
|
+
* the source string is short).
|
|
29
30
|
*/
|
|
30
31
|
export function resolveFileRefs(
|
|
31
32
|
raw: unknown,
|
|
32
|
-
|
|
33
|
+
projectRoot: string,
|
|
33
34
|
): { resolved: unknown; refs: string[] } {
|
|
34
35
|
const refs = new Set<string>();
|
|
35
|
-
let
|
|
36
|
+
let projectRealRoot: string;
|
|
36
37
|
try {
|
|
37
|
-
|
|
38
|
+
projectRealRoot = existsSync(projectRoot) ? realpathSync(projectRoot) : projectRoot;
|
|
38
39
|
} catch {
|
|
39
|
-
|
|
40
|
+
projectRealRoot = projectRoot;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const resolveString = (s: string, nodeId: string | null): string => {
|
|
@@ -45,26 +46,26 @@ export function resolveFileRefs(
|
|
|
45
46
|
if (!isCleanRelativePath(relPath)) return invalidMarker(relPath);
|
|
46
47
|
if (nodeId === null) return invalidMarker(relPath);
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
-
const abs = join(
|
|
50
|
-
if (!existsSync(abs)) return missingMarker(
|
|
49
|
+
const projectRelPath = `nodes/${nodeId}/${relPath}`;
|
|
50
|
+
const abs = join(projectRoot, projectRelPath);
|
|
51
|
+
if (!existsSync(abs)) return missingMarker(projectRelPath);
|
|
51
52
|
|
|
52
53
|
// Symlink-escape defense: resolve realpath and confirm it stays inside root.
|
|
53
54
|
let realAbs: string;
|
|
54
55
|
try {
|
|
55
56
|
realAbs = realpathSync(abs);
|
|
56
57
|
} catch {
|
|
57
|
-
return missingMarker(
|
|
58
|
+
return missingMarker(projectRelPath);
|
|
58
59
|
}
|
|
59
|
-
const rel = relative(
|
|
60
|
+
const rel = relative(projectRealRoot, realAbs);
|
|
60
61
|
if (rel.startsWith('..') || isAbsolute(rel)) return invalidMarker(relPath);
|
|
61
62
|
|
|
62
63
|
try {
|
|
63
64
|
const content = readFileSync(realAbs, 'utf8');
|
|
64
|
-
refs.add(
|
|
65
|
+
refs.add(projectRelPath);
|
|
65
66
|
return content;
|
|
66
67
|
} catch {
|
|
67
|
-
return missingMarker(
|
|
68
|
+
return missingMarker(projectRelPath);
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
|
package/src/mcp.ts
CHANGED
|
@@ -21,13 +21,12 @@ import {
|
|
|
21
21
|
} from './operations.ts';
|
|
22
22
|
import type { Registry } from './registry.ts';
|
|
23
23
|
import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
|
|
24
|
+
import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
24
25
|
import type { FlowWatcher } from './watcher.ts';
|
|
25
26
|
|
|
26
27
|
export interface CreateMcpServerOptions {
|
|
27
28
|
registry: Registry;
|
|
28
29
|
watcher?: FlowWatcher;
|
|
29
|
-
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
30
|
-
projectBaseDir?: string;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
// Tools are pushed into this in-memory list inside `createMcpServer`. Each
|
|
@@ -231,6 +230,57 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
231
230
|
return okResult({ name, schemas: payload.schemas, notes: payload.notes });
|
|
232
231
|
},
|
|
233
232
|
},
|
|
233
|
+
{
|
|
234
|
+
name: 'seeflow_ids',
|
|
235
|
+
description:
|
|
236
|
+
'Batch-mint canonical short ids. `type` is `node` (emits `node-<10 base62 chars>`) ' +
|
|
237
|
+
'or `connector` (emits `conn-<10 base62 chars>`); `count` is an integer in ' +
|
|
238
|
+
'[1, 100]. Pure compute — no flow side effects, no studio state read. Use ' +
|
|
239
|
+
'before authoring a flow.json so minted ids match every other id producer ' +
|
|
240
|
+
'in the studio (canvas, server, upload regex). Call once per type when ' +
|
|
241
|
+
'seeding a flow (one call for nodes, one for connectors).',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
type: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
enum: [...ID_TYPES],
|
|
248
|
+
description: "Id kind: 'node' (→ `node-…`) or 'connector' (→ `conn-…`)",
|
|
249
|
+
},
|
|
250
|
+
count: {
|
|
251
|
+
type: 'integer',
|
|
252
|
+
minimum: 1,
|
|
253
|
+
maximum: MAX_ID_COUNT,
|
|
254
|
+
description: `How many ids to mint (1..${MAX_ID_COUNT})`,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ['type', 'count'],
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
},
|
|
260
|
+
handler: async (args) => {
|
|
261
|
+
const body =
|
|
262
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
263
|
+
? (args as { type?: unknown; count?: unknown })
|
|
264
|
+
: {};
|
|
265
|
+
const { type, count } = body;
|
|
266
|
+
if (!isIdType(type)) {
|
|
267
|
+
return errorResult(
|
|
268
|
+
`invalid type: ${String(type)} (expected one of: ${ID_TYPES.join(', ')})`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
typeof count !== 'number' ||
|
|
273
|
+
!Number.isInteger(count) ||
|
|
274
|
+
count < 1 ||
|
|
275
|
+
count > MAX_ID_COUNT
|
|
276
|
+
) {
|
|
277
|
+
return errorResult(
|
|
278
|
+
`invalid count: ${String(count)} (expected an integer in [1, ${MAX_ID_COUNT}])`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return okResult({ ids: generateIds(type, count) });
|
|
282
|
+
},
|
|
283
|
+
},
|
|
234
284
|
{
|
|
235
285
|
name: 'validate_seeflow',
|
|
236
286
|
description:
|
|
@@ -368,8 +418,6 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
368
418
|
return errorResult(
|
|
369
419
|
`Flow file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
370
420
|
);
|
|
371
|
-
case 'sdkWriteFailed':
|
|
372
|
-
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
373
421
|
}
|
|
374
422
|
},
|
|
375
423
|
},
|
|
@@ -391,7 +439,8 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
391
439
|
},
|
|
392
440
|
{
|
|
393
441
|
name: 'seeflow_create_project',
|
|
394
|
-
description:
|
|
442
|
+
description:
|
|
443
|
+
'Scaffold a new SeeFlow project at the given path. Errors if a project already exists there.',
|
|
395
444
|
inputSchema: inputSchemaFromZod(CreateProjectBodySchema),
|
|
396
445
|
handler: async (args) => {
|
|
397
446
|
const parsed = CreateProjectBodySchema.safeParse(args);
|
|
@@ -402,23 +451,17 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
402
451
|
switch (result.kind) {
|
|
403
452
|
case 'ok':
|
|
404
453
|
return okResult(result.data);
|
|
405
|
-
case '
|
|
406
|
-
return errorResult(`
|
|
407
|
-
case 'badSchema':
|
|
408
|
-
return errorResult(
|
|
409
|
-
`Existing demo file failed schema validation: ${JSON.stringify(result.issues)}`,
|
|
410
|
-
);
|
|
454
|
+
case 'alreadyExists':
|
|
455
|
+
return errorResult(`Project already exists at ${result.path}`);
|
|
411
456
|
case 'scaffoldFailed':
|
|
412
457
|
return errorResult(`Failed to scaffold project: ${result.message}`);
|
|
413
|
-
case 'sdkWriteFailed':
|
|
414
|
-
return errorResult(`Failed to write SDK helper: ${result.message}`);
|
|
415
458
|
}
|
|
416
459
|
},
|
|
417
460
|
},
|
|
418
461
|
{
|
|
419
462
|
name: 'seeflow_add_node',
|
|
420
463
|
description:
|
|
421
|
-
'Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on htmlNode) are auto-externalized to <project
|
|
464
|
+
'Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on htmlNode) are auto-externalized to <project>/nodes/<id>/ and stored as file:// refs in flow.json; reads inline the resolved content transparently.',
|
|
422
465
|
inputSchema: inputSchemaFromZod(AddNodeInputSchema),
|
|
423
466
|
handler: async (args) => {
|
|
424
467
|
const parsed = AddNodeInputSchema.safeParse(args);
|
|
@@ -542,7 +585,7 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
542
585
|
{
|
|
543
586
|
name: 'seeflow_patch_node',
|
|
544
587
|
description:
|
|
545
|
-
'Update fields on an existing node (position, name, description, detail, icon, colors, border, font, shape, dimensions, autoSize, plus iconNode-only color/strokeWidth/alt). Setting detail (every node) or html (htmlNode) writes the content to <project
|
|
588
|
+
'Update fields on an existing node (position, name, description, detail, icon, colors, border, font, shape, dimensions, autoSize, plus iconNode-only color/strokeWidth/alt). Setting detail (every node) or html (htmlNode) writes the content to <project>/nodes/<id>/{detail.md|view.html}; the file:// ref on the node persists. Empty-string detail empties the file but keeps the ref.',
|
|
546
589
|
inputSchema: inputSchemaFromZod(PatchNodeInputSchema),
|
|
547
590
|
handler: async (args) => {
|
|
548
591
|
const parsed = PatchNodeInputSchema.safeParse(args);
|
|
@@ -710,7 +753,6 @@ export function createMcpServer(options: CreateMcpServerOptions): Server {
|
|
|
710
753
|
const ops = createOperations({
|
|
711
754
|
registry: options.registry,
|
|
712
755
|
watcher: options.watcher,
|
|
713
|
-
projectBaseDir: options.projectBaseDir,
|
|
714
756
|
});
|
|
715
757
|
const tools = buildTools(ops);
|
|
716
758
|
|
package/src/merge.ts
CHANGED
package/src/node-files.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { writeFileAtomic } from './atomic-write.ts';
|
|
4
4
|
|
|
5
5
|
// Spec for fields that the studio externalizes to disk under
|
|
6
|
-
// `<project
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// `<project>/nodes/<id>/<fileName>`. `nodeTypes` (when present) scopes the
|
|
7
|
+
// spec entry to specific node types; absent means "applies to every node
|
|
8
|
+
// type". Adding a future text field is one line.
|
|
9
9
|
export interface ExternalizedFieldSpec {
|
|
10
10
|
field: string;
|
|
11
11
|
fileName: string;
|
|
@@ -36,7 +36,7 @@ export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
|
36
36
|
export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
|
|
37
37
|
|
|
38
38
|
export const nodeFileAbsPath = (repoPath: string, nodeId: string, fileName: string): string =>
|
|
39
|
-
join(repoPath,
|
|
39
|
+
join(repoPath, nodeFileRelPath(nodeId, fileName));
|
|
40
40
|
|
|
41
41
|
export function writeNodeFile(absPath: string, content: string): void {
|
|
42
42
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
@@ -44,5 +44,5 @@ export function writeNodeFile(absPath: string, content: string): void {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function removeNodeDir(repoPath: string, nodeId: string): void {
|
|
47
|
-
rmSync(join(repoPath, '
|
|
47
|
+
rmSync(join(repoPath, 'nodes', nodeId), { recursive: true, force: true });
|
|
48
48
|
}
|