@axintai/compiler 0.2.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/LICENSE +190 -0
- package/README.md +319 -0
- package/dist/cli/index.js +868 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +233 -0
- package/dist/core/index.js +776 -0
- package/dist/core/index.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.js +1320 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/sdk/index.d.ts +179 -0
- package/dist/sdk/index.js +41 -0
- package/dist/sdk/index.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,1320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp/server.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
11
|
+
import { resolve, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
|
|
14
|
+
// src/core/compiler.ts
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
|
|
17
|
+
// src/core/parser.ts
|
|
18
|
+
import ts from "typescript";
|
|
19
|
+
|
|
20
|
+
// src/core/types.ts
|
|
21
|
+
var PARAM_TYPES = /* @__PURE__ */ new Set([
|
|
22
|
+
"string",
|
|
23
|
+
"int",
|
|
24
|
+
"double",
|
|
25
|
+
"float",
|
|
26
|
+
"boolean",
|
|
27
|
+
"date",
|
|
28
|
+
"duration",
|
|
29
|
+
"url"
|
|
30
|
+
]);
|
|
31
|
+
var LEGACY_PARAM_ALIASES = {
|
|
32
|
+
number: "int"
|
|
33
|
+
};
|
|
34
|
+
var SWIFT_TYPE_MAP = {
|
|
35
|
+
string: "String",
|
|
36
|
+
int: "Int",
|
|
37
|
+
double: "Double",
|
|
38
|
+
float: "Float",
|
|
39
|
+
boolean: "Bool",
|
|
40
|
+
date: "Date",
|
|
41
|
+
duration: "Measurement<UnitDuration>",
|
|
42
|
+
url: "URL"
|
|
43
|
+
};
|
|
44
|
+
function irTypeToSwift(type) {
|
|
45
|
+
switch (type.kind) {
|
|
46
|
+
case "primitive":
|
|
47
|
+
return SWIFT_TYPE_MAP[type.value];
|
|
48
|
+
case "array":
|
|
49
|
+
return `[${irTypeToSwift(type.elementType)}]`;
|
|
50
|
+
case "optional":
|
|
51
|
+
return `${irTypeToSwift(type.innerType)}?`;
|
|
52
|
+
case "entity":
|
|
53
|
+
return type.entityName;
|
|
54
|
+
case "enum":
|
|
55
|
+
return type.name;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/core/parser.ts
|
|
60
|
+
function parseIntentSource(source, filePath = "<stdin>") {
|
|
61
|
+
const sourceFile = ts.createSourceFile(
|
|
62
|
+
filePath,
|
|
63
|
+
source,
|
|
64
|
+
ts.ScriptTarget.Latest,
|
|
65
|
+
true,
|
|
66
|
+
// setParentNodes
|
|
67
|
+
ts.ScriptKind.TS
|
|
68
|
+
);
|
|
69
|
+
const defineIntentCall = findDefineIntentCall(sourceFile);
|
|
70
|
+
if (!defineIntentCall) {
|
|
71
|
+
throw new ParserError(
|
|
72
|
+
"AX001",
|
|
73
|
+
`No defineIntent() call found in ${filePath}`,
|
|
74
|
+
filePath,
|
|
75
|
+
void 0,
|
|
76
|
+
"Ensure your file contains a `defineIntent({ ... })` call."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const arg = defineIntentCall.arguments[0];
|
|
80
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
|
81
|
+
throw new ParserError(
|
|
82
|
+
"AX001",
|
|
83
|
+
"defineIntent() must be called with an object literal",
|
|
84
|
+
filePath,
|
|
85
|
+
posOf(sourceFile, defineIntentCall),
|
|
86
|
+
"Pass an object: defineIntent({ name, title, description, params, perform })"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const props = propertyMap(arg);
|
|
90
|
+
const name = readStringLiteral(props.get("name"));
|
|
91
|
+
const title = readStringLiteral(props.get("title"));
|
|
92
|
+
const description = readStringLiteral(props.get("description"));
|
|
93
|
+
const domain = readStringLiteral(props.get("domain"));
|
|
94
|
+
const category = readStringLiteral(props.get("category"));
|
|
95
|
+
const isDiscoverable = readBooleanLiteral(props.get("isDiscoverable"));
|
|
96
|
+
if (!name) {
|
|
97
|
+
throw new ParserError(
|
|
98
|
+
"AX002",
|
|
99
|
+
"Missing required field: name",
|
|
100
|
+
filePath,
|
|
101
|
+
posOf(sourceFile, arg),
|
|
102
|
+
'Add a name field: name: "MyIntent"'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!title) {
|
|
106
|
+
throw new ParserError(
|
|
107
|
+
"AX003",
|
|
108
|
+
"Missing required field: title",
|
|
109
|
+
filePath,
|
|
110
|
+
posOf(sourceFile, arg),
|
|
111
|
+
'Add a title field: title: "My Intent Title"'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!description) {
|
|
115
|
+
throw new ParserError(
|
|
116
|
+
"AX004",
|
|
117
|
+
"Missing required field: description",
|
|
118
|
+
filePath,
|
|
119
|
+
posOf(sourceFile, arg),
|
|
120
|
+
'Add a description field: description: "What this intent does"'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const paramsNode = props.get("params");
|
|
124
|
+
const parameters = paramsNode ? extractParameters(paramsNode, filePath, sourceFile) : [];
|
|
125
|
+
const performNode = props.get("perform");
|
|
126
|
+
const returnType = inferReturnType(performNode);
|
|
127
|
+
const entitlementsNode = props.get("entitlements");
|
|
128
|
+
const entitlements = readStringArray(entitlementsNode);
|
|
129
|
+
const infoPlistNode = props.get("infoPlistKeys");
|
|
130
|
+
const infoPlistKeys = readStringRecord(infoPlistNode);
|
|
131
|
+
return {
|
|
132
|
+
name,
|
|
133
|
+
title,
|
|
134
|
+
description,
|
|
135
|
+
domain: domain || void 0,
|
|
136
|
+
category: category || void 0,
|
|
137
|
+
parameters,
|
|
138
|
+
returnType,
|
|
139
|
+
sourceFile: filePath,
|
|
140
|
+
entitlements: entitlements.length > 0 ? entitlements : void 0,
|
|
141
|
+
infoPlistKeys: Object.keys(infoPlistKeys).length > 0 ? infoPlistKeys : void 0,
|
|
142
|
+
isDiscoverable: isDiscoverable ?? void 0
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function findDefineIntentCall(node) {
|
|
146
|
+
let found;
|
|
147
|
+
const visit = (n) => {
|
|
148
|
+
if (found) return;
|
|
149
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "defineIntent") {
|
|
150
|
+
found = n;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
ts.forEachChild(n, visit);
|
|
154
|
+
};
|
|
155
|
+
visit(node);
|
|
156
|
+
return found;
|
|
157
|
+
}
|
|
158
|
+
function propertyMap(obj) {
|
|
159
|
+
const map = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const prop of obj.properties) {
|
|
161
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
162
|
+
const key = propertyKeyName(prop.name);
|
|
163
|
+
if (key) map.set(key, prop.initializer);
|
|
164
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
165
|
+
map.set(prop.name.text, prop.name);
|
|
166
|
+
} else if (ts.isMethodDeclaration(prop)) {
|
|
167
|
+
const key = propertyKeyName(prop.name);
|
|
168
|
+
if (key) map.set(key, prop);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}
|
|
173
|
+
function propertyKeyName(name) {
|
|
174
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
175
|
+
if (ts.isStringLiteral(name)) return name.text;
|
|
176
|
+
if (ts.isNumericLiteral(name)) return name.text;
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
function readStringLiteral(node) {
|
|
180
|
+
if (!node) return null;
|
|
181
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
182
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function readBooleanLiteral(node) {
|
|
186
|
+
if (!node) return void 0;
|
|
187
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
188
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
function readStringArray(node) {
|
|
192
|
+
if (!node || !ts.isArrayLiteralExpression(node)) return [];
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const el of node.elements) {
|
|
195
|
+
const s = readStringLiteral(el);
|
|
196
|
+
if (s !== null) out.push(s);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function readStringRecord(node) {
|
|
201
|
+
if (!node || !ts.isObjectLiteralExpression(node)) return {};
|
|
202
|
+
const rec = {};
|
|
203
|
+
for (const prop of node.properties) {
|
|
204
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
205
|
+
const key = propertyKeyName(prop.name);
|
|
206
|
+
const val = readStringLiteral(prop.initializer);
|
|
207
|
+
if (key && val !== null) rec[key] = val;
|
|
208
|
+
}
|
|
209
|
+
return rec;
|
|
210
|
+
}
|
|
211
|
+
function extractParameters(node, filePath, sourceFile) {
|
|
212
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
213
|
+
throw new ParserError(
|
|
214
|
+
"AX006",
|
|
215
|
+
"`params` must be an object literal",
|
|
216
|
+
filePath,
|
|
217
|
+
posOf(sourceFile, node),
|
|
218
|
+
"Use params: { name: param.string(...), ... }"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const params = [];
|
|
222
|
+
for (const prop of node.properties) {
|
|
223
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
224
|
+
const paramName = propertyKeyName(prop.name);
|
|
225
|
+
if (!paramName) continue;
|
|
226
|
+
const { typeName, description, configObject } = extractParamCall(
|
|
227
|
+
prop.initializer,
|
|
228
|
+
filePath,
|
|
229
|
+
sourceFile
|
|
230
|
+
);
|
|
231
|
+
const resolvedType = resolveParamType(typeName, filePath, sourceFile, prop);
|
|
232
|
+
const isOptional = configObject ? readBooleanLiteral(configObject.get("required")) === false : false;
|
|
233
|
+
const defaultExpr = configObject?.get("default");
|
|
234
|
+
const defaultValue = defaultExpr ? evaluateLiteral(defaultExpr) : void 0;
|
|
235
|
+
const titleFromConfig = configObject ? readStringLiteral(configObject.get("title")) : null;
|
|
236
|
+
const irType = isOptional ? {
|
|
237
|
+
kind: "optional",
|
|
238
|
+
innerType: { kind: "primitive", value: resolvedType }
|
|
239
|
+
} : { kind: "primitive", value: resolvedType };
|
|
240
|
+
params.push({
|
|
241
|
+
name: paramName,
|
|
242
|
+
type: irType,
|
|
243
|
+
title: titleFromConfig || prettyTitle(paramName),
|
|
244
|
+
description,
|
|
245
|
+
isOptional,
|
|
246
|
+
defaultValue
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return params;
|
|
250
|
+
}
|
|
251
|
+
function extractParamCall(expr, filePath, sourceFile) {
|
|
252
|
+
if (!ts.isCallExpression(expr)) {
|
|
253
|
+
throw new ParserError(
|
|
254
|
+
"AX007",
|
|
255
|
+
"Parameter value must be a call to a param.* helper",
|
|
256
|
+
filePath,
|
|
257
|
+
posOf(sourceFile, expr),
|
|
258
|
+
"Use param.string(...), param.int(...), param.date(...), etc."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (!ts.isPropertyAccessExpression(expr.expression) || !ts.isIdentifier(expr.expression.expression) || expr.expression.expression.text !== "param") {
|
|
262
|
+
throw new ParserError(
|
|
263
|
+
"AX007",
|
|
264
|
+
"Parameter value must be a call to a param.* helper",
|
|
265
|
+
filePath,
|
|
266
|
+
posOf(sourceFile, expr),
|
|
267
|
+
"Use param.string(...), param.int(...), param.date(...), etc."
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const typeName = expr.expression.name.text;
|
|
271
|
+
const descriptionArg = expr.arguments[0];
|
|
272
|
+
const configArg = expr.arguments[1];
|
|
273
|
+
const description = descriptionArg ? readStringLiteral(descriptionArg) : null;
|
|
274
|
+
if (description === null) {
|
|
275
|
+
throw new ParserError(
|
|
276
|
+
"AX008",
|
|
277
|
+
`param.${typeName}() requires a string description as the first argument`,
|
|
278
|
+
filePath,
|
|
279
|
+
posOf(sourceFile, expr),
|
|
280
|
+
`Example: param.${typeName}("Human-readable description")`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const configObject = configArg && ts.isObjectLiteralExpression(configArg) ? propertyMap(configArg) : null;
|
|
284
|
+
return { typeName, description, configObject };
|
|
285
|
+
}
|
|
286
|
+
function resolveParamType(typeName, filePath, sourceFile, node) {
|
|
287
|
+
if (PARAM_TYPES.has(typeName)) {
|
|
288
|
+
return typeName;
|
|
289
|
+
}
|
|
290
|
+
if (typeName in LEGACY_PARAM_ALIASES) {
|
|
291
|
+
return LEGACY_PARAM_ALIASES[typeName];
|
|
292
|
+
}
|
|
293
|
+
throw new ParserError(
|
|
294
|
+
"AX005",
|
|
295
|
+
`Unknown param type: param.${typeName}`,
|
|
296
|
+
filePath,
|
|
297
|
+
posOf(sourceFile, node),
|
|
298
|
+
`Supported types: ${[...PARAM_TYPES].join(", ")}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
function evaluateLiteral(node) {
|
|
302
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
303
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
304
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
305
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
306
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
307
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
308
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
|
|
309
|
+
return -Number(node.operand.text);
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
function inferReturnType(performNode) {
|
|
314
|
+
const defaultType = { kind: "primitive", value: "string" };
|
|
315
|
+
if (!performNode) return defaultType;
|
|
316
|
+
if (ts.isMethodDeclaration(performNode)) {
|
|
317
|
+
return inferFromReturnStatements(performNode.body);
|
|
318
|
+
}
|
|
319
|
+
if (ts.isArrowFunction(performNode)) {
|
|
320
|
+
if (performNode.body && ts.isBlock(performNode.body)) {
|
|
321
|
+
return inferFromReturnStatements(performNode.body);
|
|
322
|
+
}
|
|
323
|
+
return inferFromExpression(performNode.body);
|
|
324
|
+
}
|
|
325
|
+
if (ts.isFunctionExpression(performNode)) {
|
|
326
|
+
return inferFromReturnStatements(performNode.body);
|
|
327
|
+
}
|
|
328
|
+
return defaultType;
|
|
329
|
+
}
|
|
330
|
+
function inferFromReturnStatements(block) {
|
|
331
|
+
const defaultType = { kind: "primitive", value: "string" };
|
|
332
|
+
if (!block) return defaultType;
|
|
333
|
+
let inferred;
|
|
334
|
+
const visit = (n) => {
|
|
335
|
+
if (inferred) return;
|
|
336
|
+
if (ts.isReturnStatement(n) && n.expression) {
|
|
337
|
+
inferred = inferFromExpression(n.expression);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
ts.forEachChild(n, visit);
|
|
344
|
+
};
|
|
345
|
+
visit(block);
|
|
346
|
+
return inferred ?? defaultType;
|
|
347
|
+
}
|
|
348
|
+
function inferFromExpression(expr) {
|
|
349
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
350
|
+
return { kind: "primitive", value: "string" };
|
|
351
|
+
}
|
|
352
|
+
if (ts.isNumericLiteral(expr)) {
|
|
353
|
+
return expr.text.includes(".") ? { kind: "primitive", value: "double" } : { kind: "primitive", value: "int" };
|
|
354
|
+
}
|
|
355
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
356
|
+
return { kind: "primitive", value: "boolean" };
|
|
357
|
+
}
|
|
358
|
+
return { kind: "primitive", value: "string" };
|
|
359
|
+
}
|
|
360
|
+
function prettyTitle(name) {
|
|
361
|
+
const spaced = name.replace(/([A-Z])/g, " $1").trim();
|
|
362
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
363
|
+
}
|
|
364
|
+
function posOf(sourceFile, node) {
|
|
365
|
+
try {
|
|
366
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
367
|
+
return line + 1;
|
|
368
|
+
} catch {
|
|
369
|
+
return void 0;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
var ParserError = class extends Error {
|
|
373
|
+
constructor(code, message, file, line, suggestion) {
|
|
374
|
+
super(message);
|
|
375
|
+
this.code = code;
|
|
376
|
+
this.file = file;
|
|
377
|
+
this.line = line;
|
|
378
|
+
this.suggestion = suggestion;
|
|
379
|
+
this.name = "ParserError";
|
|
380
|
+
}
|
|
381
|
+
code;
|
|
382
|
+
file;
|
|
383
|
+
line;
|
|
384
|
+
suggestion;
|
|
385
|
+
format() {
|
|
386
|
+
let output = `
|
|
387
|
+
error[${this.code}]: ${this.message}
|
|
388
|
+
`;
|
|
389
|
+
if (this.file) output += ` --> ${this.file}`;
|
|
390
|
+
if (this.line) output += `:${this.line}`;
|
|
391
|
+
output += "\n";
|
|
392
|
+
if (this.suggestion) {
|
|
393
|
+
output += ` = help: ${this.suggestion}
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
return output;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// src/core/generator.ts
|
|
401
|
+
function escapeSwiftString(s) {
|
|
402
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
403
|
+
}
|
|
404
|
+
function escapeXml(s) {
|
|
405
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
406
|
+
}
|
|
407
|
+
function generateSwift(intent) {
|
|
408
|
+
const lines = [];
|
|
409
|
+
lines.push(`// ${intent.name}Intent.swift`);
|
|
410
|
+
lines.push(`// Generated by Axint \u2014 https://github.com/agenticempire/axint`);
|
|
411
|
+
lines.push(`// Do not edit manually. Re-run \`axint compile\` to regenerate.`);
|
|
412
|
+
lines.push(``);
|
|
413
|
+
lines.push(`import AppIntents`);
|
|
414
|
+
lines.push(`import Foundation`);
|
|
415
|
+
lines.push(``);
|
|
416
|
+
lines.push(`struct ${intent.name}Intent: AppIntent {`);
|
|
417
|
+
lines.push(
|
|
418
|
+
` static let title: LocalizedStringResource = "${escapeSwiftString(intent.title)}"`
|
|
419
|
+
);
|
|
420
|
+
lines.push(
|
|
421
|
+
` static let description: IntentDescription = IntentDescription("${escapeSwiftString(intent.description)}")`
|
|
422
|
+
);
|
|
423
|
+
if (intent.isDiscoverable !== void 0) {
|
|
424
|
+
lines.push(` static let isDiscoverable: Bool = ${intent.isDiscoverable}`);
|
|
425
|
+
}
|
|
426
|
+
lines.push(``);
|
|
427
|
+
for (const param of intent.parameters) {
|
|
428
|
+
lines.push(generateParameter(param));
|
|
429
|
+
}
|
|
430
|
+
if (intent.parameters.length > 0) {
|
|
431
|
+
lines.push(``);
|
|
432
|
+
}
|
|
433
|
+
const returnTypeSignature = generateReturnSignature(intent.returnType);
|
|
434
|
+
lines.push(` func perform() async throws -> ${returnTypeSignature} {`);
|
|
435
|
+
lines.push(` // TODO: Implement your intent logic here.`);
|
|
436
|
+
if (intent.parameters.length > 0) {
|
|
437
|
+
const paramList = intent.parameters.map((p) => `\\(${p.name})`).join(", ");
|
|
438
|
+
lines.push(` // Parameters available: ${paramList}`);
|
|
439
|
+
}
|
|
440
|
+
lines.push(generatePerformReturn(intent.returnType));
|
|
441
|
+
lines.push(` }`);
|
|
442
|
+
lines.push(`}`);
|
|
443
|
+
lines.push(``);
|
|
444
|
+
return lines.join("\n");
|
|
445
|
+
}
|
|
446
|
+
function generateInfoPlistFragment(intent) {
|
|
447
|
+
const keys = intent.infoPlistKeys;
|
|
448
|
+
if (!keys || Object.keys(keys).length === 0) return void 0;
|
|
449
|
+
const lines = [];
|
|
450
|
+
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
451
|
+
lines.push(
|
|
452
|
+
`<!-- Info.plist fragment generated by Axint for ${intent.name}Intent -->`
|
|
453
|
+
);
|
|
454
|
+
lines.push(`<!-- Merge these keys into your app's Info.plist. -->`);
|
|
455
|
+
lines.push(`<plist version="1.0">`);
|
|
456
|
+
lines.push(`<dict>`);
|
|
457
|
+
for (const [key, desc] of Object.entries(keys)) {
|
|
458
|
+
lines.push(` <key>${escapeXml(key)}</key>`);
|
|
459
|
+
lines.push(` <string>${escapeXml(desc)}</string>`);
|
|
460
|
+
}
|
|
461
|
+
lines.push(`</dict>`);
|
|
462
|
+
lines.push(`</plist>`);
|
|
463
|
+
lines.push(``);
|
|
464
|
+
return lines.join("\n");
|
|
465
|
+
}
|
|
466
|
+
function generateEntitlementsFragment(intent) {
|
|
467
|
+
const ents = intent.entitlements;
|
|
468
|
+
if (!ents || ents.length === 0) return void 0;
|
|
469
|
+
const lines = [];
|
|
470
|
+
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
471
|
+
lines.push(
|
|
472
|
+
`<!-- Entitlements fragment generated by Axint for ${intent.name}Intent -->`
|
|
473
|
+
);
|
|
474
|
+
lines.push(`<!-- Merge these into your target's .entitlements file. -->`);
|
|
475
|
+
lines.push(`<!-- Note: entitlements requiring typed values (string/array) -->`);
|
|
476
|
+
lines.push(`<!-- need manual adjustment \u2014 defaults below are boolean true. -->`);
|
|
477
|
+
lines.push(
|
|
478
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`
|
|
479
|
+
);
|
|
480
|
+
lines.push(`<plist version="1.0">`);
|
|
481
|
+
lines.push(`<dict>`);
|
|
482
|
+
for (const ent of ents) {
|
|
483
|
+
lines.push(` <key>${escapeXml(ent)}</key>`);
|
|
484
|
+
lines.push(` <true/>`);
|
|
485
|
+
}
|
|
486
|
+
lines.push(`</dict>`);
|
|
487
|
+
lines.push(`</plist>`);
|
|
488
|
+
lines.push(``);
|
|
489
|
+
return lines.join("\n");
|
|
490
|
+
}
|
|
491
|
+
function generateParameter(param) {
|
|
492
|
+
const swiftType = irTypeToSwift(param.type);
|
|
493
|
+
const lines = [];
|
|
494
|
+
const attrs = [];
|
|
495
|
+
attrs.push(`title: "${escapeSwiftString(param.title)}"`);
|
|
496
|
+
if (param.description) {
|
|
497
|
+
attrs.push(`description: "${escapeSwiftString(param.description)}"`);
|
|
498
|
+
}
|
|
499
|
+
const decorator = ` @Parameter(${attrs.join(", ")})`;
|
|
500
|
+
lines.push(decorator);
|
|
501
|
+
if (param.defaultValue !== void 0) {
|
|
502
|
+
const defaultStr = formatSwiftDefault(param.defaultValue, param.type);
|
|
503
|
+
lines.push(` var ${param.name}: ${swiftType} = ${defaultStr}`);
|
|
504
|
+
} else {
|
|
505
|
+
lines.push(` var ${param.name}: ${swiftType}`);
|
|
506
|
+
}
|
|
507
|
+
lines.push(``);
|
|
508
|
+
return lines.join("\n");
|
|
509
|
+
}
|
|
510
|
+
function generateReturnSignature(type) {
|
|
511
|
+
if (type.kind === "primitive") {
|
|
512
|
+
const swift = irTypeToSwift(type);
|
|
513
|
+
return `some IntentResult & ReturnsValue<${swift}>`;
|
|
514
|
+
}
|
|
515
|
+
if (type.kind === "optional" && type.innerType.kind === "primitive") {
|
|
516
|
+
const swift = irTypeToSwift(type.innerType);
|
|
517
|
+
return `some IntentResult & ReturnsValue<${swift}>`;
|
|
518
|
+
}
|
|
519
|
+
return `some IntentResult`;
|
|
520
|
+
}
|
|
521
|
+
function generatePerformReturn(type) {
|
|
522
|
+
const indent = " ";
|
|
523
|
+
if (type.kind === "primitive") {
|
|
524
|
+
return `${indent}return .result(value: ${defaultLiteralFor(type.value)})`;
|
|
525
|
+
}
|
|
526
|
+
if (type.kind === "optional" && type.innerType.kind === "primitive") {
|
|
527
|
+
return `${indent}return .result(value: ${defaultLiteralFor(type.innerType.value)})`;
|
|
528
|
+
}
|
|
529
|
+
return `${indent}return .result()`;
|
|
530
|
+
}
|
|
531
|
+
function defaultLiteralFor(primitive) {
|
|
532
|
+
switch (primitive) {
|
|
533
|
+
case "string":
|
|
534
|
+
return `""`;
|
|
535
|
+
case "int":
|
|
536
|
+
return `0`;
|
|
537
|
+
case "double":
|
|
538
|
+
return `0.0`;
|
|
539
|
+
case "float":
|
|
540
|
+
return `Float(0)`;
|
|
541
|
+
case "boolean":
|
|
542
|
+
return `false`;
|
|
543
|
+
case "date":
|
|
544
|
+
return `Date()`;
|
|
545
|
+
case "duration":
|
|
546
|
+
return `Measurement<UnitDuration>(value: 0, unit: .seconds)`;
|
|
547
|
+
case "url":
|
|
548
|
+
return `URL(string: "about:blank")!`;
|
|
549
|
+
default:
|
|
550
|
+
return `""`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function formatSwiftDefault(value, _type) {
|
|
554
|
+
if (typeof value === "string") return `"${escapeSwiftString(value)}"`;
|
|
555
|
+
if (typeof value === "number") {
|
|
556
|
+
if (!Number.isFinite(value)) return `0`;
|
|
557
|
+
return `${value}`;
|
|
558
|
+
}
|
|
559
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
560
|
+
return `"${escapeSwiftString(String(value))}"`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/core/validator.ts
|
|
564
|
+
var MAX_PARAMETERS = 10;
|
|
565
|
+
var MAX_TITLE_LENGTH = 60;
|
|
566
|
+
function validateIntent(intent) {
|
|
567
|
+
const diagnostics = [];
|
|
568
|
+
if (!intent.name || !/^[A-Z][a-zA-Z0-9]*$/.test(intent.name)) {
|
|
569
|
+
diagnostics.push({
|
|
570
|
+
code: "AX100",
|
|
571
|
+
severity: "error",
|
|
572
|
+
message: `Intent name "${intent.name}" must be PascalCase (e.g., "CreateEvent")`,
|
|
573
|
+
file: intent.sourceFile,
|
|
574
|
+
suggestion: `Rename to "${toPascalCase(intent.name)}"`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (!intent.title || intent.title.trim().length === 0) {
|
|
578
|
+
diagnostics.push({
|
|
579
|
+
code: "AX101",
|
|
580
|
+
severity: "error",
|
|
581
|
+
message: "Intent title must not be empty",
|
|
582
|
+
file: intent.sourceFile,
|
|
583
|
+
suggestion: "Add a human-readable title for Siri and Shortcuts display"
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (!intent.description || intent.description.trim().length === 0) {
|
|
587
|
+
diagnostics.push({
|
|
588
|
+
code: "AX102",
|
|
589
|
+
severity: "error",
|
|
590
|
+
message: "Intent description must not be empty",
|
|
591
|
+
file: intent.sourceFile,
|
|
592
|
+
suggestion: "Add a description explaining what this intent does"
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
for (const param of intent.parameters) {
|
|
596
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(param.name)) {
|
|
597
|
+
diagnostics.push({
|
|
598
|
+
code: "AX103",
|
|
599
|
+
severity: "error",
|
|
600
|
+
message: `Parameter name "${param.name}" is not a valid Swift identifier`,
|
|
601
|
+
file: intent.sourceFile,
|
|
602
|
+
suggestion: `Rename to "${param.name.replace(/[^a-zA-Z0-9_]/g, "_")}"`
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (!param.description || param.description.trim().length === 0) {
|
|
606
|
+
diagnostics.push({
|
|
607
|
+
code: "AX104",
|
|
608
|
+
severity: "warning",
|
|
609
|
+
message: `Parameter "${param.name}" has no description \u2014 Siri will display it without context`,
|
|
610
|
+
file: intent.sourceFile,
|
|
611
|
+
suggestion: "Add a description for better Siri/Shortcuts display"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (intent.parameters.length > MAX_PARAMETERS) {
|
|
616
|
+
diagnostics.push({
|
|
617
|
+
code: "AX105",
|
|
618
|
+
severity: "warning",
|
|
619
|
+
message: `Intent has ${intent.parameters.length} parameters. Apple recommends ${MAX_PARAMETERS} or fewer for usability.`,
|
|
620
|
+
file: intent.sourceFile,
|
|
621
|
+
suggestion: "Consider splitting into multiple intents or grouping parameters into an entity"
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (intent.title && intent.title.length > MAX_TITLE_LENGTH) {
|
|
625
|
+
diagnostics.push({
|
|
626
|
+
code: "AX106",
|
|
627
|
+
severity: "warning",
|
|
628
|
+
message: `Intent title is ${intent.title.length} characters. Siri display may truncate titles over ${MAX_TITLE_LENGTH} characters.`,
|
|
629
|
+
file: intent.sourceFile
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
633
|
+
for (const param of intent.parameters) {
|
|
634
|
+
if (seen.has(param.name)) {
|
|
635
|
+
diagnostics.push({
|
|
636
|
+
code: "AX107",
|
|
637
|
+
severity: "error",
|
|
638
|
+
message: `Duplicate parameter name "${param.name}"`,
|
|
639
|
+
file: intent.sourceFile,
|
|
640
|
+
suggestion: "Each parameter in a single intent must have a unique name"
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
seen.add(param.name);
|
|
644
|
+
}
|
|
645
|
+
for (const ent of intent.entitlements ?? []) {
|
|
646
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(ent) || !ent.includes(".")) {
|
|
647
|
+
diagnostics.push({
|
|
648
|
+
code: "AX108",
|
|
649
|
+
severity: "warning",
|
|
650
|
+
message: `Entitlement "${ent}" does not look like a valid reverse-DNS identifier`,
|
|
651
|
+
file: intent.sourceFile,
|
|
652
|
+
suggestion: 'Use reverse-DNS, e.g., "com.apple.developer.siri" or "com.apple.security.app-sandbox"'
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (const key of Object.keys(intent.infoPlistKeys ?? {})) {
|
|
657
|
+
if (!/^(NS|UI|LS|CF|CA|CK)[A-Za-z0-9]+$/.test(key)) {
|
|
658
|
+
diagnostics.push({
|
|
659
|
+
code: "AX109",
|
|
660
|
+
severity: "warning",
|
|
661
|
+
message: `Info.plist key "${key}" does not match Apple's usual naming conventions`,
|
|
662
|
+
file: intent.sourceFile,
|
|
663
|
+
suggestion: 'Apple keys generally start with "NS" (e.g., "NSCalendarsUsageDescription")'
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return diagnostics;
|
|
668
|
+
}
|
|
669
|
+
function validateSwiftSource(swift) {
|
|
670
|
+
const diagnostics = [];
|
|
671
|
+
if (!swift.includes("import AppIntents")) {
|
|
672
|
+
diagnostics.push({
|
|
673
|
+
code: "AX200",
|
|
674
|
+
severity: "error",
|
|
675
|
+
message: 'Generated Swift is missing "import AppIntents"'
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (!swift.includes(": AppIntent")) {
|
|
679
|
+
diagnostics.push({
|
|
680
|
+
code: "AX201",
|
|
681
|
+
severity: "error",
|
|
682
|
+
message: "Generated struct does not conform to AppIntent protocol"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (!swift.includes("func perform()")) {
|
|
686
|
+
diagnostics.push({
|
|
687
|
+
code: "AX202",
|
|
688
|
+
severity: "error",
|
|
689
|
+
message: "Generated struct is missing the perform() function"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return diagnostics;
|
|
693
|
+
}
|
|
694
|
+
function toPascalCase(s) {
|
|
695
|
+
if (!s) return "UnnamedIntent";
|
|
696
|
+
return s.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/core/compiler.ts
|
|
700
|
+
function compileSource(source, fileName = "<stdin>", options = {}) {
|
|
701
|
+
const diagnostics = [];
|
|
702
|
+
let ir;
|
|
703
|
+
try {
|
|
704
|
+
ir = parseIntentSource(source, fileName);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
if (err instanceof ParserError) {
|
|
707
|
+
return {
|
|
708
|
+
success: false,
|
|
709
|
+
diagnostics: [
|
|
710
|
+
{
|
|
711
|
+
code: err.code,
|
|
712
|
+
severity: "error",
|
|
713
|
+
message: err.message,
|
|
714
|
+
file: err.file,
|
|
715
|
+
line: err.line,
|
|
716
|
+
suggestion: err.suggestion
|
|
717
|
+
}
|
|
718
|
+
]
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
const irDiagnostics = validateIntent(ir);
|
|
724
|
+
diagnostics.push(...irDiagnostics);
|
|
725
|
+
if (irDiagnostics.some((d) => d.severity === "error")) {
|
|
726
|
+
return { success: false, diagnostics };
|
|
727
|
+
}
|
|
728
|
+
const swiftCode = generateSwift(ir);
|
|
729
|
+
if (options.validate !== false) {
|
|
730
|
+
const swiftDiagnostics = validateSwiftSource(swiftCode);
|
|
731
|
+
diagnostics.push(...swiftDiagnostics);
|
|
732
|
+
if (swiftDiagnostics.some((d) => d.severity === "error")) {
|
|
733
|
+
return { success: false, diagnostics };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const infoPlistFragment = options.emitInfoPlist ? generateInfoPlistFragment(ir) : void 0;
|
|
737
|
+
const entitlementsFragment = options.emitEntitlements ? generateEntitlementsFragment(ir) : void 0;
|
|
738
|
+
const intentFileName = `${ir.name}Intent.swift`;
|
|
739
|
+
const outputPath = options.outDir ? `${options.outDir}/${intentFileName}` : intentFileName;
|
|
740
|
+
return {
|
|
741
|
+
success: true,
|
|
742
|
+
output: {
|
|
743
|
+
outputPath,
|
|
744
|
+
swiftCode,
|
|
745
|
+
infoPlistFragment,
|
|
746
|
+
entitlementsFragment,
|
|
747
|
+
ir,
|
|
748
|
+
diagnostics
|
|
749
|
+
},
|
|
750
|
+
diagnostics
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/mcp/scaffold.ts
|
|
755
|
+
function scaffoldIntent(input) {
|
|
756
|
+
const name = toPascalCase2(input.name);
|
|
757
|
+
const title = humanize(name);
|
|
758
|
+
const desc = sanitize(input.description);
|
|
759
|
+
const domain = input.domain ? sanitize(input.domain) : void 0;
|
|
760
|
+
const paramLines = [];
|
|
761
|
+
const destructureNames = [];
|
|
762
|
+
for (const p of input.params ?? []) {
|
|
763
|
+
const type = resolveType(p.type);
|
|
764
|
+
const safeName = toCamelCase(p.name);
|
|
765
|
+
destructureNames.push(safeName);
|
|
766
|
+
paramLines.push(
|
|
767
|
+
` ${safeName}: param.${type}(${JSON.stringify(sanitize(p.description))}),`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const paramsBlock = paramLines.length > 0 ? `
|
|
771
|
+
${paramLines.join("\n")}
|
|
772
|
+
` : "";
|
|
773
|
+
const destructure = destructureNames.length > 0 ? `{ ${destructureNames.join(", ")} }` : "_";
|
|
774
|
+
const lines = [];
|
|
775
|
+
lines.push(`/**`);
|
|
776
|
+
lines.push(` * ${name}Intent`);
|
|
777
|
+
lines.push(` *`);
|
|
778
|
+
lines.push(` * ${desc}`);
|
|
779
|
+
lines.push(` *`);
|
|
780
|
+
lines.push(` * Generated by axint_scaffold. Edit freely, then run:`);
|
|
781
|
+
lines.push(` * npx axint compile src/intents/${kebab(name)}.ts`);
|
|
782
|
+
lines.push(` */`);
|
|
783
|
+
lines.push(`import { defineIntent, param } from "axint";`);
|
|
784
|
+
lines.push(``);
|
|
785
|
+
lines.push(`export default defineIntent({`);
|
|
786
|
+
lines.push(` name: ${JSON.stringify(name)},`);
|
|
787
|
+
lines.push(` title: ${JSON.stringify(title)},`);
|
|
788
|
+
lines.push(` description: ${JSON.stringify(desc)},`);
|
|
789
|
+
if (domain) lines.push(` domain: ${JSON.stringify(domain)},`);
|
|
790
|
+
lines.push(` params: {${paramsBlock}},`);
|
|
791
|
+
lines.push(` perform: async (${destructure}) => {`);
|
|
792
|
+
lines.push(` // TODO: Implement ${name}`);
|
|
793
|
+
lines.push(` return { success: true };`);
|
|
794
|
+
lines.push(` },`);
|
|
795
|
+
lines.push(`});`);
|
|
796
|
+
lines.push(``);
|
|
797
|
+
return lines.join("\n");
|
|
798
|
+
}
|
|
799
|
+
function resolveType(raw) {
|
|
800
|
+
const lc = raw.toLowerCase();
|
|
801
|
+
if (PARAM_TYPES.has(lc)) return lc;
|
|
802
|
+
if (lc in LEGACY_PARAM_ALIASES) return LEGACY_PARAM_ALIASES[lc];
|
|
803
|
+
return "string";
|
|
804
|
+
}
|
|
805
|
+
function toPascalCase2(s) {
|
|
806
|
+
if (!s) return "UnnamedIntent";
|
|
807
|
+
return s.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
|
|
808
|
+
}
|
|
809
|
+
function toCamelCase(s) {
|
|
810
|
+
const pascal = toPascalCase2(s);
|
|
811
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
812
|
+
}
|
|
813
|
+
function humanize(pascal) {
|
|
814
|
+
return pascal.replace(/([A-Z])/g, " $1").trim();
|
|
815
|
+
}
|
|
816
|
+
function kebab(pascal) {
|
|
817
|
+
return pascal.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/\s+/g, "-").toLowerCase();
|
|
818
|
+
}
|
|
819
|
+
function sanitize(s) {
|
|
820
|
+
return s.replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/templates/index.ts
|
|
824
|
+
var sendMessage = {
|
|
825
|
+
id: "send-message",
|
|
826
|
+
name: "send-message",
|
|
827
|
+
title: "Send Message",
|
|
828
|
+
domain: "messaging",
|
|
829
|
+
category: "messaging",
|
|
830
|
+
description: "Send a text message to a contact.",
|
|
831
|
+
source: `import { defineIntent, param } from "axint";
|
|
832
|
+
|
|
833
|
+
export default defineIntent({
|
|
834
|
+
name: "SendMessage",
|
|
835
|
+
title: "Send Message",
|
|
836
|
+
description: "Sends a message to a specified contact.",
|
|
837
|
+
domain: "messaging",
|
|
838
|
+
params: {
|
|
839
|
+
recipient: param.string("Who to send the message to"),
|
|
840
|
+
body: param.string("The message content"),
|
|
841
|
+
},
|
|
842
|
+
perform: async ({ recipient, body }) => {
|
|
843
|
+
// TODO: Integrate with your messaging backend
|
|
844
|
+
return { sent: true };
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
`
|
|
848
|
+
};
|
|
849
|
+
var createEvent = {
|
|
850
|
+
id: "create-event",
|
|
851
|
+
name: "create-event",
|
|
852
|
+
title: "Create Calendar Event",
|
|
853
|
+
domain: "productivity",
|
|
854
|
+
category: "productivity",
|
|
855
|
+
description: "Create a calendar event with a title, date, and duration.",
|
|
856
|
+
source: `import { defineIntent, param } from "axint";
|
|
857
|
+
|
|
858
|
+
export default defineIntent({
|
|
859
|
+
name: "CreateEvent",
|
|
860
|
+
title: "Create Calendar Event",
|
|
861
|
+
description: "Creates a new event in the user's calendar.",
|
|
862
|
+
domain: "productivity",
|
|
863
|
+
entitlements: ["com.apple.developer.siri"],
|
|
864
|
+
infoPlistKeys: {
|
|
865
|
+
NSCalendarsUsageDescription: "Access to your calendar to create events.",
|
|
866
|
+
},
|
|
867
|
+
params: {
|
|
868
|
+
title: param.string("Event title"),
|
|
869
|
+
date: param.date("Event date"),
|
|
870
|
+
durationMinutes: param.int("Duration in minutes", { default: 30 }),
|
|
871
|
+
allDay: param.boolean("All-day event", { required: false }),
|
|
872
|
+
},
|
|
873
|
+
perform: async ({ title, date }) => {
|
|
874
|
+
return { eventId: "evt_placeholder" };
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
`
|
|
878
|
+
};
|
|
879
|
+
var bookRide = {
|
|
880
|
+
id: "book-ride",
|
|
881
|
+
name: "book-ride",
|
|
882
|
+
title: "Book a Ride",
|
|
883
|
+
domain: "navigation",
|
|
884
|
+
category: "navigation",
|
|
885
|
+
description: "Request a ride from a pickup location to a destination.",
|
|
886
|
+
source: `import { defineIntent, param } from "axint";
|
|
887
|
+
|
|
888
|
+
export default defineIntent({
|
|
889
|
+
name: "BookRide",
|
|
890
|
+
title: "Book a Ride",
|
|
891
|
+
description: "Requests a ride from a pickup location to a destination.",
|
|
892
|
+
domain: "navigation",
|
|
893
|
+
params: {
|
|
894
|
+
pickup: param.string("Pickup location"),
|
|
895
|
+
destination: param.string("Destination address"),
|
|
896
|
+
passengers: param.int("Number of passengers", { default: 1 }),
|
|
897
|
+
},
|
|
898
|
+
perform: async ({ pickup, destination }) => {
|
|
899
|
+
return { rideId: "ride_placeholder", eta: 300 };
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
`
|
|
903
|
+
};
|
|
904
|
+
var getDirections = {
|
|
905
|
+
id: "get-directions",
|
|
906
|
+
name: "get-directions",
|
|
907
|
+
title: "Get Directions",
|
|
908
|
+
domain: "navigation",
|
|
909
|
+
category: "navigation",
|
|
910
|
+
description: "Get turn-by-turn directions to a destination.",
|
|
911
|
+
source: `import { defineIntent, param } from "axint";
|
|
912
|
+
|
|
913
|
+
export default defineIntent({
|
|
914
|
+
name: "GetDirections",
|
|
915
|
+
title: "Get Directions",
|
|
916
|
+
description: "Returns turn-by-turn directions to a destination.",
|
|
917
|
+
domain: "navigation",
|
|
918
|
+
params: {
|
|
919
|
+
destination: param.string("Where to navigate to"),
|
|
920
|
+
mode: param.string("Travel mode (driving, walking, transit)", {
|
|
921
|
+
default: "driving",
|
|
922
|
+
}),
|
|
923
|
+
},
|
|
924
|
+
perform: async ({ destination }) => {
|
|
925
|
+
return { routeId: "route_placeholder" };
|
|
926
|
+
},
|
|
927
|
+
});
|
|
928
|
+
`
|
|
929
|
+
};
|
|
930
|
+
var playTrack = {
|
|
931
|
+
id: "play-track",
|
|
932
|
+
name: "play-track",
|
|
933
|
+
title: "Play Track",
|
|
934
|
+
domain: "media",
|
|
935
|
+
category: "media",
|
|
936
|
+
description: "Play a specific track or song.",
|
|
937
|
+
source: `import { defineIntent, param } from "axint";
|
|
938
|
+
|
|
939
|
+
export default defineIntent({
|
|
940
|
+
name: "PlayTrack",
|
|
941
|
+
title: "Play Track",
|
|
942
|
+
description: "Plays a specific track by title and artist.",
|
|
943
|
+
domain: "media",
|
|
944
|
+
params: {
|
|
945
|
+
track: param.string("Track title"),
|
|
946
|
+
artist: param.string("Artist name", { required: false }),
|
|
947
|
+
shuffle: param.boolean("Shuffle mode", { required: false }),
|
|
948
|
+
},
|
|
949
|
+
perform: async ({ track }) => {
|
|
950
|
+
return { playing: true };
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
`
|
|
954
|
+
};
|
|
955
|
+
var createNote = {
|
|
956
|
+
id: "create-note",
|
|
957
|
+
name: "create-note",
|
|
958
|
+
title: "Create Note",
|
|
959
|
+
domain: "productivity",
|
|
960
|
+
category: "productivity",
|
|
961
|
+
description: "Create a new note with a title and body.",
|
|
962
|
+
source: `import { defineIntent, param } from "axint";
|
|
963
|
+
|
|
964
|
+
export default defineIntent({
|
|
965
|
+
name: "CreateNote",
|
|
966
|
+
title: "Create Note",
|
|
967
|
+
description: "Creates a new note with a title and body.",
|
|
968
|
+
domain: "productivity",
|
|
969
|
+
params: {
|
|
970
|
+
title: param.string("Note title"),
|
|
971
|
+
body: param.string("Note body"),
|
|
972
|
+
pinned: param.boolean("Pin the note", { required: false }),
|
|
973
|
+
},
|
|
974
|
+
perform: async ({ title, body }) => {
|
|
975
|
+
return { noteId: "note_placeholder" };
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
`
|
|
979
|
+
};
|
|
980
|
+
var logExpense = {
|
|
981
|
+
id: "log-expense",
|
|
982
|
+
name: "log-expense",
|
|
983
|
+
title: "Log Expense",
|
|
984
|
+
domain: "finance",
|
|
985
|
+
category: "finance",
|
|
986
|
+
description: "Log a financial expense with amount, category, and note.",
|
|
987
|
+
source: `import { defineIntent, param } from "axint";
|
|
988
|
+
|
|
989
|
+
export default defineIntent({
|
|
990
|
+
name: "LogExpense",
|
|
991
|
+
title: "Log Expense",
|
|
992
|
+
description: "Logs a financial expense with amount, category, and note.",
|
|
993
|
+
domain: "finance",
|
|
994
|
+
params: {
|
|
995
|
+
amount: param.double("Expense amount"),
|
|
996
|
+
currency: param.string("ISO currency code (e.g., USD)", {
|
|
997
|
+
default: "USD",
|
|
998
|
+
}),
|
|
999
|
+
category: param.string("Expense category"),
|
|
1000
|
+
note: param.string("Optional note", { required: false }),
|
|
1001
|
+
},
|
|
1002
|
+
perform: async ({ amount, category }) => {
|
|
1003
|
+
return { expenseId: "exp_placeholder" };
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
`
|
|
1007
|
+
};
|
|
1008
|
+
var logWorkout = {
|
|
1009
|
+
id: "log-workout",
|
|
1010
|
+
name: "log-workout",
|
|
1011
|
+
title: "Log Workout",
|
|
1012
|
+
domain: "health",
|
|
1013
|
+
category: "health",
|
|
1014
|
+
description: "Log a workout with duration, type, and calories burned.",
|
|
1015
|
+
source: `import { defineIntent, param } from "axint";
|
|
1016
|
+
|
|
1017
|
+
export default defineIntent({
|
|
1018
|
+
name: "LogWorkout",
|
|
1019
|
+
title: "Log Workout",
|
|
1020
|
+
description: "Logs a workout with duration, type, and calories burned.",
|
|
1021
|
+
domain: "health",
|
|
1022
|
+
entitlements: ["com.apple.developer.healthkit"],
|
|
1023
|
+
infoPlistKeys: {
|
|
1024
|
+
NSHealthShareUsageDescription: "Read workout history to track progress.",
|
|
1025
|
+
NSHealthUpdateUsageDescription: "Save new workouts you log.",
|
|
1026
|
+
},
|
|
1027
|
+
params: {
|
|
1028
|
+
type: param.string("Workout type (e.g., running, cycling)"),
|
|
1029
|
+
duration: param.duration("Workout duration"),
|
|
1030
|
+
calories: param.int("Calories burned", { required: false }),
|
|
1031
|
+
},
|
|
1032
|
+
perform: async ({ type, duration }) => {
|
|
1033
|
+
return { workoutId: "wo_placeholder" };
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
`
|
|
1037
|
+
};
|
|
1038
|
+
var setThermostat = {
|
|
1039
|
+
id: "set-thermostat",
|
|
1040
|
+
name: "set-thermostat",
|
|
1041
|
+
title: "Set Thermostat",
|
|
1042
|
+
domain: "smart-home",
|
|
1043
|
+
category: "smart-home",
|
|
1044
|
+
description: "Set a smart-home thermostat to a target temperature.",
|
|
1045
|
+
source: `import { defineIntent, param } from "axint";
|
|
1046
|
+
|
|
1047
|
+
export default defineIntent({
|
|
1048
|
+
name: "SetThermostat",
|
|
1049
|
+
title: "Set Thermostat",
|
|
1050
|
+
description: "Sets a smart-home thermostat to a target temperature.",
|
|
1051
|
+
domain: "smart-home",
|
|
1052
|
+
params: {
|
|
1053
|
+
room: param.string("Which room"),
|
|
1054
|
+
temperature: param.double("Target temperature"),
|
|
1055
|
+
unit: param.string("Temperature unit (F or C)", { default: "F" }),
|
|
1056
|
+
},
|
|
1057
|
+
perform: async ({ room, temperature }) => {
|
|
1058
|
+
return { set: true };
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
`
|
|
1062
|
+
};
|
|
1063
|
+
var placeOrder = {
|
|
1064
|
+
id: "place-order",
|
|
1065
|
+
name: "place-order",
|
|
1066
|
+
title: "Place Order",
|
|
1067
|
+
domain: "commerce",
|
|
1068
|
+
category: "commerce",
|
|
1069
|
+
description: "Place a commerce order for a product.",
|
|
1070
|
+
source: `import { defineIntent, param } from "axint";
|
|
1071
|
+
|
|
1072
|
+
export default defineIntent({
|
|
1073
|
+
name: "PlaceOrder",
|
|
1074
|
+
title: "Place Order",
|
|
1075
|
+
description: "Places an order for a product.",
|
|
1076
|
+
domain: "commerce",
|
|
1077
|
+
params: {
|
|
1078
|
+
productId: param.string("Product identifier"),
|
|
1079
|
+
quantity: param.int("Quantity", { default: 1 }),
|
|
1080
|
+
shippingAddress: param.string("Shipping address", { required: false }),
|
|
1081
|
+
},
|
|
1082
|
+
perform: async ({ productId, quantity }) => {
|
|
1083
|
+
return { orderId: "ord_placeholder", total: 0 };
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
`
|
|
1087
|
+
};
|
|
1088
|
+
var TEMPLATES = [
|
|
1089
|
+
sendMessage,
|
|
1090
|
+
createEvent,
|
|
1091
|
+
bookRide,
|
|
1092
|
+
getDirections,
|
|
1093
|
+
playTrack,
|
|
1094
|
+
createNote,
|
|
1095
|
+
logExpense,
|
|
1096
|
+
logWorkout,
|
|
1097
|
+
setThermostat,
|
|
1098
|
+
placeOrder
|
|
1099
|
+
];
|
|
1100
|
+
function getTemplate(id) {
|
|
1101
|
+
return TEMPLATES.find((t) => t.id === id);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/mcp/server.ts
|
|
1105
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1106
|
+
var pkg = JSON.parse(
|
|
1107
|
+
readFileSync2(resolve(__dirname, "../../package.json"), "utf-8")
|
|
1108
|
+
);
|
|
1109
|
+
async function startMCPServer() {
|
|
1110
|
+
const server = new Server(
|
|
1111
|
+
{ name: "axint", version: pkg.version },
|
|
1112
|
+
{ capabilities: { tools: {} } }
|
|
1113
|
+
);
|
|
1114
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1115
|
+
tools: [
|
|
1116
|
+
{
|
|
1117
|
+
name: "axint_scaffold",
|
|
1118
|
+
description: "Generate a starter TypeScript intent file using the axint SDK. Pass a PascalCase name, a description, and optionally a domain (messaging, productivity, health, finance, commerce, media, navigation, smart-home) and a list of parameters. Returns ready-to-save source code that compiles with `axint compile`.",
|
|
1119
|
+
inputSchema: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
name: {
|
|
1123
|
+
type: "string",
|
|
1124
|
+
description: "PascalCase name for the intent, e.g., 'CreateEvent'"
|
|
1125
|
+
},
|
|
1126
|
+
description: {
|
|
1127
|
+
type: "string",
|
|
1128
|
+
description: "Human-readable description of what the intent does"
|
|
1129
|
+
},
|
|
1130
|
+
domain: {
|
|
1131
|
+
type: "string",
|
|
1132
|
+
description: "Optional Apple App Intent domain (messaging, productivity, health, finance, commerce, media, navigation, smart-home)"
|
|
1133
|
+
},
|
|
1134
|
+
params: {
|
|
1135
|
+
type: "array",
|
|
1136
|
+
description: "Optional initial parameters. Each item: { name, type, description }. Supported types: string, int, double, float, boolean, date, duration, url.",
|
|
1137
|
+
items: {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: {
|
|
1140
|
+
name: { type: "string" },
|
|
1141
|
+
type: { type: "string" },
|
|
1142
|
+
description: { type: "string" }
|
|
1143
|
+
},
|
|
1144
|
+
required: ["name", "type", "description"]
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
required: ["name", "description"]
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
name: "axint_compile",
|
|
1153
|
+
description: "Compile a TypeScript intent definition into a native Swift App Intent. Optionally emits Info.plist and entitlements fragments alongside the Swift file. Pass the full TypeScript source code using the defineIntent() API.",
|
|
1154
|
+
inputSchema: {
|
|
1155
|
+
type: "object",
|
|
1156
|
+
properties: {
|
|
1157
|
+
source: {
|
|
1158
|
+
type: "string",
|
|
1159
|
+
description: "TypeScript source code containing a defineIntent() call"
|
|
1160
|
+
},
|
|
1161
|
+
fileName: {
|
|
1162
|
+
type: "string",
|
|
1163
|
+
description: "Optional file name for error messages"
|
|
1164
|
+
},
|
|
1165
|
+
emitInfoPlist: {
|
|
1166
|
+
type: "boolean",
|
|
1167
|
+
description: "When true, also returns an Info.plist XML fragment for the intent's declared infoPlistKeys"
|
|
1168
|
+
},
|
|
1169
|
+
emitEntitlements: {
|
|
1170
|
+
type: "boolean",
|
|
1171
|
+
description: "When true, also returns an .entitlements XML fragment for the intent's declared entitlements"
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
required: ["source"]
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "axint_validate",
|
|
1179
|
+
description: "Validate a TypeScript intent definition without generating Swift output. Returns diagnostics with error codes and fix suggestions.",
|
|
1180
|
+
inputSchema: {
|
|
1181
|
+
type: "object",
|
|
1182
|
+
properties: {
|
|
1183
|
+
source: {
|
|
1184
|
+
type: "string",
|
|
1185
|
+
description: "TypeScript source code containing a defineIntent() call"
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
required: ["source"]
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
name: "axint_list_templates",
|
|
1193
|
+
description: "List the bundled reference templates. Use `axint_template` to fetch the full source of a specific template by id.",
|
|
1194
|
+
inputSchema: {
|
|
1195
|
+
type: "object",
|
|
1196
|
+
properties: {}
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "axint_template",
|
|
1201
|
+
description: "Return the full TypeScript source code of a bundled reference template by id. Use `axint_list_templates` to discover valid ids.",
|
|
1202
|
+
inputSchema: {
|
|
1203
|
+
type: "object",
|
|
1204
|
+
properties: {
|
|
1205
|
+
id: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
description: "Template id (e.g., 'send-message', 'create-event')"
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
required: ["id"]
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
]
|
|
1214
|
+
}));
|
|
1215
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1216
|
+
const { name, arguments: args } = request.params;
|
|
1217
|
+
try {
|
|
1218
|
+
if (name === "axint_scaffold") {
|
|
1219
|
+
const a = args;
|
|
1220
|
+
const source = scaffoldIntent({
|
|
1221
|
+
name: a.name,
|
|
1222
|
+
description: a.description,
|
|
1223
|
+
domain: a.domain,
|
|
1224
|
+
params: a.params
|
|
1225
|
+
});
|
|
1226
|
+
return { content: [{ type: "text", text: source }] };
|
|
1227
|
+
}
|
|
1228
|
+
if (name === "axint_compile") {
|
|
1229
|
+
const a = args;
|
|
1230
|
+
const result = compileSource(a.source, a.fileName || "<mcp>", {
|
|
1231
|
+
emitInfoPlist: a.emitInfoPlist,
|
|
1232
|
+
emitEntitlements: a.emitEntitlements
|
|
1233
|
+
});
|
|
1234
|
+
if (result.success && result.output) {
|
|
1235
|
+
const parts = [
|
|
1236
|
+
"// \u2500\u2500\u2500 Swift \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1237
|
+
result.output.swiftCode
|
|
1238
|
+
];
|
|
1239
|
+
if (result.output.infoPlistFragment) {
|
|
1240
|
+
parts.push("// \u2500\u2500\u2500 Info.plist fragment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1241
|
+
parts.push(result.output.infoPlistFragment);
|
|
1242
|
+
}
|
|
1243
|
+
if (result.output.entitlementsFragment) {
|
|
1244
|
+
parts.push("// \u2500\u2500\u2500 .entitlements fragment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1245
|
+
parts.push(result.output.entitlementsFragment);
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const errorText = result.diagnostics.map((d) => `[${d.code}] ${d.severity}: ${d.message}`).join("\n");
|
|
1252
|
+
return {
|
|
1253
|
+
content: [{ type: "text", text: errorText }],
|
|
1254
|
+
isError: true
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
if (name === "axint_validate") {
|
|
1258
|
+
const a = args;
|
|
1259
|
+
const result = compileSource(a.source, "<validate>");
|
|
1260
|
+
const text = result.diagnostics.length > 0 ? result.diagnostics.map((d) => `[${d.code}] ${d.severity}: ${d.message}`).join("\n") : "Valid intent definition. No issues found.";
|
|
1261
|
+
return { content: [{ type: "text", text }] };
|
|
1262
|
+
}
|
|
1263
|
+
if (name === "axint_list_templates") {
|
|
1264
|
+
const list = TEMPLATES.map(
|
|
1265
|
+
(t) => `${t.id} \u2014 ${t.title}${t.domain ? ` [${t.domain}]` : ""}`
|
|
1266
|
+
).join("\n");
|
|
1267
|
+
return {
|
|
1268
|
+
content: [
|
|
1269
|
+
{
|
|
1270
|
+
type: "text",
|
|
1271
|
+
text: list || "No templates registered."
|
|
1272
|
+
}
|
|
1273
|
+
]
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
if (name === "axint_template") {
|
|
1277
|
+
const a = args;
|
|
1278
|
+
const tpl = getTemplate(a.id);
|
|
1279
|
+
if (!tpl) {
|
|
1280
|
+
return {
|
|
1281
|
+
content: [
|
|
1282
|
+
{
|
|
1283
|
+
type: "text",
|
|
1284
|
+
text: `Unknown template id: ${a.id}. Use axint_list_templates to see available ids.`
|
|
1285
|
+
}
|
|
1286
|
+
],
|
|
1287
|
+
isError: true
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
return { content: [{ type: "text", text: tpl.source }] };
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
1294
|
+
isError: true
|
|
1295
|
+
};
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
return {
|
|
1298
|
+
content: [
|
|
1299
|
+
{
|
|
1300
|
+
type: "text",
|
|
1301
|
+
text: `Tool error: ${err instanceof Error ? err.message : String(err)}`
|
|
1302
|
+
}
|
|
1303
|
+
],
|
|
1304
|
+
isError: true
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
const transport = new StdioServerTransport();
|
|
1309
|
+
await server.connect(transport);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/mcp/index.ts
|
|
1313
|
+
startMCPServer().catch((err) => {
|
|
1314
|
+
console.error("Failed to start MCP server:", err);
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
});
|
|
1317
|
+
export {
|
|
1318
|
+
startMCPServer
|
|
1319
|
+
};
|
|
1320
|
+
//# sourceMappingURL=index.js.map
|