agentopology 0.1.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.
Files changed (174) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +450 -0
  3. package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
  4. package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
  5. package/dist/analyzer/__tests__/analyzer.test.js +258 -0
  6. package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
  7. package/dist/analyzer/index.d.ts +44 -0
  8. package/dist/analyzer/index.d.ts.map +1 -0
  9. package/dist/analyzer/index.js +308 -0
  10. package/dist/analyzer/index.js.map +1 -0
  11. package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
  12. package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
  13. package/dist/bindings/__tests__/bindings.test.js +2089 -0
  14. package/dist/bindings/__tests__/bindings.test.js.map +1 -0
  15. package/dist/bindings/claude-code.d.ts +12 -0
  16. package/dist/bindings/claude-code.d.ts.map +1 -0
  17. package/dist/bindings/claude-code.js +2063 -0
  18. package/dist/bindings/claude-code.js.map +1 -0
  19. package/dist/bindings/codex.d.ts +18 -0
  20. package/dist/bindings/codex.d.ts.map +1 -0
  21. package/dist/bindings/codex.js +1633 -0
  22. package/dist/bindings/codex.js.map +1 -0
  23. package/dist/bindings/copilot-cli.d.ts +20 -0
  24. package/dist/bindings/copilot-cli.d.ts.map +1 -0
  25. package/dist/bindings/copilot-cli.js +1515 -0
  26. package/dist/bindings/copilot-cli.js.map +1 -0
  27. package/dist/bindings/cursor.d.ts +17 -0
  28. package/dist/bindings/cursor.d.ts.map +1 -0
  29. package/dist/bindings/cursor.js +1715 -0
  30. package/dist/bindings/cursor.js.map +1 -0
  31. package/dist/bindings/gemini-cli.d.ts +18 -0
  32. package/dist/bindings/gemini-cli.d.ts.map +1 -0
  33. package/dist/bindings/gemini-cli.js +1327 -0
  34. package/dist/bindings/gemini-cli.js.map +1 -0
  35. package/dist/bindings/index.d.ts +20 -0
  36. package/dist/bindings/index.d.ts.map +1 -0
  37. package/dist/bindings/index.js +33 -0
  38. package/dist/bindings/index.js.map +1 -0
  39. package/dist/bindings/kiro.d.ts +17 -0
  40. package/dist/bindings/kiro.d.ts.map +1 -0
  41. package/dist/bindings/kiro.js +1624 -0
  42. package/dist/bindings/kiro.js.map +1 -0
  43. package/dist/bindings/openclaw.d.ts +18 -0
  44. package/dist/bindings/openclaw.d.ts.map +1 -0
  45. package/dist/bindings/openclaw.js +1578 -0
  46. package/dist/bindings/openclaw.js.map +1 -0
  47. package/dist/bindings/types.d.ts +33 -0
  48. package/dist/bindings/types.d.ts.map +1 -0
  49. package/dist/bindings/types.js +23 -0
  50. package/dist/bindings/types.js.map +1 -0
  51. package/dist/cli/index.d.ts +19 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +679 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/docs/__tests__/docs.test.d.ts +2 -0
  56. package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
  57. package/dist/docs/__tests__/docs.test.js +438 -0
  58. package/dist/docs/__tests__/docs.test.js.map +1 -0
  59. package/dist/docs/content.d.ts +16 -0
  60. package/dist/docs/content.d.ts.map +1 -0
  61. package/dist/docs/content.js +3273 -0
  62. package/dist/docs/content.js.map +1 -0
  63. package/dist/docs/index.d.ts +17 -0
  64. package/dist/docs/index.d.ts.map +1 -0
  65. package/dist/docs/index.js +117 -0
  66. package/dist/docs/index.js.map +1 -0
  67. package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
  68. package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
  69. package/dist/exporters/__tests__/exporters.test.js +731 -0
  70. package/dist/exporters/__tests__/exporters.test.js.map +1 -0
  71. package/dist/exporters/index.d.ts +13 -0
  72. package/dist/exporters/index.d.ts.map +1 -0
  73. package/dist/exporters/index.js +18 -0
  74. package/dist/exporters/index.js.map +1 -0
  75. package/dist/exporters/json.d.ts +8 -0
  76. package/dist/exporters/json.d.ts.map +1 -0
  77. package/dist/exporters/json.js +25 -0
  78. package/dist/exporters/json.js.map +1 -0
  79. package/dist/exporters/markdown.d.ts +9 -0
  80. package/dist/exporters/markdown.d.ts.map +1 -0
  81. package/dist/exporters/markdown.js +686 -0
  82. package/dist/exporters/markdown.js.map +1 -0
  83. package/dist/exporters/mermaid.d.ts +11 -0
  84. package/dist/exporters/mermaid.d.ts.map +1 -0
  85. package/dist/exporters/mermaid.js +161 -0
  86. package/dist/exporters/mermaid.js.map +1 -0
  87. package/dist/exporters/types.d.ts +22 -0
  88. package/dist/exporters/types.d.ts.map +1 -0
  89. package/dist/exporters/types.js +10 -0
  90. package/dist/exporters/types.js.map +1 -0
  91. package/dist/import/__tests__/import.test.d.ts +2 -0
  92. package/dist/import/__tests__/import.test.d.ts.map +1 -0
  93. package/dist/import/__tests__/import.test.js +781 -0
  94. package/dist/import/__tests__/import.test.js.map +1 -0
  95. package/dist/import/claude-code.d.ts +38 -0
  96. package/dist/import/claude-code.d.ts.map +1 -0
  97. package/dist/import/claude-code.js +897 -0
  98. package/dist/import/claude-code.js.map +1 -0
  99. package/dist/import/index.d.ts +18 -0
  100. package/dist/import/index.d.ts.map +1 -0
  101. package/dist/import/index.js +28 -0
  102. package/dist/import/index.js.map +1 -0
  103. package/dist/import/serializer.d.ts +11 -0
  104. package/dist/import/serializer.d.ts.map +1 -0
  105. package/dist/import/serializer.js +960 -0
  106. package/dist/import/serializer.js.map +1 -0
  107. package/dist/index.d.ts +26 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +21 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/parser/__tests__/parser.test.d.ts +2 -0
  112. package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
  113. package/dist/parser/__tests__/parser.test.js +6514 -0
  114. package/dist/parser/__tests__/parser.test.js.map +1 -0
  115. package/dist/parser/ast.d.ts +708 -0
  116. package/dist/parser/ast.d.ts.map +1 -0
  117. package/dist/parser/ast.js +12 -0
  118. package/dist/parser/ast.js.map +1 -0
  119. package/dist/parser/index.d.ts +253 -0
  120. package/dist/parser/index.d.ts.map +1 -0
  121. package/dist/parser/index.js +1985 -0
  122. package/dist/parser/index.js.map +1 -0
  123. package/dist/parser/lexer.d.ts +100 -0
  124. package/dist/parser/lexer.d.ts.map +1 -0
  125. package/dist/parser/lexer.js +304 -0
  126. package/dist/parser/lexer.js.map +1 -0
  127. package/dist/parser/validator.d.ts +42 -0
  128. package/dist/parser/validator.d.ts.map +1 -0
  129. package/dist/parser/validator.js +2568 -0
  130. package/dist/parser/validator.js.map +1 -0
  131. package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
  132. package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
  133. package/dist/scaffold/__tests__/scaffold.test.js +295 -0
  134. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
  135. package/dist/scaffold/incremental.d.ts +43 -0
  136. package/dist/scaffold/incremental.d.ts.map +1 -0
  137. package/dist/scaffold/incremental.js +185 -0
  138. package/dist/scaffold/incremental.js.map +1 -0
  139. package/dist/scaffold/index.d.ts +13 -0
  140. package/dist/scaffold/index.d.ts.map +1 -0
  141. package/dist/scaffold/index.js +13 -0
  142. package/dist/scaffold/index.js.map +1 -0
  143. package/dist/scaffold/manifest.d.ts +30 -0
  144. package/dist/scaffold/manifest.d.ts.map +1 -0
  145. package/dist/scaffold/manifest.js +71 -0
  146. package/dist/scaffold/manifest.js.map +1 -0
  147. package/dist/scaffold/merge.d.ts +52 -0
  148. package/dist/scaffold/merge.d.ts.map +1 -0
  149. package/dist/scaffold/merge.js +134 -0
  150. package/dist/scaffold/merge.js.map +1 -0
  151. package/dist/scaffold/types.d.ts +51 -0
  152. package/dist/scaffold/types.d.ts.map +1 -0
  153. package/dist/scaffold/types.js +10 -0
  154. package/dist/scaffold/types.js.map +1 -0
  155. package/dist/sync/index.d.ts +34 -0
  156. package/dist/sync/index.d.ts.map +1 -0
  157. package/dist/sync/index.js +392 -0
  158. package/dist/sync/index.js.map +1 -0
  159. package/dist/visualizer/index.d.ts +17 -0
  160. package/dist/visualizer/index.d.ts.map +1 -0
  161. package/dist/visualizer/index.js +1976 -0
  162. package/dist/visualizer/index.js.map +1 -0
  163. package/examples/code-review.at +167 -0
  164. package/examples/data-processing.at +296 -0
  165. package/examples/openclaw-assistant.at +142 -0
  166. package/examples/scheduled-monitor.at +193 -0
  167. package/examples/simple-pipeline.at +163 -0
  168. package/package.json +77 -0
  169. package/skill/SKILL.md +403 -0
  170. package/skill/scripts/emit.ts +853 -0
  171. package/skill/scripts/visualize.ts +89 -0
  172. package/spec/grammar.md +1466 -0
  173. package/spec/reserved-keywords.md +147 -0
  174. package/spec/validation.md +535 -0
