@darkhorseprojects/circuitry 0.3.0 → 0.3.1

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.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/node.ts
4
+ import { access, readFile, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+
7
+ // src/yaml.ts
8
+ import YAML from "yaml";
9
+
10
+ // src/graph.ts
11
+ var CIRCUITRY_SPEC_VERSION = "0.3.1";
12
+ var DEFAULT_CIRCUITRY_VALIDATION_RULES = [
13
+ "no-self-loops",
14
+ "no-unknown-edge-endpoints",
15
+ "require-executable-inputs",
16
+ "no-cycles"
17
+ ];
18
+ var DEFAULT_CIRCUITRY_VALIDATION_STANDARD = {
19
+ version: CIRCUITRY_SPEC_VERSION,
20
+ requireSpecVersion: true,
21
+ rules: DEFAULT_CIRCUITRY_VALIDATION_RULES,
22
+ executableKinds: ["agent", "tool", "output"]
23
+ };
24
+ var executableKindsFromRules = (rules = []) => {
25
+ const executableRule = rules.find(
26
+ (entry) => typeof entry !== "string" && entry.rule === "require-executable-inputs"
27
+ );
28
+ return executableRule?.executableKinds;
29
+ };
30
+ var createCircuitryValidationStandard = (standard = {}, graph) => {
31
+ const rules = [
32
+ ...DEFAULT_CIRCUITRY_VALIDATION_RULES,
33
+ ...graph?.validation?.rules || [],
34
+ ...standard.rules || []
35
+ ];
36
+ return {
37
+ version: standard.version || DEFAULT_CIRCUITRY_VALIDATION_STANDARD.version,
38
+ requireSpecVersion: standard.requireSpecVersion ?? DEFAULT_CIRCUITRY_VALIDATION_STANDARD.requireSpecVersion,
39
+ rules,
40
+ executableKinds: standard.executableKinds || executableKindsFromRules(rules) || [
41
+ ...DEFAULT_CIRCUITRY_VALIDATION_STANDARD.executableKinds
42
+ ]
43
+ };
44
+ };
45
+ var expandResources = (resources) => {
46
+ const nodes = [];
47
+ const edges = [];
48
+ for (const [id, resource] of Object.entries(resources)) {
49
+ if (resource.type === "text") {
50
+ nodes.push({
51
+ id,
52
+ kind: "input",
53
+ label: resource.label || id,
54
+ input: { type: "text", value: resource.value }
55
+ });
56
+ } else if (resource.type === "agent") {
57
+ const agent = resource;
58
+ nodes.push({
59
+ id,
60
+ kind: "agent",
61
+ label: agent.label || agent.identity || id,
62
+ agent: {
63
+ identity: agent.identity || agent.label || id,
64
+ model: agent.model,
65
+ thinkingLevel: agent.thinkingLevel,
66
+ tools: agent.tools,
67
+ instructions: agent.instructions,
68
+ personality: agent.personality,
69
+ context: agent.context
70
+ },
71
+ skills: agent.skills,
72
+ expect: agent.expect
73
+ });
74
+ for (const inputRef of agent.inputs || []) {
75
+ edges.push({ from: inputRef, to: id, kind: "dependency" });
76
+ }
77
+ } else if (resource.type === "tool") {
78
+ const tool = resource;
79
+ nodes.push({
80
+ id,
81
+ kind: "tool",
82
+ label: tool.label || id,
83
+ agent: { instructions: tool.instructions }
84
+ });
85
+ for (const inputRef of tool.inputs || []) {
86
+ edges.push({ from: inputRef, to: id, kind: "dependency" });
87
+ }
88
+ } else {
89
+ const input = resource;
90
+ nodes.push({
91
+ id,
92
+ kind: "input",
93
+ label: input.label || id,
94
+ input: {
95
+ type: input.type,
96
+ value: input.value,
97
+ uri: input.uri || input.path,
98
+ mimeType: input.mimeType,
99
+ data: input.data
100
+ }
101
+ });
102
+ }
103
+ }
104
+ return { nodes, edges };
105
+ };
106
+ var normalizeCircuitryGraph = (graph) => {
107
+ if (!graph.resources || Object.keys(graph.resources).length === 0) return { ...graph };
108
+ const { nodes, edges } = expandResources(graph.resources);
109
+ return { ...graph, nodes, edges };
110
+ };
111
+ var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set([CIRCUITRY_SPEC_VERSION]);
112
+ var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
113
+ const resolvedStandard = createCircuitryValidationStandard(
114
+ standard,
115
+ graph && typeof graph === "object" && !Array.isArray(graph) ? graph : void 0
116
+ );
117
+ const errors = [];
118
+ const addError = (code, message, path3) => errors.push({ code, message, ...path3 ? { path: path3 } : {} });
119
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
120
+ addError("invalid_graph", "Circuitry graph must be an object");
121
+ return { ok: false, errors, standard: resolvedStandard };
122
+ }
123
+ const graphObject = graph;
124
+ const rules = resolvedStandard.rules;
125
+ const hasRule = (name) => rules.some((entry) => (typeof entry === "string" ? entry : entry.rule) === name);
126
+ if (resolvedStandard.requireSpecVersion && !VALID_SPEC_VERSIONS.has(String(graphObject.circuitry))) {
127
+ addError(
128
+ "invalid_spec_version",
129
+ `Expected circuitry: "${CIRCUITRY_SPEC_VERSION}"`,
130
+ ["circuitry"]
131
+ );
132
+ }
133
+ if (options.sourceFormat) {
134
+ if (!graphObject.resources || Object.keys(graphObject.resources).length === 0) {
135
+ addError("missing_resources", "Circuitry graphs must use a resources: section", ["resources"]);
136
+ }
137
+ if (graphObject.agents && Object.keys(graphObject.agents).length > 0) {
138
+ addError("forbidden_agents", "Circuitry graph files must not use top-level agents:; use resources:", ["agents"]);
139
+ }
140
+ if (graphObject.inputs && Object.keys(graphObject.inputs).length > 0) {
141
+ addError("forbidden_inputs", "Circuitry graph files use args: for runtime arguments and resource inputs for edges", ["inputs"]);
142
+ }
143
+ if (graphObject.nodes && graphObject.nodes.length > 0) {
144
+ addError("forbidden_nodes", "Circuitry graph files must not use top-level nodes:; use resources:", ["nodes"]);
145
+ }
146
+ if (graphObject.edges && graphObject.edges.length > 0) {
147
+ addError("forbidden_edges", "Circuitry graph files must not use top-level edges:; use resource inputs:", ["edges"]);
148
+ }
149
+ if (graphObject.links && graphObject.links.length > 0) {
150
+ addError("forbidden_links", "Circuitry graph files use imports: for multi-file graphs", ["links"]);
151
+ }
152
+ }
153
+ const normalized = normalizeCircuitryGraph(graphObject);
154
+ const ids = /* @__PURE__ */ new Set();
155
+ for (const [index, node] of (normalized.nodes || []).entries()) {
156
+ if (!node.id) {
157
+ addError("missing_node_id", "Node is missing id", ["nodes", index, "id"]);
158
+ continue;
159
+ }
160
+ if (ids.has(node.id)) {
161
+ addError("duplicate_node_id", `Duplicate node id: ${node.id}`, ["nodes", index, "id"]);
162
+ }
163
+ ids.add(node.id);
164
+ if (!node.kind) {
165
+ addError("missing_node_kind", `Node ${node.id} is missing kind`, ["nodes", index, "kind"]);
166
+ }
167
+ }
168
+ const adjacency = /* @__PURE__ */ new Map();
169
+ const incomingCount = /* @__PURE__ */ new Map();
170
+ for (const id of ids) {
171
+ adjacency.set(id, []);
172
+ incomingCount.set(id, 0);
173
+ }
174
+ for (const [index, edge] of (normalized.edges || []).entries()) {
175
+ if (!edge.from || !edge.to) {
176
+ addError("missing_edge_endpoint", "Edge is missing from/to", ["edges", index]);
177
+ continue;
178
+ }
179
+ if (hasRule("no-self-loops") && edge.from === edge.to) {
180
+ addError("self_loop", `Edge creates self loop: ${edge.from} -> ${edge.to}`, ["edges", index]);
181
+ continue;
182
+ }
183
+ if (hasRule("no-unknown-edge-endpoints") && !ids.has(edge.from)) {
184
+ addError("unknown_edge_source", `Edge references unknown source: ${edge.from}`, ["edges", index, "from"]);
185
+ }
186
+ if (hasRule("no-unknown-edge-endpoints") && !ids.has(edge.to)) {
187
+ addError("unknown_edge_target", `Edge references unknown target: ${edge.to}`, ["edges", index, "to"]);
188
+ }
189
+ if (ids.has(edge.from) && ids.has(edge.to)) {
190
+ adjacency.get(edge.from).push(edge.to);
191
+ incomingCount.set(edge.to, (incomingCount.get(edge.to) || 0) + 1);
192
+ }
193
+ }
194
+ const executableKinds = new Set(resolvedStandard.executableKinds);
195
+ if (hasRule("require-executable-inputs")) {
196
+ for (const [index, node] of (normalized.nodes || []).entries()) {
197
+ if (executableKinds.has(node.kind) && (incomingCount.get(node.id) || 0) === 0) {
198
+ addError("executable_without_inputs", `Executable node has no inputs: ${node.id}`, ["nodes", index]);
199
+ }
200
+ }
201
+ }
202
+ const visiting = /* @__PURE__ */ new Set();
203
+ const visited = /* @__PURE__ */ new Set();
204
+ const path2 = [];
205
+ let cycleMessage = "";
206
+ const visit = (id) => {
207
+ if (cycleMessage) return;
208
+ if (visiting.has(id)) {
209
+ cycleMessage = [...path2.slice(path2.indexOf(id)), id].join(" -> ");
210
+ return;
211
+ }
212
+ if (visited.has(id)) return;
213
+ visiting.add(id);
214
+ path2.push(id);
215
+ for (const next of adjacency.get(id) || []) visit(next);
216
+ path2.pop();
217
+ visiting.delete(id);
218
+ visited.add(id);
219
+ };
220
+ if (hasRule("no-cycles")) {
221
+ for (const id of ids) {
222
+ visit(id);
223
+ if (cycleMessage) {
224
+ addError("cycle", `Graph contains cycle: ${cycleMessage}`);
225
+ break;
226
+ }
227
+ }
228
+ }
229
+ const additionalRules = rules.filter((rule) => {
230
+ if (typeof rule === "string") return false;
231
+ return rule.rule === "require-graph-field" || rule.rule === "require-node-field" || rule.rule === "require-edge-field" || rule.rule === "require-string" || rule.rule === "reject-string";
232
+ });
233
+ const fieldPath = (field) => field.split(".").filter(Boolean);
234
+ const getField = (value, field) => fieldPath(field).reduce((current, key) => {
235
+ if (!current || typeof current !== "object") return void 0;
236
+ return current[key];
237
+ }, value);
238
+ const isPresent = (value) => value !== void 0 && value !== null && value !== "";
239
+ for (const rule of additionalRules) {
240
+ if (rule.rule === "require-graph-field") {
241
+ if (!isPresent(getField(normalized, rule.field))) {
242
+ addError("missing_required_graph_field", `Graph is missing required field: ${rule.field}`, fieldPath(rule.field));
243
+ }
244
+ continue;
245
+ }
246
+ if (rule.rule === "require-node-field") {
247
+ const nodeKinds = new Set(rule.nodeKinds || []);
248
+ for (const [index, node] of (normalized.nodes || []).entries()) {
249
+ if (nodeKinds.size > 0 && !nodeKinds.has(node.kind)) continue;
250
+ if (!isPresent(getField(node, rule.field))) {
251
+ addError("missing_required_node_field", `Node ${node.id || "<missing id>"} is missing required field: ${rule.field}`, ["nodes", index, ...fieldPath(rule.field)]);
252
+ }
253
+ }
254
+ continue;
255
+ }
256
+ if (rule.rule === "require-edge-field") {
257
+ const edgeKinds = new Set(rule.edgeKinds || []);
258
+ for (const [index, edge] of (normalized.edges || []).entries()) {
259
+ if (edgeKinds.size > 0 && (!edge.kind || !edgeKinds.has(edge.kind))) continue;
260
+ if (!isPresent(getField(edge, rule.field))) {
261
+ addError("missing_required_edge_field", `Edge ${edge.id || index} is missing required field: ${rule.field}`, ["edges", index, ...fieldPath(rule.field)]);
262
+ }
263
+ }
264
+ continue;
265
+ }
266
+ const checkString = (target, subject, id, match, path3) => {
267
+ const value = getField(subject, match.field);
268
+ const text = typeof value === "string" ? value : "";
269
+ const includes = text.includes(match.value);
270
+ if (rule.rule === "require-string" && !includes) {
271
+ addError("missing_required_string", `${target} ${id} field ${match.field} must include string: ${match.value}`, path3);
272
+ }
273
+ if (rule.rule === "reject-string" && includes) {
274
+ addError("rejected_string", `${target} ${id} field ${match.field} must not include string: ${match.value}`, path3);
275
+ }
276
+ };
277
+ for (const match of rule.graph || []) checkString("graph", normalized, "<root>", match, fieldPath(match.field));
278
+ for (const match of rule.nodes || []) {
279
+ const nodeKinds = new Set(match.nodeKinds || []);
280
+ for (const [index, node] of (normalized.nodes || []).entries()) {
281
+ if (nodeKinds.size > 0 && !nodeKinds.has(node.kind)) continue;
282
+ checkString("node", node, node.id || "<missing id>", match, ["nodes", index, ...fieldPath(match.field)]);
283
+ }
284
+ }
285
+ for (const match of rule.edges || []) {
286
+ const edgeKinds = new Set(match.edgeKinds || []);
287
+ for (const [index, edge] of (normalized.edges || []).entries()) {
288
+ if (edgeKinds.size > 0 && (!edge.kind || !edgeKinds.has(edge.kind))) continue;
289
+ checkString("edge", edge, edge.id || String(index), match, ["edges", index, ...fieldPath(match.field)]);
290
+ }
291
+ }
292
+ }
293
+ return { ok: errors.length === 0, errors, standard: resolvedStandard };
294
+ };
295
+ var validateCircuitryGraphWithStandard = (graph, standard = {}) => validateCircuitryGraphInternal(graph, standard, { sourceFormat: true });
296
+ var validateCircuitryExecutionGraphWithStandard = (graph, standard = {}) => validateCircuitryGraphInternal(graph, standard, { sourceFormat: false });
297
+ var validateCircuitryGraph = (graph, standard = {}) => validateCircuitryGraphWithStandard(graph, standard).errors.map(
298
+ (error) => error.message
299
+ );
300
+ var parseCircuitryJson = (text, standard = {}, options = {}) => {
301
+ let graph;
302
+ try {
303
+ graph = JSON.parse(text);
304
+ } catch (error) {
305
+ const message = error instanceof Error ? error.message : String(error);
306
+ throw new Error(`Could not parse Circuitry JSON. Check commas, quotes, and braces.
307
+ ${message}`);
308
+ }
309
+ if (options.validate === false) return graph;
310
+ const errors = validateCircuitryGraph(graph, standard);
311
+ if (errors.length) {
312
+ throw new Error(`Invalid Circuitry graph:
313
+ ${errors.join("\n")}`);
314
+ }
315
+ return normalizeCircuitryGraph(graph);
316
+ };
317
+
318
+ // src/yaml.ts
319
+ var parseCircuitryYaml = (text, standard = {}, options = {}) => {
320
+ let graph;
321
+ try {
322
+ graph = YAML.parse(text);
323
+ } catch (error) {
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ throw new Error(`Could not parse Circuitry YAML. Check indentation and quotes.
326
+ ${message}`);
327
+ }
328
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
329
+ throw new Error("Could not parse Circuitry YAML. Expected a graph object.");
330
+ }
331
+ if (options.validate === false) return graph;
332
+ const errors = validateCircuitryGraph(graph, standard);
333
+ if (errors.length) {
334
+ throw new Error(`Invalid Circuitry graph:
335
+ ${errors.join("\n")}`);
336
+ }
337
+ return normalizeCircuitryGraph(graph);
338
+ };
339
+ var parseCircuitryText = (text, filename2 = "graph.circuitry.yaml", standard = {}, options = {}) => {
340
+ return filename2.endsWith(".json") ? parseCircuitryJson(text, standard, options) : parseCircuitryYaml(text, standard, options);
341
+ };
342
+
343
+ // src/node.ts
344
+ var defaultFilename = "graph.circuitry.yaml";
345
+ var resourceInputs = (resource) => "inputs" in resource && Array.isArray(resource.inputs) ? resource.inputs : [];
346
+ var withResourceInputs = (resource, inputs) => "inputs" in resource ? { ...resource, inputs } : resource;
347
+ var selectedImportResources = (imp, resources) => {
348
+ if (imp.resources === "*") return Object.fromEntries(Object.keys(resources).map((id) => [id, id]));
349
+ if (Array.isArray(imp.resources)) return Object.fromEntries(imp.resources.map((id) => [id, id]));
350
+ return imp.resources;
351
+ };
352
+ var importedId = (localId, prefix) => `${prefix || ""}${localId}`;
353
+ var rewriteResourceInputs = (resource, aliasMap) => {
354
+ const inputs = resourceInputs(resource);
355
+ if (inputs.length === 0) return resource;
356
+ return withResourceInputs(resource, inputs.map((input) => aliasMap[input] || input));
357
+ };
358
+ var resolveCircuitryGraph = async (parsed, graphFile, text, standard, stack = []) => {
359
+ if (stack.includes(graphFile)) {
360
+ throw new Error(`Circuitry graph import cycle: ${[...stack, graphFile].join(" -> ")}`);
361
+ }
362
+ const nextStack = [...stack, graphFile];
363
+ const resources = {};
364
+ const origins = {};
365
+ for (const imp of parsed.imports || []) {
366
+ const linkedFile = path.resolve(path.dirname(graphFile), imp.path);
367
+ const loaded = await loadCircuitryGraphFile(linkedFile, standard, nextStack);
368
+ const available = loaded.graph.resources || {};
369
+ const aliases = selectedImportResources(imp, available);
370
+ const aliasMap = {};
371
+ for (const [sourceId, localId] of Object.entries(aliases)) aliasMap[sourceId] = importedId(localId, imp.prefix);
372
+ for (const [sourceId, localId] of Object.entries(aliases)) {
373
+ const resource = available[sourceId];
374
+ if (!resource) throw new Error(`Imported resource not found: ${sourceId} in ${imp.path}`);
375
+ const nextId = importedId(localId, imp.prefix);
376
+ if (resources[nextId]) throw new Error(`Imported Circuitry resource id collision: ${nextId} from ${imp.path}`);
377
+ resources[nextId] = rewriteResourceInputs(resource, aliasMap);
378
+ origins[nextId] = loaded.origins[sourceId] || linkedFile;
379
+ }
380
+ }
381
+ for (const [id, resource] of Object.entries(parsed.resources || {})) {
382
+ if (resources[id]) throw new Error(`Circuitry resource id collision: ${id} in ${graphFile}`);
383
+ resources[id] = resource;
384
+ origins[id] = graphFile;
385
+ }
386
+ const graph = normalizeCircuitryGraph({ ...parsed, imports: [], resources });
387
+ if (stack.length === 0) {
388
+ const validation = validateCircuitryExecutionGraphWithStandard(graph, standard);
389
+ if (validation.errors.length) {
390
+ throw new Error(`Invalid Circuitry graph:
391
+ ${validation.errors.map((e) => e.message).join("\n")}`);
392
+ }
393
+ }
394
+ return { filename: graphFile, text, graph, origins };
395
+ };
396
+ var loadCircuitryGraphFile = async (filename2 = defaultFilename, standard, stack = []) => {
397
+ const graphFile = path.resolve(process.cwd(), filename2);
398
+ const text = await readFile(graphFile, "utf8");
399
+ const parsed = parseCircuitryText(text, graphFile, standard, { validate: false });
400
+ return resolveCircuitryGraph(parsed, graphFile, text, standard, stack);
401
+ };
402
+
403
+ // src/cli.ts
404
+ var [, , command, filename] = process.argv;
405
+ var usage = () => {
406
+ console.error("usage: circuitry <check|normalize> <graph.circuitry.yaml>");
407
+ process.exit(2);
408
+ };
409
+ if (!command || !filename) usage();
410
+ try {
411
+ const loaded = await loadCircuitryGraphFile(filename);
412
+ if (command === "check") {
413
+ const result = validateCircuitryExecutionGraphWithStandard(loaded.graph);
414
+ if (!result.ok) {
415
+ console.error(result.errors.map((error) => error.message).join("\n"));
416
+ process.exit(1);
417
+ }
418
+ console.log(`ok: ${loaded.filename}`);
419
+ } else if (command === "normalize") {
420
+ console.log(JSON.stringify({ graph: loaded.graph, origins: loaded.origins }, null, 2));
421
+ } else {
422
+ usage();
423
+ }
424
+ } catch (error) {
425
+ console.error(error instanceof Error ? error.message : String(error));
426
+ process.exit(1);
427
+ }
package/dist/graph.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export declare const CIRCUITRY_SPEC_VERSION: "0.3.0";
2
- export type CircuitrySpecVersion = typeof CIRCUITRY_SPEC_VERSION | "0.2";
1
+ export declare const CIRCUITRY_SPEC_VERSION: "0.3.1";
2
+ export type CircuitrySpecVersion = typeof CIRCUITRY_SPEC_VERSION;
3
3
  export type CircuitryNodeKind = "agent" | "input" | "tool" | "output" | string;
