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