@@ -0,0 +1,853 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * emit.ts — Generate valid AgenTopology (.at) files from structured JSON input.
4
+ *
5
+ * Usage:
6
+ * echo '{"topology":...}' | npx tsx emit.ts # stdin -> stdout
7
+ * npx tsx emit.ts input.json # file -> stdout
8
+ * npx tsx emit.ts input.json output.at # file -> file
9
+ */
10
+
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ interface TopologyInput {
19
+ topology: {
20
+ name: string;
21
+ version: string;
22
+ description: string;
23
+ patterns: string[];
24
+ foundations?: string[];
25
+ advanced?: string[];
26
+ };
27
+ nodes: Array<NodeInput>;
28
+ edges: Array<EdgeInput>;
29
+ orchestrator?: {
30
+ model: string;
31
+ generates?: string;
32
+ handles: string[];
33
+ outputs?: Record<string, string[]>;
34
+ };
35
+ roles?: Record<string, string>;
36
+ memory?: Record<string, Record<string, any>>;
37
+ triggers?: Array<{ name: string; pattern: string; argument?: string }>;
38
+ hooks?: Array<{ name: string; on: string; matcher?: string; run: string; type?: string }>;
39
+ settings?: { allow?: string[]; deny?: string[]; ask?: string[] };
40
+ mcpServers?: Record<string, { type: string; url?: string; command?: string; args?: string[]; env?: Record<string, string> }>;
41
+ providers?: Array<{ name: string; apiKey?: string; baseUrl?: string; models: string[]; default?: boolean; extra?: Record<string, unknown> }>;
42
+ }
43
+
44
+ interface NodeInput {
45
+ id: string;
46
+ type: "agent" | "action" | "gate" | "orchestrator";
47
+ label?: string;
48
+ model?: string;
49
+ permissions?: string;
50
+ prompt?: string;
51
+ phase?: number;
52
+ tools?: string[];
53
+ disallowedTools?: string[];
54
+ reads?: string[];
55
+ writes?: string[];
56
+ outputs?: Record<string, string[]>;
57
+ skip?: string;
58
+ retry?: number;
59
+ isolation?: string;
60
+ invocation?: string;
61
+ behavior?: string;
62
+ role?: string;
63
+ memory?: string;
64
+ skills?: string[];
65
+ mcpServers?: string[];
66
+ background?: boolean;
67
+ hooks?: Array<{ name: string; on: string; matcher?: string; run: string; type?: string; timeout?: number }>;
68
+ // action fields
69
+ kind?: string;
70
+ source?: string;
71
+ commands?: string[];
72
+ description?: string;
73
+ // gate fields
74
+ after?: string;
75
+ before?: string;
76
+ run?: string;
77
+ checks?: string[];
78
+ onFail?: string;
79
+ }
80
+
81
+ interface EdgeInput {
82
+ from: string;
83
+ to: string | string[];
84
+ condition?: string | null;
85
+ maxIterations?: number | null;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Formatting helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const I = " "; // 2-space indent
93
+
94
+ /** Wrap a value in quotes. */
95
+ function quoteStr(s: string): string {
96
+ return `"${s}"`;
97
+ }
98
+
99
+ /** Check if a value looks like an identifier (lowercase, hyphens, starts with letter). */
100
+ function isIdentifier(s: string): boolean {
101
+ return /^[a-z][a-z0-9-]*$/.test(s);
102
+ }
103
+
104
+ /** Check if a value looks like a template-var (uppercase). */
105
+ function isTemplateVar(s: string): boolean {
106
+ return /^[A-Z][A-Z0-9_-]*$/.test(s);
107
+ }
108
+
109
+ /** Format a value — identifiers and booleans/numbers unquoted, strings quoted. */
110
+ function fmtValue(v: string | number | boolean): string {
111
+ if (typeof v === "number") return String(v);
112
+ if (typeof v === "boolean") return String(v);
113
+ if (v === "true" || v === "false") return v;
114
+ if (isIdentifier(v)) return v;
115
+ if (isTemplateVar(v)) return v;
116
+ // Numbers as strings
117
+ if (/^[0-9]+(\.[0-9]+)?$/.test(v)) return v;
118
+ return quoteStr(v);
119
+ }
120
+
121
+ /**
122
+ * Format a list — single line if total width <= maxWidth, multi-line otherwise.
123
+ * maxWidth defaults to 90 (comfortable .at line width).
124
+ */
125
+ function fmtList(
126
+ items: string[],
127
+ indent: string,
128
+ maxWidth: number = 90,
129
+ formatter: (s: string) => string = fmtValue as (s: string) => string,
130
+ ): string {
131
+ if (items.length === 0) return "[]";
132
+ const formatted = items.map(formatter);
133
+ const singleLine = `[${formatted.join(", ")}]`;
134
+ if (singleLine.length <= maxWidth) {
135
+ return singleLine;
136
+ }
137
+ const inner = formatted.map((f) => `${indent}${I}${f}`).join(",\n");
138
+ return `[\n${inner}\n${indent}]`;
139
+ }
140
+
141
+ /** Format a string list (always quoted). */
142
+ function fmtStringList(items: string[], indent: string, maxWidth: number = 90): string {
143
+ return fmtList(items, indent, maxWidth, quoteStr);
144
+ }
145
+
146
+ /** Format a tool item — core tools unquoted, mcp.* unquoted, Bash("x") unquoted. */
147
+ function fmtToolItem(t: string): string {
148
+ // Already formatted like Read, Write, Bash("pattern"), mcp.gitnexus.*
149
+ return t;
150
+ }
151
+
152
+ /**
153
+ * Categorize and sort tools: core tools first, then Bash patterns, then mcp.* grouped by server.
154
+ */
155
+ function sortTools(tools: string[]): string[] {
156
+ const coreOrder = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "Agent"];
157
+ const core: string[] = [];
158
+ const bashPatterns: string[] = [];
159
+ const mcp: string[] = [];
160
+
161
+ for (const t of tools) {
162
+ if (t.startsWith("mcp.")) {
163
+ mcp.push(t);
164
+ } else if (t.startsWith("Bash(")) {
165
+ bashPatterns.push(t);
166
+ } else {
167
+ core.push(t);
168
+ }
169
+ }
170
+
171
+ // Sort core by canonical order
172
+ core.sort((a, b) => {
173
+ const ai = coreOrder.indexOf(a);
174
+ const bi = coreOrder.indexOf(b);
175
+ if (ai === -1 && bi === -1) return a.localeCompare(b);
176
+ if (ai === -1) return 1;
177
+ if (bi === -1) return -1;
178
+ return ai - bi;
179
+ });
180
+
181
+ // Sort MCP by server name, then tool name
182
+ mcp.sort((a, b) => a.localeCompare(b));
183
+
184
+ return [...core, ...bashPatterns, ...mcp];
185
+ }
186
+
187
+ /**
188
+ * Format tool list with smart grouping — groups tools by category on the same line.
189
+ * Core tools on one line, then each MCP server's tools on one line.
190
+ */
191
+ function fmtToolList(tools: string[], indent: string): string {
192
+ if (tools.length === 0) return "[]";
193
+ const sorted = sortTools(tools);
194
+ // If 4 or fewer, single line
195
+ if (sorted.length <= 4) {
196
+ return `[${sorted.map(fmtToolItem).join(", ")}]`;
197
+ }
198
+
199
+ // Group: core tools, bash patterns, then MCP tools grouped by server
200
+ const core: string[] = [];
201
+ const bashPatterns: string[] = [];
202
+ const mcpGroups = new Map<string, string[]>();
203
+
204
+ for (const t of sorted) {
205
+ if (t.startsWith("mcp.")) {
206
+ // Group by server: mcp.<server>.* or mcp.<server>.<tool>
207
+ const parts = t.split(".");
208
+ const server = parts[1];
209
+ const group = mcpGroups.get(server) || [];
210
+ group.push(t);
211
+ mcpGroups.set(server, group);
212
+ } else if (t.startsWith("Bash(")) {
213
+ bashPatterns.push(t);
214
+ } else {
215
+ core.push(t);
216
+ }
217
+ }
218
+
219
+ // Build rows: core on one line, each MCP server on one line
220
+ const rows: string[] = [];
221
+ if (core.length > 0) {
222
+ const coreLine = core.join(", ");
223
+ if (bashPatterns.length > 0) {
224
+ rows.push(coreLine + ",");
225
+ rows.push(bashPatterns.join(", "));
226
+ } else {
227
+ rows.push(coreLine);
228
+ }
229
+ } else if (bashPatterns.length > 0) {
230
+ rows.push(bashPatterns.join(", "));
231
+ }
232
+
233
+ const serverNames = Array.from(mcpGroups.keys()).sort();
234
+ for (const server of serverNames) {
235
+ const serverTools = mcpGroups.get(server)!;
236
+ rows.push(serverTools.join(", "));
237
+ }
238
+
239
+ // Check if the whole thing fits on one line
240
+ const allItems = sorted.map(fmtToolItem).join(", ");
241
+ const singleLine = `[${allItems}]`;
242
+ if (singleLine.length <= 90) {
243
+ return singleLine;
244
+ }
245
+
246
+ // Multi-line with grouped rows
247
+ const lines: string[] = [];
248
+ lines.push("[");
249
+ for (let i = 0; i < rows.length; i++) {
250
+ const isLast = i === rows.length - 1;
251
+ const row = rows[i];
252
+ // If row doesn't end with comma and isn't last, add comma
253
+ if (!isLast && !row.endsWith(",")) {
254
+ lines.push(`${indent}${I}${row},`);
255
+ } else {
256
+ lines.push(`${indent}${I}${row}`);
257
+ }
258
+ }
259
+ lines.push(`${indent}]`);
260
+ return lines.join("\n");
261
+ }
262
+
263
+ /** Format path-list — items are quoted except `ticket` keyword. */
264
+ function fmtPathList(items: string[], indent: string): string {
265
+ if (items.length === 0) return "[]";
266
+ const formatted = items.map((item) => (item === "ticket" ? "ticket" : quoteStr(item)));
267
+ const singleLine = `[${formatted.join(", ")}]`;
268
+ if (singleLine.length <= 90) {
269
+ return singleLine;
270
+ }
271
+ const inner = formatted.map((f) => `${indent}${I}${f}`).join(",\n");
272
+ return `[\n${inner}\n${indent}]`;
273
+ }
274
+
275
+ /** Format typed-map outputs block. */
276
+ function fmtOutputs(outputs: Record<string, string[]>, indent: string): string {
277
+ const entries = Object.entries(outputs);
278
+ if (entries.length === 0) return "{}";
279
+ const lines: string[] = [];
280
+ lines.push("{");
281
+ for (const [key, values] of entries) {
282
+ lines.push(`${indent}${I}${key}: ${values.join(" | ")}`);
283
+ }
284
+ lines.push(`${indent}}`);
285
+ return lines.join("\n");
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Section emitters
290
+ // ---------------------------------------------------------------------------
291
+
292
+ function emitMeta(input: TopologyInput): string[] {
293
+ const lines: string[] = [];
294
+ lines.push(`${I}meta {`);
295
+ lines.push(`${I}${I}version: ${quoteStr(input.topology.version)}`);
296
+ lines.push(`${I}${I}description: ${quoteStr(input.topology.description)}`);
297
+ if (input.topology.foundations && input.topology.foundations.length > 0) {
298
+ lines.push(`${I}${I}foundations: ${fmtList(input.topology.foundations, `${I}${I}`)}`);
299
+ }
300
+ if (input.topology.advanced && input.topology.advanced.length > 0) {
301
+ lines.push(`${I}${I}advanced: ${fmtList(input.topology.advanced, `${I}${I}`)}`);
302
+ }
303
+ lines.push(`${I}}`);
304
+ return lines;
305
+ }
306
+
307
+ function emitOrchestrator(input: TopologyInput): string[] {
308
+ if (!input.orchestrator) return [];
309
+ const o = input.orchestrator;
310
+ const lines: string[] = [];
311
+ lines.push(`${I}orchestrator {`);
312
+ lines.push(`${I}${I}model: ${o.model}`);
313
+ if (o.generates) {
314
+ lines.push(`${I}${I}generates: ${quoteStr(o.generates)}`);
315
+ }
316
+ lines.push(`${I}${I}handles: ${fmtList(o.handles, `${I}${I}`)}`);
317
+ if (o.outputs && Object.keys(o.outputs).length > 0) {
318
+ lines.push(`${I}${I}outputs: ${fmtOutputs(o.outputs, `${I}${I}`)}`);
319
+ }
320
+ lines.push(`${I}}`);
321
+ return lines;
322
+ }
323
+
324
+ function emitRoles(input: TopologyInput): string[] {
325
+ if (!input.roles || Object.keys(input.roles).length === 0) return [];
326
+ const lines: string[] = [];
327
+ lines.push(`${I}roles {`);
328
+ for (const [name, desc] of Object.entries(input.roles)) {
329
+ lines.push(`${I}${I}${name}: ${quoteStr(desc)}`);
330
+ }
331
+ lines.push(`${I}}`);
332
+ return lines;
333
+ }
334
+
335
+ function emitActions(nodes: NodeInput[]): string[] {
336
+ const actions = nodes.filter((n) => n.type === "action");
337
+ if (actions.length === 0) return [];
338
+ const lines: string[] = [];
339
+ lines.push(`${I}# --- Actions (orchestrator-handled steps) ---`);
340
+ for (const action of actions) {
341
+ lines.push("");
342
+ lines.push(`${I}action ${action.id} {`);
343
+ if (action.kind) {
344
+ lines.push(`${I}${I}kind: ${action.kind}`);
345
+ }
346
+ if (action.source) {
347
+ lines.push(`${I}${I}source: ${quoteStr(action.source)}`);
348
+ }
349
+ if (action.commands && action.commands.length > 0) {
350
+ lines.push(`${I}${I}commands: ${fmtList(action.commands, `${I}${I}`)}`);
351
+ }
352
+ if (action.description) {
353
+ lines.push(`${I}${I}description: ${quoteStr(action.description)}`);
354
+ }
355
+ lines.push(`${I}}`);
356
+ }
357
+ return lines;
358
+ }
359
+
360
+ function emitAgent(node: NodeInput, indent: string): string[] {
361
+ const lines: string[] = [];
362
+ if (node.model) lines.push(`${indent}model: ${node.model}`);
363
+ if (node.permissions && node.permissions !== "auto") {
364
+ lines.push(`${indent}permissions: ${node.permissions}`);
365
+ }
366
+ if (node.prompt) {
367
+ lines.push(`${indent}prompt {`);
368
+ // Indent each line of the prompt content
369
+ for (const promptLine of node.prompt.split("\n")) {
370
+ if (promptLine.trim()) {
371
+ lines.push(`${indent}${I}${promptLine}`);
372
+ } else {
373
+ lines.push("");
374
+ }
375
+ }
376
+ lines.push(`${indent}}`);
377
+ }
378
+ if (node.phase !== undefined && node.phase !== null) {
379
+ lines.push(`${indent}phase: ${node.phase}`);
380
+ }
381
+ if (node.disallowedTools && node.disallowedTools.length > 0) {
382
+ lines.push(`${indent}disallowed-tools: ${fmtToolList(node.disallowedTools, indent)}`);
383
+ }
384
+ if (node.tools && node.tools.length > 0) {
385
+ lines.push(`${indent}tools: ${fmtToolList(node.tools, indent)}`);
386
+ }
387
+ if (node.reads && node.reads.length > 0) {
388
+ lines.push(`${indent}reads: ${fmtPathList(node.reads, indent)}`);
389
+ }
390
+ if (node.writes && node.writes.length > 0) {
391
+ lines.push(`${indent}writes: ${fmtPathList(node.writes, indent)}`);
392
+ }
393
+ if (node.outputs && Object.keys(node.outputs).length > 0) {
394
+ lines.push(`${indent}outputs: ${fmtOutputs(node.outputs, indent)}`);
395
+ }
396
+ if (node.skip) lines.push(`${indent}skip: ${node.skip}`);
397
+ if (node.retry && node.retry > 0) lines.push(`${indent}retry: ${node.retry}`);
398
+ if (node.isolation) lines.push(`${indent}isolation: ${node.isolation}`);
399
+ if (node.behavior && node.behavior !== "blocking") {
400
+ lines.push(`${indent}behavior: ${node.behavior}`);
401
+ }
402
+ if (node.invocation && node.invocation !== "auto") {
403
+ lines.push(`${indent}invocation: ${node.invocation}`);
404
+ }
405
+ if (node.role) lines.push(`${indent}role: ${node.role}`);
406
+ if (node.memory) lines.push(`${indent}memory: ${node.memory}`);
407
+ if (node.skills && node.skills.length > 0) {
408
+ lines.push(`${indent}skills: ${fmtList(node.skills, indent)}`);
409
+ }
410
+ if (node.mcpServers && node.mcpServers.length > 0) {
411
+ lines.push(`${indent}mcp-servers: ${fmtList(node.mcpServers, indent)}`);
412
+ }
413
+ if (node.background === true) lines.push(`${indent}background: true`);
414
+ // Per-agent hooks
415
+ if (node.hooks && node.hooks.length > 0) {
416
+ lines.push(`${indent}hooks {`);
417
+ for (let i = 0; i < node.hooks.length; i++) {
418
+ const h = node.hooks[i];
419
+ if (i > 0) lines.push("");
420
+ lines.push(`${indent}${I}hook ${h.name} {`);
421
+ lines.push(`${indent}${I}${I}on: ${h.on}`);
422
+ if (h.matcher) lines.push(`${indent}${I}${I}matcher: ${quoteStr(h.matcher)}`);
423
+ lines.push(`${indent}${I}${I}run: ${quoteStr(h.run)}`);
424
+ if (h.type) lines.push(`${indent}${I}${I}type: ${h.type}`);
425
+ if (h.timeout && h.timeout !== 600) lines.push(`${indent}${I}${I}timeout: ${h.timeout}`);
426
+ lines.push(`${indent}${I}}`);
427
+ }
428
+ lines.push(`${indent}}`);
429
+ }
430
+ return lines;
431
+ }
432
+
433
+ function emitAgents(nodes: NodeInput[]): string[] {
434
+ const agents = nodes.filter((n) => n.type === "agent");
435
+ if (agents.length === 0) return [];
436
+ const lines: string[] = [];
437
+ lines.push(`${I}# --- Agents ---`);
438
+
439
+ // Sort agents by phase (undefined phase goes last), then by id
440
+ const sorted = [...agents].sort((a, b) => {
441
+ const ap = a.phase ?? 999;
442
+ const bp = b.phase ?? 999;
443
+ if (ap !== bp) return ap - bp;
444
+ return a.id.localeCompare(b.id);
445
+ });
446
+
447
+ for (const agent of sorted) {
448
+ lines.push("");
449
+ lines.push(`${I}agent ${agent.id} {`);
450
+ lines.push(...emitAgent(agent, `${I}${I}`));
451
+ lines.push(`${I}}`);
452
+ }
453
+ return lines;
454
+ }
455
+
456
+ function emitMemory(memory: Record<string, Record<string, any>> | undefined): string[] {
457
+ if (!memory || Object.keys(memory).length === 0) return [];
458
+ const lines: string[] = [];
459
+ lines.push(`${I}# --- Memory (Blackboard) ---`);
460
+ lines.push("");
461
+ lines.push(`${I}memory {`);
462
+
463
+ const blockNames = Object.keys(memory);
464
+ for (let i = 0; i < blockNames.length; i++) {
465
+ const name = blockNames[i];
466
+ const block = memory[name];
467
+ if (i > 0) lines.push("");
468
+ lines.push(`${I}${I}${name} {`);
469
+ for (const [key, value] of Object.entries(block)) {
470
+ if (Array.isArray(value)) {
471
+ // Check if items look like identifiers or strings
472
+ const allIdentifiers = value.every((v: any) => typeof v === "string" && isIdentifier(v));
473
+ if (allIdentifiers) {
474
+ lines.push(`${I}${I}${I}${key}: ${fmtList(value, `${I}${I}${I}`)}`);
475
+ } else {
476
+ lines.push(`${I}${I}${I}${key}: ${fmtStringList(value, `${I}${I}${I}`)}`);
477
+ }
478
+ } else if (typeof value === "string") {
479
+ if (isIdentifier(value)) {
480
+ lines.push(`${I}${I}${I}${key}: ${value}`);
481
+ } else {
482
+ lines.push(`${I}${I}${I}${key}: ${quoteStr(value)}`);
483
+ }
484
+ } else {
485
+ lines.push(`${I}${I}${I}${key}: ${fmtValue(value)}`);
486
+ }
487
+ }
488
+ lines.push(`${I}${I}}`);
489
+ }
490
+ lines.push(`${I}}`);
491
+ return lines;
492
+ }
493
+
494
+ function emitFlow(edges: EdgeInput[]): string[] {
495
+ if (edges.length === 0) return [];
496
+ const lines: string[] = [];
497
+ lines.push(`${I}# --- Flow ---`);
498
+ lines.push("");
499
+ lines.push(`${I}flow {`);
500
+
501
+ // Build chain-merged flow statements.
502
+ // Detect sequences of unconditional edges (a->b, b->c) and merge into a->b->c.
503
+ // Only merge when both edges are unconditional, single-target, and the intermediate
504
+ // node has exactly one unconditional incoming and one unconditional outgoing edge.
505
+ type FlowStmt = { chain: string[]; condition?: string; maxIterations?: number };
506
+ const stmts: FlowStmt[] = [];
507
+
508
+ // Index: for each node, which unconditional edges go out?
509
+ const unconditionalOutgoing = new Map<string, EdgeInput[]>();
510
+ const unconditionalIncoming = new Map<string, EdgeInput[]>();
511
+ for (const e of edges) {
512
+ if (!e.condition && !Array.isArray(e.to)) {
513
+ const list = unconditionalOutgoing.get(e.from) || [];
514
+ list.push(e);
515
+ unconditionalOutgoing.set(e.from, list);
516
+ const inList = unconditionalIncoming.get(e.to as string) || [];
517
+ inList.push(e);
518
+ unconditionalIncoming.set(e.to as string, inList);
519
+ }
520
+ }
521
+
522
+ // Track which edges have been consumed by chaining
523
+ const consumed = new Set<number>();
524
+
525
+ for (let i = 0; i < edges.length; i++) {
526
+ if (consumed.has(i)) continue;
527
+ const edge = edges[i];
528
+
529
+ // Try to build a chain starting from this edge (only for unconditional single-target edges)
530
+ if (!edge.condition && !edge.maxIterations && !Array.isArray(edge.to)) {
531
+ const chain = [edge.from, edge.to as string];
532
+ consumed.add(i);
533
+
534
+ // Extend chain: look for the next edge starting from chain's tail
535
+ let extended = true;
536
+ while (extended) {
537
+ extended = false;
538
+ const tail = chain[chain.length - 1];
539
+ // Only chain if tail has exactly one unconditional outgoing and one unconditional incoming
540
+ const outEdges = unconditionalOutgoing.get(tail) || [];
541
+ const inEdges = unconditionalIncoming.get(tail) || [];
542
+ if (outEdges.length === 1 && inEdges.length === 1) {
543
+ const nextEdge = outEdges[0];
544
+ const nextIdx = edges.indexOf(nextEdge);
545
+ if (nextIdx >= 0 && !consumed.has(nextIdx) && !nextEdge.condition && !nextEdge.maxIterations && !Array.isArray(nextEdge.to)) {
546
+ chain.push(nextEdge.to as string);
547
+ consumed.add(nextIdx);
548
+ extended = true;
549
+ }
550
+ }
551
+ }
552
+
553
+ stmts.push({ chain });
554
+ } else {
555
+ // Conditional or fan-out edge — emit directly
556
+ consumed.add(i);
557
+ const to = Array.isArray(edge.to) ? edge.to : [edge.to];
558
+ stmts.push({
559
+ chain: [edge.from, ...(Array.isArray(edge.to) ? [] : to)],
560
+ condition: edge.condition || undefined,
561
+ maxIterations: edge.maxIterations || undefined,
562
+ // For fan-out, store the array
563
+ ...(Array.isArray(edge.to) ? { _fanOut: edge.to } : {}),
564
+ });
565
+ }
566
+ }
567
+
568
+ // Format flow lines
569
+ const flowLines: Array<{ text: string; attrs: string }> = [];
570
+ for (const stmt of stmts) {
571
+ const s = stmt as any;
572
+ let base: string;
573
+ if (s._fanOut) {
574
+ base = `${stmt.chain[0]} -> [${s._fanOut.join(", ")}]`;
575
+ } else {
576
+ base = stmt.chain.join(" -> ");
577
+ }
578
+
579
+ const attrs: string[] = [];
580
+ if (stmt.condition) {
581
+ attrs.push(`when ${stmt.condition}`);
582
+ }
583
+ if (stmt.maxIterations) {
584
+ attrs.push(`max ${stmt.maxIterations}`);
585
+ }
586
+
587
+ flowLines.push({ text: base, attrs: attrs.length > 0 ? `[${attrs.join(", ")}]` : "" });
588
+ }
589
+
590
+ // Calculate alignment column for conditions
591
+ const condLines = flowLines.filter((f) => f.attrs);
592
+ const maxBase = condLines.length > 0 ? Math.max(...condLines.map((f) => f.text.length)) : 0;
593
+ const alignCol = maxBase + 2;
594
+
595
+ for (const fl of flowLines) {
596
+ if (fl.attrs) {
597
+ const padded = fl.text.padEnd(alignCol);
598
+ lines.push(`${I}${I}${padded}${fl.attrs}`);
599
+ } else {
600
+ lines.push(`${I}${I}${fl.text}`);
601
+ }
602
+ }
603
+
604
+ lines.push(`${I}}`);
605
+ return lines;
606
+ }
607
+
608
+ function emitGates(nodes: NodeInput[]): string[] {
609
+ const gates = nodes.filter((n) => n.type === "gate");
610
+ if (gates.length === 0) return [];
611
+ const lines: string[] = [];
612
+ lines.push(`${I}# --- Gates ---`);
613
+ lines.push("");
614
+ lines.push(`${I}gates {`);
615
+ for (let i = 0; i < gates.length; i++) {
616
+ const gate = gates[i];
617
+ if (i > 0) lines.push("");
618
+ lines.push(`${I}${I}gate ${gate.id} {`);
619
+ if (gate.after) lines.push(`${I}${I}${I}after: ${gate.after}`);
620
+ if (gate.before) lines.push(`${I}${I}${I}before: ${gate.before}`);
621
+ if (gate.run) lines.push(`${I}${I}${I}run: ${quoteStr(gate.run)}`);
622
+ if (gate.checks && gate.checks.length > 0) {
623
+ lines.push(`${I}${I}${I}checks: ${fmtList(gate.checks, `${I}${I}${I}`)}`);
624
+ }
625
+ if (gate.retry && gate.retry > 0) {
626
+ lines.push(`${I}${I}${I}retry: ${gate.retry}`);
627
+ }
628
+ if (gate.onFail) {
629
+ lines.push(`${I}${I}${I}on-fail: ${gate.onFail}`);
630
+ }
631
+ if (gate.behavior && gate.behavior !== "blocking") {
632
+ lines.push(`${I}${I}${I}behavior: ${gate.behavior}`);
633
+ }
634
+ lines.push(`${I}${I}}`);
635
+ }
636
+ lines.push(`${I}}`);
637
+ return lines;
638
+ }
639
+
640
+ function emitTriggers(triggers: TopologyInput["triggers"]): string[] {
641
+ if (!triggers || triggers.length === 0) return [];
642
+ const lines: string[] = [];
643
+ lines.push(`${I}# --- Triggers ---`);
644
+ lines.push("");
645
+ lines.push(`${I}triggers {`);
646
+ for (let i = 0; i < triggers.length; i++) {
647
+ const t = triggers[i];
648
+ if (i > 0) lines.push("");
649
+ lines.push(`${I}${I}command ${t.name} {`);
650
+ lines.push(`${I}${I}${I}pattern: ${quoteStr(t.pattern)}`);
651
+ if (t.argument) {
652
+ lines.push(`${I}${I}${I}argument: ${t.argument}`);
653
+ }
654
+ lines.push(`${I}${I}}`);
655
+ }
656
+ lines.push(`${I}}`);
657
+ return lines;
658
+ }
659
+
660
+ function emitHooks(hooks: TopologyInput["hooks"]): string[] {
661
+ if (!hooks || hooks.length === 0) return [];
662
+ const lines: string[] = [];
663
+ lines.push(`${I}# --- Hooks ---`);
664
+ lines.push("");
665
+ lines.push(`${I}hooks {`);
666
+ for (let i = 0; i < hooks.length; i++) {
667
+ const h = hooks[i];
668
+ if (i > 0) lines.push("");
669
+ lines.push(`${I}${I}hook ${h.name} {`);
670
+ lines.push(`${I}${I}${I}on: ${h.on}`);
671
+ if (h.matcher) lines.push(`${I}${I}${I}matcher: ${quoteStr(h.matcher)}`);
672
+ lines.push(`${I}${I}${I}run: ${quoteStr(h.run)}`);
673
+ if (h.type) lines.push(`${I}${I}${I}type: ${h.type}`);
674
+ lines.push(`${I}${I}}`);
675
+ }
676
+ lines.push(`${I}}`);
677
+ return lines;
678
+ }
679
+
680
+ function emitSettings(settings: TopologyInput["settings"]): string[] {
681
+ if (!settings) return [];
682
+ const has = (settings.allow && settings.allow.length > 0)
683
+ || (settings.deny && settings.deny.length > 0)
684
+ || (settings.ask && settings.ask.length > 0);
685
+ if (!has) return [];
686
+ const lines: string[] = [];
687
+ lines.push(`${I}# --- Settings ---`);
688
+ lines.push("");
689
+ lines.push(`${I}settings {`);
690
+ if (settings.allow && settings.allow.length > 0) {
691
+ lines.push(`${I}${I}allow: ${fmtStringList(settings.allow, `${I}${I}`)}`);
692
+ }
693
+ if (settings.deny !== undefined) {
694
+ if (settings.deny.length === 0) {
695
+ lines.push(`${I}${I}deny: []`);
696
+ } else {
697
+ lines.push(`${I}${I}deny: ${fmtStringList(settings.deny, `${I}${I}`)}`);
698
+ }
699
+ }
700
+ if (settings.ask !== undefined) {
701
+ if (settings.ask.length === 0) {
702
+ lines.push(`${I}${I}ask: []`);
703
+ } else {
704
+ lines.push(`${I}${I}ask: ${fmtStringList(settings.ask, `${I}${I}`)}`);
705
+ }
706
+ }
707
+ lines.push(`${I}}`);
708
+ return lines;
709
+ }
710
+
711
+ function emitMcpServers(servers: TopologyInput["mcpServers"]): string[] {
712
+ if (!servers || Object.keys(servers).length === 0) return [];
713
+ const lines: string[] = [];
714
+ lines.push(`${I}# --- MCP Servers ---`);
715
+ lines.push("");
716
+ lines.push(`${I}mcp-servers {`);
717
+ const names = Object.keys(servers);
718
+ for (let i = 0; i < names.length; i++) {
719
+ const name = names[i];
720
+ const srv = servers[name];
721
+ if (i > 0) lines.push("");
722
+ lines.push(`${I}${I}${name} {`);
723
+ lines.push(`${I}${I}${I}type: ${srv.type}`);
724
+ if (srv.url) lines.push(`${I}${I}${I}url: ${quoteStr(srv.url)}`);
725
+ if (srv.command) lines.push(`${I}${I}${I}command: ${quoteStr(srv.command)}`);
726
+ if (srv.args && srv.args.length > 0) {
727
+ lines.push(`${I}${I}${I}args: ${fmtStringList(srv.args, `${I}${I}${I}`)}`);
728
+ }
729
+ if (srv.env && Object.keys(srv.env).length > 0) {
730
+ lines.push(`${I}${I}${I}env: {`);
731
+ for (const [k, v] of Object.entries(srv.env)) {
732
+ lines.push(`${I}${I}${I}${I}${k}: ${quoteStr(v)}`);
733
+ }
734
+ lines.push(`${I}${I}${I}}`);
735
+ }
736
+ lines.push(`${I}${I}}`);
737
+ }
738
+ lines.push(`${I}}`);
739
+ return lines;
740
+ }
741
+
742
+ function emitProviders(providers: TopologyInput["providers"]): string[] {
743
+ if (!providers || providers.length === 0) return [];
744
+ const lines: string[] = [];
745
+ lines.push(`${I}# --- Providers ---`);
746
+ lines.push("");
747
+ lines.push(`${I}providers {`);
748
+ for (let i = 0; i < providers.length; i++) {
749
+ const p = providers[i];
750
+ if (i > 0) lines.push("");
751
+ lines.push(`${I}${I}${p.name} {`);
752
+ if (p.apiKey) lines.push(`${I}${I}${I}api-key: ${quoteStr(p.apiKey)}`);
753
+ if (p.baseUrl) lines.push(`${I}${I}${I}base-url: ${quoteStr(p.baseUrl)}`);
754
+ if (p.models && p.models.length > 0) {
755
+ lines.push(`${I}${I}${I}models: ${fmtList(p.models, `${I}${I}${I}`)}`);
756
+ }
757
+ if (p.default === true) lines.push(`${I}${I}${I}default: true`);
758
+ if (p.extra) {
759
+ for (const [key, value] of Object.entries(p.extra)) {
760
+ lines.push(`${I}${I}${I}${key}: ${fmtValue(value as string | number | boolean)}`);
761
+ }
762
+ }
763
+ lines.push(`${I}${I}}`);
764
+ }
765
+ lines.push(`${I}}`);
766
+ return lines;
767
+ }
768
+
769
+ // ---------------------------------------------------------------------------
770
+ // Main emitter
771
+ // ---------------------------------------------------------------------------
772
+
773
+ function emit(input: TopologyInput): string {
774
+ const sections: string[][] = [];
775
+
776
+ // Header comment
777
+ const header = [
778
+ `# ${input.topology.name}.at — ${input.topology.description}`,
779
+ `# AgenTopology v1.0`,
780
+ ];
781
+
782
+ // Topology declaration
783
+ const patterns = input.topology.patterns.join(", ");
784
+ const topOpen = `topology ${input.topology.name} : [${patterns}] {`;
785
+
786
+ // Build sections in canonical order
787
+ sections.push(emitMeta(input));
788
+ sections.push(emitOrchestrator(input));
789
+ sections.push(emitRoles(input));
790
+ sections.push(emitActions(input.nodes));
791
+ sections.push(emitAgents(input.nodes));
792
+ sections.push(emitMemory(input.memory));
793
+ sections.push(emitFlow(input.edges));
794
+ sections.push(emitGates(input.nodes));
795
+ sections.push(emitTriggers(input.triggers));
796
+ sections.push(emitHooks(input.hooks));
797
+ sections.push(emitProviders(input.providers));
798
+ sections.push(emitSettings(input.settings));
799
+ sections.push(emitMcpServers(input.mcpServers));
800
+
801
+ // Assemble
802
+ const parts: string[] = [];
803
+ parts.push(header.join("\n"));
804
+ parts.push("");
805
+ parts.push(topOpen);
806
+
807
+ for (const section of sections) {
808
+ if (section.length === 0) continue;
809
+ parts.push("");
810
+ parts.push(section.join("\n"));
811
+ }
812
+
813
+ parts.push("}");
814
+ parts.push(""); // trailing newline
815
+
816
+ return parts.join("\n");
817
+ }
818
+
819
+ // ---------------------------------------------------------------------------
820
+ // CLI entry point
821
+ // ---------------------------------------------------------------------------
822
+
823
+ async function main(): Promise<void> {
824
+ let jsonStr: string;
825
+
826
+ if (process.argv[2]) {
827
+ // Read from file
828
+ const inputPath = path.resolve(process.argv[2]);
829
+ jsonStr = fs.readFileSync(inputPath, "utf-8");
830
+ } else {
831
+ // Read from stdin
832
+ const chunks: Buffer[] = [];
833
+ for await (const chunk of process.stdin) {
834
+ chunks.push(chunk as Buffer);
835
+ }
836
+ jsonStr = Buffer.concat(chunks).toString("utf-8");
837
+ }
838
+
839
+ const input: TopologyInput = JSON.parse(jsonStr);
840
+ const output = emit(input);
841
+
842
+ if (process.argv[3]) {
843
+ const outputPath = path.resolve(process.argv[3]);
844
+ fs.writeFileSync(outputPath, output, "utf-8");
845
+ } else {
846
+ process.stdout.write(output);
847
+ }
848
+ }
849
+
850
+ main().catch((err) => {
851
+ console.error("emit.ts error:", err.message);
852
+ process.exit(1);
853
+ });