4
4
  export type CircuitryEdgeKind = "context" | "dependency" | "message" | "control" | string;
5
5
  export type CircuitryInputKind = "text" | "file" | "url" | "image" | "uri" | "canvas" | "mcp" | string;
@@ -89,7 +89,16 @@ export type CircuitryResourceTool = {
89
89
  inputs?: string[];
90
90
  instructions?: string;
91
91
  };
92
- export type CircuitryResourceEntry = CircuitryResourceText | CircuitryResourceAgent | CircuitryResourceTool;
92
+ export type CircuitryResourceData = {
93
+ type: CircuitryInputKind;
94
+ value?: string;
95
+ uri?: string;
96
+ path?: string;
97
+ mimeType?: string;
98
+ label?: string;
99
+ data?: Record<string, unknown>;
100
+ };
101
+ export type CircuitryResourceEntry = CircuitryResourceText | CircuitryResourceAgent | CircuitryResourceTool | CircuitryResourceData;
93
102
  export type CircuitrySandboxProvider = "local" | "docker" | "e2b" | "daytona" | "cloudflare" | "modal" | "openai" | (string & {});
94
103
  export type CircuitrySandboxClient = {
95
104
  provider: CircuitrySandboxProvider;
@@ -161,6 +170,14 @@ export type CircuitryValidationRuleEntry = CircuitryBuiltInValidationRule | {
161
170
  export type CircuitryGraphValidation = {
162
171
  rules?: CircuitryValidationRuleEntry[];
163
172
  };
173
+ export type CircuitryGraphArg = {
174
+ type: CircuitryInputKind;
175
+ required?: boolean;
176
+ label?: string;
177
+ description?: string;
178
+ default?: unknown;
179
+ mimeType?: string;
180
+ };
164
181
  export type CircuitryRuntimeInputs = Record<string, unknown>;
165
182
  /**
166
183
  * Return a graph copy with runtime input values applied to existing text inputs.
@@ -174,28 +191,28 @@ export declare const applyCircuitryRuntimeInputs: (graph: CircuitryGraph, inputs
174
191
  export type CircuitryImport = {
175
192
  /** Path to another .circuitry.yaml/.json file, resolved relative to this file. */
176
193
  path: string;
177
- /** Resource id to import from the source file. */
178
- resource: string;
179
- /** Optional local name (alias) for the imported resource. */
180
- as?: string;
194
+ /** Import all resources, a resource list, or an old->new resource id map. */
195
+ resources: "*" | string[] | Record<string, string>;
196
+ /** Optional namespace applied to imported resource ids and their internal refs. */
197
+ prefix?: string;
181
198
  };
182
199
  export type CircuitryGraph = {
183
- /** Spec version. Use "0.3.0" for import-based graph files. */
200
+ /** Spec version. */
184
201
  circuitry: CircuitrySpecVersion | string;
185
202
  id?: string;
186
203
  title?: string;
187
204
  description?: string;
188
- /** Explicit imports of specific resources from other graph files. */
205
+ /** Explicit imports of resources from other graph files. */
189
206
  imports?: CircuitryImport[];
207
+ /** Runtime call contract. Args bind to resources with matching ids. */
208
+ args?: Record<string, CircuitryGraphArg>;
190
209
  resources?: Record<string, CircuitryResourceEntry>;
191
210
  /** Internal normalized execution nodes. Authored graph files must not set this. */
192
211
  nodes?: CircuitryNode[];
193
212
  /** Internal normalized execution edges. Authored graph files must not set this. */
194
213
  edges?: CircuitryEdge[];
195
- /** Forbidden in authored v0.2 graph files. */
214
+ /** Forbidden in authored graph files. */
196
215
  agents?: never;
197
- /** Forbidden in authored v0.2 graph files. */
198
- inputs?: never;
199
216
  runtime?: CircuitryRuntime;
200
217
  validation?: CircuitryGraphValidation;
201
218
  metadata?: Record<string, unknown>;
@@ -226,7 +243,7 @@ export type CircuitryValidationResult = {
226
243
  };
227
244
  export declare const DEFAULT_CIRCUITRY_VALIDATION_RULES: CircuitryValidationRuleEntry[];
228
245
  export declare const DEFAULT_CIRCUITRY_VALIDATION_STANDARD: {
229
- readonly version: "0.3.0";
246
+ readonly version: "0.3.1";
230
247
  readonly requireSpecVersion: true;
231
248
  readonly rules: CircuitryValidationRuleEntry[];
232
249
  readonly executableKinds: ["agent", "tool", "output"];
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var isNodeElement = (element) => {
27
27
  };
28
28
 
29
29
  // src/graph.ts
30
- var CIRCUITRY_SPEC_VERSION = "0.3.0";
30
+ var CIRCUITRY_SPEC_VERSION = "0.3.1";
31
31
  var runtimeInputToText = (value) => {
32
32
  if (typeof value === "string") return value;
33
33
  if (value === void 0) return "";
@@ -98,35 +98,51 @@ var expandResources = (resources) => {
98
98
  input: { type: "text", value: resource.value }
99
99
  });
100
100
  } else if (resource.type === "agent") {
101
+ const agent = resource;
101
102
  nodes.push({
102
103
  id,
103
104
  kind: "agent",
104
- label: resource.label || resource.identity || id,
105
+ label: agent.label || agent.identity || id,
105
106
  agent: {
106
- identity: resource.identity || resource.label || id,
107
- model: resource.model,
108
- thinkingLevel: resource.thinkingLevel,
109
- tools: resource.tools,
110
- instructions: resource.instructions,
111
- personality: resource.personality,
112
- context: resource.context
107
+ identity: agent.identity || agent.label || id,
108
+ model: agent.model,
109
+ thinkingLevel: agent.thinkingLevel,
110
+ tools: agent.tools,
111
+ instructions: agent.instructions,
112
+ personality: agent.personality,
113
+ context: agent.context
113
114
  },
114
- skills: resource.skills,
115
- expect: resource.expect
115
+ skills: agent.skills,
116
+ expect: agent.expect
116
117
  });
117
- for (const inputRef of resource.inputs || []) {
118
+ for (const inputRef of agent.inputs || []) {
118
119
  edges.push({ from: inputRef, to: id, kind: "dependency" });
119
120
  }
120
121
  } else if (resource.type === "tool") {
122
+ const tool = resource;
121
123
  nodes.push({
122
124
  id,
123
125
  kind: "tool",
124
- label: resource.label || id,
125
- agent: { instructions: resource.instructions }
126
+ label: tool.label || id,
127
+ agent: { instructions: tool.instructions }
126
128
  });
127
- for (const inputRef of resource.inputs || []) {
129
+ for (const inputRef of tool.inputs || []) {
128
130
  edges.push({ from: inputRef, to: id, kind: "dependency" });
129
131
  }
132
+ } else {
133
+ const input = resource;
134
+ nodes.push({
135
+ id,
136
+ kind: "input",
137
+ label: input.label || id,
138
+ input: {
139
+ type: input.type,
140
+ value: input.value,
141
+ uri: input.uri || input.path,
142
+ mimeType: input.mimeType,
143
+ data: input.data
144
+ }
145
+ });
130
146
  }
131
147
  }
132
148
  return { nodes, edges };
@@ -136,7 +152,7 @@ var normalizeCircuitryGraph = (graph) => {
136
152
  const { nodes, edges } = expandResources(graph.resources);
137
153
  return { ...graph, nodes, edges };
138
154
  };
139
- var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set(["0.3"]);
155
+ var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set([CIRCUITRY_SPEC_VERSION]);
140
156
  var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
141
157
  const resolvedStandard = createCircuitryValidationStandard(
142
158
  standard,
@@ -160,22 +176,22 @@ var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
160
176
  }
161
177
  if (options.sourceFormat) {
162
178
  if (!graphObject.resources || Object.keys(graphObject.resources).length === 0) {
163
- addError("missing_resources", "Circuitry v0.3 graphs must use a resources: section", ["resources"]);
179
+ addError("missing_resources", "Circuitry graphs must use a resources: section", ["resources"]);
164
180
  }
165
181
  if (graphObject.agents && Object.keys(graphObject.agents).length > 0) {
166
- addError("forbidden_agents", "Circuitry v0.3 graph files must not use top-level agents:; use resources:", ["agents"]);
182
+ addError("forbidden_agents", "Circuitry graph files must not use top-level agents:; use resources:", ["agents"]);
167
183
  }
168
184
  if (graphObject.inputs && Object.keys(graphObject.inputs).length > 0) {
169
- addError("forbidden_inputs", "Circuitry v0.3 graph files must not use top-level inputs:; use resources:", ["inputs"]);
185
+ addError("forbidden_inputs", "Circuitry graph files use args: for runtime arguments and resource inputs for edges", ["inputs"]);
170
186
  }
171
187
  if (graphObject.nodes && graphObject.nodes.length > 0) {
172
- addError("forbidden_nodes", "Circuitry v0.3 graph files must not use top-level nodes:; use resources:", ["nodes"]);
188
+ addError("forbidden_nodes", "Circuitry graph files must not use top-level nodes:; use resources:", ["nodes"]);
173
189
  }
174
190
  if (graphObject.edges && graphObject.edges.length > 0) {
175
- addError("forbidden_edges", "Circuitry v0.3 graph files must not use top-level edges:; use resource inputs:", ["edges"]);
191
+ addError("forbidden_edges", "Circuitry graph files must not use top-level edges:; use resource inputs:", ["edges"]);
176
192
  }
177
193
  if (graphObject.links && graphObject.links.length > 0) {
178
- addError("forbidden_links", "Circuitry v0.3 graph files must use imports: instead of links:", ["links"]);
194
+ addError("forbidden_links", "Circuitry graph files use imports: for multi-file graphs", ["links"]);
179
195
  }
180
196
  }
181
197
  const normalized = normalizeCircuitryGraph(graphObject);
package/dist/node.d.ts CHANGED
@@ -2,15 +2,20 @@ import type { CircuitryHost } from "./host";
2
2
  import type { CircuitryGraph, CircuitryRuntimeInputs, CircuitryValidationStandard } from "./graph";
3
3
  import type { NodeExecutionRequest, NodeExecutionResult, SimulationRunItem } from "./simulation";
4
4
  export declare const runNodeWithPiSDK: ({ model, prompt, images, tools, thinkingLevel, }: NodeExecutionRequest) => Promise<NodeExecutionResult>;
5
+ export type CircuitryResourceOrigins = Record<string, string>;
6
+ export type LoadedCircuitryGraph = {
7
+ filename: string;
8
+ text: string;
9
+ graph: CircuitryGraph;
10
+ origins: CircuitryResourceOrigins;
11
+ };
12
+ export declare const loadCircuitryGraphFile: (filename?: string, standard?: CircuitryValidationStandard, stack?: string[]) => Promise<LoadedCircuitryGraph>;
5
13
  export declare class NodeCircuitryHost implements CircuitryHost {
6
14
  private resolveGraphFile;
7
15
  private runFileFor;
8
16
  private exists;
9
17
  private parseIssue;
10
18
  private parseGraphForValidation;
11
- private resolveGraphImports;
12
- /** Rewrite inputs in imported resource to use aliased resource ids. */
13
- private rewriteImportInputs;
14
19
  private parseAndResolveGraph;
15
20
  private readGraphFile;
16
21
  readGraph(input?: {
package/dist/node.js CHANGED
@@ -8,7 +8,7 @@ import os from "node:os";
8
8
  import YAML from "yaml";
9
9
 
10
10
  // src/graph.ts
11
- var CIRCUITRY_SPEC_VERSION = "0.3.0";
11
+ var CIRCUITRY_SPEC_VERSION = "0.3.1";
12
12
  var runtimeInputToText = (value) => {
13
13
  if (typeof value === "string") return value;
14
14
  if (value === void 0) return "";
@@ -79,35 +79,51 @@ var expandResources = (resources) => {
79
79
  input: { type: "text", value: resource.value }
80
80
  });
81
81
  } else if (resource.type === "agent") {
82
+ const agent = resource;
82
83
  nodes.push({
83
84
  id,
84
85
  kind: "agent",
85
- label: resource.label || resource.identity || id,
86
+ label: agent.label || agent.identity || id,
86
87
  agent: {
87
- identity: resource.identity || resource.label || id,
88
- model: resource.model,
89
- thinkingLevel: resource.thinkingLevel,
90
- tools: resource.tools,
91
- instructions: resource.instructions,
92
- personality: resource.personality,
93
- context: resource.context
88
+ identity: agent.identity || agent.label || id,
89
+ model: agent.model,
90
+ thinkingLevel: agent.thinkingLevel,
91
+ tools: agent.tools,
92
+ instructions: agent.instructions,
93
+ personality: agent.personality,
94
+ context: agent.context
94
95
  },
95
- skills: resource.skills,
96
- expect: resource.expect
96
+ skills: agent.skills,
97
+ expect: agent.expect
97
98
  });
98
- for (const inputRef of resource.inputs || []) {
99
+ for (const inputRef of agent.inputs || []) {
99
100
  edges.push({ from: inputRef, to: id, kind: "dependency" });
100
101
  }
101
102
  } else if (resource.type === "tool") {
103
+ const tool = resource;
102
104
  nodes.push({
103
105
  id,
104
106
  kind: "tool",
105
- label: resource.label || id,
106
- agent: { instructions: resource.instructions }
107
+ label: tool.label || id,
108
+ agent: { instructions: tool.instructions }
107
109
  });
108
- for (const inputRef of resource.inputs || []) {
110
+ for (const inputRef of tool.inputs || []) {
109
111
  edges.push({ from: inputRef, to: id, kind: "dependency" });
110
112
  }
113
+ } else {
114
+ const input = resource;
115
+ nodes.push({
116
+ id,
117
+ kind: "input",
118
+ label: input.label || id,
119
+ input: {
120
+ type: input.type,
121
+ value: input.value,
122
+ uri: input.uri || input.path,
123
+ mimeType: input.mimeType,
124
+ data: input.data
125
+ }
126
+ });
111
127
  }
112
128
  }
113
129
  return { nodes, edges };
@@ -117,7 +133,7 @@ var normalizeCircuitryGraph = (graph) => {
117
133
  const { nodes, edges } = expandResources(graph.resources);
118
134
  return { ...graph, nodes, edges };
119
135
  };
120
- var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set(["0.3"]);
136
+ var VALID_SPEC_VERSIONS = /* @__PURE__ */ new Set([CIRCUITRY_SPEC_VERSION]);
121
137
  var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
122
138
  const resolvedStandard = createCircuitryValidationStandard(
123
139
  standard,
@@ -141,22 +157,22 @@ var validateCircuitryGraphInternal = (graph, standard = {}, options) => {
141
157
  }
142
158
  if (options.sourceFormat) {
143
159
  if (!graphObject.resources || Object.keys(graphObject.resources).length === 0) {
144
- addError("missing_resources", "Circuitry v0.3 graphs must use a resources: section", ["resources"]);
160
+ addError("missing_resources", "Circuitry graphs must use a resources: section", ["resources"]);
145
161
  }
146
162
  if (graphObject.agents && Object.keys(graphObject.agents).length > 0) {
147
- addError("forbidden_agents", "Circuitry v0.3 graph files must not use top-level agents:; use resources:", ["agents"]);
163
+ addError("forbidden_agents", "Circuitry graph files must not use top-level agents:; use resources:", ["agents"]);
148
164
  }
149
165
  if (graphObject.inputs && Object.keys(graphObject.inputs).length > 0) {
150
- addError("forbidden_inputs", "Circuitry v0.3 graph files must not use top-level inputs:; use resources:", ["inputs"]);
166
+ addError("forbidden_inputs", "Circuitry graph files use args: for runtime arguments and resource inputs for edges", ["inputs"]);
151
167
  }
152
168
  if (graphObject.nodes && graphObject.nodes.length > 0) {
153
- addError("forbidden_nodes", "Circuitry v0.3 graph files must not use top-level nodes:; use resources:", ["nodes"]);
169
+ addError("forbidden_nodes", "Circuitry graph files must not use top-level nodes:; use resources:", ["nodes"]);
154
170
  }
155
171
  if (graphObject.edges && graphObject.edges.length > 0) {
156
- addError("forbidden_edges", "Circuitry v0.3 graph files must not use top-level edges:; use resource inputs:", ["edges"]);
172
+ addError("forbidden_edges", "Circuitry graph files must not use top-level edges:; use resource inputs:", ["edges"]);
157
173
  }
158
174
  if (graphObject.links && graphObject.links.length > 0) {
159
- addError("forbidden_links", "Circuitry v0.3 graph files must use imports: instead of links:", ["links"]);
175
+ addError("forbidden_links", "Circuitry graph files use imports: for multi-file graphs", ["links"]);
160
176
  }
161
177
  }
162
178
  const normalized = normalizeCircuitryGraph(graphObject);
@@ -804,6 +820,63 @@ var runNodeWithPiSDK = async ({
804
820
  }
805
821
  };
806
822
  var defaultFilename = "graph.circuitry.yaml";
823
+ var resourceInputs = (resource) => "inputs" in resource && Array.isArray(resource.inputs) ? resource.inputs : [];
824
+ var withResourceInputs = (resource, inputs) => "inputs" in resource ? { ...resource, inputs } : resource;
825
+ var selectedImportResources = (imp, resources) => {
826
+ if (imp.resources === "*") return Object.fromEntries(Object.keys(resources).map((id) => [id, id]));
827
+ if (Array.isArray(imp.resources)) return Object.fromEntries(imp.resources.map((id) => [id, id]));
828
+ return imp.resources;
829
+ };
830
+ var importedId = (localId, prefix) => `${prefix || ""}${localId}`;
831
+ var rewriteResourceInputs = (resource, aliasMap) => {
832
+ const inputs = resourceInputs(resource);
833
+ if (inputs.length === 0) return resource;
834
+ return withResourceInputs(resource, inputs.map((input) => aliasMap[input] || input));
835
+ };
836
+ var resolveCircuitryGraph = async (parsed, graphFile, text, standard, stack = []) => {
837
+ if (stack.includes(graphFile)) {
838
+ throw new Error(`Circuitry graph import cycle: ${[...stack, graphFile].join(" -> ")}`);
839
+ }
840
+ const nextStack = [...stack, graphFile];
841
+ const resources = {};
842
+ const origins = {};
843
+ for (const imp of parsed.imports || []) {
844
+ const linkedFile = path.resolve(path.dirname(graphFile), imp.path);
845
+ const loaded = await loadCircuitryGraphFile(linkedFile, standard, nextStack);
846
+ const available = loaded.graph.resources || {};
847
+ const aliases = selectedImportResources(imp, available);
848
+ const aliasMap = {};
849
+ for (const [sourceId, localId] of Object.entries(aliases)) aliasMap[sourceId] = importedId(localId, imp.prefix);
850
+ for (const [sourceId, localId] of Object.entries(aliases)) {
851
+ const resource = available[sourceId];
852
+ if (!resource) throw new Error(`Imported resource not found: ${sourceId} in ${imp.path}`);
853
+ const nextId = importedId(localId, imp.prefix);
854
+ if (resources[nextId]) throw new Error(`Imported Circuitry resource id collision: ${nextId} from ${imp.path}`);
855
+ resources[nextId] = rewriteResourceInputs(resource, aliasMap);
856
+ origins[nextId] = loaded.origins[sourceId] || linkedFile;
857
+ }
858
+ }
859
+ for (const [id, resource] of Object.entries(parsed.resources || {})) {
860
+ if (resources[id]) throw new Error(`Circuitry resource id collision: ${id} in ${graphFile}`);
861
+ resources[id] = resource;
862
+ origins[id] = graphFile;
863
+ }
864
+ const graph = normalizeCircuitryGraph({ ...parsed, imports: [], resources });
865
+ if (stack.length === 0) {
866
+ const validation = validateCircuitryExecutionGraphWithStandard(graph, standard);
867
+ if (validation.errors.length) {
868
+ throw new Error(`Invalid Circuitry graph:
869
+ ${validation.errors.map((e) => e.message).join("\n")}`);
870
+ }
871
+ }
872
+ return { filename: graphFile, text, graph, origins };
873
+ };
874
+ var loadCircuitryGraphFile = async (filename = defaultFilename, standard, stack = []) => {
875
+ const graphFile = path.resolve(process.cwd(), filename);
876
+ const text = await readFile(graphFile, "utf8");
877
+ const parsed = parseCircuitryText(text, graphFile, standard, { validate: false });
878
+ return resolveCircuitryGraph(parsed, graphFile, text, standard, stack);
879
+ };
807
880
  var NodeCircuitryHost = class {
808
881
  resolveGraphFile(filename) {
809
882
  return path.resolve(
@@ -840,71 +913,19 @@ var NodeCircuitryHost = class {
840
913
  return { error: this.parseIssue(error, filename) };
841
914
  }
842
915
  }
843
- async resolveGraphImports(graph, filename, standard, stack = []) {
844
- const graphFile = path.resolve(filename);
845
- if (stack.includes(graphFile)) {
846
- throw new Error(`Circuitry graph import cycle: ${[...stack, graphFile].join(" -> ")}`);
847
- }
848
- const importedResources = {};
849
- const aliasMap = {};
850
- const nextStack = [...stack, graphFile];
851
- for (const imp of graph.imports || []) {
852
- const linkedFile = path.resolve(path.dirname(graphFile), imp.path);
853
- const linkedText = await readFile(linkedFile, "utf8");
854
- const parsed = parseCircuitryText(linkedText, linkedFile, standard, { validate: false });
855
- const resolved = await this.resolveGraphImports(parsed, linkedFile, standard, nextStack);
856
- const resourceId = imp.resource;
857
- const resource = resolved.resources?.[resourceId];
858
- if (!resource) {
859
- throw new Error(`Imported resource not found: ${resourceId} in ${imp.path}`);
860
- }
861
- const localId = imp.as || resourceId;
862
- if (importedResources[localId]) {
863
- throw new Error(`Imported Circuitry resource id collision: ${localId} from ${imp.path}`);
864
- }
865
- if (imp.as) aliasMap[resourceId] = localId;
866
- const resourceWithRewrittenInputs = this.rewriteImportInputs(resource, aliasMap);
867
- importedResources[localId] = resourceWithRewrittenInputs;
868
- }
869
- return {
870
- ...graph,
871
- resources: {
872
- ...importedResources,
873
- ...graph.resources || {}
874
- }
875
- };
876
- }
877
- /** Rewrite inputs in imported resource to use aliased resource ids. */
878
- rewriteImportInputs(resource, aliasMap) {
879
- if (resource.type === "agent" || resource.type === "tool") {
880
- const newInputs = (resource.inputs || []).map((input) => aliasMap[input] || input);
881
- return { ...resource, inputs: newInputs };
882
- }
883
- return resource;
884
- }
885
916
  async parseAndResolveGraph(text, filename, standard) {
886
- const parsed = this.parseGraphForValidation(text, filename, standard);
887
- if (!parsed.graph) return parsed;
888
917
  try {
889
- return { graph: await this.resolveGraphImports(parsed.graph, filename, standard) };
918
+ const graphFile = path.resolve(process.cwd(), filename);
919
+ const source = text || "";
920
+ const parsed = parseCircuitryText(source, graphFile, standard, { validate: false });
921
+ return { graph: (await resolveCircuitryGraph(parsed, graphFile, source, standard)).graph };
890
922
  } catch (error) {
891
923
  return { error: this.parseIssue(error, filename) };
892
924
  }
893
925
  }
894
926
  async readGraphFile(filename) {
895
- const graphFile = this.resolveGraphFile(filename);
896
- const text = await readFile(graphFile, "utf8");
897
- const parsed = await this.parseAndResolveGraph(text, graphFile);
898
- if (!parsed.graph) throw new Error(`Invalid Circuitry graph:
899
- ${parsed.error?.message}`);
900
- const validation = validateCircuitryGraphWithStandard(parsed.graph);
901
- if (validation.errors.length) {
902
- throw new Error(
903
- `Invalid Circuitry graph:
904
- ${validation.errors.map((e) => e.message).join("\n")}`
905
- );
906
- }
907
- return { graphFile, text, graph: normalizeCircuitryGraph(parsed.graph) };
927
+ const loaded = await loadCircuitryGraphFile(this.resolveGraphFile(filename));
928
+ return { graphFile: loaded.filename, text: loaded.text, graph: loaded.graph, origins: loaded.origins };
908
929
  }
909
930
  async readGraph(input = {}) {
910
931
  const { graphFile, text, graph } = await this.readGraphFile(input.filename);
@@ -917,10 +938,10 @@ ${validation.errors.map((e) => e.message).join("\n")}`
917
938
  return validateCircuitryGraphWithStandard(input.graph, input.standard);
918
939
  const filename = input.filename || defaultFilename;
919
940
  const parsed = await this.parseAndResolveGraph(input.text, filename, input.standard);
920
- if ("error" in parsed) {
941
+ if (!("graph" in parsed)) {
921
942
  return {
922
943
  ok: false,
923
- errors: [parsed.error],
944
+ errors: parsed.error ? [parsed.error] : [],
924
945
  standard: validateCircuitryGraphWithStandard({}, input.standard).standard
925
946
  };
926
947
  }
@@ -933,11 +954,12 @@ ${validation.errors.map((e) => e.message).join("\n")}`
933
954
  input.filename || graphFile,
934
955
  input.standard
935
956
  );
936
- if (!parsed.graph)
957
+ if (!("graph" in parsed))
937
958
  throw new Error(`Invalid Circuitry graph:
938
959
  ${parsed.error?.message}`);
960
+ const graph = parsed.graph;
939
961
  const validation = await this.validateGraph({
940
- graph: parsed.graph,
962
+ graph,
941
963
  standard: input.standard
942
964
  });
943
965
  if (validation.errors.length)
@@ -947,12 +969,12 @@ ${validation.errors.map((e) => e.message).join("\n")}`
947
969
  );
948
970
  await writeFile(
949
971
  graphFile,
950
- stringifyCircuitryText(parsed.graph, graphFile),
972
+ stringifyCircuitryText(graph, graphFile),
951
973
  "utf8"
952
974
  );
953
975
  return {
954
976
  filename: graphFile,
955
- graph: parsed.graph,
977
+ graph,
956
978
  mode: "replace"
957
979
  };
958
980
  }
@@ -994,5 +1016,6 @@ ${validation.errors.map((e) => e.message).join("\n")}`
994
1016
  };
995
1017
  export {
996
1018
  NodeCircuitryHost,
1019
+ loadCircuitryGraphFile,
997
1020
  runNodeWithPiSDK
998
1021
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhorseprojects/circuitry",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,6 +14,9 @@
14
14
  "import": "./dist/node.js"
15
15
  }
16
16
  },
17
+ "bin": {
18
+ "circuitry": "./dist/cli.js"
19
+ },
17
20
  "license": "LGPL-3.0-only",
18
21
  "files": [
19
22
  "dist",