@dot-agent/compiler 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.
package/dist/index.js ADDED
@@ -0,0 +1,831 @@
1
+ import {
2
+ aboutmeToJson,
3
+ buildAboutme,
4
+ buildId,
5
+ createZip,
6
+ extractDigest,
7
+ extractFiles,
8
+ extractName,
9
+ parseAboutme,
10
+ parseId
11
+ } from "./chunk-IT25ILTQ.js";
12
+
13
+ // src/parser.ts
14
+ import { createRequire } from "module";
15
+ import { Parser, Language } from "web-tree-sitter";
16
+ import bpInit, { parse_behavior as bpParseBehavior, parse_description as bpParseDescription, get_graph } from "@dot-agent/parser-dsl";
17
+ var require2 = createRequire(import.meta.url);
18
+ var { descriptionWasmPath, behaviorWasmPath } = require2("@dot-agent/tree-sitter");
19
+ var _initialized = false;
20
+ var _descriptionParser;
21
+ var _behaviorParser;
22
+ var _bpInitialized = false;
23
+ async function initBehaviorParser() {
24
+ if (_bpInitialized) return;
25
+ await bpInit();
26
+ _bpInitialized = true;
27
+ }
28
+ function parseBehaviorFile(text) {
29
+ return JSON.parse(bpParseBehavior(text));
30
+ }
31
+ function parseDescriptionFile(text) {
32
+ return JSON.parse(bpParseDescription(text));
33
+ }
34
+ function getBehaviorScxml(text) {
35
+ return get_graph(text);
36
+ }
37
+ async function initParsers() {
38
+ if (_initialized) return;
39
+ await Parser.init();
40
+ const descriptionLang = await Language.load(descriptionWasmPath);
41
+ const behaviorLang = await Language.load(behaviorWasmPath);
42
+ _descriptionParser = new Parser();
43
+ _descriptionParser.setLanguage(descriptionLang);
44
+ _behaviorParser = new Parser();
45
+ _behaviorParser.setLanguage(behaviorLang);
46
+ _initialized = true;
47
+ }
48
+ async function parse(langId, text, previousTree) {
49
+ await initParsers();
50
+ const parser = langId === "behavior" ? _behaviorParser : _descriptionParser;
51
+ return parser.parse(text, previousTree);
52
+ }
53
+ function parseSync(langId, text, previousTree) {
54
+ if (!_initialized) return null;
55
+ const parser = langId === "behavior" ? _behaviorParser : _descriptionParser;
56
+ return parser.parse(text, previousTree);
57
+ }
58
+ function nodesOfType(tree, type) {
59
+ if (!tree) return [];
60
+ return tree.rootNode.descendantsOfType(type).filter((n) => n !== null);
61
+ }
62
+ function nodeAtOffset(tree, offset) {
63
+ if (!tree) return null;
64
+ return tree.rootNode.descendantForIndex(offset);
65
+ }
66
+ function nodeToRange(node) {
67
+ return {
68
+ start: { line: node.startPosition.row, character: node.startPosition.column },
69
+ end: { line: node.endPosition.row, character: node.endPosition.column }
70
+ };
71
+ }
72
+ function positionToOffset(text, line, character) {
73
+ let offset = 0;
74
+ for (let i = 0; i < line; i++) {
75
+ const nl = text.indexOf("\n", offset);
76
+ offset = nl === -1 ? text.length : nl + 1;
77
+ }
78
+ return offset + character;
79
+ }
80
+ function getContextNode(tree, offset) {
81
+ let node = nodeAtOffset(tree, offset);
82
+ while (node && (node.isError || node.isMissing)) {
83
+ node = node.parent;
84
+ }
85
+ if (!node || node.type === "ERROR") {
86
+ const raw = nodeAtOffset(tree, offset);
87
+ const parent = raw?.parent;
88
+ if (parent) {
89
+ const siblings = parent.children;
90
+ const idx = siblings.findIndex((c) => c !== null && c.startIndex > offset);
91
+ const prev = idx > 0 ? siblings[idx - 1] : siblings[siblings.length - 1];
92
+ if (prev && !prev.isError) node = prev;
93
+ }
94
+ }
95
+ return node ?? tree.rootNode;
96
+ }
97
+
98
+ // src/linter.ts
99
+ import { readFileSync } from "fs";
100
+ import { resolve, dirname } from "path";
101
+ function nodePosition(node) {
102
+ return { line: node.startPosition.row + 1, col: node.startPosition.column + 1 };
103
+ }
104
+ var MISSING_HINTS = {
105
+ interact_stmt: "Missing 'interact' \u2014 oriented states require 'goal \u2026 interact \u2026 on intent \u2026 on offtopic'.",
106
+ offtopic_handler: "Missing 'on offtopic' handler \u2014 every state with 'interact' must handle the offtopic case.",
107
+ intent_handler: "Missing at least one 'on intent' handler after 'interact'.",
108
+ goal_stmt: "Missing 'goal' \u2014 every state with 'interact' must declare a goal.",
109
+ state_name: "Missing state name after 'state' keyword.",
110
+ quoted_string: "Missing quoted string \u2014 expected a value in double quotes."
111
+ };
112
+ function collectSyntaxErrors(root, messages, file) {
113
+ const seen = /* @__PURE__ */ new Set();
114
+ function push(node, message, code, severity = "error") {
115
+ const key = `${node.startIndex}:${node.endIndex}:${message}`;
116
+ if (seen.has(key)) return;
117
+ seen.add(key);
118
+ const { line, col } = nodePosition(node);
119
+ messages.push({ file, line, col, severity, code, message });
120
+ }
121
+ function walk(node) {
122
+ if (node.isMissing) {
123
+ const hint = MISSING_HINTS[node.type];
124
+ push(node, hint ?? `Syntax error: missing '${node.type}'.`, "E004");
125
+ return;
126
+ }
127
+ if (node.type === "ERROR") {
128
+ const hasNestedError = node.descendantsOfType("ERROR").some((e) => e !== null && e.id !== node.id);
129
+ if (!hasNestedError) {
130
+ const snippet = node.text.replace(/\s+/g, " ").trim().slice(0, 40);
131
+ push(node, snippet ? `Syntax error near '${snippet}'.` : "Syntax error.", "E004");
132
+ return;
133
+ }
134
+ }
135
+ for (const child of node.children) {
136
+ if (child) walk(child);
137
+ }
138
+ }
139
+ walk(root);
140
+ }
141
+ var STRICT_BLOCK_TYPES = {
142
+ input_block: "input",
143
+ output_block: "output",
144
+ requires_block: "requires",
145
+ capabilities_block: "capabilities"
146
+ };
147
+ async function lintDescription(text, file = "agent.description") {
148
+ const tree = await parse("description", text);
149
+ const messages = [];
150
+ collectSyntaxErrors(tree.rootNode, messages, file);
151
+ for (const metaNode of nodesOfType(tree, "agent_meta")) {
152
+ const keyNode = metaNode.childForFieldName("key");
153
+ if (keyNode?.text !== "domain") continue;
154
+ const valueNode = metaNode.childForFieldName("value");
155
+ if (valueNode?.text === "example.com") {
156
+ const { line, col } = nodePosition(valueNode);
157
+ messages.push({
158
+ file,
159
+ line,
160
+ col,
161
+ severity: "warning",
162
+ code: "W003",
163
+ message: 'domain still has default value "example.com"'
164
+ });
165
+ }
166
+ }
167
+ const hasDomain = nodesOfType(tree, "agent_meta").some(
168
+ (n) => n.childForFieldName("key")?.text === "domain" && n.childForFieldName("value")?.text?.trim()
169
+ );
170
+ if (!hasDomain) {
171
+ messages.push({
172
+ file,
173
+ line: 1,
174
+ col: 1,
175
+ severity: "warning",
176
+ code: "W007",
177
+ message: "No domain declared \u2014 agent will be packaged as unknown/name. Interoperability between runtimes is not guaranteed."
178
+ });
179
+ }
180
+ const declaredTypes = new Set(
181
+ nodesOfType(tree, "type_decl").map((n) => n.childForFieldName("name")?.text).filter(Boolean)
182
+ );
183
+ if (declaredTypes.size > 0) {
184
+ for (const blockType of Object.keys(STRICT_BLOCK_TYPES)) {
185
+ for (const blockNode of nodesOfType(tree, blockType)) {
186
+ for (const typeRefNode of blockNode.descendantsOfType("type_reference").filter((n) => n !== null)) {
187
+ const idNode = typeRefNode.firstNamedChild;
188
+ if (!idNode) continue;
189
+ const typeName = idNode.text;
190
+ if (!declaredTypes.has(typeName)) {
191
+ const { line, col } = nodePosition(idNode);
192
+ messages.push({
193
+ file,
194
+ line,
195
+ col,
196
+ severity: "warning",
197
+ code: "W004",
198
+ message: `Type '${typeName}' is not declared in this file (assuming native or external).`
199
+ });
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ return messages;
206
+ }
207
+ function levenshtein(a, b) {
208
+ const la = a.length, lb = b.length;
209
+ const dp = Array.from(
210
+ { length: la + 1 },
211
+ (_, i) => Array.from({ length: lb + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
212
+ );
213
+ for (let i = 1; i <= la; i++) {
214
+ for (let j = 1; j <= lb; j++) {
215
+ 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]);
216
+ }
217
+ }
218
+ return dp[la][lb];
219
+ }
220
+ function closestName(token, candidates) {
221
+ let best;
222
+ let bestDist = Infinity;
223
+ for (const name of candidates) {
224
+ const d = levenshtein(token.toLowerCase(), name.toLowerCase());
225
+ if (d > 0 && d <= 2 && d < bestDist) {
226
+ bestDist = d;
227
+ best = name;
228
+ }
229
+ }
230
+ return best;
231
+ }
232
+ var KERNEL_LIFECYCLE_NAMES = ["init", "welcome", "end", "online", "offline"];
233
+ function collectTransitionTargets(stmts) {
234
+ const targets = [];
235
+ for (const stmt of stmts) {
236
+ if (!stmt || typeof stmt !== "object") continue;
237
+ if (stmt.type === "transition_stmt" && typeof stmt["state"] === "string") {
238
+ targets.push(stmt["state"]);
239
+ }
240
+ if (stmt.type === "intent_handler" && typeof stmt["body"] === "string") {
241
+ targets.push(stmt["body"]);
242
+ }
243
+ const recurse = (v) => {
244
+ if (Array.isArray(v)) targets.push(...collectTransitionTargets(v));
245
+ };
246
+ if (stmt.type !== "intent_handler") recurse(stmt["body"]);
247
+ recurse(stmt["handlers"]);
248
+ recurse(stmt["then"]);
249
+ recurse(stmt["else"]);
250
+ recurse(stmt["on_complete"]);
251
+ recurse(stmt["on_failure"]);
252
+ }
253
+ return targets;
254
+ }
255
+ function collectMergedStates(tree, docDir, states, visited = /* @__PURE__ */ new Set()) {
256
+ for (const mergeNode of nodesOfType(tree, "merge_decl")) {
257
+ const pathNode = mergeNode.childForFieldName("path");
258
+ if (!pathNode) continue;
259
+ const filename = pathNode.text.replace(/^"|"$/g, "");
260
+ const absPath = resolve(docDir, filename);
261
+ if (visited.has(absPath)) continue;
262
+ visited.add(absPath);
263
+ let mergedText;
264
+ try {
265
+ mergedText = readFileSync(absPath, "utf-8");
266
+ } catch {
267
+ continue;
268
+ }
269
+ const mergedTree = parseSync("behavior", mergedText);
270
+ if (!mergedTree) continue;
271
+ nodesOfType(mergedTree, "state_decl").forEach((n) => {
272
+ const name = n.childForFieldName("name")?.text;
273
+ if (name) states.add(name);
274
+ });
275
+ collectMergedStates(mergedTree, dirname(absPath), states, visited);
276
+ }
277
+ }
278
+ async function lintBehavior(text, file = "agent.behavior", docPath) {
279
+ await initBehaviorParser();
280
+ if (!text.endsWith("\n")) text = text + "\n";
281
+ const tree = await parse("behavior", text);
282
+ const messages = [];
283
+ collectSyntaxErrors(tree.rootNode, messages, file);
284
+ for (const stateNode of nodesOfType(tree, "state_decl")) {
285
+ if (stateNode.hasError) continue;
286
+ const nameNode = stateNode.childForFieldName("name");
287
+ const stateName = nameNode?.text ?? "?";
288
+ const hasInteract = stateNode.descendantsOfType("interact_stmt").length > 0;
289
+ const hasGoal = stateNode.descendantsOfType("goal_stmt").length > 0;
290
+ const intentHandlers = stateNode.descendantsOfType("intent_handler");
291
+ if (hasInteract && !hasGoal) {
292
+ const { line, col } = nodePosition(nameNode ?? stateNode);
293
+ messages.push({
294
+ file,
295
+ line,
296
+ col,
297
+ severity: "warning",
298
+ code: "W013",
299
+ message: `'interact' without 'goal' \u2014 the prettifier will insert one. To set it explicitly, add 'goal "..."' before 'interact'.`
300
+ });
301
+ }
302
+ if (hasGoal && !hasInteract) {
303
+ const { line, col } = nodePosition(nameNode ?? stateNode);
304
+ messages.push({
305
+ file,
306
+ line,
307
+ col,
308
+ severity: "warning",
309
+ code: "W012",
310
+ message: `'goal' is only valid in an oriented state. Add 'interact' or remove 'goal' \u2014 the prettifier can adjust this.`
311
+ });
312
+ }
313
+ if (hasInteract && intentHandlers.length === 0) {
314
+ const { line, col } = nodePosition(nameNode ?? stateNode);
315
+ messages.push({
316
+ file,
317
+ line,
318
+ col,
319
+ severity: "error",
320
+ code: "E009",
321
+ message: `Oriented state '${stateName}' has no 'on intent' handlers. At least one is required.`
322
+ });
323
+ }
324
+ const seenIntents = /* @__PURE__ */ new Map();
325
+ for (const handler of intentHandlers) {
326
+ const intentNode = handler.childForFieldName("intent");
327
+ if (!intentNode) continue;
328
+ const label = intentNode.text.replace(/^"|"$/g, "");
329
+ if (seenIntents.has(label)) {
330
+ const { line, col } = nodePosition(handler);
331
+ messages.push({
332
+ file,
333
+ line,
334
+ col,
335
+ severity: "error",
336
+ code: "W008",
337
+ message: `Duplicate 'on intent "${label}"' in state '${stateName}'. The FSM cannot route ambiguous intents \u2014 use a unique label for each handler.`
338
+ });
339
+ } else {
340
+ seenIntents.set(label, true);
341
+ }
342
+ }
343
+ for (const handler of intentHandlers) {
344
+ for (const t of handler.descendantsOfType("transition_stmt")) {
345
+ const targetNode = t.childForFieldName("state");
346
+ if (targetNode?.text === stateName) {
347
+ const { line, col } = nodePosition(handler);
348
+ messages.push({
349
+ file,
350
+ line,
351
+ col,
352
+ severity: "warning",
353
+ code: "W011",
354
+ 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?`
355
+ });
356
+ }
357
+ }
358
+ }
359
+ if (stateName === "init" || stateName === "end") {
360
+ const { line, col } = nodePosition(nameNode ?? stateNode);
361
+ if (stateName === "init") {
362
+ messages.push({
363
+ file,
364
+ line,
365
+ col,
366
+ severity: "info",
367
+ code: "I001",
368
+ message: `State 'init' overrides the kernel's default entry lifecycle. The kernel will use this definition instead of its built-in init sequence.`
369
+ });
370
+ } else {
371
+ messages.push({
372
+ file,
373
+ line,
374
+ col,
375
+ severity: "info",
376
+ code: "I002",
377
+ message: `State 'end' overrides the kernel's default terminal lifecycle. The kernel will treat this state as the canonical exit point.`
378
+ });
379
+ }
380
+ }
381
+ if (!KERNEL_LIFECYCLE_NAMES.includes(stateName)) {
382
+ const closest = closestName(stateName, KERNEL_LIFECYCLE_NAMES);
383
+ if (closest) {
384
+ const { line, col } = nodePosition(nameNode ?? stateNode);
385
+ messages.push({
386
+ file,
387
+ line,
388
+ col,
389
+ severity: "hint",
390
+ code: "H001",
391
+ message: `State name '${stateName}' resembles the kernel lifecycle name '${closest}'. If this is intentional, ignore this hint.`
392
+ });
393
+ }
394
+ }
395
+ }
396
+ for (const parallelNode of nodesOfType(tree, "parallel_stmt")) {
397
+ if (parallelNode.hasError) continue;
398
+ if (parallelNode.descendantsOfType("run_stmt").length === 0) {
399
+ const { line, col } = nodePosition(parallelNode);
400
+ let stateName = "?";
401
+ let ancestor = parallelNode.parent;
402
+ while (ancestor && ancestor.type !== "state_decl") ancestor = ancestor.parent;
403
+ if (ancestor) stateName = ancestor.childForFieldName("name")?.text ?? "?";
404
+ messages.push({
405
+ file,
406
+ line,
407
+ col,
408
+ severity: "warning",
409
+ code: "E010",
410
+ message: `'parallel' block in state '${stateName}' has no 'run' statements and does nothing.`
411
+ });
412
+ }
413
+ }
414
+ for (const afterNode of nodesOfType(tree, "after_stmt")) {
415
+ if (afterNode.hasError) continue;
416
+ const countNode = afterNode.childForFieldName("count");
417
+ if (countNode?.text === "0") {
418
+ const { line, col } = nodePosition(countNode);
419
+ messages.push({
420
+ file,
421
+ line,
422
+ col,
423
+ severity: "error",
424
+ code: "E011",
425
+ message: `'after 0' is invalid \u2014 prompts must be \u2265 1. Use 'after 1' for the next prompt.`
426
+ });
427
+ }
428
+ }
429
+ for (const stmtNode of nodesOfType(tree, "goal_stmt")) {
430
+ const strNode = stmtNode.descendantsOfType("quoted_string").find((n) => n !== null);
431
+ if (!strNode) continue;
432
+ const content = strNode.text.replace(/^"|"$/g, "");
433
+ if (content.length > 280) {
434
+ const { line, col } = nodePosition(strNode);
435
+ messages.push({
436
+ file,
437
+ line,
438
+ col,
439
+ severity: "warning",
440
+ code: "W002",
441
+ message: `Text block exceeds 280 characters (${content.length}). Consider using 'teach' to load long goal text from an external file.`
442
+ });
443
+ }
444
+ }
445
+ for (const stmtNode of nodesOfType(tree, "guide_stmt")) {
446
+ const strNode = stmtNode.descendantsOfType("quoted_string").find((n) => n !== null);
447
+ if (!strNode) continue;
448
+ const content = strNode.text.replace(/^"|"$/g, "");
449
+ if (content.length > 280) {
450
+ const { line, col } = nodePosition(strNode);
451
+ let p = stmtNode.parent;
452
+ while (p && p.type !== "state_decl") p = p.parent;
453
+ const stateName = p?.childForFieldName("name")?.text ?? "?";
454
+ messages.push({
455
+ file,
456
+ line,
457
+ col,
458
+ severity: "warning",
459
+ code: "W010",
460
+ message: `'guide' text in state '${stateName}' is ${content.length} characters (limit: 280). Consider using an external file.`
461
+ });
462
+ }
463
+ }
464
+ const definedStates = new Set(
465
+ nodesOfType(tree, "state_decl").map((n) => n.childForFieldName("name")?.text).filter(Boolean)
466
+ );
467
+ if (docPath) {
468
+ try {
469
+ collectMergedStates(tree, dirname(docPath), definedStates);
470
+ } catch {
471
+ }
472
+ }
473
+ const stateNameList = Array.from(definedStates);
474
+ for (const transitionNode of nodesOfType(tree, "transition_stmt")) {
475
+ const stateNode = transitionNode.childForFieldName("state");
476
+ if (!stateNode) continue;
477
+ const target = stateNode.text;
478
+ if (!definedStates.has(target)) {
479
+ const { line, col } = nodePosition(stateNode);
480
+ const isExternal = target.includes(".");
481
+ const hint = !isExternal ? closestName(target, stateNameList) : void 0;
482
+ messages.push({
483
+ file,
484
+ line,
485
+ col,
486
+ severity: isExternal ? "warning" : "error",
487
+ code: isExternal ? "W005" : "E005",
488
+ message: isExternal ? `State '${target}' is not defined locally (assuming external flow reference).` : `State '${target}' is not defined in this file.`,
489
+ ...hint ? { hint: `did you mean '${hint}'?` } : {}
490
+ });
491
+ }
492
+ }
493
+ for (const interactNode of nodesOfType(tree, "interact_stmt")) {
494
+ let ancestor = interactNode.parent;
495
+ while (ancestor && ancestor.type !== "state_decl") ancestor = ancestor.parent;
496
+ if (!ancestor) continue;
497
+ const hasHandlers = ancestor.descendantsOfType("intent_handler").length + ancestor.descendantsOfType("offtopic_handler").length > 0;
498
+ if (!hasHandlers) {
499
+ const { line, col } = nodePosition(interactNode);
500
+ messages.push({
501
+ file,
502
+ line,
503
+ col,
504
+ severity: "warning",
505
+ code: "W006",
506
+ message: "This state calls interact but has no handlers (on intent/offtopic). This will trap the agent."
507
+ });
508
+ }
509
+ }
510
+ const fsmResult = parseBehaviorFile(text);
511
+ if (fsmResult.ok === null) {
512
+ const firstMsg = fsmResult.diagnostics[0]?.message ?? "Parse error";
513
+ messages.push({
514
+ file,
515
+ line: 1,
516
+ col: 1,
517
+ severity: "error",
518
+ code: "E006",
519
+ message: `Parse error: ${firstMsg}`
520
+ });
521
+ return messages;
522
+ }
523
+ const fsm = fsmResult.ok;
524
+ const allTargets = new Set(collectTransitionTargets(fsm.states.flatMap((s) => s.body)));
525
+ for (const state of fsm.states) {
526
+ const hasOutgoing = collectTransitionTargets(state.body).length > 0;
527
+ const hasIncoming = allTargets.has(state.name);
528
+ if (!hasIncoming && !hasOutgoing) {
529
+ messages.push({
530
+ file,
531
+ line: 1,
532
+ col: 1,
533
+ severity: "warning",
534
+ code: "W001",
535
+ message: `State "${state.name}" has no transitions`
536
+ });
537
+ }
538
+ }
539
+ for (let i = 1; i < fsm.states.length; i++) {
540
+ const state = fsm.states[i];
541
+ if (!allTargets.has(state.name)) {
542
+ const stateNode = nodesOfType(tree, "state_decl").find((n) => n.childForFieldName("name")?.text === state.name);
543
+ const pos = stateNode ? nodePosition(stateNode) : { line: 1, col: 1 };
544
+ messages.push({
545
+ file,
546
+ line: pos.line,
547
+ col: pos.col,
548
+ severity: "warning",
549
+ code: "W009",
550
+ message: `State '${state.name}' is unreachable \u2014 no other state transitions to it.`
551
+ });
552
+ }
553
+ }
554
+ return messages;
555
+ }
556
+ async function createLinter() {
557
+ return { lintDescription, lintBehavior };
558
+ }
559
+
560
+ // src/zip.ts
561
+ import JSZip from "jszip";
562
+ import { readFile, writeFile } from "fs/promises";
563
+ var MAX_ZIP_SIZE = 500 * 1024 * 1024;
564
+ var MAX_COMPRESSION_RATIO = 100;
565
+ async function readZip(filePath) {
566
+ const data = await readFile(filePath);
567
+ return JSZip.loadAsync(data);
568
+ }
569
+ async function validateZipBomb(filePath) {
570
+ const data = await readFile(filePath);
571
+ const zip = await JSZip.loadAsync(data);
572
+ let totalUncompressed = 0;
573
+ zip.forEach((_relativePath, file) => {
574
+ if (!file.dir) {
575
+ totalUncompressed += file._data?.uncompressedSize || 0;
576
+ }
577
+ });
578
+ const compressedSize = data.length;
579
+ const ratio = totalUncompressed / compressedSize;
580
+ if (totalUncompressed > MAX_ZIP_SIZE) {
581
+ throw new Error(`ZIP uncompressed size exceeds 500MB limit: ${totalUncompressed}`);
582
+ }
583
+ if (ratio > MAX_COMPRESSION_RATIO) {
584
+ throw new Error(`ZIP compression ratio exceeds 100x limit: ${ratio.toFixed(1)}x`);
585
+ }
586
+ return true;
587
+ }
588
+ async function validateMagicBytes(filePath) {
589
+ const data = await readFile(filePath);
590
+ const magic = data.subarray(0, 4);
591
+ if (!(magic[0] === 80 && magic[1] === 75 && magic[2] === 3 && magic[3] === 4)) {
592
+ throw new Error("File is not a valid ZIP (invalid magic bytes)");
593
+ }
594
+ return true;
595
+ }
596
+ async function writeZip(zip, outPath) {
597
+ const data = await zip.generateAsync({ type: "arraybuffer" });
598
+ await writeFile(outPath, Buffer.from(data));
599
+ }
600
+
601
+ // src/pack.ts
602
+ import { readFile as readFile2, stat, readdir } from "fs/promises";
603
+ import { join, basename } from "path";
604
+ import { execSync } from "child_process";
605
+ import { createHash } from "crypto";
606
+ import JSZip2 from "jszip";
607
+
608
+ // src/schema.ts
609
+ var STD_BASE = "https://dot-agent.dev/schema/std/v1/";
610
+ function propertyTypeToSchema(pt) {
611
+ switch (pt.kind) {
612
+ case "primitive":
613
+ return { type: pt.value };
614
+ case "reference":
615
+ return pt.value.startsWith("std.") ? { $ref: `${STD_BASE}${pt.value.slice(4)}.json` } : { $ref: `#/$defs/${pt.value}` };
616
+ case "array":
617
+ return { type: "array", items: propertyTypeToSchema(pt.value) };
618
+ case "enum":
619
+ return { type: "string", enum: pt.value };
620
+ }
621
+ }
622
+ function typeDefinitionToSchema(td) {
623
+ const required = td.properties.filter((p) => !p.is_optional).map((p) => p.name);
624
+ const properties = {};
625
+ for (const p of td.properties) {
626
+ properties[p.name] = {
627
+ ...propertyTypeToSchema(p.type),
628
+ ...p.description ? { description: p.description } : {}
629
+ };
630
+ }
631
+ return {
632
+ $schema: "https://json-schema.org/draft/2020-12/schema",
633
+ "x-category": td.category.uri,
634
+ ...td.category.label ? { "x-category-label": td.category.label } : {},
635
+ ...td.concept ? { "x-concept": td.concept.uri } : {},
636
+ ...td.concept?.label ? { "x-concept-label": td.concept.label } : {},
637
+ type: "object",
638
+ properties,
639
+ ...required.length ? { required } : {}
640
+ };
641
+ }
642
+ function refFromAnnotatedRef(ref) {
643
+ return ref.name.startsWith("std.") ? { $ref: `${STD_BASE}${ref.name.slice(4)}.json` } : { $ref: `#/$defs/${ref.name}` };
644
+ }
645
+ function buildTypesJson(df) {
646
+ if (df.types.length === 0 && df.input.length === 0 && df.output.length === 0) return null;
647
+ const defs = {};
648
+ for (const td of df.types) {
649
+ defs[td.name] = typeDefinitionToSchema(td);
650
+ }
651
+ return JSON.stringify(
652
+ {
653
+ ...df.input.length ? { input: df.input.map(refFromAnnotatedRef) } : {},
654
+ ...df.output.length ? { output: df.output.map(refFromAnnotatedRef) } : {},
655
+ ...Object.keys(defs).length ? { $defs: defs } : {}
656
+ },
657
+ null,
658
+ 2
659
+ );
660
+ }
661
+
662
+ // src/pack.ts
663
+ function gitDescribeTags() {
664
+ try {
665
+ return execSync("git describe --tags --abbrev=0", { stdio: "pipe", encoding: "utf-8" }).trim();
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ function gitRevParseShort() {
671
+ try {
672
+ return execSync("git rev-parse --short HEAD", { stdio: "pipe", encoding: "utf-8" }).trim();
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ async function resolveVersion(explicit) {
678
+ if (explicit) return explicit;
679
+ return gitDescribeTags() ?? "v1.0.0";
680
+ }
681
+ async function resolveCommit(explicit) {
682
+ if (explicit) return explicit;
683
+ return gitRevParseShort() ?? void 0;
684
+ }
685
+ async function collectFiles(dir) {
686
+ const files = /* @__PURE__ */ new Map();
687
+ async function walk(subdir, prefix = "") {
688
+ try {
689
+ const entries = await readdir(subdir);
690
+ for (const entry of entries) {
691
+ if (entry === ".gitkeep" || entry.startsWith(".")) continue;
692
+ const fullPath = join(subdir, entry);
693
+ const stats = await stat(fullPath);
694
+ const relativePath = prefix ? `${prefix}/${entry}` : entry;
695
+ if (stats.isDirectory()) {
696
+ await walk(fullPath, relativePath);
697
+ } else {
698
+ files.set(relativePath, await readFile2(fullPath, "utf-8"));
699
+ }
700
+ }
701
+ } catch {
702
+ }
703
+ }
704
+ files.set("agent.description", await readFile2(join(dir, "agent.description"), "utf-8"));
705
+ files.set("agent.behavior", await readFile2(join(dir, "agent.behavior"), "utf-8"));
706
+ try {
707
+ files.set("SOUL.md", await readFile2(join(dir, "SOUL.md"), "utf-8"));
708
+ } catch {
709
+ }
710
+ await walk(join(dir, "behaviors"), "behaviors");
711
+ await walk(join(dir, "guides"), "guides");
712
+ await walk(join(dir, "knowledge"), "knowledge");
713
+ return files;
714
+ }
715
+ async function pack(options = {}) {
716
+ const dir = options.dir ?? process.cwd();
717
+ const outPath = options.out ?? join(dir, `${basename(dir)}.agent`);
718
+ let descriptionText;
719
+ let behaviorText;
720
+ try {
721
+ descriptionText = await readFile2(join(dir, "agent.description"), "utf-8");
722
+ } catch {
723
+ throw new Error("E003: File agent.description not found");
724
+ }
725
+ try {
726
+ behaviorText = await readFile2(join(dir, "agent.behavior"), "utf-8");
727
+ } catch {
728
+ throw new Error("E007: File agent.behavior not found");
729
+ }
730
+ const [descriptionMessages, behaviorMessages] = await Promise.all([
731
+ lintDescription(descriptionText),
732
+ lintBehavior(behaviorText)
733
+ ]);
734
+ const allMessages = [...descriptionMessages, ...behaviorMessages];
735
+ const errors = allMessages.filter((m) => m.severity === "error");
736
+ const warnings = allMessages.filter((m) => m.severity === "warning");
737
+ if (errors.length > 0) {
738
+ throw new Error(
739
+ `Lint failed:
740
+ ${errors.map((e) => ` ${e.file}:${e.line}:${e.col} ${e.code} ${e.message}`).join("\n")}`
741
+ );
742
+ }
743
+ await initBehaviorParser();
744
+ const descResult = parseDescriptionFile(descriptionText);
745
+ if (descResult.ok === null) {
746
+ const firstError = descResult.diagnostics.find((d) => d.severity === "error");
747
+ throw new Error(`E_DESC: ${firstError?.message ?? "parse failed"}`);
748
+ }
749
+ const df = descResult.ok;
750
+ const version = await resolveVersion(options.version);
751
+ const commit = await resolveCommit(options.commit);
752
+ const allFiles = await collectFiles(dir);
753
+ const contentForHash = Array.from(allFiles.values()).join("");
754
+ const sha256 = createHash("sha256").update(contentForHash).digest("hex");
755
+ const namespace = df.agent.domain ?? "unknown";
756
+ const id = buildId({ namespace, name: df.agent.name, version, digest: commit });
757
+ const typesJson = buildTypesJson(df);
758
+ const integrity = {
759
+ sha256,
760
+ ...typesJson ? { types: ".agent/types.json" } : {},
761
+ files: ".agent/files.json"
762
+ };
763
+ const aboutme = buildAboutme({
764
+ id,
765
+ name: df.agent.name,
766
+ description: df.description ?? "",
767
+ version,
768
+ domain: df.agent.domain ?? "",
769
+ license: df.agent.license ?? "",
770
+ persona: df.persona ?? "SOUL.md",
771
+ purpose: "unknown",
772
+ compiler: "dot-agent/1.0.0",
773
+ commit,
774
+ capabilities: df.capabilities.map((c) => ({ id: c.name, description: c.description ?? "" })),
775
+ requires: df.requires,
776
+ integrity
777
+ });
778
+ const zip = new JSZip2();
779
+ const agentFolder = zip.folder(".agent");
780
+ agentFolder.file("aboutme.json", aboutmeToJson(aboutme));
781
+ if (typesJson) {
782
+ agentFolder.file("types.json", typesJson);
783
+ }
784
+ const gitkeepPaths = /* @__PURE__ */ new Set(["behaviors/.gitkeep", "guides/.gitkeep", "knowledge/.gitkeep"]);
785
+ const filesJson = {
786
+ description: "agent.description",
787
+ behavior: "agent.behavior",
788
+ behaviors: Array.from(allFiles.keys()).filter((f) => f.startsWith("behaviors/") && !gitkeepPaths.has(f)),
789
+ guides: Array.from(allFiles.keys()).filter((f) => f.startsWith("guides/") && !gitkeepPaths.has(f)),
790
+ knowledge: Array.from(allFiles.keys()).filter((f) => f.startsWith("knowledge/") && !gitkeepPaths.has(f))
791
+ };
792
+ agentFolder.file("files.json", JSON.stringify(filesJson, null, 2));
793
+ for (const [path, content] of allFiles) {
794
+ if (!gitkeepPaths.has(path)) zip.file(path, content);
795
+ }
796
+ await writeZip(zip, outPath);
797
+ return { path: outPath, id, warnings };
798
+ }
799
+ export {
800
+ aboutmeToJson,
801
+ buildAboutme,
802
+ buildId,
803
+ collectFiles,
804
+ createLinter,
805
+ createZip,
806
+ extractDigest,
807
+ extractFiles,
808
+ extractName,
809
+ getBehaviorScxml,
810
+ getContextNode,
811
+ initBehaviorParser,
812
+ initParsers,
813
+ lintBehavior,
814
+ lintDescription,
815
+ nodeAtOffset,
816
+ nodeToRange,
817
+ nodesOfType,
818
+ pack,
819
+ parse,
820
+ parseAboutme,
821
+ parseBehaviorFile,
822
+ parseDescriptionFile,
823
+ parseId,
824
+ parseSync,
825
+ positionToOffset,
826
+ readZip,
827
+ validateMagicBytes,
828
+ validateZipBomb,
829
+ writeZip
830
+ };
831
+ //# sourceMappingURL=index.js.map