@dot-agent/compiler 0.10.0 → 0.10.2

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/index.js DELETED
@@ -1,1134 +0,0 @@
1
- import {
2
- COMPILER_VERSION,
3
- aboutmeToJson,
4
- buildAboutme,
5
- buildId,
6
- createZip,
7
- extractDigest,
8
- extractFiles,
9
- extractName,
10
- isValidVersion,
11
- parseAboutme,
12
- parseId,
13
- slugify,
14
- validateMagicBytes,
15
- validateZipBomb
16
- } from "./chunk-AYXHOOUX.js";
17
-
18
- // src/parser.ts
19
- import { createRequire } from "module";
20
- import { Parser, Language } from "web-tree-sitter";
21
- import { init as bpInit, parse_behavior as bpParseBehavior, parse_description as bpParseDescription, get_graph } from "@dot-agent/parser-dsl";
22
- var require2 = createRequire(import.meta.url);
23
- var { descriptionWasmPath, behaviorWasmPath } = require2("@dot-agent/tree-sitter");
24
- var _initialized = false;
25
- var _descriptionParser;
26
- var _behaviorParser;
27
- var _bpInitialized = false;
28
- async function initBehaviorParser() {
29
- if (_bpInitialized) return;
30
- await bpInit();
31
- _bpInitialized = true;
32
- }
33
- function parseBehaviorFile(text) {
34
- return JSON.parse(bpParseBehavior(text));
35
- }
36
- function parseDescriptionFile(text) {
37
- return JSON.parse(bpParseDescription(text));
38
- }
39
- function getBehaviorScxml(text) {
40
- return get_graph(text);
41
- }
42
- async function initParsers() {
43
- if (_initialized) return;
44
- await Parser.init();
45
- const descriptionLang = await Language.load(descriptionWasmPath);
46
- const behaviorLang = await Language.load(behaviorWasmPath);
47
- _descriptionParser = new Parser();
48
- _descriptionParser.setLanguage(descriptionLang);
49
- _behaviorParser = new Parser();
50
- _behaviorParser.setLanguage(behaviorLang);
51
- _initialized = true;
52
- }
53
- async function parse(langId, text, previousTree) {
54
- await initParsers();
55
- const parser = langId === "behavior" ? _behaviorParser : _descriptionParser;
56
- return parser.parse(text, previousTree);
57
- }
58
- function parseSync(langId, text, previousTree) {
59
- if (!_initialized) return null;
60
- const parser = langId === "behavior" ? _behaviorParser : _descriptionParser;
61
- return parser.parse(text, previousTree);
62
- }
63
- function nodesOfType(tree, type) {
64
- if (!tree) return [];
65
- return tree.rootNode.descendantsOfType(type).filter((n) => n !== null);
66
- }
67
- function nodeAtOffset(tree, offset) {
68
- if (!tree) return null;
69
- return tree.rootNode.descendantForIndex(offset);
70
- }
71
- function nodeToRange(node) {
72
- return {
73
- start: { line: node.startPosition.row, character: node.startPosition.column },
74
- end: { line: node.endPosition.row, character: node.endPosition.column }
75
- };
76
- }
77
- function positionToOffset(text, line, character) {
78
- let offset = 0;
79
- for (let i = 0; i < line; i++) {
80
- const nl = text.indexOf("\n", offset);
81
- offset = nl === -1 ? text.length : nl + 1;
82
- }
83
- return offset + character;
84
- }
85
- function getContextNode(tree, offset) {
86
- let node = nodeAtOffset(tree, offset);
87
- while (node && (node.isError || node.isMissing)) {
88
- node = node.parent;
89
- }
90
- if (!node || node.type === "ERROR") {
91
- const raw = nodeAtOffset(tree, offset);
92
- const parent = raw?.parent;
93
- if (parent) {
94
- const siblings = parent.children;
95
- const idx = siblings.findIndex((c) => c !== null && c.startIndex > offset);
96
- const prev = idx > 0 ? siblings[idx - 1] : siblings[siblings.length - 1];
97
- if (prev && !prev.isError) node = prev;
98
- }
99
- }
100
- return node ?? tree.rootNode;
101
- }
102
-
103
- // src/linter.ts
104
- import { readFileSync } from "fs";
105
- import { resolve, dirname } from "path";
106
- function nodePosition(node) {
107
- return { line: node.startPosition.row + 1, col: node.startPosition.column + 1 };
108
- }
109
- var MISSING_HINTS = {
110
- interact_stmt: "Missing 'interact' \u2014 oriented states require 'goal \u2026 interact \u2026 on intent \u2026 on offtopic'.",
111
- offtopic_handler: "Missing 'on offtopic' handler \u2014 every state with 'interact' must handle the offtopic case.",
112
- intent_handler: "Missing at least one 'on intent' handler after 'interact'.",
113
- goal_stmt: "Missing 'goal' \u2014 every state with 'interact' must declare a goal.",
114
- state_name: "Missing state name after 'state' keyword.",
115
- quoted_string: "Missing quoted string \u2014 expected a value in double quotes."
116
- };
117
- function collectSyntaxErrors(root, messages, file) {
118
- const seen = /* @__PURE__ */ new Set();
119
- function push(node, message, code, severity = "error") {
120
- const key = `${node.startIndex}:${node.endIndex}:${message}`;
121
- if (seen.has(key)) return;
122
- seen.add(key);
123
- const { line, col } = nodePosition(node);
124
- messages.push({ file, line, col, severity, code, message });
125
- }
126
- function walk(node) {
127
- if (node.isMissing) {
128
- const hint = MISSING_HINTS[node.type];
129
- push(node, hint ?? `Syntax error: missing '${node.type}'.`, "E004");
130
- return;
131
- }
132
- if (node.type === "ERROR") {
133
- const hasNestedError = node.descendantsOfType("ERROR").some((e) => e !== null && e.id !== node.id);
134
- if (!hasNestedError) {
135
- const snippet = node.text.replace(/\s+/g, " ").trim().slice(0, 40);
136
- push(node, snippet ? `Syntax error near '${snippet}'.` : "Syntax error.", "E004");
137
- return;
138
- }
139
- }
140
- for (const child of node.children) {
141
- if (child) walk(child);
142
- }
143
- }
144
- walk(root);
145
- }
146
- var STRICT_BLOCK_TYPES = {
147
- input_block: "input",
148
- output_block: "output",
149
- requires_block: "requires",
150
- capabilities_block: "capabilities"
151
- };
152
- async function lintDescription(text, file = "agent.description") {
153
- const tree = await parse("description", text);
154
- const messages = [];
155
- collectSyntaxErrors(tree.rootNode, messages, file);
156
- for (const metaNode of nodesOfType(tree, "agent_meta")) {
157
- const keyNode = metaNode.childForFieldName("key");
158
- if (keyNode?.text !== "domain") continue;
159
- const valueNode = metaNode.childForFieldName("value");
160
- if (valueNode?.text === "example.com") {
161
- const { line, col } = nodePosition(valueNode);
162
- messages.push({
163
- file,
164
- line,
165
- col,
166
- severity: "warning",
167
- code: "W003",
168
- message: 'domain still has default value "example.com"'
169
- });
170
- }
171
- }
172
- const hasDomain = nodesOfType(tree, "agent_meta").some(
173
- (n) => n.childForFieldName("key")?.text === "domain" && n.childForFieldName("value")?.text?.trim()
174
- );
175
- if (!hasDomain) {
176
- messages.push({
177
- file,
178
- line: 1,
179
- col: 1,
180
- severity: "warning",
181
- code: "W007",
182
- message: "No domain declared \u2014 agent will be packaged as unknown/name. Interoperability between runtimes is not guaranteed."
183
- });
184
- }
185
- const behaviorBlocks = nodesOfType(tree, "behavior_block");
186
- if (behaviorBlocks.length > 1) {
187
- const { line, col } = nodePosition(behaviorBlocks[1]);
188
- messages.push({
189
- file,
190
- line,
191
- col,
192
- severity: "error",
193
- code: "E017",
194
- message: "Multiple 'behavior' declarations \u2014 only one is allowed. To combine multiple files, use 'merge' in your .behavior file."
195
- });
196
- }
197
- const declaredTypes = new Set(
198
- nodesOfType(tree, "type_decl").map((n) => n.childForFieldName("name")?.text).filter(Boolean)
199
- );
200
- if (declaredTypes.size > 0) {
201
- for (const blockType of Object.keys(STRICT_BLOCK_TYPES)) {
202
- for (const blockNode of nodesOfType(tree, blockType)) {
203
- for (const typeRefNode of blockNode.descendantsOfType("type_reference").filter((n) => n !== null)) {
204
- const idNode = typeRefNode.firstNamedChild;
205
- if (!idNode) continue;
206
- const typeName = idNode.text;
207
- if (!declaredTypes.has(typeName)) {
208
- const { line, col } = nodePosition(idNode);
209
- messages.push({
210
- file,
211
- line,
212
- col,
213
- severity: "warning",
214
- code: "W004",
215
- message: `Type '${typeName}' is not declared in this file (assuming native or external).`
216
- });
217
- }
218
- }
219
- }
220
- }
221
- }
222
- return messages;
223
- }
224
- function levenshtein(a, b) {
225
- const la = a.length, lb = b.length;
226
- const dp = Array.from(
227
- { length: la + 1 },
228
- (_, i) => Array.from({ length: lb + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
229
- );
230
- for (let i = 1; i <= la; i++) {
231
- for (let j = 1; j <= lb; j++) {
232
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
233
- }
234
- }
235
- return dp[la][lb];
236
- }
237
- function closestName(token, candidates) {
238
- let best;
239
- let bestDist = Infinity;
240
- for (const name of candidates) {
241
- const d = levenshtein(token.toLowerCase(), name.toLowerCase());
242
- if (d > 0 && d <= 2 && d < bestDist) {
243
- bestDist = d;
244
- best = name;
245
- }
246
- }
247
- return best;
248
- }
249
- var KERNEL_LIFECYCLE_NAMES = ["init", "welcome", "end", "online", "offline"];
250
- function collectTransitionTargets(stmts) {
251
- const targets = [];
252
- for (const stmt of stmts) {
253
- if (!stmt || typeof stmt !== "object") continue;
254
- if (stmt.type === "transition_stmt" && typeof stmt["state"] === "string") {
255
- targets.push(stmt["state"]);
256
- }
257
- if (stmt.type === "intent_handler" && typeof stmt["body"] === "string") {
258
- targets.push(stmt["body"]);
259
- }
260
- const recurse = (v) => {
261
- if (Array.isArray(v)) targets.push(...collectTransitionTargets(v));
262
- };
263
- if (stmt.type !== "intent_handler") recurse(stmt["body"]);
264
- recurse(stmt["handlers"]);
265
- recurse(stmt["then"]);
266
- recurse(stmt["else"]);
267
- recurse(stmt["on_complete"]);
268
- recurse(stmt["on_failure"]);
269
- }
270
- return targets;
271
- }
272
- function collectMergedStates(tree, docDir, states, visited = /* @__PURE__ */ new Set()) {
273
- for (const mergeNode of nodesOfType(tree, "merge_decl")) {
274
- const pathNode = mergeNode.childForFieldName("path");
275
- if (!pathNode) continue;
276
- const filename = pathNode.text.replace(/^"|"$/g, "");
277
- const absPath = resolve(docDir, filename);
278
- if (visited.has(absPath)) continue;
279
- visited.add(absPath);
280
- let mergedText;
281
- try {
282
- mergedText = readFileSync(absPath, "utf-8");
283
- } catch {
284
- continue;
285
- }
286
- const mergedTree = parseSync("behavior", mergedText);
287
- if (!mergedTree) continue;
288
- nodesOfType(mergedTree, "state_decl").forEach((n) => {
289
- const name = n.childForFieldName("name")?.text;
290
- if (name) states.add(name);
291
- });
292
- collectMergedStates(mergedTree, dirname(absPath), states, visited);
293
- }
294
- }
295
- async function lintBehavior(text, file = "agent.behavior", docPath, consolidated = false, externalStates, mergedText) {
296
- await initBehaviorParser();
297
- if (!text.endsWith("\n")) text = text + "\n";
298
- const tree = await parse("behavior", text);
299
- const messages = [];
300
- collectSyntaxErrors(tree.rootNode, messages, file);
301
- for (const stateNode of nodesOfType(tree, "state_decl")) {
302
- if (stateNode.hasError) continue;
303
- const nameNode = stateNode.childForFieldName("name");
304
- const stateName = nameNode?.text ?? "?";
305
- const hasInteract = stateNode.descendantsOfType("interact_stmt").length > 0;
306
- const hasGoal = stateNode.descendantsOfType("goal_stmt").length > 0;
307
- const intentHandlers = stateNode.descendantsOfType("intent_handler");
308
- if (hasInteract && !hasGoal) {
309
- const { line, col } = nodePosition(nameNode ?? stateNode);
310
- messages.push({
311
- file,
312
- line,
313
- col,
314
- severity: "warning",
315
- code: "W013",
316
- message: `'interact' without 'goal'. Add 'goal "..."' before 'interact'.`
317
- });
318
- }
319
- if (hasGoal && !hasInteract) {
320
- const { line, col } = nodePosition(nameNode ?? stateNode);
321
- messages.push({
322
- file,
323
- line,
324
- col,
325
- severity: "warning",
326
- code: "W012",
327
- message: `'goal' is only valid in an oriented state. Add 'interact' or remove 'goal'.`
328
- });
329
- }
330
- if (hasInteract && intentHandlers.length === 0) {
331
- const { line, col } = nodePosition(nameNode ?? stateNode);
332
- messages.push({
333
- file,
334
- line,
335
- col,
336
- severity: "error",
337
- code: "E009",
338
- message: `Oriented state '${stateName}' has no 'on intent' handlers. At least one is required.`
339
- });
340
- }
341
- const seenIntents = /* @__PURE__ */ new Map();
342
- for (const handler of intentHandlers) {
343
- const intentNode = handler.childForFieldName("intent");
344
- if (!intentNode) continue;
345
- const label = intentNode.text.replace(/^"|"$/g, "");
346
- if (seenIntents.has(label)) {
347
- const { line, col } = nodePosition(handler);
348
- messages.push({
349
- file,
350
- line,
351
- col,
352
- severity: "error",
353
- code: "W008",
354
- message: `Duplicate 'on intent "${label}"' in state '${stateName}'. The FSM cannot route ambiguous intents \u2014 use a unique label for each handler.`
355
- });
356
- } else {
357
- seenIntents.set(label, true);
358
- }
359
- }
360
- for (const handler of intentHandlers) {
361
- for (const t of handler.descendantsOfType("transition_stmt")) {
362
- const targetNode = t.childForFieldName("state");
363
- if (targetNode?.text === stateName) {
364
- const { line, col } = nodePosition(handler);
365
- messages.push({
366
- file,
367
- line,
368
- col,
369
- severity: "warning",
370
- code: "W011",
371
- message: `'on intent' handler in state '${stateName}' transitions back to itself. The user expressed an intent but receives no progress \u2014 did you mean a different target state?`
372
- });
373
- }
374
- }
375
- }
376
- if (stateName === "init" || stateName === "end") {
377
- const { line, col } = nodePosition(nameNode ?? stateNode);
378
- if (stateName === "init") {
379
- messages.push({
380
- file,
381
- line,
382
- col,
383
- severity: "info",
384
- code: "I001",
385
- message: `State 'init' overrides the kernel's default entry lifecycle. The kernel will use this definition instead of its built-in init sequence.`
386
- });
387
- } else {
388
- messages.push({
389
- file,
390
- line,
391
- col,
392
- severity: "info",
393
- code: "I002",
394
- message: `State 'end' overrides the kernel's default terminal lifecycle. The kernel will treat this state as the canonical exit point.`
395
- });
396
- }
397
- }
398
- if (!KERNEL_LIFECYCLE_NAMES.includes(stateName)) {
399
- const closest = closestName(stateName, KERNEL_LIFECYCLE_NAMES);
400
- if (closest) {
401
- const { line, col } = nodePosition(nameNode ?? stateNode);
402
- messages.push({
403
- file,
404
- line,
405
- col,
406
- severity: "hint",
407
- code: "H001",
408
- message: `State name '${stateName}' resembles the kernel lifecycle name '${closest}'. If this is intentional, ignore this hint.`
409
- });
410
- }
411
- }
412
- }
413
- for (const parallelNode of nodesOfType(tree, "parallel_stmt")) {
414
- if (parallelNode.hasError) continue;
415
- if (parallelNode.descendantsOfType("run_stmt").length === 0) {
416
- const { line, col } = nodePosition(parallelNode);
417
- let stateName = "?";
418
- let ancestor = parallelNode.parent;
419
- while (ancestor && ancestor.type !== "state_decl") ancestor = ancestor.parent;
420
- if (ancestor) stateName = ancestor.childForFieldName("name")?.text ?? "?";
421
- messages.push({
422
- file,
423
- line,
424
- col,
425
- severity: "warning",
426
- code: "E010",
427
- message: `'parallel' block in state '${stateName}' has no 'run' statements and does nothing.`
428
- });
429
- }
430
- }
431
- for (const afterNode of nodesOfType(tree, "after_stmt")) {
432
- if (afterNode.hasError) continue;
433
- const countNode = afterNode.childForFieldName("count");
434
- if (countNode?.text === "0") {
435
- const { line, col } = nodePosition(countNode);
436
- messages.push({
437
- file,
438
- line,
439
- col,
440
- severity: "error",
441
- code: "E011",
442
- message: `'after 0' is invalid \u2014 prompts must be \u2265 1. Use 'after 1' for the next prompt.`
443
- });
444
- }
445
- }
446
- for (const stmtNode of nodesOfType(tree, "goal_stmt")) {
447
- const strNode = stmtNode.descendantsOfType("quoted_string").find((n) => n !== null);
448
- if (!strNode) continue;
449
- const content = strNode.text.replace(/^"|"$/g, "");
450
- if (content.length > 280) {
451
- const { line, col } = nodePosition(strNode);
452
- messages.push({
453
- file,
454
- line,
455
- col,
456
- severity: "warning",
457
- code: "W002",
458
- message: `Text block exceeds 280 characters (${content.length}). Consider using 'guide' to load long orientation text from an external text file.`
459
- });
460
- }
461
- }
462
- for (const stmtNode of nodesOfType(tree, "guide_stmt")) {
463
- const strNode = stmtNode.descendantsOfType("quoted_string").find((n) => n !== null);
464
- if (!strNode) continue;
465
- const content = strNode.text.replace(/^"|"$/g, "");
466
- if (content.length > 280) {
467
- const { line, col } = nodePosition(strNode);
468
- let p = stmtNode.parent;
469
- while (p && p.type !== "state_decl") p = p.parent;
470
- const stateName = p?.childForFieldName("name")?.text ?? "?";
471
- messages.push({
472
- file,
473
- line,
474
- col,
475
- severity: "warning",
476
- code: "W010",
477
- message: `'guide' text in state '${stateName}' is ${content.length} characters. Consider using an external file.`
478
- });
479
- }
480
- }
481
- const definedStates = new Set(
482
- nodesOfType(tree, "state_decl").map((n) => n.childForFieldName("name")?.text).filter(Boolean)
483
- );
484
- if (docPath) {
485
- try {
486
- collectMergedStates(tree, dirname(docPath), definedStates);
487
- } catch {
488
- }
489
- }
490
- if (externalStates) {
491
- for (const name of externalStates) definedStates.add(name);
492
- }
493
- const stateNameList = Array.from(definedStates);
494
- for (const transitionNode of nodesOfType(tree, "transition_stmt")) {
495
- const stateNode = transitionNode.childForFieldName("state");
496
- if (!stateNode) continue;
497
- const target = stateNode.text;
498
- if (!definedStates.has(target)) {
499
- const { line, col } = nodePosition(stateNode);
500
- const isExternal = target.includes(".");
501
- const hint = !isExternal ? closestName(target, stateNameList) : void 0;
502
- messages.push({
503
- file,
504
- line,
505
- col,
506
- severity: isExternal ? "warning" : "error",
507
- code: isExternal ? "W005" : "E005",
508
- message: isExternal ? `State '${target}' is not defined locally (assuming external flow reference).` : `State '${target}' is not defined in this file.`,
509
- ...hint ? { hint: `did you mean '${hint}'?` } : {}
510
- });
511
- }
512
- }
513
- for (const interactNode of nodesOfType(tree, "interact_stmt")) {
514
- let ancestor = interactNode.parent;
515
- while (ancestor && ancestor.type !== "state_decl") ancestor = ancestor.parent;
516
- if (!ancestor) continue;
517
- const hasHandlers = ancestor.descendantsOfType("intent_handler").length + ancestor.descendantsOfType("offtopic_handler").length > 0;
518
- if (!hasHandlers) {
519
- const { line, col } = nodePosition(interactNode);
520
- messages.push({
521
- file,
522
- line,
523
- col,
524
- severity: "warning",
525
- code: "W006",
526
- message: "This state calls interact but has no handlers (on intent/offtopic). This will trap the agent."
527
- });
528
- }
529
- }
530
- const fsmResult = parseBehaviorFile(text);
531
- if (fsmResult.ok === null) {
532
- const firstMsg = fsmResult.diagnostics[0]?.message ?? "Parse error";
533
- messages.push({
534
- file,
535
- line: 1,
536
- col: 1,
537
- severity: "error",
538
- code: "E006",
539
- message: `Parse error: ${firstMsg}`
540
- });
541
- return messages;
542
- }
543
- const fsm = fsmResult.ok;
544
- const wholeFsm = mergedText !== void 0 ? parseBehaviorFile(mergedText).ok : null;
545
- const reachabilityStates = wholeFsm?.states ?? fsm.states;
546
- const allTargets = new Set(collectTransitionTargets(reachabilityStates.flatMap((s) => s.body)));
547
- for (const state of fsm.states) {
548
- const hasOutgoing = collectTransitionTargets(state.body).length > 0;
549
- const hasIncoming = allTargets.has(state.name);
550
- if (!hasIncoming && !hasOutgoing) {
551
- messages.push({
552
- file,
553
- line: 1,
554
- col: 1,
555
- severity: "warning",
556
- code: "W001",
557
- message: `State "${state.name}" has no transitions`
558
- });
559
- }
560
- }
561
- for (let i = 1; i < fsm.states.length; i++) {
562
- const state = fsm.states[i];
563
- if (!allTargets.has(state.name)) {
564
- const stateNode = nodesOfType(tree, "state_decl").find((n) => n.childForFieldName("name")?.text === state.name);
565
- const pos = stateNode ? nodePosition(stateNode) : { line: 1, col: 1 };
566
- messages.push({
567
- file,
568
- line: pos.line,
569
- col: pos.col,
570
- severity: "warning",
571
- code: "W009",
572
- message: `State '${state.name}' is unreachable \u2014 no other state transitions to it.`
573
- });
574
- }
575
- }
576
- if (consolidated) {
577
- const wholeTreeFsm = wholeFsm ?? fsm;
578
- const seenStates = /* @__PURE__ */ new Set();
579
- for (const state of wholeTreeFsm.states) {
580
- if (seenStates.has(state.name)) {
581
- messages.push({
582
- file,
583
- line: 1,
584
- col: 1,
585
- severity: "error",
586
- code: "E015",
587
- message: `Duplicate state '${state.name}' across merged files \u2014 each state name must be unique in the consolidated FSM.`
588
- });
589
- } else {
590
- seenStates.add(state.name);
591
- }
592
- }
593
- if (!wholeTreeFsm.states.some((s) => s.name === "init")) {
594
- messages.push({
595
- file,
596
- line: 1,
597
- col: 1,
598
- severity: "error",
599
- code: "E016",
600
- message: `Required 'init' state is missing. Add a state named 'init' as the entry point of your behavior.`
601
- });
602
- }
603
- const seenTriggers = /* @__PURE__ */ new Set();
604
- for (const trigger of wholeTreeFsm.global_triggers) {
605
- if (seenTriggers.has(trigger.event)) {
606
- messages.push({
607
- file,
608
- line: 1,
609
- col: 1,
610
- severity: "warning",
611
- code: "W014",
612
- message: `Duplicate global trigger '${trigger.event}' across merged files \u2014 only the last definition takes effect.`
613
- });
614
- } else {
615
- seenTriggers.add(trigger.event);
616
- }
617
- }
618
- }
619
- return messages;
620
- }
621
- async function createLinter() {
622
- return { lintDescription, lintBehavior };
623
- }
624
-
625
- // src/zip.ts
626
- import JSZip from "jszip";
627
- import { readFile, writeFile } from "fs/promises";
628
- async function readZip(filePath) {
629
- const data = await readFile(filePath);
630
- return JSZip.loadAsync(data);
631
- }
632
- async function validateZipBomb2(filePath) {
633
- const data = await readFile(filePath);
634
- const zip = await JSZip.loadAsync(data);
635
- validateZipBomb(zip, data.length);
636
- return true;
637
- }
638
- async function validateMagicBytes2(filePath) {
639
- const data = await readFile(filePath);
640
- validateMagicBytes(data);
641
- return true;
642
- }
643
- async function writeZip(zip, outPath) {
644
- const data = await zip.generateAsync({ type: "arraybuffer" });
645
- await writeFile(outPath, Buffer.from(data));
646
- }
647
-
648
- // src/pack.ts
649
- import { readFile as readFile2, stat, readdir } from "fs/promises";
650
- import { join, basename, resolve as resolve2, relative, isAbsolute, normalize, dirname as dirname2 } from "path";
651
- import { execSync } from "child_process";
652
- import { createHash } from "crypto";
653
- import JSZip2 from "jszip";
654
-
655
- // src/schema.ts
656
- var STD_BASE = "https://dot-agent.dev/schema/std/v1/";
657
- function propertyTypeToSchema(pt) {
658
- switch (pt.kind) {
659
- case "primitive":
660
- return { type: pt.value };
661
- case "reference":
662
- return pt.value.startsWith("std.") ? { $ref: `${STD_BASE}${pt.value.slice(4)}.json` } : { $ref: `#/$defs/${pt.value}` };
663
- case "array":
664
- return { type: "array", items: propertyTypeToSchema(pt.value) };
665
- case "enum":
666
- return { type: "string", enum: pt.value };
667
- }
668
- }
669
- function typeDefinitionToSchema(td) {
670
- const required = td.properties.filter((p) => !p.is_optional).map((p) => p.name);
671
- const properties = {};
672
- for (const p of td.properties) {
673
- properties[p.name] = {
674
- ...propertyTypeToSchema(p.type),
675
- ...p.description ? { description: p.description } : {}
676
- };
677
- }
678
- return {
679
- $schema: "https://json-schema.org/draft/2020-12/schema",
680
- "x-category": td.category.uri,
681
- ...td.category.label ? { "x-category-label": td.category.label } : {},
682
- ...td.concept ? { "x-concept": td.concept.uri } : {},
683
- ...td.concept?.label ? { "x-concept-label": td.concept.label } : {},
684
- type: "object",
685
- properties,
686
- ...required.length ? { required } : {}
687
- };
688
- }
689
- function refFromAnnotatedRef(ref) {
690
- return ref.name.startsWith("std.") ? { $ref: `${STD_BASE}${ref.name.slice(4)}.json` } : { $ref: `#/$defs/${ref.name}` };
691
- }
692
- function buildTypesJson(df) {
693
- if (df.types.length === 0 && df.input.length === 0 && df.output.length === 0) return null;
694
- const defs = {};
695
- for (const td of df.types) {
696
- defs[td.name] = typeDefinitionToSchema(td);
697
- }
698
- return JSON.stringify(
699
- {
700
- ...df.input.length ? { input: df.input.map(refFromAnnotatedRef) } : {},
701
- ...df.output.length ? { output: df.output.map(refFromAnnotatedRef) } : {},
702
- ...Object.keys(defs).length ? { $defs: defs } : {}
703
- },
704
- null,
705
- 2
706
- );
707
- }
708
-
709
- // src/pack.ts
710
- function gitRevParseShort() {
711
- try {
712
- return execSync("git rev-parse --short HEAD", { stdio: "pipe", encoding: "utf-8" }).trim();
713
- } catch {
714
- return null;
715
- }
716
- }
717
- async function resolveCommit(explicit) {
718
- if (explicit) return explicit;
719
- return gitRevParseShort() ?? void 0;
720
- }
721
- function recentTagCandidates(limit = 5) {
722
- let tags;
723
- try {
724
- tags = execSync("git tag --sort=-creatordate", { stdio: "pipe", encoding: "utf-8" }).trim().split("\n").filter(Boolean);
725
- } catch {
726
- return [];
727
- }
728
- return tags.slice(0, limit).map((tag) => {
729
- const atIdx = tag.lastIndexOf("@");
730
- const version = atIdx === -1 ? tag : tag.slice(atIdx + 1);
731
- const label = atIdx === -1 ? tag : `${version} (tag: ${tag})`;
732
- return { version, label };
733
- }).filter((c) => isValidVersion(c.version));
734
- }
735
- var CUSTOM_VERSION = "__dot-agent-custom-version__";
736
- var BARE_VERSION = "__dot-agent-bare-version__";
737
- async function resolveVersionInteractive(explicit) {
738
- if (explicit) {
739
- if (!isValidVersion(explicit)) {
740
- throw new Error(`E019: Invalid version format '${explicit}' \u2014 expected vX.Y[.Z][-prerelease] or X.Y[.Z][-prerelease]`);
741
- }
742
- return explicit;
743
- }
744
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
745
- process.stderr.write("[dot-agent] no TTY and no --version \u2014 packing without a version\n");
746
- return void 0;
747
- }
748
- const candidates = recentTagCandidates();
749
- const clack = await import("@clack/prompts");
750
- const choice = await clack.select({
751
- message: "Choose a version for this package",
752
- options: [
753
- ...candidates.map((c) => ({ value: c.version, label: c.label })),
754
- { value: CUSTOM_VERSION, label: "Type a custom version\u2026" },
755
- { value: BARE_VERSION, label: "None (bare \u2014 no version)" }
756
- ]
757
- });
758
- if (clack.isCancel(choice)) {
759
- clack.cancel("Pack cancelled.");
760
- process.exit(1);
761
- }
762
- if (choice === BARE_VERSION) return void 0;
763
- if (choice === CUSTOM_VERSION) {
764
- const typed = await clack.text({
765
- message: "Version",
766
- validate: (v) => v && isValidVersion(v) ? void 0 : "Invalid version format \u2014 expected vX.Y[.Z][-prerelease] or X.Y[.Z][-prerelease]"
767
- });
768
- if (clack.isCancel(typed)) {
769
- clack.cancel("Pack cancelled.");
770
- process.exit(1);
771
- }
772
- return typed;
773
- }
774
- return choice;
775
- }
776
- function isExternalPath(agentRoot, declaredPath) {
777
- if (isAbsolute(declaredPath)) return true;
778
- const normRoot = normalize(resolve2(agentRoot));
779
- const resolved = resolve2(normRoot, declaredPath);
780
- return relative(normRoot, resolved).startsWith("..");
781
- }
782
- async function discoverDescriptionFile(dir, explicit) {
783
- if (explicit) {
784
- try {
785
- await stat(join(dir, explicit));
786
- return explicit;
787
- } catch {
788
- throw new Error(`E003: Description file '${explicit}' not found in ${dir}`);
789
- }
790
- }
791
- const entries = await readdir(dir);
792
- const descFiles = entries.filter((f) => f.endsWith(".description"));
793
- if (descFiles.length === 0) {
794
- throw new Error(`E003: No .description file found in ${dir}`);
795
- }
796
- if (descFiles.length > 1) {
797
- throw new Error(`E003: Multiple .description files found: ${descFiles.join(", ")} \u2014 use PackOptions.description to specify one`);
798
- }
799
- const name = descFiles[0];
800
- if (name !== "agent.description") {
801
- console.info(`[dot-agent] using description file: ${name}`);
802
- }
803
- return name;
804
- }
805
- async function consolidate(agentRoot, entryFile) {
806
- await initBehaviorParser();
807
- const normRoot = normalize(resolve2(agentRoot));
808
- const visiting = /* @__PURE__ */ new Set();
809
- const visited = /* @__PURE__ */ new Set();
810
- const order = [];
811
- const textCache = /* @__PURE__ */ new Map();
812
- async function dfs(relPath) {
813
- const absPath = resolve2(normRoot, relPath);
814
- if (visiting.has(absPath)) {
815
- throw new Error(`E013: Circular merge dependency involving '${relPath}'`);
816
- }
817
- if (visited.has(absPath)) return;
818
- visiting.add(absPath);
819
- let fileText;
820
- try {
821
- fileText = await readFile2(absPath, "utf-8");
822
- } catch {
823
- throw new Error(`E012: Merge target not found: '${relPath}'`);
824
- }
825
- textCache.set(absPath, fileText);
826
- const result = parseBehaviorFile(fileText);
827
- const merges = result.ok?.merges ?? [];
828
- for (const mergePath of merges) {
829
- if (isAbsolute(mergePath)) {
830
- throw new Error(`E014: Merge path '${mergePath}' in '${relPath}' is absolute \u2014 only paths within the agent root are allowed`);
831
- }
832
- const mergeAbs = resolve2(dirname2(absPath), mergePath);
833
- const mergeRel = relative(normRoot, mergeAbs);
834
- if (mergeRel.startsWith("..")) {
835
- throw new Error(`E014: Merge path '${mergePath}' in '${relPath}' escapes the agent root`);
836
- }
837
- await dfs(mergeRel);
838
- }
839
- visiting.delete(absPath);
840
- visited.add(absPath);
841
- order.push(relPath);
842
- }
843
- await dfs(entryFile);
844
- const texts = order.map((relPath) => textCache.get(resolve2(normRoot, relPath)));
845
- return {
846
- mergedText: texts.join("\n"),
847
- mergeSources: order
848
- };
849
- }
850
- var FILE_REF_RE = /\.(txt|md)$/i;
851
- function collectFileRefs(stmts, out) {
852
- for (const s of stmts) {
853
- switch (s.type) {
854
- case "guide_stmt":
855
- if (FILE_REF_RE.test(s.text)) out.set(`guide:${s.text}`, { kind: "guide", text: s.text });
856
- break;
857
- case "teach_stmt":
858
- if (FILE_REF_RE.test(s.text)) out.set(`teach:${s.text}`, { kind: "teach", text: s.text });
859
- break;
860
- case "interact_stmt":
861
- collectFileRefs(s.handlers, out);
862
- break;
863
- case "intent_handler":
864
- if (Array.isArray(s.body)) collectFileRefs(s.body, out);
865
- break;
866
- case "offtopic_handler":
867
- case "after_stmt":
868
- case "parallel_stmt":
869
- collectFileRefs(s.body, out);
870
- break;
871
- case "conditional_stmt":
872
- collectFileRefs(s.then, out);
873
- if (s.else) collectFileRefs(s.else, out);
874
- break;
875
- }
876
- }
877
- }
878
- async function collectFiles(dir, descriptionFile, mergedBehaviorText, mergeSources, personaFile) {
879
- const files = /* @__PURE__ */ new Map();
880
- async function walk(subdir, prefix = "") {
881
- try {
882
- const entries = await readdir(subdir);
883
- for (const entry of entries) {
884
- if (entry === ".gitkeep" || entry.startsWith(".")) continue;
885
- const fullPath = join(subdir, entry);
886
- const stats = await stat(fullPath);
887
- const relativePath = prefix ? `${prefix}/${entry}` : entry;
888
- if (stats.isDirectory()) {
889
- await walk(fullPath, relativePath);
890
- } else {
891
- files.set(relativePath, await readFile2(fullPath, "utf-8"));
892
- }
893
- }
894
- } catch {
895
- }
896
- }
897
- files.set(descriptionFile, await readFile2(join(dir, descriptionFile), "utf-8"));
898
- const flatBehaviorText = mergedBehaviorText.replace(/^\s*merge\s+"[^"]*"\s*$/gm, "");
899
- files.set("agent.behavior", flatBehaviorText);
900
- for (const relPath of mergeSources) {
901
- files.set(`behaviors/${relPath}`, await readFile2(join(dir, relPath), "utf-8"));
902
- }
903
- if (personaFile) {
904
- if (isExternalPath(dir, personaFile)) {
905
- throw new Error(`E014: persona path '${personaFile}' in '${descriptionFile}' is absolute or escapes the agent root`);
906
- }
907
- let personaText;
908
- try {
909
- personaText = await readFile2(join(dir, personaFile), "utf-8");
910
- } catch {
911
- throw new Error(`E_DESC: persona file '${personaFile}' declared in '${descriptionFile}' not found`);
912
- }
913
- files.set(personaFile, personaText);
914
- }
915
- await walk(join(dir, "guides"), "guides");
916
- await walk(join(dir, "knowledge"), "knowledge");
917
- await initBehaviorParser();
918
- const behaviorAst = parseBehaviorFile(mergedBehaviorText).ok;
919
- if (behaviorAst) {
920
- const refs = /* @__PURE__ */ new Map();
921
- for (const state of behaviorAst.states) collectFileRefs(state.body, refs);
922
- for (const trigger of behaviorAst.global_triggers) collectFileRefs(trigger.body, refs);
923
- for (const { kind, text } of refs.values()) {
924
- if (isExternalPath(dir, text)) {
925
- throw new Error(`E014: '${kind}' path '${text}' is absolute or escapes the agent root`);
926
- }
927
- let content;
928
- try {
929
- content = await readFile2(join(dir, text), "utf-8");
930
- } catch {
931
- throw new Error(`E018: '${kind}' file '${text}' referenced in agent.behavior not found in ${dir}`);
932
- }
933
- files.set(`${kind === "guide" ? "guides" : "knowledge"}/${text}`, content);
934
- }
935
- }
936
- return files;
937
- }
938
- function withAgentExtension(path) {
939
- return path.endsWith(".agent") ? path : `${path}.agent`;
940
- }
941
- async function pack(options = {}) {
942
- const dir = options.dir ?? process.cwd();
943
- const outPath = withAgentExtension(options.out ?? join(dir, basename(dir)));
944
- const descriptionFileName = await discoverDescriptionFile(dir, options.description);
945
- const descriptionText = await readFile2(join(dir, descriptionFileName), "utf-8");
946
- const descriptionMessages = await lintDescription(descriptionText, descriptionFileName);
947
- const descErrors = descriptionMessages.filter((m) => m.severity === "error");
948
- if (descErrors.length > 0) {
949
- throw new Error(
950
- `Lint failed:
951
- ${descErrors.map((e) => ` ${e.file}:${e.line}:${e.col} ${e.code} ${e.message}`).join("\n")}`
952
- );
953
- }
954
- await initBehaviorParser();
955
- const descResult = parseDescriptionFile(descriptionText);
956
- if (descResult.ok === null) {
957
- const firstError = descResult.diagnostics.find((d) => d.severity === "error");
958
- throw new Error(`E_DESC: ${firstError?.message ?? "parse failed"}`);
959
- }
960
- const df = descResult.ok;
961
- if (!df.behavior) {
962
- throw new Error(`E_DESC: '${descriptionFileName}' must declare a behavior file \u2014 add 'behavior "agent.behavior"' (or the name of your entry .behavior file)`);
963
- }
964
- if (isExternalPath(dir, df.behavior)) {
965
- throw new Error(`E014: behavior path '${df.behavior}' in '${descriptionFileName}' is absolute or escapes the agent root`);
966
- }
967
- const { mergedText, mergeSources } = await consolidate(dir, df.behavior);
968
- const behaviorMessages = await lintBehavior(mergedText, "agent.behavior", void 0, true);
969
- const allMessages = [...descriptionMessages, ...behaviorMessages];
970
- const errors = allMessages.filter((m) => m.severity === "error");
971
- const warnings = allMessages.filter((m) => m.severity === "warning");
972
- if (errors.length > 0) {
973
- throw new Error(
974
- `Lint failed:
975
- ${errors.map((e) => ` ${e.file}:${e.line}:${e.col} ${e.code} ${e.message}`).join("\n")}`
976
- );
977
- }
978
- const version = await resolveVersionInteractive(options.version);
979
- const commit = await resolveCommit(options.commit);
980
- const allFiles = await collectFiles(dir, descriptionFileName, mergedText, mergeSources, df.persona ?? void 0);
981
- const contentForHash = Array.from(allFiles.values()).join("");
982
- const sha256 = createHash("sha256").update(contentForHash).digest("hex");
983
- const namespace = df.agent.domain ?? "unknown";
984
- const id = buildId({ namespace, name: slugify(df.agent.name), version, digest: version ? commit : void 0 });
985
- const typesJson = buildTypesJson(df);
986
- const integrity = {
987
- sha256,
988
- ...typesJson ? { types: ".agent/types.json" } : {},
989
- files: ".agent/files.json"
990
- };
991
- const aboutme = buildAboutme({
992
- id,
993
- name: df.agent.name,
994
- description: df.description ?? "",
995
- version: version ?? "",
996
- domain: df.agent.domain ?? "",
997
- license: df.agent.license ?? "",
998
- persona: df.persona ?? void 0,
999
- purpose: "unknown",
1000
- compiler: `dot-agent/${COMPILER_VERSION}`,
1001
- commit,
1002
- capabilities: df.capabilities.map((c) => ({ id: c.name, description: c.description ?? "" })),
1003
- requires: df.requires,
1004
- integrity
1005
- });
1006
- const zip = new JSZip2();
1007
- const agentFolder = zip.folder(".agent");
1008
- agentFolder.file("aboutme.json", aboutmeToJson(aboutme));
1009
- if (typesJson) {
1010
- agentFolder.file("types.json", typesJson);
1011
- }
1012
- const gitkeepPaths = /* @__PURE__ */ new Set(["guides/.gitkeep", "knowledge/.gitkeep"]);
1013
- const filesJson = {
1014
- description: descriptionFileName,
1015
- behavior: "agent.behavior",
1016
- ...df.persona ? { persona: df.persona } : {},
1017
- behaviors: Array.from(allFiles.keys()).filter((f) => f.startsWith("behaviors/")),
1018
- guides: Array.from(allFiles.keys()).filter((f) => f.startsWith("guides/") && !gitkeepPaths.has(f)),
1019
- knowledge: Array.from(allFiles.keys()).filter((f) => f.startsWith("knowledge/") && !gitkeepPaths.has(f))
1020
- };
1021
- agentFolder.file("files.json", JSON.stringify(filesJson, null, 2));
1022
- for (const [path, content] of allFiles) {
1023
- zip.file(path, content);
1024
- }
1025
- await writeZip(zip, outPath);
1026
- return { path: outPath, id, warnings };
1027
- }
1028
-
1029
- // src/bundle.ts
1030
- import { readFile as readFile3 } from "fs/promises";
1031
- import { join as join2 } from "path";
1032
- import { createHash as createHash2 } from "crypto";
1033
- var GITKEEP = /* @__PURE__ */ new Set(["guides/.gitkeep", "knowledge/.gitkeep"]);
1034
- async function bundleFromDir(dir) {
1035
- const descriptionFileName = await discoverDescriptionFile(dir);
1036
- const descriptionText = await readFile3(join2(dir, descriptionFileName), "utf-8");
1037
- const descMessages = await lintDescription(descriptionText, descriptionFileName);
1038
- const descErrors = descMessages.filter((m) => m.severity === "error");
1039
- if (descErrors.length > 0) {
1040
- throw new Error(
1041
- `Lint failed:
1042
- ${descErrors.map((e) => ` ${e.file}:${e.line}:${e.col} ${e.code} ${e.message}`).join("\n")}`
1043
- );
1044
- }
1045
- await initBehaviorParser();
1046
- const descResult = parseDescriptionFile(descriptionText);
1047
- if (descResult.ok === null) {
1048
- const firstError = descResult.diagnostics.find((d) => d.severity === "error");
1049
- throw new Error(`E_DESC: ${firstError?.message ?? "parse failed"}`);
1050
- }
1051
- const df = descResult.ok;
1052
- if (!df.behavior) {
1053
- throw new Error(`E_DESC: '${descriptionFileName}' must declare a behavior file`);
1054
- }
1055
- const { mergedText, mergeSources } = await consolidate(dir, df.behavior);
1056
- const behaviorMessages = await lintBehavior(mergedText, "agent.behavior", void 0, true);
1057
- const allMessages = [...descMessages, ...behaviorMessages];
1058
- const errors = allMessages.filter((m) => m.severity === "error");
1059
- const warnings = allMessages.filter((m) => m.severity === "warning");
1060
- if (errors.length > 0) {
1061
- throw new Error(
1062
- `Lint failed:
1063
- ${errors.map((e) => ` ${e.file}:${e.line}:${e.col} ${e.code} ${e.message}`).join("\n")}`
1064
- );
1065
- }
1066
- for (const w of warnings) {
1067
- process.stderr.write(`\x1B[33m\u26A0\x1B[0m ${w.file}:${w.line}:${w.col} ${w.code} ${w.message}
1068
- `);
1069
- }
1070
- const allFiles = await collectFiles(dir, descriptionFileName, mergedText, mergeSources, df.persona ?? void 0);
1071
- const sha256 = createHash2("sha256").update(Array.from(allFiles.values()).join("")).digest("hex");
1072
- const id = buildId({ namespace: df.agent.domain ?? "unknown", name: slugify(df.agent.name), version: "dev" });
1073
- const aboutme = buildAboutme({
1074
- id,
1075
- name: df.agent.name,
1076
- description: df.description ?? "",
1077
- version: "dev",
1078
- domain: df.agent.domain ?? "",
1079
- license: df.agent.license ?? "",
1080
- persona: df.persona ?? void 0,
1081
- purpose: "development",
1082
- compiler: `dot-agent/${COMPILER_VERSION}`,
1083
- capabilities: df.capabilities.map((c) => ({ id: c.name, description: c.description ?? "" })),
1084
- requires: df.requires,
1085
- integrity: { sha256, files: ".agent/files.json" }
1086
- });
1087
- const files = {
1088
- description: descriptionText,
1089
- // Reuse collectFiles()'s 'agent.behavior' entry rather than the raw
1090
- // mergedText — it has redundant `merge "..."` lines stripped (see
1091
- // collectFiles() for why they're dead weight once flattened).
1092
- behavior: allFiles.get("agent.behavior") ?? mergedText,
1093
- persona: df.persona ? allFiles.get(df.persona) : void 0,
1094
- guides: Array.from(allFiles.entries()).filter(([p]) => p.startsWith("guides/") && !GITKEEP.has(p)).map(([path, content]) => ({ path, content })),
1095
- knowledge: Array.from(allFiles.entries()).filter(([p]) => p.startsWith("knowledge/") && !GITKEEP.has(p)).map(([path, content]) => ({ path, content })),
1096
- behaviors: Array.from(allFiles.entries()).filter(([p]) => p.startsWith("behaviors/")).map(([path, content]) => ({ path, content }))
1097
- };
1098
- return { id, aboutme, files };
1099
- }
1100
- export {
1101
- aboutmeToJson,
1102
- buildAboutme,
1103
- buildId,
1104
- bundleFromDir,
1105
- collectFiles,
1106
- consolidate,
1107
- createLinter,
1108
- createZip,
1109
- extractDigest,
1110
- extractFiles,
1111
- extractName,
1112
- getBehaviorScxml,
1113
- getContextNode,
1114
- initBehaviorParser,
1115
- initParsers,
1116
- lintBehavior,
1117
- lintDescription,
1118
- nodeAtOffset,
1119
- nodeToRange,
1120
- nodesOfType,
1121
- pack,
1122
- parse,
1123
- parseAboutme,
1124
- parseBehaviorFile,
1125
- parseDescriptionFile,
1126
- parseId,
1127
- parseSync,
1128
- positionToOffset,
1129
- readZip,
1130
- validateMagicBytes2 as validateMagicBytes,
1131
- validateZipBomb2 as validateZipBomb,
1132
- writeZip
1133
- };
1134
- //# sourceMappingURL=index.js.map