@tdsoft-tech/aikit 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4901 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1000 -0
- package/dist/index.js +4038 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +3496 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4901 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/utils/paths.ts
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { existsSync } from "fs";
|
|
25
|
+
var paths;
|
|
26
|
+
var init_paths = __esm({
|
|
27
|
+
"src/utils/paths.ts"() {
|
|
28
|
+
"use strict";
|
|
29
|
+
init_esm_shims();
|
|
30
|
+
paths = {
|
|
31
|
+
/**
|
|
32
|
+
* Get the global AIKit configuration directory
|
|
33
|
+
* ~/.config/aikit/ on Unix, %APPDATA%/aikit/ on Windows
|
|
34
|
+
*/
|
|
35
|
+
globalConfig() {
|
|
36
|
+
const base = process.platform === "win32" ? process.env.APPDATA || join(homedir(), "AppData", "Roaming") : join(homedir(), ".config");
|
|
37
|
+
return join(base, "aikit");
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* Get the project-level AIKit configuration directory
|
|
41
|
+
*/
|
|
42
|
+
projectConfig(projectPath) {
|
|
43
|
+
const base = projectPath || process.cwd();
|
|
44
|
+
return join(base, ".aikit");
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Get the OpenCode configuration directory
|
|
48
|
+
*/
|
|
49
|
+
opencodeConfig() {
|
|
50
|
+
const base = process.platform === "win32" ? process.env.APPDATA || join(homedir(), "AppData", "Roaming") : join(homedir(), ".config");
|
|
51
|
+
return join(base, "opencode");
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Get the Beads directory for the current project
|
|
55
|
+
*/
|
|
56
|
+
beadsDir(projectPath) {
|
|
57
|
+
const base = projectPath || process.cwd();
|
|
58
|
+
return join(base, ".beads");
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Check if a project has AIKit initialized
|
|
62
|
+
*/
|
|
63
|
+
hasProjectConfig(projectPath) {
|
|
64
|
+
return existsSync(this.projectConfig(projectPath));
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Check if global AIKit is initialized
|
|
68
|
+
*/
|
|
69
|
+
hasGlobalConfig() {
|
|
70
|
+
return existsSync(this.globalConfig());
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Get effective config path (project takes precedence over global)
|
|
74
|
+
*/
|
|
75
|
+
effectiveConfig(projectPath) {
|
|
76
|
+
if (this.hasProjectConfig(projectPath)) {
|
|
77
|
+
return this.projectConfig(projectPath);
|
|
78
|
+
}
|
|
79
|
+
if (this.hasGlobalConfig()) {
|
|
80
|
+
return this.globalConfig();
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Get skills directory
|
|
86
|
+
*/
|
|
87
|
+
skills(configPath) {
|
|
88
|
+
return join(configPath, "skills");
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Get agents directory
|
|
92
|
+
*/
|
|
93
|
+
agents(configPath) {
|
|
94
|
+
return join(configPath, "agents");
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Get commands directory
|
|
98
|
+
*/
|
|
99
|
+
commands(configPath) {
|
|
100
|
+
return join(configPath, "commands");
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Get tools directory
|
|
104
|
+
*/
|
|
105
|
+
tools(configPath) {
|
|
106
|
+
return join(configPath, "tools");
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* Get plugins directory
|
|
110
|
+
*/
|
|
111
|
+
plugins(configPath) {
|
|
112
|
+
return join(configPath, "plugins");
|
|
113
|
+
},
|
|
114
|
+
/**
|
|
115
|
+
* Get memory directory
|
|
116
|
+
*/
|
|
117
|
+
memory(configPath) {
|
|
118
|
+
return join(configPath, "memory");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// src/utils/logger.ts
|
|
125
|
+
import chalk from "chalk";
|
|
126
|
+
var logger;
|
|
127
|
+
var init_logger = __esm({
|
|
128
|
+
"src/utils/logger.ts"() {
|
|
129
|
+
"use strict";
|
|
130
|
+
init_esm_shims();
|
|
131
|
+
logger = {
|
|
132
|
+
info(...args) {
|
|
133
|
+
console.log(chalk.blue("\u2139"), ...args);
|
|
134
|
+
},
|
|
135
|
+
success(...args) {
|
|
136
|
+
console.log(chalk.green("\u2713"), ...args);
|
|
137
|
+
},
|
|
138
|
+
warn(...args) {
|
|
139
|
+
console.log(chalk.yellow("\u26A0"), ...args);
|
|
140
|
+
},
|
|
141
|
+
error(...args) {
|
|
142
|
+
console.error(chalk.red("\u2716"), ...args);
|
|
143
|
+
},
|
|
144
|
+
debug(...args) {
|
|
145
|
+
if (process.env.DEBUG || process.env.AIKIT_DEBUG) {
|
|
146
|
+
console.log(chalk.gray("\u22EF"), ...args);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
step(step, total, message) {
|
|
150
|
+
console.log(chalk.cyan(`[${step}/${total}]`), message);
|
|
151
|
+
},
|
|
152
|
+
header(message) {
|
|
153
|
+
console.log(chalk.bold.underline(`
|
|
154
|
+
${message}
|
|
155
|
+
`));
|
|
156
|
+
},
|
|
157
|
+
list(items, prefix = "\u2022") {
|
|
158
|
+
for (const item of items) {
|
|
159
|
+
console.log(` ${prefix} ${item}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/core/tools/figma-mcp.ts
|
|
167
|
+
var figma_mcp_exports = {};
|
|
168
|
+
__export(figma_mcp_exports, {
|
|
169
|
+
FigmaMcpClient: () => FigmaMcpClient
|
|
170
|
+
});
|
|
171
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
172
|
+
import { join as join5 } from "path";
|
|
173
|
+
import { existsSync as existsSync2 } from "fs";
|
|
174
|
+
var FigmaMcpClient;
|
|
175
|
+
var init_figma_mcp = __esm({
|
|
176
|
+
"src/core/tools/figma-mcp.ts"() {
|
|
177
|
+
"use strict";
|
|
178
|
+
init_esm_shims();
|
|
179
|
+
init_logger();
|
|
180
|
+
FigmaMcpClient = class {
|
|
181
|
+
apiKey;
|
|
182
|
+
configManager;
|
|
183
|
+
constructor(apiKey, configManager) {
|
|
184
|
+
this.apiKey = apiKey;
|
|
185
|
+
this.configManager = configManager;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Fetch helper with simple retry/backoff for 429/5xx
|
|
189
|
+
*/
|
|
190
|
+
async fetchWithRetry(url, options, label, retries = 3, backoffMs = 1500) {
|
|
191
|
+
let attempt = 0;
|
|
192
|
+
let lastError;
|
|
193
|
+
while (attempt <= retries) {
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(url, options);
|
|
196
|
+
if (res.ok) return res;
|
|
197
|
+
if (res.status === 429 || res.status >= 500) {
|
|
198
|
+
const retryAfter = Number(res.headers.get("retry-after")) || 0;
|
|
199
|
+
const delay = retryAfter > 0 ? retryAfter * 1e3 : backoffMs * (attempt + 1);
|
|
200
|
+
logger.warn(`${label} failed (status ${res.status}), retrying in ${Math.round(delay / 1e3)}s...`);
|
|
201
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
202
|
+
attempt += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const text = await res.text();
|
|
206
|
+
throw new Error(`${label} error: ${res.status} ${res.statusText}
|
|
207
|
+
${text}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
lastError = err;
|
|
210
|
+
logger.warn(`${label} network error, attempt ${attempt + 1}/${retries + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
211
|
+
if (attempt >= retries) break;
|
|
212
|
+
await new Promise((r) => setTimeout(r, backoffMs * (attempt + 1)));
|
|
213
|
+
attempt += 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw lastError instanceof Error ? lastError : new Error(`${label} failed after retries`);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Extract file key from Figma URL
|
|
220
|
+
*/
|
|
221
|
+
extractFileKey(url) {
|
|
222
|
+
const match = url.match(/figma\.com\/design\/([a-zA-Z0-9]+)/);
|
|
223
|
+
return match ? match[1] : null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extract node ID from Figma URL
|
|
227
|
+
*/
|
|
228
|
+
extractNodeId(url) {
|
|
229
|
+
const match = url.match(/[?&]node-id=([^&]+)/);
|
|
230
|
+
if (!match) return null;
|
|
231
|
+
let nodeId = decodeURIComponent(match[1]);
|
|
232
|
+
if (nodeId.includes("-") && !nodeId.includes(":")) {
|
|
233
|
+
nodeId = nodeId.replace(/-/g, ":");
|
|
234
|
+
}
|
|
235
|
+
return nodeId;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get Figma file data using API
|
|
239
|
+
*/
|
|
240
|
+
async getFileData(url) {
|
|
241
|
+
const fileKey = this.extractFileKey(url);
|
|
242
|
+
if (!fileKey) {
|
|
243
|
+
throw new Error(`Invalid Figma URL: ${url}`);
|
|
244
|
+
}
|
|
245
|
+
const nodeId = this.extractNodeId(url);
|
|
246
|
+
const apiUrl = nodeId ? `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${encodeURIComponent(nodeId)}` : `https://api.figma.com/v1/files/${fileKey}`;
|
|
247
|
+
const response = await this.fetchWithRetry(apiUrl, {
|
|
248
|
+
headers: {
|
|
249
|
+
"X-Figma-Token": this.apiKey
|
|
250
|
+
}
|
|
251
|
+
}, "Figma file fetch");
|
|
252
|
+
const data = await response.json();
|
|
253
|
+
if (nodeId) {
|
|
254
|
+
const nodes = data.nodes;
|
|
255
|
+
const nodeData = Object.values(nodes)[0];
|
|
256
|
+
if (!nodeData) {
|
|
257
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
document: nodeData.document,
|
|
261
|
+
components: {},
|
|
262
|
+
styles: {}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return data;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Extract design tokens from Figma file
|
|
269
|
+
*/
|
|
270
|
+
async extractDesignTokens(url, downloadAssets = true, assetsDir) {
|
|
271
|
+
const fileData = await this.getFileData(url);
|
|
272
|
+
const fileKey = this.extractFileKey(url);
|
|
273
|
+
if (!fileKey) {
|
|
274
|
+
throw new Error(`Invalid Figma URL: ${url}`);
|
|
275
|
+
}
|
|
276
|
+
const tokens = {
|
|
277
|
+
colors: [],
|
|
278
|
+
typography: [],
|
|
279
|
+
spacing: {
|
|
280
|
+
unit: 8,
|
|
281
|
+
// Default 8px grid
|
|
282
|
+
scale: []
|
|
283
|
+
},
|
|
284
|
+
components: [],
|
|
285
|
+
screens: [],
|
|
286
|
+
breakpoints: [375, 768, 1024, 1280, 1920]
|
|
287
|
+
// Common breakpoints
|
|
288
|
+
};
|
|
289
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
290
|
+
this.extractColors(fileData.document, colorMap);
|
|
291
|
+
tokens.colors = Array.from(colorMap.entries()).map(([name, hex]) => ({
|
|
292
|
+
name,
|
|
293
|
+
hex,
|
|
294
|
+
rgba: hex
|
|
295
|
+
// Simplified
|
|
296
|
+
}));
|
|
297
|
+
const typographyMap = /* @__PURE__ */ new Map();
|
|
298
|
+
this.extractTypography(fileData.document, typographyMap);
|
|
299
|
+
tokens.typography = Array.from(typographyMap.values());
|
|
300
|
+
Object.values(fileData.components).forEach((component) => {
|
|
301
|
+
tokens.components.push({
|
|
302
|
+
name: component.name,
|
|
303
|
+
type: "component",
|
|
304
|
+
description: component.description
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
this.extractScreens(fileData.document, tokens.screens);
|
|
308
|
+
tokens.structure = this.extractStructure(fileData.document);
|
|
309
|
+
if (downloadAssets && tokens.structure) {
|
|
310
|
+
try {
|
|
311
|
+
tokens.assets = await this.downloadAssets(
|
|
312
|
+
fileKey,
|
|
313
|
+
fileData.document,
|
|
314
|
+
assetsDir || "./assets/images"
|
|
315
|
+
);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.warn(`Failed to download assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return tokens;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Recursively extract colors from nodes
|
|
324
|
+
*/
|
|
325
|
+
extractColors(node, colorMap) {
|
|
326
|
+
if (node.fills && Array.isArray(node.fills)) {
|
|
327
|
+
node.fills.forEach((fill) => {
|
|
328
|
+
if (fill.type === "SOLID" && fill.color) {
|
|
329
|
+
const { r, g, b, a } = fill.color;
|
|
330
|
+
const hex = this.rgbaToHex(r, g, b, a);
|
|
331
|
+
const name = node.name || "Color";
|
|
332
|
+
if (!colorMap.has(hex)) {
|
|
333
|
+
colorMap.set(hex, hex);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (node.strokes && Array.isArray(node.strokes)) {
|
|
339
|
+
node.strokes.forEach((stroke) => {
|
|
340
|
+
if (stroke.type === "SOLID" && stroke.color) {
|
|
341
|
+
const { r, g, b, a } = stroke.color;
|
|
342
|
+
const hex = this.rgbaToHex(r, g, b, a);
|
|
343
|
+
if (!colorMap.has(hex)) {
|
|
344
|
+
colorMap.set(hex, hex);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (node.children) {
|
|
350
|
+
node.children.forEach((child) => this.extractColors(child, colorMap));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Recursively extract typography from nodes
|
|
355
|
+
*/
|
|
356
|
+
extractTypography(node, typographyMap) {
|
|
357
|
+
if (node.type === "TEXT" && node.style) {
|
|
358
|
+
const key = `${node.style.fontFamily}-${node.style.fontSize}-${node.style.fontWeight}`;
|
|
359
|
+
if (!typographyMap.has(key)) {
|
|
360
|
+
typographyMap.set(key, {
|
|
361
|
+
name: `${node.style.fontSize}px ${node.style.fontFamily}`,
|
|
362
|
+
fontFamily: node.style.fontFamily || "Inter",
|
|
363
|
+
fontSize: node.style.fontSize || 16,
|
|
364
|
+
fontWeight: node.style.fontWeight || 400,
|
|
365
|
+
lineHeight: node.style.lineHeightPx || node.style.fontSize || 16,
|
|
366
|
+
letterSpacing: node.style.letterSpacing
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (node.children) {
|
|
371
|
+
node.children.forEach((child) => this.extractTypography(child, typographyMap));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Extract screens/frames with detailed information
|
|
376
|
+
*/
|
|
377
|
+
extractScreens(node, screens) {
|
|
378
|
+
if (node.type === "FRAME" || node.type === "COMPONENT") {
|
|
379
|
+
if (node.absoluteBoundingBox) {
|
|
380
|
+
const isMainScreen = node.absoluteBoundingBox.width >= 800 && node.absoluteBoundingBox.height >= 400;
|
|
381
|
+
if (isMainScreen) {
|
|
382
|
+
screens.push({
|
|
383
|
+
id: node.id,
|
|
384
|
+
name: node.name,
|
|
385
|
+
width: node.absoluteBoundingBox.width,
|
|
386
|
+
height: node.absoluteBoundingBox.height,
|
|
387
|
+
type: node.type,
|
|
388
|
+
childrenCount: node.children?.length || 0
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (node.children) {
|
|
394
|
+
node.children.forEach((child) => this.extractScreens(child, screens));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extract structure, content, and layout from nodes
|
|
399
|
+
*/
|
|
400
|
+
extractStructure(node, depth = 0) {
|
|
401
|
+
const nodes = [];
|
|
402
|
+
const hierarchyLines = [];
|
|
403
|
+
const processNode = (n, level = 0) => {
|
|
404
|
+
const indent = " ".repeat(level);
|
|
405
|
+
const childIds = [];
|
|
406
|
+
const nodeData = {
|
|
407
|
+
id: n.id,
|
|
408
|
+
name: n.name || "Unnamed",
|
|
409
|
+
type: n.type
|
|
410
|
+
};
|
|
411
|
+
if (n.absoluteBoundingBox) {
|
|
412
|
+
nodeData.position = {
|
|
413
|
+
x: n.absoluteBoundingBox.x,
|
|
414
|
+
y: n.absoluteBoundingBox.y,
|
|
415
|
+
width: n.absoluteBoundingBox.width,
|
|
416
|
+
height: n.absoluteBoundingBox.height
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (n.type === "TEXT" && n.characters) {
|
|
420
|
+
nodeData.content = n.characters;
|
|
421
|
+
}
|
|
422
|
+
const styles = {};
|
|
423
|
+
if (n.fills && Array.isArray(n.fills)) {
|
|
424
|
+
const solidFill = n.fills.find((f) => f.type === "SOLID" && f.color);
|
|
425
|
+
if (solidFill && solidFill.color) {
|
|
426
|
+
const { r, g, b, a } = solidFill.color;
|
|
427
|
+
styles.backgroundColor = this.rgbaToHex(r, g, b, a);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (n.style) {
|
|
431
|
+
if (n.style.fontFamily) styles.fontFamily = n.style.fontFamily;
|
|
432
|
+
if (n.style.fontSize) styles.fontSize = n.style.fontSize;
|
|
433
|
+
if (n.style.fontWeight) styles.fontWeight = n.style.fontWeight;
|
|
434
|
+
if (n.style.lineHeightPx) styles.lineHeight = n.style.lineHeightPx;
|
|
435
|
+
if (n.fills && Array.isArray(n.fills)) {
|
|
436
|
+
const textFill = n.fills.find((f) => f.type === "SOLID" && f.color);
|
|
437
|
+
if (textFill && textFill.color) {
|
|
438
|
+
const { r, g, b, a } = textFill.color;
|
|
439
|
+
styles.color = this.rgbaToHex(r, g, b, a);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (n.layoutMode) {
|
|
444
|
+
styles.layout = n.layoutMode;
|
|
445
|
+
}
|
|
446
|
+
if (n.itemSpacing !== void 0) {
|
|
447
|
+
styles.gap = n.itemSpacing;
|
|
448
|
+
}
|
|
449
|
+
if (n.paddingLeft || n.paddingRight || n.paddingTop || n.paddingBottom) {
|
|
450
|
+
styles.padding = {
|
|
451
|
+
top: n.paddingTop || 0,
|
|
452
|
+
right: n.paddingRight || 0,
|
|
453
|
+
bottom: n.paddingBottom || 0,
|
|
454
|
+
left: n.paddingLeft || 0
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (Object.keys(styles).length > 0) {
|
|
458
|
+
nodeData.styles = styles;
|
|
459
|
+
}
|
|
460
|
+
if (n.children && n.children.length > 0) {
|
|
461
|
+
n.children.forEach((child) => {
|
|
462
|
+
const childNodeIds = processNode(child, level + 1);
|
|
463
|
+
childIds.push(child.id);
|
|
464
|
+
childIds.push(...childNodeIds);
|
|
465
|
+
});
|
|
466
|
+
nodeData.children = n.children.map((c) => c.id);
|
|
467
|
+
}
|
|
468
|
+
nodes.push(nodeData);
|
|
469
|
+
const typeLabel = n.type.toLowerCase();
|
|
470
|
+
const nameLabel = n.name || "Unnamed";
|
|
471
|
+
const contentPreview = n.type === "TEXT" && n.characters ? `: "${n.characters.substring(0, 50)}${n.characters.length > 50 ? "..." : ""}"` : "";
|
|
472
|
+
const sizeLabel = n.absoluteBoundingBox ? ` [${Math.round(n.absoluteBoundingBox.width)}\xD7${Math.round(n.absoluteBoundingBox.height)}]` : "";
|
|
473
|
+
hierarchyLines.push(`${indent}${typeLabel} "${nameLabel}"${contentPreview}${sizeLabel}`);
|
|
474
|
+
return [n.id, ...childIds];
|
|
475
|
+
};
|
|
476
|
+
processNode(node, depth);
|
|
477
|
+
return {
|
|
478
|
+
nodes,
|
|
479
|
+
hierarchy: hierarchyLines.join("\n")
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Find all nodes that can be exported as images (optionally filtered by screen)
|
|
484
|
+
*/
|
|
485
|
+
findImageNodes(node, imageNodes = [], screenId, isWithinScreen = false) {
|
|
486
|
+
const currentIsScreen = node.id === screenId;
|
|
487
|
+
const nowWithinScreen = isWithinScreen || currentIsScreen;
|
|
488
|
+
if (screenId && !nowWithinScreen && node.type !== "PAGE") {
|
|
489
|
+
if (node.children) {
|
|
490
|
+
node.children.forEach((child) => this.findImageNodes(child, imageNodes, screenId, false));
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const exportableTypes = ["VECTOR", "COMPONENT", "INSTANCE", "FRAME", "GROUP", "RECTANGLE", "ELLIPSE"];
|
|
495
|
+
const hasImageFill = node.fills?.some((fill) => fill.type === "IMAGE" || fill.imageRef);
|
|
496
|
+
const isExportable = exportableTypes.includes(node.type) || hasImageFill;
|
|
497
|
+
if (isExportable && node.absoluteBoundingBox) {
|
|
498
|
+
const minSize = 16;
|
|
499
|
+
if (node.absoluteBoundingBox.width >= minSize && node.absoluteBoundingBox.height >= minSize) {
|
|
500
|
+
imageNodes.push({
|
|
501
|
+
id: node.id,
|
|
502
|
+
name: node.name || "Unnamed",
|
|
503
|
+
type: node.type,
|
|
504
|
+
width: node.absoluteBoundingBox.width,
|
|
505
|
+
height: node.absoluteBoundingBox.height
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (node.children) {
|
|
510
|
+
node.children.forEach((child) => this.findImageNodes(child, imageNodes, screenId, nowWithinScreen));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Download images/assets from Figma (optionally filtered by screen)
|
|
515
|
+
*/
|
|
516
|
+
async downloadAssets(fileKey, rootNode, assetsDir, screenId) {
|
|
517
|
+
const imageNodes = [];
|
|
518
|
+
this.findImageNodes(rootNode, imageNodes, screenId);
|
|
519
|
+
if (imageNodes.length === 0) {
|
|
520
|
+
logger.info("No image nodes found to download");
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
logger.info(`Found ${imageNodes.length} image nodes to download`);
|
|
524
|
+
const nodesToDownload = imageNodes.slice(0, 50);
|
|
525
|
+
const nodeIds = nodesToDownload.map((n) => n.id).join(",");
|
|
526
|
+
const imageUrl = `https://api.figma.com/v1/images/${fileKey}?ids=${encodeURIComponent(nodeIds)}&format=png&scale=2`;
|
|
527
|
+
const response = await this.fetchWithRetry(imageUrl, {
|
|
528
|
+
headers: {
|
|
529
|
+
"X-Figma-Token": this.apiKey
|
|
530
|
+
}
|
|
531
|
+
}, "Figma images listing");
|
|
532
|
+
const imageData = await response.json();
|
|
533
|
+
const images = imageData.images;
|
|
534
|
+
const fullAssetsDir = assetsDir.startsWith("/") ? assetsDir : join5(process.cwd(), assetsDir);
|
|
535
|
+
if (!existsSync2(fullAssetsDir)) {
|
|
536
|
+
await mkdir3(fullAssetsDir, { recursive: true });
|
|
537
|
+
}
|
|
538
|
+
const downloadedAssets = [];
|
|
539
|
+
for (const node of nodesToDownload) {
|
|
540
|
+
const imageUrl2 = images[node.id];
|
|
541
|
+
if (!imageUrl2) {
|
|
542
|
+
logger.warn(`No image URL returned for node ${node.id} (${node.name})`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const imageResponse = await this.fetchWithRetry(
|
|
547
|
+
imageUrl2,
|
|
548
|
+
{},
|
|
549
|
+
`Download image ${node.id}`
|
|
550
|
+
);
|
|
551
|
+
const imageBuffer = await imageResponse.arrayBuffer();
|
|
552
|
+
const safeName = node.name.replace(/[^a-z0-9]/gi, "_").toLowerCase().substring(0, 50);
|
|
553
|
+
const extension = "png";
|
|
554
|
+
const filename = `${safeName}_${node.id.substring(0, 8)}.${extension}`;
|
|
555
|
+
const filePath = join5(fullAssetsDir, filename);
|
|
556
|
+
await writeFile3(filePath, Buffer.from(imageBuffer));
|
|
557
|
+
downloadedAssets.push({
|
|
558
|
+
nodeId: node.id,
|
|
559
|
+
nodeName: node.name,
|
|
560
|
+
nodeType: node.type,
|
|
561
|
+
format: extension,
|
|
562
|
+
path: filePath,
|
|
563
|
+
url: imageUrl2,
|
|
564
|
+
width: node.width,
|
|
565
|
+
height: node.height
|
|
566
|
+
});
|
|
567
|
+
logger.info(`Downloaded: ${filename} (${node.name})`);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
logger.warn(`Error downloading image for node ${node.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
logger.info(`Downloaded ${downloadedAssets.length} assets to ${fullAssetsDir}`);
|
|
573
|
+
return downloadedAssets;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Convert RGBA to hex
|
|
577
|
+
*/
|
|
578
|
+
rgbaToHex(r, g, b, a = 1) {
|
|
579
|
+
const toHex = (n) => {
|
|
580
|
+
const hex = Math.round(n * 255).toString(16);
|
|
581
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
582
|
+
};
|
|
583
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${a < 1 ? toHex(a) : ""}`;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// src/core/tools/figma-screen-developer.ts
|
|
590
|
+
var figma_screen_developer_exports = {};
|
|
591
|
+
__export(figma_screen_developer_exports, {
|
|
592
|
+
checkCurrentCodeStatus: () => checkCurrentCodeStatus,
|
|
593
|
+
compareCodeWithFigma: () => compareCodeWithFigma
|
|
594
|
+
});
|
|
595
|
+
import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
|
|
596
|
+
import { join as join6 } from "path";
|
|
597
|
+
import { existsSync as existsSync3 } from "fs";
|
|
598
|
+
async function checkCurrentCodeStatus(projectPath = process.cwd()) {
|
|
599
|
+
const status = {
|
|
600
|
+
hasHTML: false,
|
|
601
|
+
htmlFile: void 0,
|
|
602
|
+
hasCSS: false,
|
|
603
|
+
cssFiles: [],
|
|
604
|
+
hasAssets: false,
|
|
605
|
+
assetCount: 0,
|
|
606
|
+
sections: []
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
const htmlFiles = ["index.html", "index.htm", "main.html"].filter(
|
|
610
|
+
(file) => existsSync3(join6(projectPath, file))
|
|
611
|
+
);
|
|
612
|
+
if (htmlFiles.length > 0) {
|
|
613
|
+
status.hasHTML = true;
|
|
614
|
+
status.htmlFile = htmlFiles[0];
|
|
615
|
+
try {
|
|
616
|
+
const htmlContent = await readFile4(join6(projectPath, htmlFiles[0]), "utf-8");
|
|
617
|
+
const sectionMatches = htmlContent.match(/<(section|div|header|footer|main|article|aside)[^>]*(?:id|class)=["']([^"']+)["']/gi);
|
|
618
|
+
if (sectionMatches) {
|
|
619
|
+
status.sections = sectionMatches.map((match) => {
|
|
620
|
+
const idMatch = match.match(/id=["']([^"']+)["']/i);
|
|
621
|
+
const classMatch = match.match(/class=["']([^"']+)["']/i);
|
|
622
|
+
return idMatch ? idMatch[1] : classMatch ? classMatch[1].split(" ")[0] : "";
|
|
623
|
+
}).filter(Boolean);
|
|
624
|
+
}
|
|
625
|
+
} catch (e) {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const stylesDir = join6(projectPath, "styles");
|
|
629
|
+
if (existsSync3(stylesDir)) {
|
|
630
|
+
try {
|
|
631
|
+
const files = await readdir3(stylesDir);
|
|
632
|
+
const cssFiles = files.filter((f) => f.endsWith(".css"));
|
|
633
|
+
if (cssFiles.length > 0) {
|
|
634
|
+
status.hasCSS = true;
|
|
635
|
+
status.cssFiles = cssFiles.map((f) => join6(stylesDir, f));
|
|
636
|
+
}
|
|
637
|
+
} catch (e) {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const assetsDir = join6(projectPath, "assets", "images");
|
|
641
|
+
if (existsSync3(assetsDir)) {
|
|
642
|
+
try {
|
|
643
|
+
const files = await readdir3(assetsDir);
|
|
644
|
+
const imageFiles = files.filter((f) => /\.(png|jpg|jpeg|svg|webp)$/i.test(f));
|
|
645
|
+
if (imageFiles.length > 0) {
|
|
646
|
+
status.hasAssets = true;
|
|
647
|
+
status.assetCount = imageFiles.length;
|
|
648
|
+
}
|
|
649
|
+
} catch (e) {
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
logger.warn(`Error checking code status: ${error instanceof Error ? error.message : String(error)}`);
|
|
654
|
+
}
|
|
655
|
+
return status;
|
|
656
|
+
}
|
|
657
|
+
async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath = process.cwd()) {
|
|
658
|
+
const codeStatus = await checkCurrentCodeStatus(projectPath);
|
|
659
|
+
const result = {
|
|
660
|
+
missingSections: [],
|
|
661
|
+
missingAssets: [],
|
|
662
|
+
needsUpdate: false,
|
|
663
|
+
recommendations: []
|
|
664
|
+
};
|
|
665
|
+
const selectedScreen = figmaTokens.screens?.find((s) => s.id === selectedScreenId);
|
|
666
|
+
if (!selectedScreen) {
|
|
667
|
+
result.recommendations.push("Selected screen not found in Figma design");
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
const figmaSections = [];
|
|
671
|
+
if (figmaTokens.structure?.nodes) {
|
|
672
|
+
const screenNode = figmaTokens.structure.nodes.find((n) => n.id === selectedScreenId);
|
|
673
|
+
if (screenNode?.children) {
|
|
674
|
+
screenNode.children.forEach((childId) => {
|
|
675
|
+
const childNode = figmaTokens.structure.nodes.find((n) => n.id === childId);
|
|
676
|
+
if (childNode && (childNode.type === "FRAME" || childNode.type === "COMPONENT")) {
|
|
677
|
+
const sectionName = childNode.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-");
|
|
678
|
+
figmaSections.push(sectionName);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const existingSections = codeStatus.sections.map((s) => s.toLowerCase());
|
|
684
|
+
result.missingSections = figmaSections.filter(
|
|
685
|
+
(s) => !existingSections.some((existing) => existing.includes(s) || s.includes(existing))
|
|
686
|
+
);
|
|
687
|
+
if (codeStatus.assetCount === 0) {
|
|
688
|
+
result.missingAssets.push("All assets need to be downloaded");
|
|
689
|
+
result.needsUpdate = true;
|
|
690
|
+
}
|
|
691
|
+
if (!codeStatus.hasHTML) {
|
|
692
|
+
result.recommendations.push("Create index.html with HTML5 structure");
|
|
693
|
+
}
|
|
694
|
+
if (!codeStatus.hasCSS) {
|
|
695
|
+
result.recommendations.push("Create CSS files (variables.css, base.css, components.css)");
|
|
696
|
+
}
|
|
697
|
+
if (result.missingSections.length > 0) {
|
|
698
|
+
result.recommendations.push(`Implement missing sections: ${result.missingSections.join(", ")}`);
|
|
699
|
+
}
|
|
700
|
+
if (result.missingAssets.length > 0) {
|
|
701
|
+
result.recommendations.push("Download required assets from Figma");
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
var init_figma_screen_developer = __esm({
|
|
706
|
+
"src/core/tools/figma-screen-developer.ts"() {
|
|
707
|
+
"use strict";
|
|
708
|
+
init_esm_shims();
|
|
709
|
+
init_logger();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// src/core/memory.ts
|
|
714
|
+
var memory_exports = {};
|
|
715
|
+
__export(memory_exports, {
|
|
716
|
+
MemoryManager: () => MemoryManager
|
|
717
|
+
});
|
|
718
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, access as access2, constants as constants2 } from "fs/promises";
|
|
719
|
+
import { join as join7 } from "path";
|
|
720
|
+
var MemoryManager;
|
|
721
|
+
var init_memory = __esm({
|
|
722
|
+
"src/core/memory.ts"() {
|
|
723
|
+
"use strict";
|
|
724
|
+
init_esm_shims();
|
|
725
|
+
init_paths();
|
|
726
|
+
MemoryManager = class {
|
|
727
|
+
config;
|
|
728
|
+
constructor(config) {
|
|
729
|
+
this.config = config;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* List all memory entries
|
|
733
|
+
*/
|
|
734
|
+
async list() {
|
|
735
|
+
const memories = [];
|
|
736
|
+
const memoryPath = paths.memory(this.config.configPath);
|
|
737
|
+
const subDirs = ["observations", "handoffs", "research"];
|
|
738
|
+
for (const subDir of subDirs) {
|
|
739
|
+
const dirPath = join7(memoryPath, subDir);
|
|
740
|
+
try {
|
|
741
|
+
const { readdir: readdir7 } = await import("fs/promises");
|
|
742
|
+
const files = await readdir7(dirPath);
|
|
743
|
+
for (const file of files) {
|
|
744
|
+
if (!file.endsWith(".md")) continue;
|
|
745
|
+
const content = await readFile5(join7(dirPath, file), "utf-8");
|
|
746
|
+
const summary = this.extractSummary(content);
|
|
747
|
+
memories.push({
|
|
748
|
+
key: `${subDir}/${file.replace(".md", "")}`,
|
|
749
|
+
content,
|
|
750
|
+
summary,
|
|
751
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
752
|
+
// Would get from file stats
|
|
753
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
754
|
+
type: subDir
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return memories;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Read a memory entry
|
|
764
|
+
*/
|
|
765
|
+
async read(key) {
|
|
766
|
+
const memoryPath = paths.memory(this.config.configPath);
|
|
767
|
+
let filePath;
|
|
768
|
+
if (key.includes("/")) {
|
|
769
|
+
filePath = join7(memoryPath, `${key}.md`);
|
|
770
|
+
} else {
|
|
771
|
+
const subDirs = ["observations", "handoffs", "research", "_templates"];
|
|
772
|
+
for (const subDir of subDirs) {
|
|
773
|
+
const testPath = join7(memoryPath, subDir, `${key}.md`);
|
|
774
|
+
try {
|
|
775
|
+
await access2(testPath, constants2.R_OK);
|
|
776
|
+
filePath = testPath;
|
|
777
|
+
break;
|
|
778
|
+
} catch {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
filePath = filePath || join7(memoryPath, `${key}.md`);
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
return await readFile5(filePath, "utf-8");
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Update a memory entry
|
|
792
|
+
*/
|
|
793
|
+
async update(key, content, options) {
|
|
794
|
+
const memoryPath = paths.memory(this.config.configPath);
|
|
795
|
+
const type = options?.type || "custom";
|
|
796
|
+
let filePath;
|
|
797
|
+
if (key.includes("/")) {
|
|
798
|
+
filePath = join7(memoryPath, `${key}.md`);
|
|
799
|
+
} else {
|
|
800
|
+
const subDir = type === "observation" ? "observations" : type === "handoff" ? "handoffs" : type === "research" ? "research" : "";
|
|
801
|
+
filePath = join7(memoryPath, subDir, `${key}.md`);
|
|
802
|
+
}
|
|
803
|
+
const { dirname: dirname2 } = await import("path");
|
|
804
|
+
await mkdir4(dirname2(filePath), { recursive: true });
|
|
805
|
+
if (options?.append) {
|
|
806
|
+
try {
|
|
807
|
+
const existing = await readFile5(filePath, "utf-8");
|
|
808
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
809
|
+
content = `${existing}
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
_Updated: ${timestamp}_
|
|
813
|
+
|
|
814
|
+
${content}`;
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
await writeFile4(filePath, content);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Create a handoff bundle
|
|
822
|
+
*/
|
|
823
|
+
async createHandoff(summary) {
|
|
824
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
825
|
+
const key = `handoffs/${timestamp}`;
|
|
826
|
+
const content = `# Handoff: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
827
|
+
|
|
828
|
+
## Completed
|
|
829
|
+
${summary.completed.map((item) => `- [x] ${item}`).join("\n") || "- None"}
|
|
830
|
+
|
|
831
|
+
## In Progress
|
|
832
|
+
${summary.inProgress.map((item) => `- [ ] ${item}`).join("\n") || "- None"}
|
|
833
|
+
|
|
834
|
+
## Remaining
|
|
835
|
+
${summary.remaining.map((item) => `- [ ] ${item}`).join("\n") || "- None"}
|
|
836
|
+
|
|
837
|
+
## Context
|
|
838
|
+
${summary.context || "No additional context."}
|
|
839
|
+
|
|
840
|
+
## Next Steps
|
|
841
|
+
${summary.nextSteps.map((step, i) => `${i + 1}. ${step}`).join("\n") || "- Continue from where left off"}
|
|
842
|
+
`;
|
|
843
|
+
await this.update(key, content, { type: "handoff" });
|
|
844
|
+
return key;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Get the latest handoff
|
|
848
|
+
*/
|
|
849
|
+
async getLatestHandoff() {
|
|
850
|
+
const memories = await this.list();
|
|
851
|
+
const handoffs = memories.filter((m) => m.type === "handoff");
|
|
852
|
+
if (handoffs.length === 0) return null;
|
|
853
|
+
handoffs.sort((a, b) => b.key.localeCompare(a.key));
|
|
854
|
+
return handoffs[0];
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Create an observation
|
|
858
|
+
*/
|
|
859
|
+
async createObservation(title, observation) {
|
|
860
|
+
const slug = title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
861
|
+
const key = `observations/${slug}`;
|
|
862
|
+
const content = `# ${title}
|
|
863
|
+
|
|
864
|
+
## What
|
|
865
|
+
${observation.what}
|
|
866
|
+
|
|
867
|
+
## Why
|
|
868
|
+
${observation.why}
|
|
869
|
+
|
|
870
|
+
## Impact
|
|
871
|
+
${observation.impact}
|
|
872
|
+
|
|
873
|
+
${observation.tags?.length ? `## Tags
|
|
874
|
+
${observation.tags.map((t) => `- ${t}`).join("\n")}` : ""}
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
_Created: ${(/* @__PURE__ */ new Date()).toISOString()}_
|
|
878
|
+
`;
|
|
879
|
+
await this.update(key, content, { type: "observation" });
|
|
880
|
+
return key;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Save research findings
|
|
884
|
+
*/
|
|
885
|
+
async saveResearch(topic, findings) {
|
|
886
|
+
const slug = topic.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
887
|
+
const key = `research/${slug}`;
|
|
888
|
+
const content = `# Research: ${topic}
|
|
889
|
+
|
|
890
|
+
## Summary
|
|
891
|
+
${findings.summary}
|
|
892
|
+
|
|
893
|
+
## Sources
|
|
894
|
+
${findings.sources.map((s) => `- ${s}`).join("\n")}
|
|
895
|
+
|
|
896
|
+
## Recommendations
|
|
897
|
+
${findings.recommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}
|
|
898
|
+
|
|
899
|
+
${findings.codeExamples ? `## Code Examples
|
|
900
|
+
\`\`\`
|
|
901
|
+
${findings.codeExamples}
|
|
902
|
+
\`\`\`` : ""}
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
_Researched: ${(/* @__PURE__ */ new Date()).toISOString()}_
|
|
906
|
+
`;
|
|
907
|
+
await this.update(key, content, { type: "research" });
|
|
908
|
+
return key;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Extract a summary from content (first paragraph or heading)
|
|
912
|
+
*/
|
|
913
|
+
extractSummary(content) {
|
|
914
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
915
|
+
for (const line of lines) {
|
|
916
|
+
if (!line.startsWith("#") && line.trim().length > 0) {
|
|
917
|
+
return line.slice(0, 100) + (line.length > 100 ? "..." : "");
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (lines[0]?.startsWith("#")) {
|
|
921
|
+
return lines[0].replace(/^#+\s*/, "");
|
|
922
|
+
}
|
|
923
|
+
return "No summary available";
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// src/core/tool-config.ts
|
|
930
|
+
var tool_config_exports = {};
|
|
931
|
+
__export(tool_config_exports, {
|
|
932
|
+
ToolConfigManager: () => ToolConfigManager
|
|
933
|
+
});
|
|
934
|
+
import { readFile as readFile7, writeFile as writeFile8, mkdir as mkdir8, access as access4, constants as constants4 } from "fs/promises";
|
|
935
|
+
import { join as join11 } from "path";
|
|
936
|
+
import { z as z3 } from "zod";
|
|
937
|
+
var ToolConfigSchema, REGISTERED_TOOLS, ToolConfigManager;
|
|
938
|
+
var init_tool_config = __esm({
|
|
939
|
+
"src/core/tool-config.ts"() {
|
|
940
|
+
"use strict";
|
|
941
|
+
init_esm_shims();
|
|
942
|
+
ToolConfigSchema = z3.object({
|
|
943
|
+
name: z3.string(),
|
|
944
|
+
status: z3.enum(["ready", "needs_config", "error"]),
|
|
945
|
+
description: z3.string(),
|
|
946
|
+
configMethod: z3.enum(["oauth", "manual", "none"]),
|
|
947
|
+
config: z3.record(z3.unknown()).optional(),
|
|
948
|
+
errorMessage: z3.string().optional()
|
|
949
|
+
});
|
|
950
|
+
REGISTERED_TOOLS = [
|
|
951
|
+
{
|
|
952
|
+
name: "figma-analysis",
|
|
953
|
+
description: "Analyze Figma designs and extract design tokens using Figma API",
|
|
954
|
+
configMethod: "oauth"
|
|
955
|
+
}
|
|
956
|
+
// Add more tools here as needed
|
|
957
|
+
];
|
|
958
|
+
ToolConfigManager = class {
|
|
959
|
+
config;
|
|
960
|
+
toolsConfigPath;
|
|
961
|
+
constructor(config) {
|
|
962
|
+
this.config = config;
|
|
963
|
+
this.toolsConfigPath = join11(this.config.configPath, "config", "tools.json");
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get all registered tools with their current status
|
|
967
|
+
*/
|
|
968
|
+
async listTools() {
|
|
969
|
+
const savedConfigs = await this.loadConfigs();
|
|
970
|
+
const tools = [];
|
|
971
|
+
for (const tool of REGISTERED_TOOLS) {
|
|
972
|
+
const saved = savedConfigs[tool.name];
|
|
973
|
+
const toolConfig = {
|
|
974
|
+
...tool,
|
|
975
|
+
status: this.determineStatus(tool, saved),
|
|
976
|
+
config: saved?.config,
|
|
977
|
+
errorMessage: saved?.errorMessage
|
|
978
|
+
};
|
|
979
|
+
tools.push(toolConfig);
|
|
980
|
+
}
|
|
981
|
+
return tools;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get configuration for a specific tool
|
|
985
|
+
*/
|
|
986
|
+
async getToolConfig(toolName) {
|
|
987
|
+
const tools = await this.listTools();
|
|
988
|
+
return tools.find((t) => t.name === toolName) || null;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Update tool configuration
|
|
992
|
+
*/
|
|
993
|
+
async updateToolConfig(toolName, updates) {
|
|
994
|
+
const savedConfigs = await this.loadConfigs();
|
|
995
|
+
const tool = REGISTERED_TOOLS.find((t) => t.name === toolName);
|
|
996
|
+
if (!tool) {
|
|
997
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
998
|
+
}
|
|
999
|
+
const existing = savedConfigs[toolName] || {};
|
|
1000
|
+
savedConfigs[toolName] = {
|
|
1001
|
+
...existing,
|
|
1002
|
+
...updates
|
|
1003
|
+
};
|
|
1004
|
+
await this.saveConfigs(savedConfigs);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Check if a tool is ready to use
|
|
1008
|
+
*/
|
|
1009
|
+
async isToolReady(toolName) {
|
|
1010
|
+
const toolConfig = await this.getToolConfig(toolName);
|
|
1011
|
+
return toolConfig?.status === "ready";
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Get API key for a tool (if configured)
|
|
1015
|
+
*/
|
|
1016
|
+
async getApiKey(toolName) {
|
|
1017
|
+
const toolConfig = await this.getToolConfig(toolName);
|
|
1018
|
+
if (toolConfig?.config?.apiKey && typeof toolConfig.config.apiKey === "string") {
|
|
1019
|
+
return toolConfig.config.apiKey;
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Determine tool status based on configuration
|
|
1025
|
+
*/
|
|
1026
|
+
determineStatus(tool, saved) {
|
|
1027
|
+
if (tool.configMethod === "none") {
|
|
1028
|
+
return "ready";
|
|
1029
|
+
}
|
|
1030
|
+
if (saved?.errorMessage) {
|
|
1031
|
+
return "error";
|
|
1032
|
+
}
|
|
1033
|
+
if (tool.configMethod === "oauth" || tool.configMethod === "manual") {
|
|
1034
|
+
if (saved?.config?.apiKey && typeof saved.config.apiKey === "string" && saved.config.apiKey.length > 0) {
|
|
1035
|
+
return "ready";
|
|
1036
|
+
}
|
|
1037
|
+
return "needs_config";
|
|
1038
|
+
}
|
|
1039
|
+
return "needs_config";
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Load saved configurations
|
|
1043
|
+
*/
|
|
1044
|
+
async loadConfigs() {
|
|
1045
|
+
try {
|
|
1046
|
+
await access4(this.toolsConfigPath, constants4.R_OK);
|
|
1047
|
+
const content = await readFile7(this.toolsConfigPath, "utf-8");
|
|
1048
|
+
return JSON.parse(content);
|
|
1049
|
+
} catch {
|
|
1050
|
+
return {};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Save configurations
|
|
1055
|
+
*/
|
|
1056
|
+
async saveConfigs(configs) {
|
|
1057
|
+
const configDir = join11(this.config.configPath, "config");
|
|
1058
|
+
await mkdir8(configDir, { recursive: true });
|
|
1059
|
+
await writeFile8(this.toolsConfigPath, JSON.stringify(configs, null, 2));
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// src/core/auth/figma-oauth.ts
|
|
1066
|
+
var figma_oauth_exports = {};
|
|
1067
|
+
__export(figma_oauth_exports, {
|
|
1068
|
+
FigmaOAuth: () => FigmaOAuth
|
|
1069
|
+
});
|
|
1070
|
+
import open from "open";
|
|
1071
|
+
var FigmaOAuth;
|
|
1072
|
+
var init_figma_oauth = __esm({
|
|
1073
|
+
"src/core/auth/figma-oauth.ts"() {
|
|
1074
|
+
"use strict";
|
|
1075
|
+
init_esm_shims();
|
|
1076
|
+
init_logger();
|
|
1077
|
+
FigmaOAuth = class {
|
|
1078
|
+
configManager;
|
|
1079
|
+
constructor(configManager) {
|
|
1080
|
+
this.configManager = configManager;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Start OAuth flow for Figma
|
|
1084
|
+
*
|
|
1085
|
+
* Since Figma uses Personal Access Tokens (not OAuth 2.0),
|
|
1086
|
+
* we'll open browser to token creation page and guide user
|
|
1087
|
+
*/
|
|
1088
|
+
async authenticate() {
|
|
1089
|
+
console.log("\n\u{1F510} Figma Authentication\n");
|
|
1090
|
+
console.log("Figma uses Personal Access Tokens for API access.");
|
|
1091
|
+
console.log("We will open your browser to create a token.\n");
|
|
1092
|
+
const tokenUrl = "https://www.figma.com/developers/api#access-tokens";
|
|
1093
|
+
console.log(`Opening: ${tokenUrl}`);
|
|
1094
|
+
try {
|
|
1095
|
+
await open(tokenUrl);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
console.log("\n\u26A0\uFE0F Could not open browser automatically.");
|
|
1098
|
+
console.log(`Please visit: ${tokenUrl}`);
|
|
1099
|
+
}
|
|
1100
|
+
console.log("\n\u{1F4CB} Instructions:");
|
|
1101
|
+
console.log('1. In the browser, scroll to "Personal access tokens" section');
|
|
1102
|
+
console.log('2. Click "Create new token"');
|
|
1103
|
+
console.log('3. Give it a name (e.g., "AIKit")');
|
|
1104
|
+
console.log("4. Copy the token (you won't see it again!)");
|
|
1105
|
+
console.log("5. Paste it here when prompted\n");
|
|
1106
|
+
const { default: inquirer } = await import("inquirer");
|
|
1107
|
+
const { token } = await inquirer.prompt([
|
|
1108
|
+
{
|
|
1109
|
+
type: "password",
|
|
1110
|
+
name: "token",
|
|
1111
|
+
message: "Paste your Figma Personal Access Token:",
|
|
1112
|
+
validate: (input) => {
|
|
1113
|
+
if (!input || input.trim().length === 0) {
|
|
1114
|
+
return "Token cannot be empty";
|
|
1115
|
+
}
|
|
1116
|
+
if (input.length < 20) {
|
|
1117
|
+
return "Token seems too short. Please check and try again.";
|
|
1118
|
+
}
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
]);
|
|
1123
|
+
const trimmedToken = token.trim();
|
|
1124
|
+
await this.configManager.updateToolConfig("figma-analysis", {
|
|
1125
|
+
config: {
|
|
1126
|
+
apiKey: trimmedToken
|
|
1127
|
+
},
|
|
1128
|
+
status: "ready"
|
|
1129
|
+
});
|
|
1130
|
+
logger.success("Figma token saved successfully!");
|
|
1131
|
+
return trimmedToken;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Alternative: Manual token input
|
|
1135
|
+
*/
|
|
1136
|
+
async authenticateManual() {
|
|
1137
|
+
const { default: inquirer } = await import("inquirer");
|
|
1138
|
+
console.log("\n\u{1F510} Figma Authentication (Manual)\n");
|
|
1139
|
+
console.log("To get your Figma Personal Access Token:");
|
|
1140
|
+
console.log("1. Visit: https://www.figma.com/developers/api#access-tokens");
|
|
1141
|
+
console.log('2. Scroll to "Personal access tokens"');
|
|
1142
|
+
console.log('3. Click "Create new token"');
|
|
1143
|
+
console.log("4. Copy the token and paste it below\n");
|
|
1144
|
+
const { token } = await inquirer.prompt([
|
|
1145
|
+
{
|
|
1146
|
+
type: "password",
|
|
1147
|
+
name: "token",
|
|
1148
|
+
message: "Enter your Figma Personal Access Token:",
|
|
1149
|
+
validate: (input) => {
|
|
1150
|
+
if (!input || input.trim().length === 0) {
|
|
1151
|
+
return "Token cannot be empty";
|
|
1152
|
+
}
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
]);
|
|
1157
|
+
const trimmedToken = token.trim();
|
|
1158
|
+
await this.configManager.updateToolConfig("figma-analysis", {
|
|
1159
|
+
config: {
|
|
1160
|
+
apiKey: trimmedToken
|
|
1161
|
+
},
|
|
1162
|
+
status: "ready"
|
|
1163
|
+
});
|
|
1164
|
+
logger.success("Figma token saved successfully!");
|
|
1165
|
+
return trimmedToken;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Test if token is valid by making a simple API call
|
|
1169
|
+
*/
|
|
1170
|
+
async validateToken(token) {
|
|
1171
|
+
try {
|
|
1172
|
+
const response = await fetch("https://api.figma.com/v1/me", {
|
|
1173
|
+
headers: {
|
|
1174
|
+
"X-Figma-Token": token
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
if (response.ok) {
|
|
1178
|
+
const data = await response.json();
|
|
1179
|
+
logger.success(`Token validated! Logged in as: ${data.email || "Unknown"}`);
|
|
1180
|
+
return true;
|
|
1181
|
+
} else {
|
|
1182
|
+
logger.error(`Token validation failed: ${response.status} ${response.statusText}`);
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
logger.error(`Token validation error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// src/cli.ts
|
|
1195
|
+
init_esm_shims();
|
|
1196
|
+
import { Command } from "commander";
|
|
1197
|
+
import chalk2 from "chalk";
|
|
1198
|
+
|
|
1199
|
+
// src/index.ts
|
|
1200
|
+
init_esm_shims();
|
|
1201
|
+
|
|
1202
|
+
// src/core/config.ts
|
|
1203
|
+
init_esm_shims();
|
|
1204
|
+
init_paths();
|
|
1205
|
+
import { readFile, access, constants } from "fs/promises";
|
|
1206
|
+
import { join as join2 } from "path";
|
|
1207
|
+
import { z } from "zod";
|
|
1208
|
+
var ConfigSchema = z.object({
|
|
1209
|
+
version: z.string(),
|
|
1210
|
+
skills: z.object({
|
|
1211
|
+
enabled: z.boolean().default(true),
|
|
1212
|
+
directory: z.string().optional()
|
|
1213
|
+
}).default({}),
|
|
1214
|
+
agents: z.object({
|
|
1215
|
+
enabled: z.boolean().default(true),
|
|
1216
|
+
default: z.string().default("build")
|
|
1217
|
+
}).default({}),
|
|
1218
|
+
commands: z.object({
|
|
1219
|
+
enabled: z.boolean().default(true)
|
|
1220
|
+
}).default({}),
|
|
1221
|
+
tools: z.object({
|
|
1222
|
+
enabled: z.boolean().default(true)
|
|
1223
|
+
}).default({}),
|
|
1224
|
+
plugins: z.object({
|
|
1225
|
+
enabled: z.boolean().default(true),
|
|
1226
|
+
autoload: z.array(z.string()).optional()
|
|
1227
|
+
}).default({}),
|
|
1228
|
+
memory: z.object({
|
|
1229
|
+
enabled: z.boolean().default(true),
|
|
1230
|
+
maxSize: z.number().optional()
|
|
1231
|
+
}).default({}),
|
|
1232
|
+
beads: z.object({
|
|
1233
|
+
enabled: z.boolean().default(true),
|
|
1234
|
+
autoInit: z.boolean().default(false)
|
|
1235
|
+
}).default({}),
|
|
1236
|
+
antiHallucination: z.object({
|
|
1237
|
+
enabled: z.boolean().default(true),
|
|
1238
|
+
specFile: z.string().default("spec.md"),
|
|
1239
|
+
reviewFile: z.string().default("review.md")
|
|
1240
|
+
}).default({}),
|
|
1241
|
+
mcp: z.object({
|
|
1242
|
+
context7: z.boolean().default(false),
|
|
1243
|
+
githubGrep: z.boolean().default(false),
|
|
1244
|
+
gkg: z.boolean().default(false)
|
|
1245
|
+
}).optional()
|
|
1246
|
+
});
|
|
1247
|
+
var Config = class {
|
|
1248
|
+
config;
|
|
1249
|
+
constructor(config) {
|
|
1250
|
+
this.config = config;
|
|
1251
|
+
}
|
|
1252
|
+
get() {
|
|
1253
|
+
return this.config;
|
|
1254
|
+
}
|
|
1255
|
+
get skills() {
|
|
1256
|
+
return this.config.skills;
|
|
1257
|
+
}
|
|
1258
|
+
get agents() {
|
|
1259
|
+
return this.config.agents;
|
|
1260
|
+
}
|
|
1261
|
+
get commands() {
|
|
1262
|
+
return this.config.commands;
|
|
1263
|
+
}
|
|
1264
|
+
get tools() {
|
|
1265
|
+
return this.config.tools;
|
|
1266
|
+
}
|
|
1267
|
+
get plugins() {
|
|
1268
|
+
return this.config.plugins;
|
|
1269
|
+
}
|
|
1270
|
+
get memory() {
|
|
1271
|
+
return this.config.memory;
|
|
1272
|
+
}
|
|
1273
|
+
get beads() {
|
|
1274
|
+
return this.config.beads;
|
|
1275
|
+
}
|
|
1276
|
+
get antiHallucination() {
|
|
1277
|
+
return this.config.antiHallucination;
|
|
1278
|
+
}
|
|
1279
|
+
get configPath() {
|
|
1280
|
+
return this.config.configPath;
|
|
1281
|
+
}
|
|
1282
|
+
get projectPath() {
|
|
1283
|
+
return this.config.projectPath;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get path to a specific resource
|
|
1287
|
+
*/
|
|
1288
|
+
getPath(resource) {
|
|
1289
|
+
return paths[resource](this.configPath);
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
async function loadConfig(projectPath) {
|
|
1293
|
+
const project = projectPath || process.cwd();
|
|
1294
|
+
const projectConfigPath = paths.projectConfig(project);
|
|
1295
|
+
const globalConfigPath = paths.globalConfig();
|
|
1296
|
+
let configPath;
|
|
1297
|
+
let configData = {};
|
|
1298
|
+
try {
|
|
1299
|
+
await access(join2(globalConfigPath, "aikit.json"), constants.R_OK);
|
|
1300
|
+
const globalContent = await readFile(join2(globalConfigPath, "aikit.json"), "utf-8");
|
|
1301
|
+
configData = JSON.parse(globalContent);
|
|
1302
|
+
configPath = globalConfigPath;
|
|
1303
|
+
} catch {
|
|
1304
|
+
configPath = projectConfigPath;
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
await access(join2(projectConfigPath, "aikit.json"), constants.R_OK);
|
|
1308
|
+
const projectContent = await readFile(join2(projectConfigPath, "aikit.json"), "utf-8");
|
|
1309
|
+
const projectData = JSON.parse(projectContent);
|
|
1310
|
+
configData = deepMerge(configData, projectData);
|
|
1311
|
+
configPath = projectConfigPath;
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
if (!configData.version) {
|
|
1315
|
+
configData.version = "0.1.0";
|
|
1316
|
+
}
|
|
1317
|
+
const parsed = ConfigSchema.parse(configData);
|
|
1318
|
+
return new Config({
|
|
1319
|
+
...parsed,
|
|
1320
|
+
configPath,
|
|
1321
|
+
projectPath: project
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
function deepMerge(base, override) {
|
|
1325
|
+
const result = { ...base };
|
|
1326
|
+
for (const key in override) {
|
|
1327
|
+
const baseValue = base[key];
|
|
1328
|
+
const overrideValue = override[key];
|
|
1329
|
+
if (typeof baseValue === "object" && baseValue !== null && typeof overrideValue === "object" && overrideValue !== null && !Array.isArray(baseValue) && !Array.isArray(overrideValue)) {
|
|
1330
|
+
result[key] = deepMerge(
|
|
1331
|
+
baseValue,
|
|
1332
|
+
overrideValue
|
|
1333
|
+
);
|
|
1334
|
+
} else if (overrideValue !== void 0) {
|
|
1335
|
+
result[key] = overrideValue;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/core/skills.ts
|
|
1342
|
+
init_esm_shims();
|
|
1343
|
+
init_paths();
|
|
1344
|
+
import { readFile as readFile2, readdir, writeFile, mkdir } from "fs/promises";
|
|
1345
|
+
import { join as join3, basename, extname } from "path";
|
|
1346
|
+
import matter from "gray-matter";
|
|
1347
|
+
var SkillEngine = class {
|
|
1348
|
+
config;
|
|
1349
|
+
skillsCache = /* @__PURE__ */ new Map();
|
|
1350
|
+
constructor(config) {
|
|
1351
|
+
this.config = config;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* List all available skills
|
|
1355
|
+
*/
|
|
1356
|
+
async listSkills() {
|
|
1357
|
+
const skills = [];
|
|
1358
|
+
const globalSkillsPath = paths.skills(paths.globalConfig());
|
|
1359
|
+
try {
|
|
1360
|
+
const globalSkills = await this.loadSkillsFromDir(globalSkillsPath);
|
|
1361
|
+
skills.push(...globalSkills);
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
const projectSkillsPath = paths.skills(this.config.configPath);
|
|
1365
|
+
if (projectSkillsPath !== globalSkillsPath) {
|
|
1366
|
+
try {
|
|
1367
|
+
const projectSkills = await this.loadSkillsFromDir(projectSkillsPath);
|
|
1368
|
+
for (const skill of projectSkills) {
|
|
1369
|
+
const existingIndex = skills.findIndex((s) => s.name === skill.name);
|
|
1370
|
+
if (existingIndex >= 0) {
|
|
1371
|
+
skills[existingIndex] = skill;
|
|
1372
|
+
} else {
|
|
1373
|
+
skills.push(skill);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Get a specific skill by name
|
|
1383
|
+
*/
|
|
1384
|
+
async getSkill(name) {
|
|
1385
|
+
if (this.skillsCache.has(name)) {
|
|
1386
|
+
return this.skillsCache.get(name);
|
|
1387
|
+
}
|
|
1388
|
+
const skills = await this.listSkills();
|
|
1389
|
+
const skill = skills.find((s) => s.name === name);
|
|
1390
|
+
if (skill) {
|
|
1391
|
+
this.skillsCache.set(name, skill);
|
|
1392
|
+
}
|
|
1393
|
+
return skill || null;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Find skills matching a query
|
|
1397
|
+
*/
|
|
1398
|
+
async findSkills(query) {
|
|
1399
|
+
const skills = await this.listSkills();
|
|
1400
|
+
if (!query) {
|
|
1401
|
+
return skills;
|
|
1402
|
+
}
|
|
1403
|
+
const lowerQuery = query.toLowerCase();
|
|
1404
|
+
return skills.filter(
|
|
1405
|
+
(skill) => skill.name.toLowerCase().includes(lowerQuery) || skill.description.toLowerCase().includes(lowerQuery) || skill.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)) || skill.category.toLowerCase().includes(lowerQuery)
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Create a new skill
|
|
1410
|
+
*/
|
|
1411
|
+
async createSkill(name, options) {
|
|
1412
|
+
const configPath = options?.global ? paths.globalConfig() : this.config.configPath;
|
|
1413
|
+
const skillsDir = paths.skills(configPath);
|
|
1414
|
+
await mkdir(skillsDir, { recursive: true });
|
|
1415
|
+
const fileName = `${name.replace(/\s+/g, "-").toLowerCase()}.md`;
|
|
1416
|
+
const filePath = join3(skillsDir, fileName);
|
|
1417
|
+
const frontmatter = {
|
|
1418
|
+
name,
|
|
1419
|
+
description: options?.description || `Use when you need to ${name}`,
|
|
1420
|
+
useWhen: options?.useWhen || `The user asks you to ${name}`,
|
|
1421
|
+
category: options?.category || "custom",
|
|
1422
|
+
tags: options?.tags || ["custom"]
|
|
1423
|
+
};
|
|
1424
|
+
const content = options?.content || `## ${name}
|
|
1425
|
+
|
|
1426
|
+
### Overview
|
|
1427
|
+
Describe what this skill does.
|
|
1428
|
+
|
|
1429
|
+
### Workflow
|
|
1430
|
+
|
|
1431
|
+
#### Step 1: Understand the Task
|
|
1432
|
+
- Gather context
|
|
1433
|
+
- Clarify requirements
|
|
1434
|
+
|
|
1435
|
+
#### Step 2: Plan the Approach
|
|
1436
|
+
- Break down into sub-tasks
|
|
1437
|
+
- Identify dependencies
|
|
1438
|
+
|
|
1439
|
+
#### Step 3: Execute
|
|
1440
|
+
- Follow TDD principles
|
|
1441
|
+
- Write tests first
|
|
1442
|
+
|
|
1443
|
+
#### Step 4: Verify
|
|
1444
|
+
- Run all tests
|
|
1445
|
+
- Check for regressions
|
|
1446
|
+
|
|
1447
|
+
### Checklist
|
|
1448
|
+
- [ ] Requirements understood
|
|
1449
|
+
- [ ] Tests written
|
|
1450
|
+
- [ ] Implementation complete
|
|
1451
|
+
- [ ] All tests passing
|
|
1452
|
+
- [ ] Code reviewed
|
|
1453
|
+
`;
|
|
1454
|
+
const fileContent = matter.stringify(content, frontmatter);
|
|
1455
|
+
await writeFile(filePath, fileContent);
|
|
1456
|
+
const skill = {
|
|
1457
|
+
name,
|
|
1458
|
+
description: frontmatter.description,
|
|
1459
|
+
useWhen: frontmatter.useWhen,
|
|
1460
|
+
category: frontmatter.category,
|
|
1461
|
+
tags: frontmatter.tags,
|
|
1462
|
+
content,
|
|
1463
|
+
filePath
|
|
1464
|
+
};
|
|
1465
|
+
this.skillsCache.set(name, skill);
|
|
1466
|
+
return skill;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Sync global skills to project directory
|
|
1470
|
+
*/
|
|
1471
|
+
async syncSkillsToProject() {
|
|
1472
|
+
const globalSkillsPath = paths.skills(paths.globalConfig());
|
|
1473
|
+
const projectSkillsPath = paths.skills(this.config.configPath);
|
|
1474
|
+
if (globalSkillsPath === projectSkillsPath) {
|
|
1475
|
+
return { count: 0, synced: [] };
|
|
1476
|
+
}
|
|
1477
|
+
const globalSkills = await this.loadSkillsFromDir(globalSkillsPath);
|
|
1478
|
+
await mkdir(projectSkillsPath, { recursive: true });
|
|
1479
|
+
const synced = [];
|
|
1480
|
+
for (const skill of globalSkills) {
|
|
1481
|
+
const fileName = `${skill.name.replace(/\s+/g, "-").toLowerCase()}.md`;
|
|
1482
|
+
const destPath = join3(projectSkillsPath, fileName);
|
|
1483
|
+
const srcContent = await readFile2(skill.filePath, "utf-8");
|
|
1484
|
+
await writeFile(destPath, srcContent);
|
|
1485
|
+
synced.push(skill.name);
|
|
1486
|
+
}
|
|
1487
|
+
return { count: synced.length, synced };
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Format skill for agent consumption
|
|
1491
|
+
*/
|
|
1492
|
+
formatForAgent(skill) {
|
|
1493
|
+
return `# Skill: ${skill.name}
|
|
1494
|
+
|
|
1495
|
+
## When to Use
|
|
1496
|
+
${skill.useWhen}
|
|
1497
|
+
|
|
1498
|
+
## Description
|
|
1499
|
+
${skill.description}
|
|
1500
|
+
|
|
1501
|
+
## Workflow
|
|
1502
|
+
${skill.content}
|
|
1503
|
+
|
|
1504
|
+
---
|
|
1505
|
+
**IMPORTANT**: Follow this workflow step by step. Do not skip steps.
|
|
1506
|
+
`;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Load skills from a directory
|
|
1510
|
+
*/
|
|
1511
|
+
async loadSkillsFromDir(dir) {
|
|
1512
|
+
const files = await readdir(dir);
|
|
1513
|
+
const skills = [];
|
|
1514
|
+
for (const file of files) {
|
|
1515
|
+
if (extname(file) !== ".md") continue;
|
|
1516
|
+
const filePath = join3(dir, file);
|
|
1517
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1518
|
+
const { data, content: body } = matter(content);
|
|
1519
|
+
const frontmatter = data;
|
|
1520
|
+
const name = frontmatter.name || basename(file, ".md");
|
|
1521
|
+
skills.push({
|
|
1522
|
+
name,
|
|
1523
|
+
description: frontmatter.description || "",
|
|
1524
|
+
useWhen: frontmatter.useWhen || "",
|
|
1525
|
+
category: frontmatter.category || "general",
|
|
1526
|
+
tags: frontmatter.tags || [],
|
|
1527
|
+
content: body.trim(),
|
|
1528
|
+
filePath
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
return skills;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/core/agents.ts
|
|
1536
|
+
init_esm_shims();
|
|
1537
|
+
var DEFAULT_AGENTS = [
|
|
1538
|
+
{
|
|
1539
|
+
name: "planner",
|
|
1540
|
+
displayName: "@planner",
|
|
1541
|
+
description: "Strategic planning and multi-agent coordination",
|
|
1542
|
+
useWhen: "Complex tasks requiring architecture decisions, multi-step planning, or coordination between specialists",
|
|
1543
|
+
capabilities: [
|
|
1544
|
+
"Break down complex tasks into sub-tasks",
|
|
1545
|
+
"Coordinate between specialist agents",
|
|
1546
|
+
"Make architecture decisions",
|
|
1547
|
+
"Create implementation plans"
|
|
1548
|
+
],
|
|
1549
|
+
systemPrompt: `You are a strategic planner agent. Your role is to:
|
|
1550
|
+
|
|
1551
|
+
1. ANALYZE the task and understand the full scope
|
|
1552
|
+
2. BREAK DOWN complex tasks into smaller, manageable sub-tasks
|
|
1553
|
+
3. DELEGATE to appropriate specialist agents
|
|
1554
|
+
4. COORDINATE the overall workflow
|
|
1555
|
+
5. VERIFY completion of the overall goal
|
|
1556
|
+
|
|
1557
|
+
When delegating:
|
|
1558
|
+
- Use @build for implementation tasks
|
|
1559
|
+
- Use @scout for external research
|
|
1560
|
+
- Use @review for code review and security audits
|
|
1561
|
+
- Use @explore for codebase navigation
|
|
1562
|
+
- Use @vision for visual analysis
|
|
1563
|
+
|
|
1564
|
+
Always create a clear plan before delegating. Track progress and ensure all sub-tasks complete successfully.`,
|
|
1565
|
+
delegatesTo: ["build", "scout", "review", "explore", "vision"]
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
name: "build",
|
|
1569
|
+
displayName: "@build",
|
|
1570
|
+
description: "Primary execution agent for implementing features",
|
|
1571
|
+
useWhen: "Implementing features, writing code, making changes to the codebase",
|
|
1572
|
+
capabilities: [
|
|
1573
|
+
"Write production code",
|
|
1574
|
+
"Write tests",
|
|
1575
|
+
"Refactor code",
|
|
1576
|
+
"Fix bugs",
|
|
1577
|
+
"Implement features"
|
|
1578
|
+
],
|
|
1579
|
+
systemPrompt: `You are the build agent. Your role is to implement code changes.
|
|
1580
|
+
|
|
1581
|
+
Follow these principles:
|
|
1582
|
+
1. TEST-DRIVEN DEVELOPMENT: Write tests before implementation
|
|
1583
|
+
2. INCREMENTAL CHANGES: Make small, focused commits
|
|
1584
|
+
3. VERIFY: Run tests and type checks after each change
|
|
1585
|
+
4. DOCUMENT: Add comments for complex logic
|
|
1586
|
+
|
|
1587
|
+
Before starting:
|
|
1588
|
+
- Understand the requirements fully
|
|
1589
|
+
- Check existing patterns in the codebase
|
|
1590
|
+
- Plan the implementation approach
|
|
1591
|
+
|
|
1592
|
+
After completing:
|
|
1593
|
+
- Ensure all tests pass
|
|
1594
|
+
- Run linting and type checks
|
|
1595
|
+
- Create a clear commit message`,
|
|
1596
|
+
delegatesTo: ["review", "explore"]
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
name: "rush",
|
|
1600
|
+
displayName: "@rush",
|
|
1601
|
+
description: "Fast execution with minimal planning",
|
|
1602
|
+
useWhen: "Quick fixes, hotfixes, simple edits that need minimal planning",
|
|
1603
|
+
capabilities: [
|
|
1604
|
+
"Quick bug fixes",
|
|
1605
|
+
"Simple refactoring",
|
|
1606
|
+
"Minor changes",
|
|
1607
|
+
"Hotfixes"
|
|
1608
|
+
],
|
|
1609
|
+
systemPrompt: `You are the rush agent. Your role is to make quick, focused changes.
|
|
1610
|
+
|
|
1611
|
+
Guidelines:
|
|
1612
|
+
1. ACT FAST: Minimal planning, direct execution
|
|
1613
|
+
2. FOCUS: One change at a time
|
|
1614
|
+
3. VERIFY: Quick sanity check after change
|
|
1615
|
+
4. MINIMAL SCOPE: Don't expand beyond the immediate task
|
|
1616
|
+
|
|
1617
|
+
Use for:
|
|
1618
|
+
- Typo fixes
|
|
1619
|
+
- Simple bug fixes
|
|
1620
|
+
- Minor adjustments
|
|
1621
|
+
- Urgent hotfixes`,
|
|
1622
|
+
delegatesTo: []
|
|
1623
|
+
},
|
|
1624
|
+
{
|
|
1625
|
+
name: "review",
|
|
1626
|
+
displayName: "@review",
|
|
1627
|
+
description: "Code review, debugging, and security audits",
|
|
1628
|
+
useWhen: "Reviewing code quality, finding bugs, security review, debugging issues",
|
|
1629
|
+
capabilities: [
|
|
1630
|
+
"Code review",
|
|
1631
|
+
"Security audit",
|
|
1632
|
+
"Performance analysis",
|
|
1633
|
+
"Bug finding",
|
|
1634
|
+
"Best practices enforcement"
|
|
1635
|
+
],
|
|
1636
|
+
systemPrompt: `You are the review agent. Your role is to review and improve code quality.
|
|
1637
|
+
|
|
1638
|
+
Review checklist:
|
|
1639
|
+
1. CORRECTNESS: Does the code do what it's supposed to?
|
|
1640
|
+
2. SECURITY: Are there any security vulnerabilities?
|
|
1641
|
+
3. PERFORMANCE: Are there performance issues?
|
|
1642
|
+
4. MAINTAINABILITY: Is the code clean and readable?
|
|
1643
|
+
5. TESTS: Are there adequate tests?
|
|
1644
|
+
6. PATTERNS: Does it follow project conventions?
|
|
1645
|
+
|
|
1646
|
+
When reviewing:
|
|
1647
|
+
- Be specific about issues
|
|
1648
|
+
- Suggest fixes, not just problems
|
|
1649
|
+
- Prioritize by severity
|
|
1650
|
+
- Check for edge cases`,
|
|
1651
|
+
delegatesTo: []
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
name: "scout",
|
|
1655
|
+
displayName: "@scout",
|
|
1656
|
+
description: "External research - library docs, GitHub patterns, frameworks",
|
|
1657
|
+
useWhen: "Researching external libraries, finding code patterns on GitHub, learning about frameworks",
|
|
1658
|
+
capabilities: [
|
|
1659
|
+
"Web research",
|
|
1660
|
+
"GitHub code search",
|
|
1661
|
+
"Documentation lookup",
|
|
1662
|
+
"Framework exploration",
|
|
1663
|
+
"Best practices research"
|
|
1664
|
+
],
|
|
1665
|
+
systemPrompt: `You are the scout agent. Your role is to research external resources.
|
|
1666
|
+
|
|
1667
|
+
Research strategy:
|
|
1668
|
+
1. UNDERSTAND what information is needed
|
|
1669
|
+
2. SEARCH appropriate sources (docs, GitHub, web)
|
|
1670
|
+
3. EVALUATE quality and relevance of findings
|
|
1671
|
+
4. SUMMARIZE key findings concisely
|
|
1672
|
+
5. PROVIDE actionable recommendations
|
|
1673
|
+
|
|
1674
|
+
Sources to use:
|
|
1675
|
+
- Official documentation
|
|
1676
|
+
- GitHub code examples
|
|
1677
|
+
- Stack Overflow (verified answers)
|
|
1678
|
+
- Framework guides
|
|
1679
|
+
- Community best practices
|
|
1680
|
+
|
|
1681
|
+
Always cite your sources and verify information accuracy.`,
|
|
1682
|
+
delegatesTo: []
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
name: "explore",
|
|
1686
|
+
displayName: "@explore",
|
|
1687
|
+
description: "Fast codebase navigation and pattern search",
|
|
1688
|
+
useWhen: "Finding files, understanding codebase structure, searching for patterns in code",
|
|
1689
|
+
capabilities: [
|
|
1690
|
+
"File discovery",
|
|
1691
|
+
"Pattern search",
|
|
1692
|
+
"Codebase navigation",
|
|
1693
|
+
"Structure analysis",
|
|
1694
|
+
"Dependency mapping"
|
|
1695
|
+
],
|
|
1696
|
+
systemPrompt: `You are the explore agent. Your role is to navigate and understand the codebase.
|
|
1697
|
+
|
|
1698
|
+
Exploration techniques:
|
|
1699
|
+
1. FILE STRUCTURE: Understand project organization
|
|
1700
|
+
2. GREP SEARCH: Find specific patterns or usages
|
|
1701
|
+
3. DEPENDENCY ANALYSIS: Map relationships between modules
|
|
1702
|
+
4. PATTERN DISCOVERY: Find existing patterns to follow
|
|
1703
|
+
5. QUICK CONTEXT: Gather just enough context for the task
|
|
1704
|
+
|
|
1705
|
+
Focus on speed and accuracy. Provide concise summaries of findings.`,
|
|
1706
|
+
delegatesTo: []
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
name: "vision",
|
|
1710
|
+
displayName: "@vision",
|
|
1711
|
+
description: "Multimodal analysis - mockups, PDFs, diagrams",
|
|
1712
|
+
useWhen: "Analyzing images, mockups, screenshots, PDFs, or diagrams",
|
|
1713
|
+
capabilities: [
|
|
1714
|
+
"Image analysis",
|
|
1715
|
+
"Mockup interpretation",
|
|
1716
|
+
"PDF extraction",
|
|
1717
|
+
"Diagram understanding",
|
|
1718
|
+
"UI/UX analysis"
|
|
1719
|
+
],
|
|
1720
|
+
systemPrompt: `You are the vision agent. Your role is to analyze visual content.
|
|
1721
|
+
|
|
1722
|
+
Analysis approach:
|
|
1723
|
+
1. OBSERVE: Carefully examine the visual content
|
|
1724
|
+
2. EXTRACT: Identify key information, text, structure
|
|
1725
|
+
3. INTERPRET: Understand the intent and requirements
|
|
1726
|
+
4. TRANSLATE: Convert visual specs to actionable tasks
|
|
1727
|
+
5. VALIDATE: Ensure accurate interpretation
|
|
1728
|
+
|
|
1729
|
+
For mockups:
|
|
1730
|
+
- Identify components and layout
|
|
1731
|
+
- Note colors, spacing, typography
|
|
1732
|
+
- Extract interaction patterns
|
|
1733
|
+
|
|
1734
|
+
For diagrams:
|
|
1735
|
+
- Understand relationships
|
|
1736
|
+
- Map to code structure
|
|
1737
|
+
- Note data flow`,
|
|
1738
|
+
delegatesTo: []
|
|
1739
|
+
}
|
|
1740
|
+
];
|
|
1741
|
+
var AgentManager = class {
|
|
1742
|
+
config;
|
|
1743
|
+
agents;
|
|
1744
|
+
constructor(config) {
|
|
1745
|
+
this.config = config;
|
|
1746
|
+
this.agents = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const agent of DEFAULT_AGENTS) {
|
|
1748
|
+
this.agents.set(agent.name, agent);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* List all available agents
|
|
1753
|
+
*/
|
|
1754
|
+
listAgents() {
|
|
1755
|
+
return Array.from(this.agents.values());
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Get a specific agent
|
|
1759
|
+
*/
|
|
1760
|
+
getAgent(name) {
|
|
1761
|
+
return this.agents.get(name);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Get the default agent
|
|
1765
|
+
*/
|
|
1766
|
+
getDefaultAgent() {
|
|
1767
|
+
const defaultName = this.config.agents.default;
|
|
1768
|
+
return this.agents.get(defaultName) || this.agents.get("build");
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Decide which agent should handle a task
|
|
1772
|
+
*/
|
|
1773
|
+
decideAgent(task, _context) {
|
|
1774
|
+
const lowerTask = task.toLowerCase();
|
|
1775
|
+
for (const [name, agent] of this.agents) {
|
|
1776
|
+
if (lowerTask.includes(`@${name}`)) {
|
|
1777
|
+
return {
|
|
1778
|
+
agent,
|
|
1779
|
+
reason: `Explicitly requested @${name}`,
|
|
1780
|
+
shouldDelegate: false
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (this.matchesKeywords(lowerTask, ["plan", "architect", "design", "coordinate", "complex"])) {
|
|
1785
|
+
return {
|
|
1786
|
+
agent: this.agents.get("planner"),
|
|
1787
|
+
reason: "Task requires planning and coordination",
|
|
1788
|
+
shouldDelegate: true
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
if (this.matchesKeywords(lowerTask, ["research", "docs", "documentation", "library", "framework", "how to"])) {
|
|
1792
|
+
return {
|
|
1793
|
+
agent: this.agents.get("scout"),
|
|
1794
|
+
reason: "Task requires external research",
|
|
1795
|
+
shouldDelegate: false
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
if (this.matchesKeywords(lowerTask, ["review", "audit", "security", "check", "debug"])) {
|
|
1799
|
+
return {
|
|
1800
|
+
agent: this.agents.get("review"),
|
|
1801
|
+
reason: "Task requires code review or debugging",
|
|
1802
|
+
shouldDelegate: false
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
if (this.matchesKeywords(lowerTask, ["find", "search", "where", "locate", "explore"])) {
|
|
1806
|
+
return {
|
|
1807
|
+
agent: this.agents.get("explore"),
|
|
1808
|
+
reason: "Task requires codebase exploration",
|
|
1809
|
+
shouldDelegate: false
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
if (this.matchesKeywords(lowerTask, ["image", "mockup", "screenshot", "pdf", "diagram", "visual"])) {
|
|
1813
|
+
return {
|
|
1814
|
+
agent: this.agents.get("vision"),
|
|
1815
|
+
reason: "Task requires visual analysis",
|
|
1816
|
+
shouldDelegate: false
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
if (this.matchesKeywords(lowerTask, ["fix quickly", "hotfix", "urgent", "quick fix", "typo"])) {
|
|
1820
|
+
return {
|
|
1821
|
+
agent: this.agents.get("rush"),
|
|
1822
|
+
reason: "Task is a quick fix",
|
|
1823
|
+
shouldDelegate: false
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
agent: this.agents.get("build"),
|
|
1828
|
+
reason: "Default to build agent for implementation",
|
|
1829
|
+
shouldDelegate: false
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Format agent instructions for prompt
|
|
1834
|
+
*/
|
|
1835
|
+
formatAgentPrompt(agent) {
|
|
1836
|
+
return `# Agent: ${agent.displayName}
|
|
1837
|
+
|
|
1838
|
+
${agent.systemPrompt}
|
|
1839
|
+
|
|
1840
|
+
## Capabilities
|
|
1841
|
+
${agent.capabilities.map((c) => `- ${c}`).join("\n")}
|
|
1842
|
+
|
|
1843
|
+
${agent.delegatesTo.length > 0 ? `## Can Delegate To
|
|
1844
|
+
${agent.delegatesTo.map((a) => `- @${a}`).join("\n")}` : ""}
|
|
1845
|
+
`;
|
|
1846
|
+
}
|
|
1847
|
+
matchesKeywords(text, keywords) {
|
|
1848
|
+
return keywords.some((kw) => text.includes(kw));
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// src/core/commands.ts
|
|
1853
|
+
init_esm_shims();
|
|
1854
|
+
init_paths();
|
|
1855
|
+
import { readFile as readFile3, readdir as readdir2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1856
|
+
import { join as join4, basename as basename2, extname as extname2 } from "path";
|
|
1857
|
+
import matter2 from "gray-matter";
|
|
1858
|
+
var DEFAULT_COMMANDS = [
|
|
1859
|
+
// Core Workflow Commands (Beads integration)
|
|
1860
|
+
{
|
|
1861
|
+
name: "create",
|
|
1862
|
+
description: "Create a new Beads task for tracking",
|
|
1863
|
+
category: "core",
|
|
1864
|
+
usage: "/create <task description>",
|
|
1865
|
+
examples: ["/create Add user authentication", "/create Fix navigation bug"],
|
|
1866
|
+
content: `Create a new task in the Beads system (.beads/ directory).
|
|
1867
|
+
|
|
1868
|
+
## Workflow
|
|
1869
|
+
1. Create a new bead file with unique ID
|
|
1870
|
+
2. Add task description, status, and notes
|
|
1871
|
+
3. Set status to "in-progress"
|
|
1872
|
+
4. Initialize working notes section
|
|
1873
|
+
|
|
1874
|
+
## Required Output
|
|
1875
|
+
- Task ID for reference
|
|
1876
|
+
- Confirmation of task creation
|
|
1877
|
+
- Next steps`
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
name: "plan",
|
|
1881
|
+
description: "Create a detailed implementation plan",
|
|
1882
|
+
category: "core",
|
|
1883
|
+
usage: "/plan <feature or task>",
|
|
1884
|
+
examples: ["/plan user authentication system", "/plan refactor database layer"],
|
|
1885
|
+
content: `Create a comprehensive plan before implementation.
|
|
1886
|
+
|
|
1887
|
+
## Workflow
|
|
1888
|
+
1. UNDERSTAND: Clarify requirements through Socratic questioning
|
|
1889
|
+
2. RESEARCH: Check existing patterns and dependencies
|
|
1890
|
+
3. BREAK DOWN: Create 2-5 minute sub-tasks with:
|
|
1891
|
+
- Exact file paths
|
|
1892
|
+
- Expected changes
|
|
1893
|
+
- Verification steps
|
|
1894
|
+
4. DOCUMENT: Write plan to memory/plans/
|
|
1895
|
+
|
|
1896
|
+
## Output Format
|
|
1897
|
+
\`\`\`markdown
|
|
1898
|
+
# Plan: [Feature Name]
|
|
1899
|
+
|
|
1900
|
+
## Overview
|
|
1901
|
+
Brief description of the goal.
|
|
1902
|
+
|
|
1903
|
+
## Tasks
|
|
1904
|
+
1. [ ] Task 1 - file.ts
|
|
1905
|
+
2. [ ] Task 2 - component.tsx
|
|
1906
|
+
...
|
|
1907
|
+
|
|
1908
|
+
## Dependencies
|
|
1909
|
+
- List dependencies
|
|
1910
|
+
|
|
1911
|
+
## Risks
|
|
1912
|
+
- Potential issues
|
|
1913
|
+
|
|
1914
|
+
## Verification
|
|
1915
|
+
- How to verify completion
|
|
1916
|
+
\`\`\``
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
name: "implement",
|
|
1920
|
+
description: "Implement a planned task with TDD",
|
|
1921
|
+
category: "core",
|
|
1922
|
+
usage: "/implement <task reference>",
|
|
1923
|
+
examples: ["/implement task-001", '/implement "add login form"'],
|
|
1924
|
+
content: `Implement a task following TDD principles.
|
|
1925
|
+
|
|
1926
|
+
## Workflow
|
|
1927
|
+
1. LOAD: Get task details from .beads/ or plan
|
|
1928
|
+
2. TEST: Write failing tests first (RED)
|
|
1929
|
+
3. IMPLEMENT: Write minimal code to pass (GREEN)
|
|
1930
|
+
4. REFACTOR: Clean up while keeping tests green
|
|
1931
|
+
5. VERIFY: Run full test suite
|
|
1932
|
+
|
|
1933
|
+
## Hard Gates
|
|
1934
|
+
Before marking complete:
|
|
1935
|
+
- [ ] All new tests pass
|
|
1936
|
+
- [ ] No regressions
|
|
1937
|
+
- [ ] Type check passes
|
|
1938
|
+
- [ ] Linting passes`
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
name: "finish",
|
|
1942
|
+
description: "Complete a task with quality gates",
|
|
1943
|
+
category: "core",
|
|
1944
|
+
usage: "/finish [task-id]",
|
|
1945
|
+
examples: ["/finish", "/finish task-001"],
|
|
1946
|
+
content: `Complete the current task with mandatory quality checks.
|
|
1947
|
+
|
|
1948
|
+
## Hard Gates (Must ALL Pass)
|
|
1949
|
+
1. \`npm run typecheck\` - No type errors
|
|
1950
|
+
2. \`npm run test\` - All tests pass
|
|
1951
|
+
3. \`npm run lint\` - No linting errors
|
|
1952
|
+
4. \`npm run build\` - Build succeeds
|
|
1953
|
+
|
|
1954
|
+
## Workflow
|
|
1955
|
+
1. Run all quality gates
|
|
1956
|
+
2. If any fail, report issues and stop
|
|
1957
|
+
3. If all pass, update task status to "completed"
|
|
1958
|
+
4. Create summary of changes
|
|
1959
|
+
5. Suggest commit message`
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
name: "handoff",
|
|
1963
|
+
description: "Create handoff bundle for session continuity",
|
|
1964
|
+
category: "core",
|
|
1965
|
+
usage: "/handoff",
|
|
1966
|
+
examples: ["/handoff"],
|
|
1967
|
+
content: `Create a handoff bundle for context transfer to next session.
|
|
1968
|
+
|
|
1969
|
+
## Workflow
|
|
1970
|
+
1. Summarize current progress
|
|
1971
|
+
2. Document:
|
|
1972
|
+
- What was completed
|
|
1973
|
+
- What remains
|
|
1974
|
+
- Current blockers
|
|
1975
|
+
- Key decisions made
|
|
1976
|
+
3. Save to memory/handoffs/[timestamp].md
|
|
1977
|
+
|
|
1978
|
+
## Output Format
|
|
1979
|
+
\`\`\`markdown
|
|
1980
|
+
# Handoff: [Date/Time]
|
|
1981
|
+
|
|
1982
|
+
## Completed
|
|
1983
|
+
- List of completed items
|
|
1984
|
+
|
|
1985
|
+
## In Progress
|
|
1986
|
+
- Current work state
|
|
1987
|
+
|
|
1988
|
+
## Remaining
|
|
1989
|
+
- What still needs to be done
|
|
1990
|
+
|
|
1991
|
+
## Context
|
|
1992
|
+
- Important context for next session
|
|
1993
|
+
|
|
1994
|
+
## Next Steps
|
|
1995
|
+
- Recommended actions
|
|
1996
|
+
\`\`\``
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
name: "resume",
|
|
2000
|
+
description: "Resume from last handoff",
|
|
2001
|
+
category: "core",
|
|
2002
|
+
usage: "/resume",
|
|
2003
|
+
examples: ["/resume"],
|
|
2004
|
+
content: `Resume work from the most recent handoff.
|
|
2005
|
+
|
|
2006
|
+
## Workflow
|
|
2007
|
+
1. Load latest handoff from memory/handoffs/
|
|
2008
|
+
2. Display summary to user
|
|
2009
|
+
3. Propose next actions
|
|
2010
|
+
4. Continue from where left off`
|
|
2011
|
+
},
|
|
2012
|
+
// Quick Actions
|
|
2013
|
+
{
|
|
2014
|
+
name: "fix",
|
|
2015
|
+
description: "Quick fix for an issue",
|
|
2016
|
+
category: "quick",
|
|
2017
|
+
usage: "/fix <issue description>",
|
|
2018
|
+
examples: ["/fix button not clickable", "/fix type error in auth.ts"],
|
|
2019
|
+
content: `Quick fix with minimal ceremony.
|
|
2020
|
+
|
|
2021
|
+
## Workflow
|
|
2022
|
+
1. Identify the issue
|
|
2023
|
+
2. Make minimal change to fix
|
|
2024
|
+
3. Verify fix works
|
|
2025
|
+
4. Run affected tests`
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
name: "fix-types",
|
|
2029
|
+
description: "Fix TypeScript type errors",
|
|
2030
|
+
category: "quick",
|
|
2031
|
+
usage: "/fix-types [file]",
|
|
2032
|
+
examples: ["/fix-types", "/fix-types src/auth.ts"],
|
|
2033
|
+
content: `Fix TypeScript type errors systematically.
|
|
2034
|
+
|
|
2035
|
+
## Workflow
|
|
2036
|
+
1. Run \`npm run typecheck\`
|
|
2037
|
+
2. Parse error output
|
|
2038
|
+
3. Fix each error in dependency order
|
|
2039
|
+
4. Verify all types pass`
|
|
2040
|
+
},
|
|
2041
|
+
{
|
|
2042
|
+
name: "fix-ci",
|
|
2043
|
+
description: "Fix CI/CD pipeline failures",
|
|
2044
|
+
category: "quick",
|
|
2045
|
+
usage: "/fix-ci",
|
|
2046
|
+
examples: ["/fix-ci"],
|
|
2047
|
+
content: `Diagnose and fix CI failures.
|
|
2048
|
+
|
|
2049
|
+
## Workflow
|
|
2050
|
+
1. Check CI logs for errors
|
|
2051
|
+
2. Reproduce locally if possible
|
|
2052
|
+
3. Fix issues in order:
|
|
2053
|
+
- Type errors
|
|
2054
|
+
- Test failures
|
|
2055
|
+
- Lint errors
|
|
2056
|
+
- Build errors
|
|
2057
|
+
4. Verify full CI pipeline locally`
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
name: "commit",
|
|
2061
|
+
description: "Create a well-formatted commit",
|
|
2062
|
+
category: "quick",
|
|
2063
|
+
usage: "/commit [message]",
|
|
2064
|
+
examples: ["/commit", '/commit "feat: add login"'],
|
|
2065
|
+
content: `Create a conventional commit.
|
|
2066
|
+
|
|
2067
|
+
## Workflow
|
|
2068
|
+
1. Stage changes: \`git add -A\`
|
|
2069
|
+
2. Generate commit message following conventional commits:
|
|
2070
|
+
- feat: New feature
|
|
2071
|
+
- fix: Bug fix
|
|
2072
|
+
- docs: Documentation
|
|
2073
|
+
- refactor: Code refactoring
|
|
2074
|
+
- test: Adding tests
|
|
2075
|
+
- chore: Maintenance
|
|
2076
|
+
3. Commit with message`
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
name: "pr",
|
|
2080
|
+
description: "Create a pull request",
|
|
2081
|
+
category: "quick",
|
|
2082
|
+
usage: "/pr [title]",
|
|
2083
|
+
examples: ["/pr", '/pr "Add user authentication"'],
|
|
2084
|
+
content: `Create a pull request with proper description.
|
|
2085
|
+
|
|
2086
|
+
## Workflow
|
|
2087
|
+
1. Push current branch
|
|
2088
|
+
2. Generate PR description:
|
|
2089
|
+
- Summary of changes
|
|
2090
|
+
- Related issues
|
|
2091
|
+
- Testing done
|
|
2092
|
+
- Screenshots if UI changes
|
|
2093
|
+
3. Create PR via GitHub CLI or provide URL`
|
|
2094
|
+
},
|
|
2095
|
+
// Research & Analysis
|
|
2096
|
+
{
|
|
2097
|
+
name: "research",
|
|
2098
|
+
description: "Deep research on a topic",
|
|
2099
|
+
category: "research",
|
|
2100
|
+
usage: "/research <topic>",
|
|
2101
|
+
examples: ["/research React Server Components", "/research OAuth 2.0 best practices"],
|
|
2102
|
+
content: `Conduct thorough research and document findings.
|
|
2103
|
+
|
|
2104
|
+
## Workflow
|
|
2105
|
+
1. Search documentation and resources
|
|
2106
|
+
2. Find code examples and patterns
|
|
2107
|
+
3. Evaluate options and trade-offs
|
|
2108
|
+
4. Document findings in memory/research/
|
|
2109
|
+
|
|
2110
|
+
## Output
|
|
2111
|
+
- Summary of findings
|
|
2112
|
+
- Recommended approach
|
|
2113
|
+
- Code examples
|
|
2114
|
+
- Links to resources`
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
name: "analyze-project",
|
|
2118
|
+
description: "Analyze project structure and patterns",
|
|
2119
|
+
category: "research",
|
|
2120
|
+
usage: "/analyze-project",
|
|
2121
|
+
examples: ["/analyze-project"],
|
|
2122
|
+
content: `Comprehensive project analysis.
|
|
2123
|
+
|
|
2124
|
+
## Workflow
|
|
2125
|
+
1. Scan directory structure
|
|
2126
|
+
2. Identify:
|
|
2127
|
+
- Tech stack
|
|
2128
|
+
- Architecture patterns
|
|
2129
|
+
- Key dependencies
|
|
2130
|
+
- Coding conventions
|
|
2131
|
+
3. Document findings in AGENTS.md`
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
name: "review-codebase",
|
|
2135
|
+
description: "Review codebase quality",
|
|
2136
|
+
category: "research",
|
|
2137
|
+
usage: "/review-codebase [path]",
|
|
2138
|
+
examples: ["/review-codebase", "/review-codebase src/"],
|
|
2139
|
+
content: `Review codebase for quality issues.
|
|
2140
|
+
|
|
2141
|
+
## Workflow
|
|
2142
|
+
1. Check code quality metrics
|
|
2143
|
+
2. Identify:
|
|
2144
|
+
- Code smells
|
|
2145
|
+
- Security issues
|
|
2146
|
+
- Performance concerns
|
|
2147
|
+
- Test coverage gaps
|
|
2148
|
+
3. Prioritize findings
|
|
2149
|
+
4. Suggest improvements`
|
|
2150
|
+
},
|
|
2151
|
+
// Design & Planning
|
|
2152
|
+
{
|
|
2153
|
+
name: "design",
|
|
2154
|
+
description: "Design a feature or system",
|
|
2155
|
+
category: "design",
|
|
2156
|
+
usage: "/design <feature>",
|
|
2157
|
+
examples: ["/design notification system", "/design API gateway"],
|
|
2158
|
+
content: `Design a feature with thorough planning.
|
|
2159
|
+
|
|
2160
|
+
## Workflow
|
|
2161
|
+
1. Requirements gathering (Socratic questioning)
|
|
2162
|
+
2. Research existing solutions
|
|
2163
|
+
3. Design options with trade-offs
|
|
2164
|
+
4. Choose approach
|
|
2165
|
+
5. Document design in memory/
|
|
2166
|
+
|
|
2167
|
+
## Output
|
|
2168
|
+
- Design document
|
|
2169
|
+
- Architecture diagrams (described)
|
|
2170
|
+
- API contracts
|
|
2171
|
+
- Data models`
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
name: "brainstorm",
|
|
2175
|
+
description: "Brainstorm ideas for a problem",
|
|
2176
|
+
category: "design",
|
|
2177
|
+
usage: "/brainstorm <problem>",
|
|
2178
|
+
examples: ["/brainstorm user retention", "/brainstorm performance optimization"],
|
|
2179
|
+
content: `Collaborative brainstorming session.
|
|
2180
|
+
|
|
2181
|
+
## Workflow
|
|
2182
|
+
1. Define the problem clearly
|
|
2183
|
+
2. Generate diverse ideas (no judgement)
|
|
2184
|
+
3. Group related ideas
|
|
2185
|
+
4. Evaluate feasibility
|
|
2186
|
+
5. Select top candidates`
|
|
2187
|
+
},
|
|
2188
|
+
// Git & Version Control
|
|
2189
|
+
{
|
|
2190
|
+
name: "branch",
|
|
2191
|
+
description: "Create a new feature branch",
|
|
2192
|
+
category: "git",
|
|
2193
|
+
usage: "/branch <name>",
|
|
2194
|
+
examples: ["/branch feat/auth", "/branch fix/navigation-bug"],
|
|
2195
|
+
content: `Create and switch to a new branch.
|
|
2196
|
+
|
|
2197
|
+
## Workflow
|
|
2198
|
+
1. Ensure clean working directory
|
|
2199
|
+
2. Pull latest main/master
|
|
2200
|
+
3. Create branch with naming convention:
|
|
2201
|
+
- feat/* for features
|
|
2202
|
+
- fix/* for bug fixes
|
|
2203
|
+
- refactor/* for refactoring
|
|
2204
|
+
- docs/* for documentation
|
|
2205
|
+
4. Switch to new branch`
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
name: "merge",
|
|
2209
|
+
description: "Merge current branch to target",
|
|
2210
|
+
category: "git",
|
|
2211
|
+
usage: "/merge [target]",
|
|
2212
|
+
examples: ["/merge", "/merge main"],
|
|
2213
|
+
content: `Merge current branch to target.
|
|
2214
|
+
|
|
2215
|
+
## Workflow
|
|
2216
|
+
1. Run quality gates first
|
|
2217
|
+
2. Commit any pending changes
|
|
2218
|
+
3. Switch to target branch
|
|
2219
|
+
4. Pull latest
|
|
2220
|
+
5. Merge feature branch
|
|
2221
|
+
6. Resolve conflicts if any
|
|
2222
|
+
7. Push`
|
|
2223
|
+
},
|
|
2224
|
+
// Utilities
|
|
2225
|
+
{
|
|
2226
|
+
name: "status",
|
|
2227
|
+
description: "Show current status overview",
|
|
2228
|
+
category: "utility",
|
|
2229
|
+
usage: "/status",
|
|
2230
|
+
examples: ["/status"],
|
|
2231
|
+
content: `Display comprehensive status.
|
|
2232
|
+
|
|
2233
|
+
## Shows
|
|
2234
|
+
- Current task (from Beads)
|
|
2235
|
+
- Git status
|
|
2236
|
+
- Active branch
|
|
2237
|
+
- Pending changes
|
|
2238
|
+
- Test status
|
|
2239
|
+
- Recent activity`
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
name: "help",
|
|
2243
|
+
description: "Show available commands",
|
|
2244
|
+
category: "utility",
|
|
2245
|
+
usage: "/help [command]",
|
|
2246
|
+
examples: ["/help", "/help plan"],
|
|
2247
|
+
content: `Display help information.
|
|
2248
|
+
|
|
2249
|
+
If no command specified, list all available commands.
|
|
2250
|
+
If command specified, show detailed help for that command.`
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
name: "analyze-figma",
|
|
2254
|
+
description: "Analyze Figma design and extract design tokens using Figma API",
|
|
2255
|
+
category: "design",
|
|
2256
|
+
usage: "/analyze-figma <figma-url>",
|
|
2257
|
+
examples: [
|
|
2258
|
+
"/analyze-figma https://www.figma.com/design/...",
|
|
2259
|
+
"/analyze-figma [figma-url]"
|
|
2260
|
+
],
|
|
2261
|
+
content: `Analyze a Figma design and extract all design tokens automatically using Figma API.
|
|
2262
|
+
|
|
2263
|
+
## Workflow
|
|
2264
|
+
|
|
2265
|
+
**Step 1: Extract URL from User Input**
|
|
2266
|
+
|
|
2267
|
+
The Figma URL is provided in the SAME message as the command. Extract it:
|
|
2268
|
+
- Check the full user input message
|
|
2269
|
+
- Look for URL pattern: \`https://www.figma.com/design/...\` or \`http://www.figma.com/design/...\`
|
|
2270
|
+
- Extract the ENTIRE URL including all query parameters
|
|
2271
|
+
- If URL not found in current message, check previous messages
|
|
2272
|
+
|
|
2273
|
+
**Step 2: Check Tool Configuration**
|
|
2274
|
+
|
|
2275
|
+
Before calling the tool, verify that Figma tool is configured:
|
|
2276
|
+
- If not configured, inform user to run: \`aikit skills figma-analysis config\`
|
|
2277
|
+
- The tool requires a Figma Personal Access Token
|
|
2278
|
+
|
|
2279
|
+
**Step 3: Call MCP Tool**
|
|
2280
|
+
|
|
2281
|
+
**CRITICAL**: You MUST use the MCP tool \`tool_read_figma_design\`, NOT web fetch!
|
|
2282
|
+
|
|
2283
|
+
**The correct tool name is**: \`tool_read_figma_design\` (exposed via MCP)
|
|
2284
|
+
|
|
2285
|
+
**DO NOT use**:
|
|
2286
|
+
- \u274C \`read_figma_design\` (wrong - missing "tool_" prefix)
|
|
2287
|
+
- \u274C \`figma-analysis/read_figma_design\` (wrong format)
|
|
2288
|
+
- \u274C Web fetch (file requires authentication)
|
|
2289
|
+
|
|
2290
|
+
**DO use**:
|
|
2291
|
+
- \u2705 \`tool_read_figma_design\` (correct MCP tool name)
|
|
2292
|
+
|
|
2293
|
+
Use the MCP tool:
|
|
2294
|
+
\`\`\`
|
|
2295
|
+
Use MCP tool: tool_read_figma_design
|
|
2296
|
+
Arguments: { "url": "[extracted URL]" }
|
|
2297
|
+
\`\`\`
|
|
2298
|
+
|
|
2299
|
+
The tool has the Figma API token configured and will authenticate automatically.
|
|
2300
|
+
|
|
2301
|
+
This tool will:
|
|
2302
|
+
1. Validate the Figma URL format
|
|
2303
|
+
2. Check if Figma tool is configured
|
|
2304
|
+
3. Call Figma API to fetch design data
|
|
2305
|
+
4. Extract design tokens:
|
|
2306
|
+
- Colors (from fills and strokes)
|
|
2307
|
+
- Typography (font families, sizes, weights, line heights)
|
|
2308
|
+
- Spacing system (8px grid detection)
|
|
2309
|
+
- Components (from Figma components)
|
|
2310
|
+
- Screens/Frames (dimensions and names)
|
|
2311
|
+
- Breakpoints (common responsive breakpoints)
|
|
2312
|
+
5. Return formatted markdown with all extracted tokens
|
|
2313
|
+
|
|
2314
|
+
**Step 4: Format and Save**
|
|
2315
|
+
|
|
2316
|
+
Format extracted tokens as structured markdown:
|
|
2317
|
+
\`\`\`markdown
|
|
2318
|
+
# Figma Design Analysis
|
|
2319
|
+
|
|
2320
|
+
**Source**: [Figma URL]
|
|
2321
|
+
**Analyzed**: [Date]
|
|
2322
|
+
|
|
2323
|
+
## Screens/Pages
|
|
2324
|
+
- [List all screens]
|
|
2325
|
+
|
|
2326
|
+
## Color Palette
|
|
2327
|
+
### Primary Colors
|
|
2328
|
+
- Color Name: #hexcode
|
|
2329
|
+
[Continue for all colors]
|
|
2330
|
+
|
|
2331
|
+
## Typography
|
|
2332
|
+
### Font Families
|
|
2333
|
+
- Primary: Font Name
|
|
2334
|
+
[Continue]
|
|
2335
|
+
|
|
2336
|
+
### Font Sizes
|
|
2337
|
+
- Heading 1: 48px
|
|
2338
|
+
[Continue for all sizes]
|
|
2339
|
+
|
|
2340
|
+
## Spacing System
|
|
2341
|
+
- Base unit: 8px
|
|
2342
|
+
- Values used: [list]
|
|
2343
|
+
|
|
2344
|
+
## Component Structure
|
|
2345
|
+
- Header: [description]
|
|
2346
|
+
[Continue for all components]
|
|
2347
|
+
|
|
2348
|
+
## Layout Grid
|
|
2349
|
+
- Container max-width: [value]
|
|
2350
|
+
- Columns: [value]
|
|
2351
|
+
|
|
2352
|
+
## Responsive Breakpoints
|
|
2353
|
+
- Mobile: [range]
|
|
2354
|
+
- Tablet: [range]
|
|
2355
|
+
- Desktop: [range]
|
|
2356
|
+
|
|
2357
|
+
## Assets Needed
|
|
2358
|
+
### Images
|
|
2359
|
+
- [List]
|
|
2360
|
+
|
|
2361
|
+
### Icons
|
|
2362
|
+
- [List]
|
|
2363
|
+
|
|
2364
|
+
### Fonts
|
|
2365
|
+
- [List]
|
|
2366
|
+
\`\`\`
|
|
2367
|
+
|
|
2368
|
+
Save to memory using memory-update tool:
|
|
2369
|
+
\`\`\`
|
|
2370
|
+
Use tool: memory-update
|
|
2371
|
+
Arguments: {
|
|
2372
|
+
"key": "research/figma-analysis",
|
|
2373
|
+
"content": "[formatted markdown]"
|
|
2374
|
+
}
|
|
2375
|
+
\`\`\`
|
|
2376
|
+
|
|
2377
|
+
**Step 5: Report Results**
|
|
2378
|
+
|
|
2379
|
+
Summarize what was extracted:
|
|
2380
|
+
- Number of colors found
|
|
2381
|
+
- Number of typography styles
|
|
2382
|
+
- Number of components
|
|
2383
|
+
- Number of screens/frames
|
|
2384
|
+
- Confirm save location: \`memory/research/figma-analysis.md\`
|
|
2385
|
+
|
|
2386
|
+
## Important Notes
|
|
2387
|
+
|
|
2388
|
+
- **DO NOT** ask user to provide URL again - extract it from input
|
|
2389
|
+
- **DO NOT** wait - start immediately after extracting URL
|
|
2390
|
+
- The URL is in the SAME message as the command
|
|
2391
|
+
- The tool uses Figma API, so the file must be accessible with your API token
|
|
2392
|
+
- If the tool returns an error about configuration, guide user to run: \`aikit skills figma-analysis config\`
|
|
2393
|
+
- If the tool returns an error about access, verify the file is accessible with your token
|
|
2394
|
+
|
|
2395
|
+
The analysis will be saved automatically for later reference.`
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
name: "refactor",
|
|
2399
|
+
description: "Refactor code to improve structure without changing behavior",
|
|
2400
|
+
category: "quick",
|
|
2401
|
+
usage: "/refactor [file or pattern]",
|
|
2402
|
+
examples: ["/refactor src/utils.ts", "/refactor duplicate code"],
|
|
2403
|
+
content: `Refactor code following best practices.
|
|
2404
|
+
|
|
2405
|
+
## Workflow
|
|
2406
|
+
1. Ensure tests are in place
|
|
2407
|
+
2. Identify refactoring opportunities
|
|
2408
|
+
3. Apply refactoring incrementally
|
|
2409
|
+
4. Run tests after each change
|
|
2410
|
+
5. Verify no behavior changes`
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
name: "test",
|
|
2414
|
+
description: "Run tests and show results",
|
|
2415
|
+
category: "utility",
|
|
2416
|
+
usage: "/test [pattern]",
|
|
2417
|
+
examples: ["/test", "/test auth", "/test --watch"],
|
|
2418
|
+
content: `Run test suite and display results.
|
|
2419
|
+
|
|
2420
|
+
## Workflow
|
|
2421
|
+
1. Run test command: \`npm run test\`
|
|
2422
|
+
2. Parse and display results
|
|
2423
|
+
3. Show coverage if available
|
|
2424
|
+
4. Highlight failures
|
|
2425
|
+
5. Suggest fixes for failures`
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
name: "lint",
|
|
2429
|
+
description: "Run linter and fix issues",
|
|
2430
|
+
category: "quick",
|
|
2431
|
+
usage: "/lint [--fix]",
|
|
2432
|
+
examples: ["/lint", "/lint --fix"],
|
|
2433
|
+
content: `Run linter and optionally fix issues.
|
|
2434
|
+
|
|
2435
|
+
## Workflow
|
|
2436
|
+
1. Run linter: \`npm run lint\`
|
|
2437
|
+
2. Parse errors and warnings
|
|
2438
|
+
3. If --fix flag, run auto-fix
|
|
2439
|
+
4. Report remaining issues
|
|
2440
|
+
5. Suggest manual fixes if needed`
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
name: "deploy",
|
|
2444
|
+
description: "Deploy application to production",
|
|
2445
|
+
category: "utility",
|
|
2446
|
+
usage: "/deploy [environment]",
|
|
2447
|
+
examples: ["/deploy", "/deploy staging", "/deploy production"],
|
|
2448
|
+
content: `Deploy application with quality checks.
|
|
2449
|
+
|
|
2450
|
+
## Workflow
|
|
2451
|
+
1. Run quality gates (test, lint, build)
|
|
2452
|
+
2. Check for uncommitted changes
|
|
2453
|
+
3. Build production bundle
|
|
2454
|
+
4. Deploy to target environment
|
|
2455
|
+
5. Verify deployment success`
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
name: "rollback",
|
|
2459
|
+
description: "Rollback to previous deployment",
|
|
2460
|
+
category: "utility",
|
|
2461
|
+
usage: "/rollback [version]",
|
|
2462
|
+
examples: ["/rollback", "/rollback v1.2.3"],
|
|
2463
|
+
content: `Rollback to previous version.
|
|
2464
|
+
|
|
2465
|
+
## Workflow
|
|
2466
|
+
1. Identify current version
|
|
2467
|
+
2. List available versions
|
|
2468
|
+
3. Confirm rollback target
|
|
2469
|
+
4. Execute rollback
|
|
2470
|
+
5. Verify rollback success`
|
|
2471
|
+
},
|
|
2472
|
+
{
|
|
2473
|
+
name: "logs",
|
|
2474
|
+
description: "View application logs",
|
|
2475
|
+
category: "utility",
|
|
2476
|
+
usage: "/logs [--tail] [--follow]",
|
|
2477
|
+
examples: ["/logs", "/logs --tail 100", "/logs --follow"],
|
|
2478
|
+
content: `View and filter application logs.
|
|
2479
|
+
|
|
2480
|
+
## Workflow
|
|
2481
|
+
1. Determine log location
|
|
2482
|
+
2. Apply filters if specified
|
|
2483
|
+
3. Display logs
|
|
2484
|
+
4. If --follow, stream updates
|
|
2485
|
+
5. Format for readability`
|
|
2486
|
+
}
|
|
2487
|
+
];
|
|
2488
|
+
var CommandRunner = class {
|
|
2489
|
+
config;
|
|
2490
|
+
commandsCache = /* @__PURE__ */ new Map();
|
|
2491
|
+
constructor(config) {
|
|
2492
|
+
this.config = config;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* List all available commands
|
|
2496
|
+
*/
|
|
2497
|
+
async listCommands() {
|
|
2498
|
+
const commands = [];
|
|
2499
|
+
for (const cmd of DEFAULT_COMMANDS) {
|
|
2500
|
+
commands.push({
|
|
2501
|
+
...cmd,
|
|
2502
|
+
filePath: "built-in"
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
const globalCommandsPath = paths.commands(paths.globalConfig());
|
|
2506
|
+
try {
|
|
2507
|
+
const globalCommands = await this.loadCommandsFromDir(globalCommandsPath);
|
|
2508
|
+
commands.push(...globalCommands);
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
const projectCommandsPath = paths.commands(this.config.configPath);
|
|
2512
|
+
if (projectCommandsPath !== globalCommandsPath) {
|
|
2513
|
+
try {
|
|
2514
|
+
const projectCommands = await this.loadCommandsFromDir(projectCommandsPath);
|
|
2515
|
+
for (const cmd of projectCommands) {
|
|
2516
|
+
const existingIndex = commands.findIndex((c) => c.name === cmd.name);
|
|
2517
|
+
if (existingIndex >= 0) {
|
|
2518
|
+
commands[existingIndex] = cmd;
|
|
2519
|
+
} else {
|
|
2520
|
+
commands.push(cmd);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
} catch {
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Get a specific command
|
|
2530
|
+
*/
|
|
2531
|
+
async getCommand(name) {
|
|
2532
|
+
if (this.commandsCache.has(name)) {
|
|
2533
|
+
return this.commandsCache.get(name);
|
|
2534
|
+
}
|
|
2535
|
+
const commands = await this.listCommands();
|
|
2536
|
+
const command = commands.find((c) => c.name === name);
|
|
2537
|
+
if (command) {
|
|
2538
|
+
this.commandsCache.set(name, command);
|
|
2539
|
+
}
|
|
2540
|
+
return command || null;
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Create a custom command
|
|
2544
|
+
*/
|
|
2545
|
+
async createCommand(name, options) {
|
|
2546
|
+
const configPath = options?.global ? paths.globalConfig() : this.config.configPath;
|
|
2547
|
+
const category = options?.category || "utility";
|
|
2548
|
+
const commandsDir = join4(paths.commands(configPath), category);
|
|
2549
|
+
await mkdir2(commandsDir, { recursive: true });
|
|
2550
|
+
const fileName = `${name}.md`;
|
|
2551
|
+
const filePath = join4(commandsDir, fileName);
|
|
2552
|
+
const frontmatter = {
|
|
2553
|
+
name,
|
|
2554
|
+
description: options?.description || `Custom command: ${name}`,
|
|
2555
|
+
category,
|
|
2556
|
+
usage: options?.usage || `/${name}`,
|
|
2557
|
+
examples: options?.examples || [`/${name}`]
|
|
2558
|
+
};
|
|
2559
|
+
const content = options?.content || `## /${name}
|
|
2560
|
+
|
|
2561
|
+
Describe what this command does.
|
|
2562
|
+
|
|
2563
|
+
## Workflow
|
|
2564
|
+
1. Step 1
|
|
2565
|
+
2. Step 2
|
|
2566
|
+
3. Step 3
|
|
2567
|
+
`;
|
|
2568
|
+
const fileContent = matter2.stringify(content, frontmatter);
|
|
2569
|
+
await writeFile2(filePath, fileContent);
|
|
2570
|
+
const command = {
|
|
2571
|
+
name,
|
|
2572
|
+
description: frontmatter.description,
|
|
2573
|
+
category,
|
|
2574
|
+
usage: frontmatter.usage,
|
|
2575
|
+
examples: frontmatter.examples,
|
|
2576
|
+
content,
|
|
2577
|
+
filePath
|
|
2578
|
+
};
|
|
2579
|
+
this.commandsCache.set(name, command);
|
|
2580
|
+
return command;
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Format command for agent consumption
|
|
2584
|
+
*/
|
|
2585
|
+
formatForAgent(command) {
|
|
2586
|
+
return `# Command: /${command.name}
|
|
2587
|
+
|
|
2588
|
+
## Usage
|
|
2589
|
+
\`${command.usage}\`
|
|
2590
|
+
|
|
2591
|
+
## Description
|
|
2592
|
+
${command.description}
|
|
2593
|
+
|
|
2594
|
+
## Examples
|
|
2595
|
+
${command.examples.map((e) => `- \`${e}\``).join("\n")}
|
|
2596
|
+
|
|
2597
|
+
## Workflow
|
|
2598
|
+
${command.content}
|
|
2599
|
+
`;
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Load commands from directory (recursively)
|
|
2603
|
+
*/
|
|
2604
|
+
async loadCommandsFromDir(dir) {
|
|
2605
|
+
const commands = [];
|
|
2606
|
+
const processDir = async (currentDir, category) => {
|
|
2607
|
+
try {
|
|
2608
|
+
const entries = await readdir2(currentDir, { withFileTypes: true });
|
|
2609
|
+
for (const entry of entries) {
|
|
2610
|
+
const fullPath = join4(currentDir, entry.name);
|
|
2611
|
+
if (entry.isDirectory()) {
|
|
2612
|
+
await processDir(fullPath, entry.name);
|
|
2613
|
+
} else if (extname2(entry.name) === ".md") {
|
|
2614
|
+
const content = await readFile3(fullPath, "utf-8");
|
|
2615
|
+
const { data, content: body } = matter2(content);
|
|
2616
|
+
const frontmatter = data;
|
|
2617
|
+
const name = frontmatter.name || basename2(entry.name, ".md");
|
|
2618
|
+
commands.push({
|
|
2619
|
+
name,
|
|
2620
|
+
description: frontmatter.description || "",
|
|
2621
|
+
category: frontmatter.category || category,
|
|
2622
|
+
usage: frontmatter.usage || `/${name}`,
|
|
2623
|
+
examples: frontmatter.examples || [],
|
|
2624
|
+
content: body.trim(),
|
|
2625
|
+
filePath: fullPath
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
} catch {
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
await processDir(dir, "custom");
|
|
2634
|
+
return commands;
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
// src/core/tools.ts
|
|
2639
|
+
init_esm_shims();
|
|
2640
|
+
init_paths();
|
|
2641
|
+
init_logger();
|
|
2642
|
+
import { readdir as readdir4, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
2643
|
+
import { join as join8, extname as extname3 } from "path";
|
|
2644
|
+
import { z as z2 } from "zod";
|
|
2645
|
+
var ToolArgSchema = z2.object({
|
|
2646
|
+
type: z2.enum(["string", "number", "boolean", "array", "object"]),
|
|
2647
|
+
description: z2.string(),
|
|
2648
|
+
required: z2.boolean().optional().default(true),
|
|
2649
|
+
default: z2.any().optional()
|
|
2650
|
+
});
|
|
2651
|
+
var BUILT_IN_TOOLS = [
|
|
2652
|
+
{
|
|
2653
|
+
name: "websearch",
|
|
2654
|
+
description: "Search the web for documentation, articles, and current information",
|
|
2655
|
+
args: {
|
|
2656
|
+
query: {
|
|
2657
|
+
type: "string",
|
|
2658
|
+
description: "The search query",
|
|
2659
|
+
required: true
|
|
2660
|
+
},
|
|
2661
|
+
numResults: {
|
|
2662
|
+
type: "number",
|
|
2663
|
+
description: "Number of results to return (default: 5)",
|
|
2664
|
+
required: false,
|
|
2665
|
+
default: 5
|
|
2666
|
+
}
|
|
2667
|
+
},
|
|
2668
|
+
async execute({ query }) {
|
|
2669
|
+
return `Web search results for: "${query}"
|
|
2670
|
+
|
|
2671
|
+
Note: Configure a search provider in AIKit settings for actual results.`;
|
|
2672
|
+
}
|
|
2673
|
+
},
|
|
2674
|
+
{
|
|
2675
|
+
name: "codesearch",
|
|
2676
|
+
description: "Search GitHub for code patterns and examples across millions of repositories",
|
|
2677
|
+
args: {
|
|
2678
|
+
query: {
|
|
2679
|
+
type: "string",
|
|
2680
|
+
description: "The code pattern or search query",
|
|
2681
|
+
required: true
|
|
2682
|
+
},
|
|
2683
|
+
language: {
|
|
2684
|
+
type: "string",
|
|
2685
|
+
description: "Programming language to filter by",
|
|
2686
|
+
required: false
|
|
2687
|
+
}
|
|
2688
|
+
},
|
|
2689
|
+
async execute({ query, language }) {
|
|
2690
|
+
const langFilter = language ? ` in ${language}` : "";
|
|
2691
|
+
return `GitHub code search for: "${query}"${langFilter}
|
|
2692
|
+
|
|
2693
|
+
Note: Configure GitHub integration in AIKit settings for actual results.`;
|
|
2694
|
+
}
|
|
2695
|
+
},
|
|
2696
|
+
{
|
|
2697
|
+
name: "memory-read",
|
|
2698
|
+
description: "Read from persistent memory (project or global)",
|
|
2699
|
+
args: {
|
|
2700
|
+
key: {
|
|
2701
|
+
type: "string",
|
|
2702
|
+
description: "The memory key to read",
|
|
2703
|
+
required: true
|
|
2704
|
+
}
|
|
2705
|
+
},
|
|
2706
|
+
async execute({ key }) {
|
|
2707
|
+
return `Memory read: ${key}`;
|
|
2708
|
+
}
|
|
2709
|
+
},
|
|
2710
|
+
{
|
|
2711
|
+
name: "memory-update",
|
|
2712
|
+
description: "Update persistent memory with new information",
|
|
2713
|
+
args: {
|
|
2714
|
+
key: {
|
|
2715
|
+
type: "string",
|
|
2716
|
+
description: "The memory key to update",
|
|
2717
|
+
required: true
|
|
2718
|
+
},
|
|
2719
|
+
content: {
|
|
2720
|
+
type: "string",
|
|
2721
|
+
description: "The content to write",
|
|
2722
|
+
required: true
|
|
2723
|
+
},
|
|
2724
|
+
append: {
|
|
2725
|
+
type: "boolean",
|
|
2726
|
+
description: "Whether to append to existing content (default: true)",
|
|
2727
|
+
required: false,
|
|
2728
|
+
default: true
|
|
2729
|
+
}
|
|
2730
|
+
},
|
|
2731
|
+
async execute({ key, append = true }) {
|
|
2732
|
+
return `Memory updated: ${key} (append: ${append})`;
|
|
2733
|
+
}
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
name: "find_skills",
|
|
2737
|
+
description: "Find available workflow skills",
|
|
2738
|
+
args: {
|
|
2739
|
+
query: {
|
|
2740
|
+
type: "string",
|
|
2741
|
+
description: "Optional search query to filter skills",
|
|
2742
|
+
required: false
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
async execute({ query }) {
|
|
2746
|
+
return `Skills matching: ${query || "all"}`;
|
|
2747
|
+
}
|
|
2748
|
+
},
|
|
2749
|
+
{
|
|
2750
|
+
name: "use_skill",
|
|
2751
|
+
description: "Load and use a specific skill workflow",
|
|
2752
|
+
args: {
|
|
2753
|
+
name: {
|
|
2754
|
+
type: "string",
|
|
2755
|
+
description: "Name of the skill to use",
|
|
2756
|
+
required: true
|
|
2757
|
+
}
|
|
2758
|
+
},
|
|
2759
|
+
async execute({ name }) {
|
|
2760
|
+
return `Loading skill: ${name}`;
|
|
2761
|
+
}
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
name: "read_figma_design",
|
|
2765
|
+
description: "Read and analyze a Figma design using Figma API. Extracts design tokens including colors, typography, spacing, components, and layout.",
|
|
2766
|
+
args: {
|
|
2767
|
+
url: {
|
|
2768
|
+
type: "string",
|
|
2769
|
+
description: "Figma design URL to analyze (must start with https://www.figma.com/design/)",
|
|
2770
|
+
required: true
|
|
2771
|
+
}
|
|
2772
|
+
},
|
|
2773
|
+
async execute({ url }, context) {
|
|
2774
|
+
if (!url || typeof url !== "string") {
|
|
2775
|
+
return "Error: Invalid URL provided";
|
|
2776
|
+
}
|
|
2777
|
+
if (!url.startsWith("https://www.figma.com/design/") && !url.startsWith("http://www.figma.com/design/")) {
|
|
2778
|
+
return `Error: Invalid Figma URL format. URL must start with https://www.figma.com/design/
|
|
2779
|
+
|
|
2780
|
+
Provided URL: ${url}`;
|
|
2781
|
+
}
|
|
2782
|
+
const configManager = context?.toolConfigManager;
|
|
2783
|
+
if (!configManager) {
|
|
2784
|
+
return `Error: Tool configuration manager not available. This usually means the MCP server isn't properly initialized. Please restart OpenCode.
|
|
2785
|
+
|
|
2786
|
+
If the issue persists, configure Figma tool manually: aikit skills figma-analysis config`;
|
|
2787
|
+
}
|
|
2788
|
+
const isReady = await configManager.isToolReady("figma-analysis");
|
|
2789
|
+
if (!isReady) {
|
|
2790
|
+
const toolConfig = await configManager.getToolConfig("figma-analysis");
|
|
2791
|
+
if (toolConfig?.status === "error") {
|
|
2792
|
+
return `Error: Figma tool configuration error: ${toolConfig.errorMessage || "Unknown error"}
|
|
2793
|
+
|
|
2794
|
+
Please reconfigure: aikit skills figma-analysis config`;
|
|
2795
|
+
}
|
|
2796
|
+
return `Error: Figma tool is not configured. Please run: aikit skills figma-analysis config
|
|
2797
|
+
|
|
2798
|
+
This will guide you through setting up your Figma Personal Access Token.`;
|
|
2799
|
+
}
|
|
2800
|
+
const apiKey = await configManager.getApiKey("figma-analysis");
|
|
2801
|
+
if (!apiKey) {
|
|
2802
|
+
return `Error: Figma API key not found. Please run: aikit skills figma-analysis config`;
|
|
2803
|
+
}
|
|
2804
|
+
try {
|
|
2805
|
+
const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
|
|
2806
|
+
const client = new FigmaMcpClient2(apiKey, configManager);
|
|
2807
|
+
const assetsDir = "./assets/images";
|
|
2808
|
+
const tokens = await client.extractDesignTokens(url, false, assetsDir);
|
|
2809
|
+
let result = `# Figma Design Analysis
|
|
2810
|
+
|
|
2811
|
+
`;
|
|
2812
|
+
result += `**URL**: ${url}
|
|
2813
|
+
|
|
2814
|
+
`;
|
|
2815
|
+
result += `## Design Structure & Content
|
|
2816
|
+
|
|
2817
|
+
`;
|
|
2818
|
+
if (tokens.structure) {
|
|
2819
|
+
result += `### Node Hierarchy (${tokens.structure.nodes.length} nodes)
|
|
2820
|
+
|
|
2821
|
+
`;
|
|
2822
|
+
result += `\`\`\`
|
|
2823
|
+
${tokens.structure.hierarchy}
|
|
2824
|
+
\`\`\`
|
|
2825
|
+
|
|
2826
|
+
`;
|
|
2827
|
+
const textNodes = tokens.structure.nodes.filter((n) => n.type === "TEXT" && n.content);
|
|
2828
|
+
if (textNodes.length > 0) {
|
|
2829
|
+
result += `### Text Content (${textNodes.length} text elements)
|
|
2830
|
+
|
|
2831
|
+
`;
|
|
2832
|
+
textNodes.slice(0, 20).forEach((node) => {
|
|
2833
|
+
const preview = node.content && node.content.length > 100 ? node.content.substring(0, 100) + "..." : node.content;
|
|
2834
|
+
result += `- **${node.name}**: "${preview}"
|
|
2835
|
+
`;
|
|
2836
|
+
if (node.styles) {
|
|
2837
|
+
result += ` - Style: ${node.styles.fontFamily || "N/A"} ${node.styles.fontSize || "N/A"}px, weight ${node.styles.fontWeight || "N/A"}
|
|
2838
|
+
`;
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
if (textNodes.length > 20) {
|
|
2842
|
+
result += `
|
|
2843
|
+
... and ${textNodes.length - 20} more text elements
|
|
2844
|
+
`;
|
|
2845
|
+
}
|
|
2846
|
+
result += `
|
|
2847
|
+
`;
|
|
2848
|
+
}
|
|
2849
|
+
const frameNodes = tokens.structure.nodes.filter((n) => n.type === "FRAME" || n.type === "COMPONENT");
|
|
2850
|
+
if (frameNodes.length > 0) {
|
|
2851
|
+
result += `### Layout Structure (${frameNodes.length} frames/components)
|
|
2852
|
+
|
|
2853
|
+
`;
|
|
2854
|
+
frameNodes.slice(0, 15).forEach((node) => {
|
|
2855
|
+
result += `- **${node.name}** (${node.type})
|
|
2856
|
+
`;
|
|
2857
|
+
if (node.position) {
|
|
2858
|
+
result += ` - Position: x=${Math.round(node.position.x)}, y=${Math.round(node.position.y)}
|
|
2859
|
+
`;
|
|
2860
|
+
result += ` - Size: ${Math.round(node.position.width)}\xD7${Math.round(node.position.height)}px
|
|
2861
|
+
`;
|
|
2862
|
+
}
|
|
2863
|
+
if (node.styles?.layout) {
|
|
2864
|
+
result += ` - Layout: ${node.styles.layout}${node.styles.gap ? `, gap: ${node.styles.gap}px` : ""}
|
|
2865
|
+
`;
|
|
2866
|
+
}
|
|
2867
|
+
if (node.children && node.children.length > 0) {
|
|
2868
|
+
result += ` - Children: ${node.children.length}
|
|
2869
|
+
`;
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
if (frameNodes.length > 15) {
|
|
2873
|
+
result += `
|
|
2874
|
+
... and ${frameNodes.length - 15} more frames/components
|
|
2875
|
+
`;
|
|
2876
|
+
}
|
|
2877
|
+
result += `
|
|
2878
|
+
`;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
result += `## Design Tokens
|
|
2882
|
+
|
|
2883
|
+
`;
|
|
2884
|
+
if (tokens.colors.length > 0) {
|
|
2885
|
+
result += `### Colors (${tokens.colors.length} found)
|
|
2886
|
+
|
|
2887
|
+
`;
|
|
2888
|
+
tokens.colors.slice(0, 30).forEach((color) => {
|
|
2889
|
+
result += `- \`${color.hex}\`
|
|
2890
|
+
`;
|
|
2891
|
+
});
|
|
2892
|
+
if (tokens.colors.length > 30) {
|
|
2893
|
+
result += `
|
|
2894
|
+
... and ${tokens.colors.length - 30} more colors
|
|
2895
|
+
`;
|
|
2896
|
+
}
|
|
2897
|
+
result += `
|
|
2898
|
+
`;
|
|
2899
|
+
}
|
|
2900
|
+
if (tokens.typography.length > 0) {
|
|
2901
|
+
result += `### Typography (${tokens.typography.length} styles)
|
|
2902
|
+
|
|
2903
|
+
`;
|
|
2904
|
+
tokens.typography.forEach((typography) => {
|
|
2905
|
+
result += `- **${typography.name}**: ${typography.fontFamily}, ${typography.fontSize}px, weight ${typography.fontWeight}, line-height ${typography.lineHeight}px
|
|
2906
|
+
`;
|
|
2907
|
+
});
|
|
2908
|
+
result += `
|
|
2909
|
+
`;
|
|
2910
|
+
}
|
|
2911
|
+
result += `### Spacing System
|
|
2912
|
+
|
|
2913
|
+
`;
|
|
2914
|
+
result += `- Base unit: ${tokens.spacing.unit}px
|
|
2915
|
+
`;
|
|
2916
|
+
result += `- Scale: ${tokens.spacing.scale.length > 0 ? tokens.spacing.scale.join(", ") : "Not detected"}
|
|
2917
|
+
|
|
2918
|
+
`;
|
|
2919
|
+
if (tokens.components.length > 0) {
|
|
2920
|
+
result += `### Components (${tokens.components.length} found)
|
|
2921
|
+
|
|
2922
|
+
`;
|
|
2923
|
+
tokens.components.forEach((component) => {
|
|
2924
|
+
result += `- **${component.name}**: ${component.type}${component.description ? ` - ${component.description}` : ""}
|
|
2925
|
+
`;
|
|
2926
|
+
});
|
|
2927
|
+
result += `
|
|
2928
|
+
`;
|
|
2929
|
+
}
|
|
2930
|
+
if (tokens.screens.length > 0) {
|
|
2931
|
+
result += `## Available Screens/Frames (${tokens.screens.length} found)
|
|
2932
|
+
|
|
2933
|
+
`;
|
|
2934
|
+
result += `**Please confirm which screen(s) you want to develop:**
|
|
2935
|
+
|
|
2936
|
+
`;
|
|
2937
|
+
tokens.screens.forEach((screen, index) => {
|
|
2938
|
+
result += `${index + 1}. **${screen.name}**
|
|
2939
|
+
`;
|
|
2940
|
+
result += ` - Size: ${screen.width}\xD7${screen.height}px
|
|
2941
|
+
`;
|
|
2942
|
+
result += ` - Type: ${screen.type}
|
|
2943
|
+
`;
|
|
2944
|
+
if (screen.childrenCount) {
|
|
2945
|
+
result += ` - Components: ${screen.childrenCount}
|
|
2946
|
+
`;
|
|
2947
|
+
}
|
|
2948
|
+
result += ` - ID: \`${screen.id}\`
|
|
2949
|
+
|
|
2950
|
+
`;
|
|
2951
|
+
});
|
|
2952
|
+
result += `
|
|
2953
|
+
**To proceed, simply reply with the screen number(s) or name(s) you want to develop.**
|
|
2954
|
+
`;
|
|
2955
|
+
result += `Example: "1" or "Main Page" or "1, 2, 3"
|
|
2956
|
+
|
|
2957
|
+
`;
|
|
2958
|
+
}
|
|
2959
|
+
result += `### Responsive Breakpoints
|
|
2960
|
+
|
|
2961
|
+
`;
|
|
2962
|
+
result += `- ${tokens.breakpoints.join("px, ")}px
|
|
2963
|
+
|
|
2964
|
+
`;
|
|
2965
|
+
if (tokens.assets && tokens.assets.length > 0) {
|
|
2966
|
+
result += `## Downloaded Assets (${tokens.assets.length} files)
|
|
2967
|
+
|
|
2968
|
+
`;
|
|
2969
|
+
result += `All assets have been downloaded to: \`${tokens.assets[0].path.split("/").slice(0, -1).join("/")}\`
|
|
2970
|
+
|
|
2971
|
+
`;
|
|
2972
|
+
result += `### Image Files
|
|
2973
|
+
|
|
2974
|
+
`;
|
|
2975
|
+
tokens.assets.forEach((asset) => {
|
|
2976
|
+
const relativePath = asset.path.replace(process.cwd() + "/", "");
|
|
2977
|
+
result += `- **${asset.nodeName}** (${asset.nodeType})
|
|
2978
|
+
`;
|
|
2979
|
+
result += ` - File: \`${relativePath}\`
|
|
2980
|
+
`;
|
|
2981
|
+
if (asset.width && asset.height) {
|
|
2982
|
+
result += ` - Size: ${Math.round(asset.width)}\xD7${Math.round(asset.height)}px
|
|
2983
|
+
`;
|
|
2984
|
+
}
|
|
2985
|
+
result += ` - Format: ${asset.format.toUpperCase()}
|
|
2986
|
+
`;
|
|
2987
|
+
result += ` - Usage: \`<img src="${relativePath}" alt="${asset.nodeName}" />\`
|
|
2988
|
+
|
|
2989
|
+
`;
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
result += `## Implementation Guide
|
|
2993
|
+
|
|
2994
|
+
`;
|
|
2995
|
+
result += `### Structure Analysis
|
|
2996
|
+
`;
|
|
2997
|
+
result += `The design contains ${tokens.structure?.nodes.length || 0} nodes organized in a hierarchical structure.
|
|
2998
|
+
`;
|
|
2999
|
+
result += `Use the node hierarchy above to understand:
|
|
3000
|
+
`;
|
|
3001
|
+
result += `1. **Component structure** - How elements are organized
|
|
3002
|
+
`;
|
|
3003
|
+
result += `2. **Text content** - All text content from TEXT nodes
|
|
3004
|
+
`;
|
|
3005
|
+
result += `3. **Layout properties** - Flex direction, gaps, padding
|
|
3006
|
+
`;
|
|
3007
|
+
result += `4. **Positioning** - Exact x, y, width, height values
|
|
3008
|
+
|
|
3009
|
+
`;
|
|
3010
|
+
result += `### Next Steps
|
|
3011
|
+
|
|
3012
|
+
`;
|
|
3013
|
+
result += `1. Review the structure hierarchy to understand component organization
|
|
3014
|
+
`;
|
|
3015
|
+
result += `2. Extract text content from TEXT nodes for HTML content
|
|
3016
|
+
`;
|
|
3017
|
+
result += `3. Use position and size data for pixel-perfect CSS
|
|
3018
|
+
`;
|
|
3019
|
+
result += `4. Use layout properties (HORIZONTAL/VERTICAL) for flexbox/grid
|
|
3020
|
+
`;
|
|
3021
|
+
result += `5. Use extracted design tokens (colors, typography) for styling
|
|
3022
|
+
`;
|
|
3023
|
+
result += `6. Save this analysis: \`memory-update("research/figma-analysis", "[this analysis]")\`
|
|
3024
|
+
`;
|
|
3025
|
+
return result;
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3028
|
+
return `Error analyzing Figma design: ${errorMessage}
|
|
3029
|
+
|
|
3030
|
+
Please check:
|
|
3031
|
+
1. The Figma URL is correct and accessible
|
|
3032
|
+
2. Your Figma API token has proper permissions
|
|
3033
|
+
3. The design file is shared with your account`;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
name: "analyze_figma",
|
|
3039
|
+
description: "Analyze a Figma design URL and extract all design tokens automatically. The URL should be provided in the user input after the command.",
|
|
3040
|
+
args: {
|
|
3041
|
+
url: {
|
|
3042
|
+
type: "string",
|
|
3043
|
+
description: "Figma design URL to analyze",
|
|
3044
|
+
required: true
|
|
3045
|
+
}
|
|
3046
|
+
},
|
|
3047
|
+
async execute({ url }) {
|
|
3048
|
+
return `Figma analysis tool called for: ${url}
|
|
3049
|
+
|
|
3050
|
+
Next steps:
|
|
3051
|
+
1. Use @vision agent to analyze the design
|
|
3052
|
+
2. Extract all design tokens
|
|
3053
|
+
3. Save to memory/research/figma-analysis.md`;
|
|
3054
|
+
}
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
name: "develop_figma_screen",
|
|
3058
|
+
description: "Smart workflow to develop a specific Figma screen: check current code, pull needed assets, plan, and develop. User just needs to confirm the screen number/name.",
|
|
3059
|
+
args: {
|
|
3060
|
+
figmaUrl: {
|
|
3061
|
+
type: "string",
|
|
3062
|
+
description: "Figma design URL",
|
|
3063
|
+
required: true
|
|
3064
|
+
},
|
|
3065
|
+
screenId: {
|
|
3066
|
+
type: "string",
|
|
3067
|
+
description: "Screen ID or name to develop (from read_figma_design output)",
|
|
3068
|
+
required: true
|
|
3069
|
+
}
|
|
3070
|
+
},
|
|
3071
|
+
async execute({ figmaUrl, screenId }, context) {
|
|
3072
|
+
const configManager = context?.toolConfigManager;
|
|
3073
|
+
if (!configManager) {
|
|
3074
|
+
return "Error: Tool configuration manager not available.";
|
|
3075
|
+
}
|
|
3076
|
+
const isReady = await configManager.isToolReady("figma-analysis");
|
|
3077
|
+
if (!isReady) {
|
|
3078
|
+
return "Error: Figma tool is not configured. Please run: aikit skills figma-analysis config";
|
|
3079
|
+
}
|
|
3080
|
+
const apiKey = await configManager.getApiKey("figma-analysis");
|
|
3081
|
+
if (!apiKey) {
|
|
3082
|
+
return "Error: Figma API key not found.";
|
|
3083
|
+
}
|
|
3084
|
+
try {
|
|
3085
|
+
const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
|
|
3086
|
+
const { checkCurrentCodeStatus: checkCurrentCodeStatus2, compareCodeWithFigma: compareCodeWithFigma2 } = await Promise.resolve().then(() => (init_figma_screen_developer(), figma_screen_developer_exports));
|
|
3087
|
+
const client = new FigmaMcpClient2(apiKey, configManager);
|
|
3088
|
+
const tokens = await client.extractDesignTokens(figmaUrl, false, "./assets/images");
|
|
3089
|
+
const screenIdStr = String(screenId);
|
|
3090
|
+
const selectedScreen = tokens.screens?.find(
|
|
3091
|
+
(s) => s.id === screenIdStr || s.name.toLowerCase() === screenIdStr.toLowerCase()
|
|
3092
|
+
);
|
|
3093
|
+
if (!selectedScreen) {
|
|
3094
|
+
return `Error: Screen "${screenId}" not found. Available screens:
|
|
3095
|
+
${tokens.screens?.map((s, i) => `${i + 1}. ${s.name} (ID: ${s.id})`).join("\n") || "None"}`;
|
|
3096
|
+
}
|
|
3097
|
+
const codeStatus = await checkCurrentCodeStatus2();
|
|
3098
|
+
const comparison = await compareCodeWithFigma2(tokens, selectedScreen.id);
|
|
3099
|
+
let downloadedAssets = [];
|
|
3100
|
+
const fileKey = client.extractFileKey(figmaUrl);
|
|
3101
|
+
if (fileKey) {
|
|
3102
|
+
try {
|
|
3103
|
+
const fileData = await client.getFileData(figmaUrl);
|
|
3104
|
+
const projectPath = process.cwd();
|
|
3105
|
+
const assetsDir = join8(projectPath, "assets", "images");
|
|
3106
|
+
const assets = await client.downloadAssets(fileKey, fileData.document, assetsDir, selectedScreen.id);
|
|
3107
|
+
downloadedAssets = assets || [];
|
|
3108
|
+
logger.info(`Downloaded ${downloadedAssets.length} assets for screen ${selectedScreen.name}`);
|
|
3109
|
+
} catch (error) {
|
|
3110
|
+
logger.warn(`Failed to download assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
3111
|
+
downloadedAssets = [];
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
let result = `# Development Plan for Screen: ${selectedScreen.name}
|
|
3115
|
+
|
|
3116
|
+
`;
|
|
3117
|
+
result += `## Current Code Status
|
|
3118
|
+
|
|
3119
|
+
`;
|
|
3120
|
+
result += `- HTML: ${codeStatus.hasHTML ? `\u2705 ${codeStatus.htmlFile}` : "\u274C Not found"}
|
|
3121
|
+
`;
|
|
3122
|
+
result += `- CSS: ${codeStatus.hasCSS ? `\u2705 ${codeStatus.cssFiles.length} files` : "\u274C Not found"}
|
|
3123
|
+
`;
|
|
3124
|
+
result += `- Assets: ${codeStatus.hasAssets ? `\u2705 ${codeStatus.assetCount} files` : "\u274C Not found"}
|
|
3125
|
+
`;
|
|
3126
|
+
result += `- Existing Sections: ${codeStatus.sections.length > 0 ? codeStatus.sections.join(", ") : "None"}
|
|
3127
|
+
|
|
3128
|
+
`;
|
|
3129
|
+
result += `## Comparison with Figma Design
|
|
3130
|
+
|
|
3131
|
+
`;
|
|
3132
|
+
result += `- Missing Sections: ${comparison.missingSections.length > 0 ? comparison.missingSections.join(", ") : "None"}
|
|
3133
|
+
`;
|
|
3134
|
+
result += `- Missing Assets: ${comparison.missingAssets.length > 0 ? comparison.missingAssets.join(", ") : "None"}
|
|
3135
|
+
`;
|
|
3136
|
+
result += `- Needs Update: ${comparison.needsUpdate ? "Yes" : "No"}
|
|
3137
|
+
|
|
3138
|
+
`;
|
|
3139
|
+
result += `## Recommendations
|
|
3140
|
+
|
|
3141
|
+
`;
|
|
3142
|
+
comparison.recommendations.forEach((rec, i) => {
|
|
3143
|
+
result += `${i + 1}. ${rec}
|
|
3144
|
+
`;
|
|
3145
|
+
});
|
|
3146
|
+
result += `
|
|
3147
|
+
`;
|
|
3148
|
+
result += `## Downloaded Assets (${downloadedAssets.length})
|
|
3149
|
+
|
|
3150
|
+
`;
|
|
3151
|
+
if (downloadedAssets.length > 0) {
|
|
3152
|
+
downloadedAssets.forEach((asset) => {
|
|
3153
|
+
const relativePath = asset.path.replace(process.cwd() + "/", "");
|
|
3154
|
+
result += `- **${asset.nodeName}** (${asset.nodeType})
|
|
3155
|
+
`;
|
|
3156
|
+
result += ` - File: \`${relativePath}\`
|
|
3157
|
+
`;
|
|
3158
|
+
if (asset.width && asset.height) {
|
|
3159
|
+
result += ` - Size: ${Math.round(asset.width)}\xD7${Math.round(asset.height)}px
|
|
3160
|
+
`;
|
|
3161
|
+
}
|
|
3162
|
+
result += ` - Usage: \`<img src="${relativePath}" alt="${asset.nodeName}" />\`
|
|
3163
|
+
|
|
3164
|
+
`;
|
|
3165
|
+
});
|
|
3166
|
+
result += `
|
|
3167
|
+
**\u26A0\uFE0F IMPORTANT: Use downloaded assets above instead of placeholder images!**
|
|
3168
|
+
`;
|
|
3169
|
+
result += `Replace all Unsplash/placeholder image URLs with the downloaded asset paths.
|
|
3170
|
+
|
|
3171
|
+
`;
|
|
3172
|
+
} else {
|
|
3173
|
+
result += `**No assets downloaded.** This may indicate:
|
|
3174
|
+
`;
|
|
3175
|
+
result += `1. The screen doesn't contain exportable image nodes
|
|
3176
|
+
`;
|
|
3177
|
+
result += `2. Assets download failed (check logs)
|
|
3178
|
+
`;
|
|
3179
|
+
result += `3. Figma API permissions issue
|
|
3180
|
+
|
|
3181
|
+
`;
|
|
3182
|
+
}
|
|
3183
|
+
result += `
|
|
3184
|
+
## Next Steps
|
|
3185
|
+
|
|
3186
|
+
`;
|
|
3187
|
+
result += `1. Review the plan above
|
|
3188
|
+
`;
|
|
3189
|
+
result += `2. Use @build agent to implement missing sections
|
|
3190
|
+
`;
|
|
3191
|
+
result += `3. Use extracted design tokens for CSS variables
|
|
3192
|
+
`;
|
|
3193
|
+
result += `4. Use downloaded assets in HTML
|
|
3194
|
+
`;
|
|
3195
|
+
result += `5. Verify pixel-perfect match with Figma design
|
|
3196
|
+
`;
|
|
3197
|
+
return result;
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3203
|
+
{
|
|
3204
|
+
name: "list_session",
|
|
3205
|
+
description: "List previous sessions to discover what happened and when. Use this before read_session to find the right context.",
|
|
3206
|
+
args: {
|
|
3207
|
+
limit: {
|
|
3208
|
+
type: "number",
|
|
3209
|
+
description: "Maximum number of sessions to return (default: 10)",
|
|
3210
|
+
required: false,
|
|
3211
|
+
default: 10
|
|
3212
|
+
}
|
|
3213
|
+
},
|
|
3214
|
+
async execute({ limit = 10 }, context) {
|
|
3215
|
+
const config = context?.config;
|
|
3216
|
+
if (!config) {
|
|
3217
|
+
return "Error: Configuration not available";
|
|
3218
|
+
}
|
|
3219
|
+
const limitNum = typeof limit === "number" ? limit : 10;
|
|
3220
|
+
try {
|
|
3221
|
+
const { MemoryManager: MemoryManager2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
3222
|
+
const memory = new MemoryManager2(config);
|
|
3223
|
+
const memories = await memory.list();
|
|
3224
|
+
const handoffs = memories.filter((m) => m.type === "handoff").sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).slice(0, limitNum);
|
|
3225
|
+
if (handoffs.length === 0) {
|
|
3226
|
+
return "No previous sessions found. Use /handoff to create a session handoff.";
|
|
3227
|
+
}
|
|
3228
|
+
let result = `# Previous Sessions (${handoffs.length})
|
|
3229
|
+
|
|
3230
|
+
`;
|
|
3231
|
+
handoffs.forEach((handoff, index) => {
|
|
3232
|
+
const sessionId = handoff.key.replace("handoffs/", "");
|
|
3233
|
+
result += `${index + 1}. **${sessionId}**
|
|
3234
|
+
`;
|
|
3235
|
+
result += ` - Updated: ${handoff.updatedAt.toLocaleString()}
|
|
3236
|
+
`;
|
|
3237
|
+
result += ` - Summary: ${handoff.summary}
|
|
3238
|
+
|
|
3239
|
+
`;
|
|
3240
|
+
});
|
|
3241
|
+
result += `
|
|
3242
|
+
Use \`read_session\` with a session ID to load full context.`;
|
|
3243
|
+
return result;
|
|
3244
|
+
} catch (error) {
|
|
3245
|
+
return `Error listing sessions: ${error instanceof Error ? error.message : String(error)}`;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
name: "read_session",
|
|
3251
|
+
description: "Load context from a previous session. Returns session summary, user tasks, and file changes.",
|
|
3252
|
+
args: {
|
|
3253
|
+
sessionId: {
|
|
3254
|
+
type: "string",
|
|
3255
|
+
description: 'Session ID from list_session (e.g., "2024-01-15T10-30-00")',
|
|
3256
|
+
required: true
|
|
3257
|
+
}
|
|
3258
|
+
},
|
|
3259
|
+
async execute({ sessionId }, context) {
|
|
3260
|
+
const config = context?.config;
|
|
3261
|
+
if (!config) {
|
|
3262
|
+
return "Error: Configuration not available";
|
|
3263
|
+
}
|
|
3264
|
+
try {
|
|
3265
|
+
const { MemoryManager: MemoryManager2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
3266
|
+
const memory = new MemoryManager2(config);
|
|
3267
|
+
const content = await memory.read(`handoffs/${sessionId}`);
|
|
3268
|
+
if (!content) {
|
|
3269
|
+
return `Session not found: ${sessionId}
|
|
3270
|
+
|
|
3271
|
+
Use \`list_session\` to see available sessions.`;
|
|
3272
|
+
}
|
|
3273
|
+
return `# Session Context: ${sessionId}
|
|
3274
|
+
|
|
3275
|
+
${content}
|
|
3276
|
+
|
|
3277
|
+
---
|
|
3278
|
+
|
|
3279
|
+
This context has been loaded. Use /resume to continue from this point.`;
|
|
3280
|
+
} catch (error) {
|
|
3281
|
+
return `Error reading session: ${error instanceof Error ? error.message : String(error)}`;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
];
|
|
3286
|
+
var ToolRegistry = class {
|
|
3287
|
+
config;
|
|
3288
|
+
tools = /* @__PURE__ */ new Map();
|
|
3289
|
+
toolConfigManager;
|
|
3290
|
+
constructor(config) {
|
|
3291
|
+
this.config = config;
|
|
3292
|
+
for (const tool of BUILT_IN_TOOLS) {
|
|
3293
|
+
this.tools.set(tool.name, tool);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Set tool config manager (for tools that need configuration)
|
|
3298
|
+
*/
|
|
3299
|
+
setToolConfigManager(manager) {
|
|
3300
|
+
this.toolConfigManager = manager;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* List all available tools
|
|
3304
|
+
*/
|
|
3305
|
+
async listTools() {
|
|
3306
|
+
await this.loadCustomTools();
|
|
3307
|
+
return Array.from(this.tools.values());
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Get a specific tool
|
|
3311
|
+
*/
|
|
3312
|
+
getTool(name) {
|
|
3313
|
+
return this.tools.get(name);
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Register a new tool
|
|
3317
|
+
*/
|
|
3318
|
+
registerTool(tool) {
|
|
3319
|
+
this.tools.set(tool.name, tool);
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Execute a tool
|
|
3323
|
+
*/
|
|
3324
|
+
async executeTool(name, args, context) {
|
|
3325
|
+
const tool = this.tools.get(name);
|
|
3326
|
+
if (!tool) {
|
|
3327
|
+
throw new Error(`Tool not found: ${name}`);
|
|
3328
|
+
}
|
|
3329
|
+
for (const [argName, argDef] of Object.entries(tool.args)) {
|
|
3330
|
+
if (argDef.required && args[argName] === void 0) {
|
|
3331
|
+
throw new Error(`Missing required argument: ${argName}`);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const mergedContext = {
|
|
3335
|
+
...context,
|
|
3336
|
+
toolConfigManager: context?.toolConfigManager || this.toolConfigManager
|
|
3337
|
+
};
|
|
3338
|
+
if (tool.execute.length === 2) {
|
|
3339
|
+
return await tool.execute(args, mergedContext);
|
|
3340
|
+
}
|
|
3341
|
+
return await tool.execute(args);
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Create a custom tool
|
|
3345
|
+
*/
|
|
3346
|
+
async createTool(name, options) {
|
|
3347
|
+
const configPath = options.global ? paths.globalConfig() : this.config.configPath;
|
|
3348
|
+
const toolsDir = paths.tools(configPath);
|
|
3349
|
+
await mkdir5(toolsDir, { recursive: true });
|
|
3350
|
+
const fileName = `${name}.ts`;
|
|
3351
|
+
const filePath = join8(toolsDir, fileName);
|
|
3352
|
+
const argsSchema = Object.entries(options.args).map(([argName, arg]) => ` ${argName}: {
|
|
3353
|
+
type: '${arg.type}',
|
|
3354
|
+
description: '${arg.description}',
|
|
3355
|
+
required: ${arg.required ?? true},
|
|
3356
|
+
}`).join(",\n");
|
|
3357
|
+
const content = `import { defineTool } from 'aikit';
|
|
3358
|
+
|
|
3359
|
+
export default defineTool({
|
|
3360
|
+
name: '${name}',
|
|
3361
|
+
description: '${options.description}',
|
|
3362
|
+
args: {
|
|
3363
|
+
${argsSchema}
|
|
3364
|
+
},
|
|
3365
|
+
async execute(args) {
|
|
3366
|
+
${options.code}
|
|
3367
|
+
}
|
|
3368
|
+
});
|
|
3369
|
+
`;
|
|
3370
|
+
await writeFile5(filePath, content);
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Format tool for agent consumption
|
|
3374
|
+
*/
|
|
3375
|
+
formatForAgent(tool) {
|
|
3376
|
+
const argsDesc = Object.entries(tool.args).map(([name, arg]) => ` - ${name} (${arg.type}${arg.required ? ", required" : ""}): ${arg.description}`).join("\n");
|
|
3377
|
+
return `## Tool: ${tool.name}
|
|
3378
|
+
|
|
3379
|
+
${tool.description}
|
|
3380
|
+
|
|
3381
|
+
### Arguments
|
|
3382
|
+
${argsDesc}
|
|
3383
|
+
`;
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Load custom tools from disk
|
|
3387
|
+
*/
|
|
3388
|
+
async loadCustomTools() {
|
|
3389
|
+
const globalToolsPath = paths.tools(paths.globalConfig());
|
|
3390
|
+
await this.loadToolsFromDir(globalToolsPath);
|
|
3391
|
+
const projectToolsPath = paths.tools(this.config.configPath);
|
|
3392
|
+
if (projectToolsPath !== globalToolsPath) {
|
|
3393
|
+
await this.loadToolsFromDir(projectToolsPath);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
async loadToolsFromDir(dir) {
|
|
3397
|
+
let files;
|
|
3398
|
+
try {
|
|
3399
|
+
files = await readdir4(dir);
|
|
3400
|
+
} catch {
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
for (const file of files) {
|
|
3404
|
+
if (extname3(file) !== ".ts" && extname3(file) !== ".js") continue;
|
|
3405
|
+
const filePath = join8(dir, file);
|
|
3406
|
+
try {
|
|
3407
|
+
const toolModule = await import(`file://${filePath}`);
|
|
3408
|
+
const tool = toolModule.default;
|
|
3409
|
+
if (tool?.name && typeof tool.execute === "function") {
|
|
3410
|
+
tool.filePath = filePath;
|
|
3411
|
+
this.tools.set(tool.name, tool);
|
|
3412
|
+
}
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
logger.warn(`Failed to load tool from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
// src/core/plugins.ts
|
|
3421
|
+
init_esm_shims();
|
|
3422
|
+
init_paths();
|
|
3423
|
+
init_logger();
|
|
3424
|
+
import { readdir as readdir5, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
|
|
3425
|
+
import { join as join9, basename as basename3, extname as extname4 } from "path";
|
|
3426
|
+
var PluginSystem = class {
|
|
3427
|
+
config;
|
|
3428
|
+
plugins = /* @__PURE__ */ new Map();
|
|
3429
|
+
loadedPlugins = /* @__PURE__ */ new Map();
|
|
3430
|
+
eventQueue = [];
|
|
3431
|
+
processing = false;
|
|
3432
|
+
constructor(config) {
|
|
3433
|
+
this.config = config;
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Initialize and load all plugins
|
|
3437
|
+
*/
|
|
3438
|
+
async initialize() {
|
|
3439
|
+
await this.loadPlugins();
|
|
3440
|
+
for (const [name, info] of this.plugins) {
|
|
3441
|
+
if (info.enabled) {
|
|
3442
|
+
try {
|
|
3443
|
+
const handlers = await this.initializePlugin(info);
|
|
3444
|
+
this.loadedPlugins.set(name, handlers);
|
|
3445
|
+
info.handlers = handlers;
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
logger.warn(`Failed to initialize plugin ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
/**
|
|
3453
|
+
* List all available plugins
|
|
3454
|
+
*/
|
|
3455
|
+
async listPlugins() {
|
|
3456
|
+
await this.loadPlugins();
|
|
3457
|
+
return Array.from(this.plugins.values());
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Enable a plugin
|
|
3461
|
+
*/
|
|
3462
|
+
async enablePlugin(name) {
|
|
3463
|
+
const plugin = this.plugins.get(name);
|
|
3464
|
+
if (plugin) {
|
|
3465
|
+
plugin.enabled = true;
|
|
3466
|
+
if (!this.loadedPlugins.has(name)) {
|
|
3467
|
+
const handlers = await this.initializePlugin(plugin);
|
|
3468
|
+
this.loadedPlugins.set(name, handlers);
|
|
3469
|
+
plugin.handlers = handlers;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Disable a plugin
|
|
3475
|
+
*/
|
|
3476
|
+
disablePlugin(name) {
|
|
3477
|
+
const plugin = this.plugins.get(name);
|
|
3478
|
+
if (plugin) {
|
|
3479
|
+
plugin.enabled = false;
|
|
3480
|
+
this.loadedPlugins.delete(name);
|
|
3481
|
+
plugin.handlers = void 0;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
/**
|
|
3485
|
+
* Emit an event to all plugins
|
|
3486
|
+
*/
|
|
3487
|
+
async emit(event) {
|
|
3488
|
+
this.eventQueue.push(event);
|
|
3489
|
+
if (!this.processing) {
|
|
3490
|
+
await this.processEventQueue();
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Execute before hooks for tool execution
|
|
3495
|
+
*/
|
|
3496
|
+
async executeBeforeHooks(_toolName, input) {
|
|
3497
|
+
let result = input;
|
|
3498
|
+
for (const handlers of this.loadedPlugins.values()) {
|
|
3499
|
+
if (handlers["tool.execute.before"]) {
|
|
3500
|
+
result = await handlers["tool.execute.before"](result);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
return result;
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Execute after hooks for tool execution
|
|
3507
|
+
*/
|
|
3508
|
+
async executeAfterHooks(_toolName, input, output) {
|
|
3509
|
+
let result = output;
|
|
3510
|
+
for (const handlers of this.loadedPlugins.values()) {
|
|
3511
|
+
if (handlers["tool.execute.after"]) {
|
|
3512
|
+
result = await handlers["tool.execute.after"](input, result);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
return result;
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* Create a new plugin
|
|
3519
|
+
*/
|
|
3520
|
+
async createPlugin(name, options) {
|
|
3521
|
+
const configPath = options.global ? paths.globalConfig() : this.config.configPath;
|
|
3522
|
+
const pluginsDir = paths.plugins(configPath);
|
|
3523
|
+
await mkdir6(pluginsDir, { recursive: true });
|
|
3524
|
+
const fileName = `${name}.ts`;
|
|
3525
|
+
const filePath = join9(pluginsDir, fileName);
|
|
3526
|
+
const content = `import { Plugin } from 'aikit';
|
|
3527
|
+
|
|
3528
|
+
/**
|
|
3529
|
+
* ${options.description || `Custom plugin: ${name}`}
|
|
3530
|
+
*/
|
|
3531
|
+
export const ${toPascalCase(name)}Plugin: Plugin = async ({ project, config, emit }) => {
|
|
3532
|
+
return {
|
|
3533
|
+
event: async ({ event }) => {
|
|
3534
|
+
${options.code}
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
};
|
|
3538
|
+
|
|
3539
|
+
export default ${toPascalCase(name)}Plugin;
|
|
3540
|
+
`;
|
|
3541
|
+
await writeFile6(filePath, content);
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* Process event queue
|
|
3545
|
+
*/
|
|
3546
|
+
async processEventQueue() {
|
|
3547
|
+
this.processing = true;
|
|
3548
|
+
while (this.eventQueue.length > 0) {
|
|
3549
|
+
const event = this.eventQueue.shift();
|
|
3550
|
+
for (const handlers of this.loadedPlugins.values()) {
|
|
3551
|
+
if (handlers.event) {
|
|
3552
|
+
try {
|
|
3553
|
+
await handlers.event(event);
|
|
3554
|
+
} catch (error) {
|
|
3555
|
+
logger.warn(`Plugin error handling event ${event.type}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
this.processing = false;
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Load plugins from disk
|
|
3564
|
+
*/
|
|
3565
|
+
async loadPlugins() {
|
|
3566
|
+
this.registerBuiltInPlugins();
|
|
3567
|
+
const globalPluginsPath = paths.plugins(paths.globalConfig());
|
|
3568
|
+
await this.loadPluginsFromDir(globalPluginsPath);
|
|
3569
|
+
const projectPluginsPath = paths.plugins(this.config.configPath);
|
|
3570
|
+
if (projectPluginsPath !== globalPluginsPath) {
|
|
3571
|
+
await this.loadPluginsFromDir(projectPluginsPath);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
registerBuiltInPlugins() {
|
|
3575
|
+
this.plugins.set("enforcer", {
|
|
3576
|
+
name: "enforcer",
|
|
3577
|
+
description: "Warns when session idles with TODO items remaining",
|
|
3578
|
+
enabled: true,
|
|
3579
|
+
filePath: "built-in"
|
|
3580
|
+
});
|
|
3581
|
+
this.plugins.set("compactor", {
|
|
3582
|
+
name: "compactor",
|
|
3583
|
+
description: "Warns when context usage reaches 70%, 85%, 95%",
|
|
3584
|
+
enabled: true,
|
|
3585
|
+
filePath: "built-in"
|
|
3586
|
+
});
|
|
3587
|
+
this.plugins.set("truncator", {
|
|
3588
|
+
name: "truncator",
|
|
3589
|
+
description: "Auto-truncates tool output to preserve context space",
|
|
3590
|
+
enabled: true,
|
|
3591
|
+
filePath: "built-in"
|
|
3592
|
+
});
|
|
3593
|
+
this.plugins.set("notification", {
|
|
3594
|
+
name: "notification",
|
|
3595
|
+
description: "OS notifications when OpenCode completes a session",
|
|
3596
|
+
enabled: true,
|
|
3597
|
+
filePath: "built-in"
|
|
3598
|
+
});
|
|
3599
|
+
this.plugins.set("session-management", {
|
|
3600
|
+
name: "session-management",
|
|
3601
|
+
description: "Cross-session context transfer based on handoffs",
|
|
3602
|
+
enabled: true,
|
|
3603
|
+
filePath: "built-in"
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
async loadPluginsFromDir(dir) {
|
|
3607
|
+
let files;
|
|
3608
|
+
try {
|
|
3609
|
+
files = await readdir5(dir);
|
|
3610
|
+
} catch {
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
for (const file of files) {
|
|
3614
|
+
if (extname4(file) !== ".ts" && extname4(file) !== ".js") continue;
|
|
3615
|
+
const filePath = join9(dir, file);
|
|
3616
|
+
const name = basename3(file, extname4(file));
|
|
3617
|
+
this.plugins.set(name, {
|
|
3618
|
+
name,
|
|
3619
|
+
description: `Custom plugin: ${name}`,
|
|
3620
|
+
enabled: true,
|
|
3621
|
+
filePath
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
async initializePlugin(info) {
|
|
3626
|
+
if (info.filePath === "built-in") {
|
|
3627
|
+
return this.getBuiltInPluginHandlers(info.name);
|
|
3628
|
+
}
|
|
3629
|
+
try {
|
|
3630
|
+
const pluginModule = await import(`file://${info.filePath}`);
|
|
3631
|
+
const factory = pluginModule.default;
|
|
3632
|
+
const context = {
|
|
3633
|
+
project: {
|
|
3634
|
+
path: this.config.projectPath,
|
|
3635
|
+
name: basename3(this.config.projectPath)
|
|
3636
|
+
},
|
|
3637
|
+
config: this.config,
|
|
3638
|
+
emit: this.emit.bind(this)
|
|
3639
|
+
};
|
|
3640
|
+
return await factory(context);
|
|
3641
|
+
} catch (error) {
|
|
3642
|
+
logger.warn(`Failed to load plugin ${info.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3643
|
+
return {};
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
getBuiltInPluginHandlers(name) {
|
|
3647
|
+
switch (name) {
|
|
3648
|
+
case "enforcer":
|
|
3649
|
+
return {
|
|
3650
|
+
event: async (event) => {
|
|
3651
|
+
if (event.type === "session.idle") {
|
|
3652
|
+
logger.info("[Enforcer] Session idle - check for remaining work");
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
case "compactor":
|
|
3657
|
+
return {
|
|
3658
|
+
event: async (event) => {
|
|
3659
|
+
const usage = event.properties?.contextUsage;
|
|
3660
|
+
if (usage && usage > 70) {
|
|
3661
|
+
logger.info(`[Compactor] Context usage at ${usage}%`);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
case "truncator":
|
|
3666
|
+
return {
|
|
3667
|
+
"tool.execute.after": async (_input, output) => {
|
|
3668
|
+
if (typeof output === "string" && output.length > 5e4) {
|
|
3669
|
+
return output.slice(0, 5e4) + "\n\n[Output truncated - exceeded 50KB limit]";
|
|
3670
|
+
}
|
|
3671
|
+
return output;
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
case "notification":
|
|
3675
|
+
return {
|
|
3676
|
+
event: async (event) => {
|
|
3677
|
+
if (event.type === "session.idle") {
|
|
3678
|
+
try {
|
|
3679
|
+
const { exec: exec2 } = await import("child_process");
|
|
3680
|
+
const { promisify: promisify2 } = await import("util");
|
|
3681
|
+
const execAsync2 = promisify2(exec2);
|
|
3682
|
+
const platform = process.platform;
|
|
3683
|
+
const summary = event.properties?.summary || "Session completed";
|
|
3684
|
+
if (platform === "darwin") {
|
|
3685
|
+
await execAsync2(`osascript -e 'display notification "${summary}" with title "OpenCode Session Complete"'`);
|
|
3686
|
+
} else if (platform === "linux") {
|
|
3687
|
+
await execAsync2(`notify-send "OpenCode Session Complete" "${summary}"`);
|
|
3688
|
+
} else if (platform === "win32") {
|
|
3689
|
+
await execAsync2(`powershell -Command "New-BurntToastNotification -Text 'OpenCode Session Complete', '${summary}'"`);
|
|
3690
|
+
}
|
|
3691
|
+
} catch (error) {
|
|
3692
|
+
logger.warn(`[Notification] Failed to send notification: ${error instanceof Error ? error.message : String(error)}`);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
};
|
|
3697
|
+
case "session-management":
|
|
3698
|
+
return {
|
|
3699
|
+
event: async (event) => {
|
|
3700
|
+
if (event.type === "session.idle") {
|
|
3701
|
+
logger.info("[Session Management] Session idle - context saved for next session");
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
default:
|
|
3706
|
+
return {};
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
function toPascalCase(str) {
|
|
3711
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// src/index.ts
|
|
3715
|
+
init_memory();
|
|
3716
|
+
|
|
3717
|
+
// src/core/beads.ts
|
|
3718
|
+
init_esm_shims();
|
|
3719
|
+
init_paths();
|
|
3720
|
+
init_logger();
|
|
3721
|
+
import { readFile as readFile6, writeFile as writeFile7, readdir as readdir6, access as access3, constants as constants3, mkdir as mkdir7 } from "fs/promises";
|
|
3722
|
+
import { join as join10 } from "path";
|
|
3723
|
+
import { exec } from "child_process";
|
|
3724
|
+
import { promisify } from "util";
|
|
3725
|
+
var execAsync = promisify(exec);
|
|
3726
|
+
var BeadsIntegration = class {
|
|
3727
|
+
projectPath;
|
|
3728
|
+
constructor(projectPath) {
|
|
3729
|
+
this.projectPath = projectPath || process.cwd();
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3732
|
+
* Check if Beads CLI is installed
|
|
3733
|
+
*/
|
|
3734
|
+
async isInstalled() {
|
|
3735
|
+
try {
|
|
3736
|
+
await execAsync("bd --version");
|
|
3737
|
+
return true;
|
|
3738
|
+
} catch {
|
|
3739
|
+
return false;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
/**
|
|
3743
|
+
* Get Beads version
|
|
3744
|
+
*/
|
|
3745
|
+
async getVersion() {
|
|
3746
|
+
try {
|
|
3747
|
+
const { stdout } = await execAsync("bd --version");
|
|
3748
|
+
const match = stdout.match(/bd version (\S+)/);
|
|
3749
|
+
return match?.[1] || null;
|
|
3750
|
+
} catch {
|
|
3751
|
+
return null;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Check if Beads is initialized in project
|
|
3756
|
+
*/
|
|
3757
|
+
async isInitialized() {
|
|
3758
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3759
|
+
try {
|
|
3760
|
+
await access3(beadsDir, constants3.R_OK);
|
|
3761
|
+
return true;
|
|
3762
|
+
} catch {
|
|
3763
|
+
return false;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Initialize Beads in project
|
|
3768
|
+
*/
|
|
3769
|
+
async init() {
|
|
3770
|
+
try {
|
|
3771
|
+
await execAsync("bd init", { cwd: this.projectPath });
|
|
3772
|
+
logger.success("Beads initialized");
|
|
3773
|
+
return true;
|
|
3774
|
+
} catch (error) {
|
|
3775
|
+
logger.error("Failed to initialize Beads:", error);
|
|
3776
|
+
return false;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* Get current status
|
|
3781
|
+
*/
|
|
3782
|
+
async getStatus() {
|
|
3783
|
+
const installed = await this.isInstalled();
|
|
3784
|
+
const version = await this.getVersion();
|
|
3785
|
+
const initialized = await this.isInitialized();
|
|
3786
|
+
let activeTasks = 0;
|
|
3787
|
+
let completedTasks = 0;
|
|
3788
|
+
let currentTask;
|
|
3789
|
+
if (initialized) {
|
|
3790
|
+
const beads = await this.listBeads();
|
|
3791
|
+
activeTasks = beads.filter((b) => b.status === "in-progress" || b.status === "todo").length;
|
|
3792
|
+
completedTasks = beads.filter((b) => b.status === "completed").length;
|
|
3793
|
+
const active = beads.find((b) => b.status === "in-progress");
|
|
3794
|
+
currentTask = active?.title;
|
|
3795
|
+
}
|
|
3796
|
+
return {
|
|
3797
|
+
installed,
|
|
3798
|
+
version: version || void 0,
|
|
3799
|
+
initialized,
|
|
3800
|
+
activeTasks,
|
|
3801
|
+
completedTasks,
|
|
3802
|
+
currentTask
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* List all beads in the project
|
|
3807
|
+
*/
|
|
3808
|
+
async listBeads() {
|
|
3809
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3810
|
+
const beads = [];
|
|
3811
|
+
try {
|
|
3812
|
+
const files = await readdir6(beadsDir);
|
|
3813
|
+
for (const file of files) {
|
|
3814
|
+
if (!file.match(/^bead-\d+\.md$/)) continue;
|
|
3815
|
+
const content = await readFile6(join10(beadsDir, file), "utf-8");
|
|
3816
|
+
const bead = this.parseBeadFile(file, content);
|
|
3817
|
+
if (bead) beads.push(bead);
|
|
3818
|
+
}
|
|
3819
|
+
} catch {
|
|
3820
|
+
}
|
|
3821
|
+
return beads.sort((a, b) => a.id.localeCompare(b.id));
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Get a specific bead
|
|
3825
|
+
*/
|
|
3826
|
+
async getBead(id) {
|
|
3827
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3828
|
+
const fileName = id.endsWith(".md") ? id : `${id}.md`;
|
|
3829
|
+
try {
|
|
3830
|
+
const content = await readFile6(join10(beadsDir, fileName), "utf-8");
|
|
3831
|
+
return this.parseBeadFile(fileName, content);
|
|
3832
|
+
} catch {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Create a new bead
|
|
3838
|
+
*/
|
|
3839
|
+
async createBead(title, description) {
|
|
3840
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3841
|
+
await mkdir7(beadsDir, { recursive: true });
|
|
3842
|
+
const beads = await this.listBeads();
|
|
3843
|
+
const maxId = beads.reduce((max, b) => {
|
|
3844
|
+
const num = parseInt(b.id.replace("bead-", ""), 10);
|
|
3845
|
+
return num > max ? num : max;
|
|
3846
|
+
}, 0);
|
|
3847
|
+
const id = `bead-${String(maxId + 1).padStart(3, "0")}`;
|
|
3848
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3849
|
+
const content = `---
|
|
3850
|
+
id: ${id}
|
|
3851
|
+
title: ${title}
|
|
3852
|
+
status: in-progress
|
|
3853
|
+
created: ${now}
|
|
3854
|
+
updated: ${now}
|
|
3855
|
+
---
|
|
3856
|
+
|
|
3857
|
+
# ${title}
|
|
3858
|
+
|
|
3859
|
+
## Description
|
|
3860
|
+
${description}
|
|
3861
|
+
|
|
3862
|
+
## Notes
|
|
3863
|
+
|
|
3864
|
+
|
|
3865
|
+
## Checklist
|
|
3866
|
+
- [ ] Requirements understood
|
|
3867
|
+
- [ ] Implementation complete
|
|
3868
|
+
- [ ] Tests passing
|
|
3869
|
+
- [ ] Code reviewed
|
|
3870
|
+
|
|
3871
|
+
## Progress
|
|
3872
|
+
|
|
3873
|
+
`;
|
|
3874
|
+
await writeFile7(join10(beadsDir, `${id}.md`), content);
|
|
3875
|
+
return {
|
|
3876
|
+
id,
|
|
3877
|
+
title,
|
|
3878
|
+
description,
|
|
3879
|
+
status: "in-progress",
|
|
3880
|
+
createdAt: new Date(now),
|
|
3881
|
+
updatedAt: new Date(now),
|
|
3882
|
+
notes: []
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Update bead status
|
|
3887
|
+
*/
|
|
3888
|
+
async updateBeadStatus(id, status) {
|
|
3889
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3890
|
+
const fileName = id.endsWith(".md") ? id : `${id}.md`;
|
|
3891
|
+
const filePath = join10(beadsDir, fileName);
|
|
3892
|
+
try {
|
|
3893
|
+
let content = await readFile6(filePath, "utf-8");
|
|
3894
|
+
content = content.replace(
|
|
3895
|
+
/status:\s*\w+/,
|
|
3896
|
+
`status: ${status}`
|
|
3897
|
+
);
|
|
3898
|
+
content = content.replace(
|
|
3899
|
+
/updated:\s*.+/,
|
|
3900
|
+
`updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
3901
|
+
);
|
|
3902
|
+
await writeFile7(filePath, content);
|
|
3903
|
+
return true;
|
|
3904
|
+
} catch {
|
|
3905
|
+
return false;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Add note to bead
|
|
3910
|
+
*/
|
|
3911
|
+
async addNote(id, note) {
|
|
3912
|
+
const beadsDir = paths.beadsDir(this.projectPath);
|
|
3913
|
+
const fileName = id.endsWith(".md") ? id : `${id}.md`;
|
|
3914
|
+
const filePath = join10(beadsDir, fileName);
|
|
3915
|
+
try {
|
|
3916
|
+
let content = await readFile6(filePath, "utf-8");
|
|
3917
|
+
const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n##|$)/);
|
|
3918
|
+
if (notesMatch) {
|
|
3919
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
3920
|
+
const newNote = `- [${timestamp}] ${note}`;
|
|
3921
|
+
content = content.replace(
|
|
3922
|
+
notesMatch[0],
|
|
3923
|
+
`## Notes
|
|
3924
|
+
${notesMatch[1]}${newNote}
|
|
3925
|
+
`
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
content = content.replace(
|
|
3929
|
+
/updated:\s*.+/,
|
|
3930
|
+
`updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
3931
|
+
);
|
|
3932
|
+
await writeFile7(filePath, content);
|
|
3933
|
+
return true;
|
|
3934
|
+
} catch {
|
|
3935
|
+
return false;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Complete a bead with quality gates
|
|
3940
|
+
*/
|
|
3941
|
+
async completeBead(id) {
|
|
3942
|
+
const gates = [
|
|
3943
|
+
{ name: "Type Check", command: "npm run typecheck" },
|
|
3944
|
+
{ name: "Tests", command: "npm run test" },
|
|
3945
|
+
{ name: "Lint", command: "npm run lint" },
|
|
3946
|
+
{ name: "Build", command: "npm run build" }
|
|
3947
|
+
];
|
|
3948
|
+
const results = [];
|
|
3949
|
+
for (const gate of gates) {
|
|
3950
|
+
try {
|
|
3951
|
+
await execAsync(gate.command, { cwd: this.projectPath });
|
|
3952
|
+
results.push({ name: gate.name, passed: true });
|
|
3953
|
+
logger.success(`${gate.name}: passed`);
|
|
3954
|
+
} catch (error) {
|
|
3955
|
+
results.push({
|
|
3956
|
+
name: gate.name,
|
|
3957
|
+
passed: false,
|
|
3958
|
+
error: error instanceof Error ? error.message.slice(0, 200) : "Failed"
|
|
3959
|
+
});
|
|
3960
|
+
logger.error(`${gate.name}: failed`);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
const allPassed = results.every((r) => r.passed);
|
|
3964
|
+
if (allPassed) {
|
|
3965
|
+
await this.updateBeadStatus(id, "completed");
|
|
3966
|
+
await this.addNote(id, "Task completed - all quality gates passed");
|
|
3967
|
+
} else {
|
|
3968
|
+
await this.addNote(id, "Completion attempted but quality gates failed");
|
|
3969
|
+
}
|
|
3970
|
+
return {
|
|
3971
|
+
success: allPassed,
|
|
3972
|
+
gates: results
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Get current active bead
|
|
3977
|
+
*/
|
|
3978
|
+
async getCurrentBead() {
|
|
3979
|
+
const beads = await this.listBeads();
|
|
3980
|
+
return beads.find((b) => b.status === "in-progress") || null;
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Parse a bead file
|
|
3984
|
+
*/
|
|
3985
|
+
parseBeadFile(fileName, content) {
|
|
3986
|
+
try {
|
|
3987
|
+
const id = fileName.replace(".md", "");
|
|
3988
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
3989
|
+
const frontmatter = frontmatterMatch?.[1] || "";
|
|
3990
|
+
const getValue = (key) => {
|
|
3991
|
+
const match = frontmatter.match(new RegExp(`${key}:\\s*(.+)`));
|
|
3992
|
+
return match?.[1]?.trim() || "";
|
|
3993
|
+
};
|
|
3994
|
+
const titleMatch = content.match(/^# (.+)/m);
|
|
3995
|
+
const title = getValue("title") || titleMatch?.[1] || id;
|
|
3996
|
+
const descMatch = content.match(/## Description\n([\s\S]*?)(?=\n##|$)/);
|
|
3997
|
+
const description = descMatch?.[1]?.trim() || "";
|
|
3998
|
+
const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n##|$)/);
|
|
3999
|
+
const notesContent = notesMatch?.[1] || "";
|
|
4000
|
+
const notes = notesContent.split("\n").filter((line) => line.trim().startsWith("-")).map((line) => line.replace(/^-\s*/, "").trim());
|
|
4001
|
+
return {
|
|
4002
|
+
id,
|
|
4003
|
+
title,
|
|
4004
|
+
description,
|
|
4005
|
+
status: getValue("status") || "todo",
|
|
4006
|
+
createdAt: new Date(getValue("created") || Date.now()),
|
|
4007
|
+
updatedAt: new Date(getValue("updated") || Date.now()),
|
|
4008
|
+
notes
|
|
4009
|
+
};
|
|
4010
|
+
} catch {
|
|
4011
|
+
return null;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
|
|
4016
|
+
// src/core/anti-hallucination.ts
|
|
4017
|
+
init_esm_shims();
|
|
4018
|
+
init_logger();
|
|
4019
|
+
|
|
4020
|
+
// src/index.ts
|
|
4021
|
+
init_logger();
|
|
4022
|
+
init_paths();
|
|
4023
|
+
var VERSION = "0.1.0";
|
|
4024
|
+
|
|
4025
|
+
// src/cli.ts
|
|
4026
|
+
init_memory();
|
|
4027
|
+
init_logger();
|
|
4028
|
+
init_paths();
|
|
4029
|
+
var program = new Command();
|
|
4030
|
+
program.name("aikit").description("Open-source AI coding agent toolkit for OpenCode").version(VERSION);
|
|
4031
|
+
program.command("init").description("Initialize AIKit configuration").option("-g, --global", "Initialize global configuration").option("-p, --project", "Initialize project-level configuration").action(async (options) => {
|
|
4032
|
+
const configDir = options.global ? paths.globalConfig() : paths.projectConfig();
|
|
4033
|
+
console.log(chalk2.bold("\n\u{1F680} AIKit Setup\n"));
|
|
4034
|
+
logger.info(`Initializing AIKit in ${configDir}...`);
|
|
4035
|
+
try {
|
|
4036
|
+
await initializeConfig(configDir, options.global);
|
|
4037
|
+
logger.success("\u2713 Configuration created");
|
|
4038
|
+
if (!options.global) {
|
|
4039
|
+
const config = await loadConfig();
|
|
4040
|
+
const engine = new SkillEngine(config);
|
|
4041
|
+
const result = await engine.syncSkillsToProject();
|
|
4042
|
+
if (result.count > 0) {
|
|
4043
|
+
logger.success(`\u2713 Synced ${result.count} skills`);
|
|
4044
|
+
}
|
|
4045
|
+
const opencodePath = paths.opencodeConfig();
|
|
4046
|
+
await installToOpenCode(opencodePath);
|
|
4047
|
+
console.log(chalk2.bold("\n\u2728 AIKit is ready!\n"));
|
|
4048
|
+
console.log("Usage in OpenCode:");
|
|
4049
|
+
console.log(chalk2.cyan(" /skills") + " - List all available skills");
|
|
4050
|
+
console.log(chalk2.cyan(" /plan") + " - Create implementation plan");
|
|
4051
|
+
console.log(chalk2.cyan(" /tdd") + " - Test-driven development");
|
|
4052
|
+
console.log(chalk2.cyan(" /debug") + " - Systematic debugging");
|
|
4053
|
+
console.log(chalk2.cyan(" /review") + " - Code review checklist");
|
|
4054
|
+
console.log(chalk2.cyan(" /git") + " - Git workflow");
|
|
4055
|
+
console.log(chalk2.cyan(" /frontend-aesthetics") + " - UI/UX guidelines");
|
|
4056
|
+
console.log("\nPress " + chalk2.bold("Ctrl+K") + " in OpenCode to see all commands.\n");
|
|
4057
|
+
}
|
|
4058
|
+
} catch (error) {
|
|
4059
|
+
logger.error("Failed to initialize AIKit:", error);
|
|
4060
|
+
process.exit(1);
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
program.command("install").description("Install AIKit to OpenCode configuration").action(async () => {
|
|
4064
|
+
logger.info("Installing AIKit to OpenCode...");
|
|
4065
|
+
try {
|
|
4066
|
+
const opencodePath = paths.opencodeConfig();
|
|
4067
|
+
await installToOpenCode(opencodePath);
|
|
4068
|
+
logger.success("AIKit installed to OpenCode!");
|
|
4069
|
+
} catch (error) {
|
|
4070
|
+
logger.error("Failed to install:", error);
|
|
4071
|
+
process.exit(1);
|
|
4072
|
+
}
|
|
4073
|
+
});
|
|
4074
|
+
var skillsCmd = program.command("skills").description("Manage skills");
|
|
4075
|
+
skillsCmd.command("list").description("List available skills and tools with their configuration status").action(async () => {
|
|
4076
|
+
const config = await loadConfig();
|
|
4077
|
+
const engine = new SkillEngine(config);
|
|
4078
|
+
const skills = await engine.listSkills();
|
|
4079
|
+
const { ToolConfigManager: ToolConfigManager2 } = await Promise.resolve().then(() => (init_tool_config(), tool_config_exports));
|
|
4080
|
+
const toolConfigManager = new ToolConfigManager2(config);
|
|
4081
|
+
const tools = await toolConfigManager.listTools();
|
|
4082
|
+
console.log(chalk2.bold("\n\u{1F4DA} Available Skills:\n"));
|
|
4083
|
+
for (const skill of skills) {
|
|
4084
|
+
console.log(` ${chalk2.cyan(skill.name)} - ${skill.description}`);
|
|
4085
|
+
}
|
|
4086
|
+
console.log(chalk2.bold("\n\u{1F527} Available Tools:\n"));
|
|
4087
|
+
for (const tool of tools) {
|
|
4088
|
+
let statusIcon = " ";
|
|
4089
|
+
let statusText = "";
|
|
4090
|
+
if (tool.status === "ready") {
|
|
4091
|
+
statusIcon = chalk2.green("\u2713");
|
|
4092
|
+
statusText = chalk2.gray("(ready)");
|
|
4093
|
+
} else if (tool.status === "needs_config") {
|
|
4094
|
+
statusIcon = chalk2.yellow("\u26A0");
|
|
4095
|
+
statusText = chalk2.yellow("(needs config)");
|
|
4096
|
+
} else if (tool.status === "error") {
|
|
4097
|
+
statusIcon = chalk2.red("\u2717");
|
|
4098
|
+
statusText = chalk2.red("(error)");
|
|
4099
|
+
}
|
|
4100
|
+
console.log(` ${statusIcon} ${chalk2.cyan(tool.name)} - ${tool.description} ${statusText}`);
|
|
4101
|
+
if (tool.errorMessage) {
|
|
4102
|
+
console.log(` ${chalk2.red("Error:")} ${tool.errorMessage}`);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
console.log();
|
|
4106
|
+
console.log(chalk2.gray('Tip: Use "aikit skills <tool-name> config" to configure a tool\n'));
|
|
4107
|
+
});
|
|
4108
|
+
skillsCmd.command("show <name>").description("Show skill details").action(async (name) => {
|
|
4109
|
+
const config = await loadConfig();
|
|
4110
|
+
const engine = new SkillEngine(config);
|
|
4111
|
+
const skill = await engine.getSkill(name);
|
|
4112
|
+
if (!skill) {
|
|
4113
|
+
logger.error(`Skill not found: ${name}`);
|
|
4114
|
+
process.exit(1);
|
|
4115
|
+
}
|
|
4116
|
+
console.log(chalk2.bold(`
|
|
4117
|
+
\u{1F4D6} Skill: ${skill.name}
|
|
4118
|
+
`));
|
|
4119
|
+
console.log(chalk2.gray(skill.description));
|
|
4120
|
+
console.log(chalk2.bold("\nWorkflow:"));
|
|
4121
|
+
console.log(skill.content);
|
|
4122
|
+
});
|
|
4123
|
+
skillsCmd.command("create <name>").description("Create a new skill").action(async (name) => {
|
|
4124
|
+
const config = await loadConfig();
|
|
4125
|
+
const engine = new SkillEngine(config);
|
|
4126
|
+
await engine.createSkill(name);
|
|
4127
|
+
logger.success(`Skill created: ${name}`);
|
|
4128
|
+
});
|
|
4129
|
+
skillsCmd.command("sync").description("Sync global skills to project").action(async () => {
|
|
4130
|
+
const config = await loadConfig();
|
|
4131
|
+
const engine = new SkillEngine(config);
|
|
4132
|
+
const result = await engine.syncSkillsToProject();
|
|
4133
|
+
if (result.count === 0) {
|
|
4134
|
+
logger.info("Skills already in sync or no global skills to sync");
|
|
4135
|
+
} else {
|
|
4136
|
+
console.log(chalk2.bold(`
|
|
4137
|
+
\u2713 Synced ${result.count} skills to project:
|
|
4138
|
+
`));
|
|
4139
|
+
for (const skill of result.synced) {
|
|
4140
|
+
console.log(` ${chalk2.cyan("\u2022")} ${skill}`);
|
|
4141
|
+
}
|
|
4142
|
+
console.log();
|
|
4143
|
+
}
|
|
4144
|
+
});
|
|
4145
|
+
skillsCmd.command("config <tool-name>").description("Configure a tool (e.g., config figma-analysis)").action(async (toolName) => {
|
|
4146
|
+
const config = await loadConfig();
|
|
4147
|
+
const { ToolConfigManager: ToolConfigManager2 } = await Promise.resolve().then(() => (init_tool_config(), tool_config_exports));
|
|
4148
|
+
const toolConfigManager = new ToolConfigManager2(config);
|
|
4149
|
+
const tool = await toolConfigManager.getToolConfig(toolName);
|
|
4150
|
+
if (!tool) {
|
|
4151
|
+
logger.error(`Tool not found: ${toolName}`);
|
|
4152
|
+
console.log(chalk2.gray("\nAvailable tools:"));
|
|
4153
|
+
const tools = await toolConfigManager.listTools();
|
|
4154
|
+
for (const t of tools) {
|
|
4155
|
+
console.log(` - ${chalk2.cyan(t.name)}`);
|
|
4156
|
+
}
|
|
4157
|
+
console.log();
|
|
4158
|
+
process.exit(1);
|
|
4159
|
+
}
|
|
4160
|
+
console.log(chalk2.bold(`
|
|
4161
|
+
\u{1F527} Configuring: ${tool.name}
|
|
4162
|
+
`));
|
|
4163
|
+
console.log(chalk2.gray(tool.description));
|
|
4164
|
+
console.log();
|
|
4165
|
+
if (tool.configMethod === "oauth") {
|
|
4166
|
+
if (toolName === "figma-analysis") {
|
|
4167
|
+
const { FigmaOAuth: FigmaOAuth2 } = await Promise.resolve().then(() => (init_figma_oauth(), figma_oauth_exports));
|
|
4168
|
+
const oauth = new FigmaOAuth2(toolConfigManager);
|
|
4169
|
+
try {
|
|
4170
|
+
const token = await oauth.authenticate();
|
|
4171
|
+
console.log(chalk2.gray("\nValidating token..."));
|
|
4172
|
+
const isValid = await oauth.validateToken(token);
|
|
4173
|
+
if (isValid) {
|
|
4174
|
+
logger.success(`
|
|
4175
|
+
\u2705 ${tool.name} configured successfully!`);
|
|
4176
|
+
console.log(chalk2.gray("\nYou can now use the /analyze-figma command in OpenCode.\n"));
|
|
4177
|
+
} else {
|
|
4178
|
+
await toolConfigManager.updateToolConfig(toolName, {
|
|
4179
|
+
status: "error",
|
|
4180
|
+
errorMessage: "Token validation failed"
|
|
4181
|
+
});
|
|
4182
|
+
logger.error("Token validation failed. Please try again.");
|
|
4183
|
+
process.exit(1);
|
|
4184
|
+
}
|
|
4185
|
+
} catch (error) {
|
|
4186
|
+
await toolConfigManager.updateToolConfig(toolName, {
|
|
4187
|
+
status: "error",
|
|
4188
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
4189
|
+
});
|
|
4190
|
+
logger.error(`Configuration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
}
|
|
4193
|
+
} else {
|
|
4194
|
+
logger.error(`OAuth flow not implemented for tool: ${toolName}`);
|
|
4195
|
+
process.exit(1);
|
|
4196
|
+
}
|
|
4197
|
+
} else if (tool.configMethod === "manual") {
|
|
4198
|
+
logger.info("Manual configuration not yet implemented");
|
|
4199
|
+
process.exit(1);
|
|
4200
|
+
} else {
|
|
4201
|
+
logger.info(`Tool ${tool.name} does not require configuration`);
|
|
4202
|
+
}
|
|
4203
|
+
});
|
|
4204
|
+
skillsCmd.command("*").description("Configure a tool (e.g., figma-analysis config)").allowUnknownOption().action(async () => {
|
|
4205
|
+
const args = process.argv.slice(process.argv.indexOf("skills") + 1);
|
|
4206
|
+
const toolName = args[0];
|
|
4207
|
+
const action = args[1];
|
|
4208
|
+
if (action === "config" && toolName) {
|
|
4209
|
+
const config = await loadConfig();
|
|
4210
|
+
const { ToolConfigManager: ToolConfigManager2 } = await Promise.resolve().then(() => (init_tool_config(), tool_config_exports));
|
|
4211
|
+
const toolConfigManager = new ToolConfigManager2(config);
|
|
4212
|
+
const tool = await toolConfigManager.getToolConfig(toolName);
|
|
4213
|
+
if (!tool) {
|
|
4214
|
+
logger.error(`Tool not found: ${toolName}`);
|
|
4215
|
+
console.log(chalk2.gray("\nAvailable tools:"));
|
|
4216
|
+
const tools = await toolConfigManager.listTools();
|
|
4217
|
+
for (const t of tools) {
|
|
4218
|
+
console.log(` - ${chalk2.cyan(t.name)}`);
|
|
4219
|
+
}
|
|
4220
|
+
console.log();
|
|
4221
|
+
console.log(chalk2.gray("Tip: If you meant to show a skill, use: aikit skills show <name>"));
|
|
4222
|
+
console.log();
|
|
4223
|
+
process.exit(1);
|
|
4224
|
+
}
|
|
4225
|
+
console.log(chalk2.bold(`
|
|
4226
|
+
\u{1F527} Configuring: ${tool.name}
|
|
4227
|
+
`));
|
|
4228
|
+
console.log(chalk2.gray(tool.description));
|
|
4229
|
+
console.log();
|
|
4230
|
+
if (tool.configMethod === "oauth") {
|
|
4231
|
+
if (toolName === "figma-analysis") {
|
|
4232
|
+
const { FigmaOAuth: FigmaOAuth2 } = await Promise.resolve().then(() => (init_figma_oauth(), figma_oauth_exports));
|
|
4233
|
+
const oauth = new FigmaOAuth2(toolConfigManager);
|
|
4234
|
+
try {
|
|
4235
|
+
const token = await oauth.authenticate();
|
|
4236
|
+
console.log(chalk2.gray("\nValidating token..."));
|
|
4237
|
+
const isValid = await oauth.validateToken(token);
|
|
4238
|
+
if (isValid) {
|
|
4239
|
+
logger.success(`
|
|
4240
|
+
\u2705 ${tool.name} configured successfully!`);
|
|
4241
|
+
console.log(chalk2.gray("\nYou can now use the /analyze-figma command in OpenCode.\n"));
|
|
4242
|
+
} else {
|
|
4243
|
+
await toolConfigManager.updateToolConfig(toolName, {
|
|
4244
|
+
status: "error",
|
|
4245
|
+
errorMessage: "Token validation failed"
|
|
4246
|
+
});
|
|
4247
|
+
logger.error("Token validation failed. Please try again.");
|
|
4248
|
+
process.exit(1);
|
|
4249
|
+
}
|
|
4250
|
+
} catch (error) {
|
|
4251
|
+
await toolConfigManager.updateToolConfig(toolName, {
|
|
4252
|
+
status: "error",
|
|
4253
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
4254
|
+
});
|
|
4255
|
+
logger.error(`Configuration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4256
|
+
process.exit(1);
|
|
4257
|
+
}
|
|
4258
|
+
} else {
|
|
4259
|
+
logger.error(`OAuth flow not implemented for tool: ${toolName}`);
|
|
4260
|
+
process.exit(1);
|
|
4261
|
+
}
|
|
4262
|
+
} else if (tool.configMethod === "manual") {
|
|
4263
|
+
logger.info("Manual configuration not yet implemented");
|
|
4264
|
+
process.exit(1);
|
|
4265
|
+
} else {
|
|
4266
|
+
logger.info(`Tool ${tool.name} does not require configuration`);
|
|
4267
|
+
}
|
|
4268
|
+
} else {
|
|
4269
|
+
logger.error(`Unknown command: ${toolName || "unknown"} ${action || ""}`);
|
|
4270
|
+
console.log(chalk2.gray("\nAvailable commands:"));
|
|
4271
|
+
console.log(" aikit skills list - List all skills and tools");
|
|
4272
|
+
console.log(" aikit skills show <name> - Show skill details");
|
|
4273
|
+
console.log(" aikit skills config <tool-name> - Configure a tool");
|
|
4274
|
+
console.log(" aikit skills <tool-name> config - Configure a tool (alternative syntax)");
|
|
4275
|
+
console.log();
|
|
4276
|
+
process.exit(1);
|
|
4277
|
+
}
|
|
4278
|
+
});
|
|
4279
|
+
var agentsCmd = program.command("agents").description("Manage agents");
|
|
4280
|
+
agentsCmd.command("list").description("List available agents").action(async () => {
|
|
4281
|
+
const config = await loadConfig();
|
|
4282
|
+
const manager = new AgentManager(config);
|
|
4283
|
+
const agents = manager.listAgents();
|
|
4284
|
+
console.log(chalk2.bold("\n\u{1F916} Available Agents:\n"));
|
|
4285
|
+
for (const agent of agents) {
|
|
4286
|
+
console.log(` ${chalk2.cyan(`@${agent.name}`)} - ${agent.description}`);
|
|
4287
|
+
console.log(chalk2.gray(` Use when: ${agent.useWhen}`));
|
|
4288
|
+
}
|
|
4289
|
+
console.log();
|
|
4290
|
+
});
|
|
4291
|
+
var commandsCmd = program.command("commands").description("Manage commands");
|
|
4292
|
+
commandsCmd.command("list").description("List available commands").action(async () => {
|
|
4293
|
+
const config = await loadConfig();
|
|
4294
|
+
const runner = new CommandRunner(config);
|
|
4295
|
+
const commands = await runner.listCommands();
|
|
4296
|
+
console.log(chalk2.bold("\n\u26A1 Available Commands:\n"));
|
|
4297
|
+
const groups = groupBy(commands, (c) => c.category);
|
|
4298
|
+
for (const [category, cmds] of Object.entries(groups)) {
|
|
4299
|
+
console.log(chalk2.bold.yellow(`
|
|
4300
|
+
${category}:`));
|
|
4301
|
+
for (const cmd of cmds) {
|
|
4302
|
+
console.log(` ${chalk2.cyan(`/${cmd.name}`)} - ${cmd.description}`);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
console.log();
|
|
4306
|
+
});
|
|
4307
|
+
var toolsCmd = program.command("tools").description("Manage custom tools");
|
|
4308
|
+
toolsCmd.command("list").description("List available tools").action(async () => {
|
|
4309
|
+
const config = await loadConfig();
|
|
4310
|
+
const registry = new ToolRegistry(config);
|
|
4311
|
+
const tools = await registry.listTools();
|
|
4312
|
+
console.log(chalk2.bold("\n\u{1F527} Available Tools:\n"));
|
|
4313
|
+
for (const tool of tools) {
|
|
4314
|
+
console.log(` ${chalk2.cyan(tool.name)} - ${tool.description}`);
|
|
4315
|
+
}
|
|
4316
|
+
console.log();
|
|
4317
|
+
});
|
|
4318
|
+
var pluginsCmd = program.command("plugins").description("Manage plugins");
|
|
4319
|
+
pluginsCmd.command("list").description("List available plugins").action(async () => {
|
|
4320
|
+
const config = await loadConfig();
|
|
4321
|
+
const system = new PluginSystem(config);
|
|
4322
|
+
const plugins = await system.listPlugins();
|
|
4323
|
+
console.log(chalk2.bold("\n\u{1F50C} Available Plugins:\n"));
|
|
4324
|
+
for (const plugin of plugins) {
|
|
4325
|
+
const status = plugin.enabled ? chalk2.green("\u2713") : chalk2.gray("\u25CB");
|
|
4326
|
+
console.log(` ${status} ${chalk2.cyan(plugin.name)} - ${plugin.description}`);
|
|
4327
|
+
}
|
|
4328
|
+
console.log();
|
|
4329
|
+
});
|
|
4330
|
+
var memoryCmd = program.command("memory").description("Manage persistent memory");
|
|
4331
|
+
memoryCmd.command("list").description("List memory entries").action(async () => {
|
|
4332
|
+
const config = await loadConfig();
|
|
4333
|
+
const memory = new MemoryManager(config);
|
|
4334
|
+
const entries = await memory.list();
|
|
4335
|
+
console.log(chalk2.bold("\n\u{1F9E0} Memory Entries:\n"));
|
|
4336
|
+
for (const entry of entries) {
|
|
4337
|
+
console.log(` ${chalk2.cyan(entry.key)} - ${entry.summary}`);
|
|
4338
|
+
console.log(chalk2.gray(` Updated: ${entry.updatedAt}`));
|
|
4339
|
+
}
|
|
4340
|
+
console.log();
|
|
4341
|
+
});
|
|
4342
|
+
memoryCmd.command("read <key>").description("Read a memory entry").action(async (key) => {
|
|
4343
|
+
const config = await loadConfig();
|
|
4344
|
+
const memory = new MemoryManager(config);
|
|
4345
|
+
const content = await memory.read(key);
|
|
4346
|
+
if (!content) {
|
|
4347
|
+
logger.error(`Memory entry not found: ${key}`);
|
|
4348
|
+
process.exit(1);
|
|
4349
|
+
}
|
|
4350
|
+
console.log(content);
|
|
4351
|
+
});
|
|
4352
|
+
var beadsCmd = program.command("beads").description("Beads task management integration");
|
|
4353
|
+
beadsCmd.command("status").description("Show current Beads status").action(async () => {
|
|
4354
|
+
const beads = new BeadsIntegration();
|
|
4355
|
+
const status = await beads.getStatus();
|
|
4356
|
+
console.log(chalk2.bold("\n\u{1F4FF} Beads Status:\n"));
|
|
4357
|
+
console.log(` Active tasks: ${status.activeTasks}`);
|
|
4358
|
+
console.log(` Completed: ${status.completedTasks}`);
|
|
4359
|
+
console.log(` Current: ${status.currentTask || "None"}`);
|
|
4360
|
+
console.log();
|
|
4361
|
+
});
|
|
4362
|
+
program.command("status").description("Show AIKit status").action(async () => {
|
|
4363
|
+
console.log(chalk2.bold(`
|
|
4364
|
+
\u{1F680} AIKit v${VERSION}
|
|
4365
|
+
`));
|
|
4366
|
+
try {
|
|
4367
|
+
const config = await loadConfig();
|
|
4368
|
+
console.log(chalk2.green("\u2713 Configuration loaded"));
|
|
4369
|
+
const skillEngine = new SkillEngine(config);
|
|
4370
|
+
const skills = await skillEngine.listSkills();
|
|
4371
|
+
console.log(` Skills: ${skills.length}`);
|
|
4372
|
+
const agentManager = new AgentManager(config);
|
|
4373
|
+
const agents = agentManager.listAgents();
|
|
4374
|
+
console.log(` Agents: ${agents.length}`);
|
|
4375
|
+
const commandRunner = new CommandRunner(config);
|
|
4376
|
+
const commands = await commandRunner.listCommands();
|
|
4377
|
+
console.log(` Commands: ${commands.length}`);
|
|
4378
|
+
const toolRegistry = new ToolRegistry(config);
|
|
4379
|
+
const tools = await toolRegistry.listTools();
|
|
4380
|
+
console.log(` Tools: ${tools.length}`);
|
|
4381
|
+
const beads = new BeadsIntegration();
|
|
4382
|
+
const beadsStatus = await beads.isInstalled();
|
|
4383
|
+
console.log(` Beads: ${beadsStatus ? chalk2.green("Installed") : chalk2.yellow("Not installed")}`);
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
console.log(chalk2.yellow('\u26A0 AIKit not initialized. Run "aikit init" to get started.'));
|
|
4386
|
+
}
|
|
4387
|
+
console.log();
|
|
4388
|
+
});
|
|
4389
|
+
async function initializeConfig(configDir, _isGlobal) {
|
|
4390
|
+
const { mkdir: mkdir9, writeFile: writeFile9 } = await import("fs/promises");
|
|
4391
|
+
const { join: join12 } = await import("path");
|
|
4392
|
+
const dirs = [
|
|
4393
|
+
"",
|
|
4394
|
+
"skills",
|
|
4395
|
+
"agents",
|
|
4396
|
+
"commands",
|
|
4397
|
+
"commands/build",
|
|
4398
|
+
"commands/git",
|
|
4399
|
+
"commands/plan",
|
|
4400
|
+
"commands/research",
|
|
4401
|
+
"tools",
|
|
4402
|
+
"plugins",
|
|
4403
|
+
"memory",
|
|
4404
|
+
"memory/_templates",
|
|
4405
|
+
"memory/handoffs",
|
|
4406
|
+
"memory/observations",
|
|
4407
|
+
"memory/research"
|
|
4408
|
+
];
|
|
4409
|
+
for (const dir of dirs) {
|
|
4410
|
+
await mkdir9(join12(configDir, dir), { recursive: true });
|
|
4411
|
+
}
|
|
4412
|
+
const defaultConfig = {
|
|
4413
|
+
version: VERSION,
|
|
4414
|
+
skills: { enabled: true },
|
|
4415
|
+
agents: { enabled: true, default: "build" },
|
|
4416
|
+
commands: { enabled: true },
|
|
4417
|
+
tools: { enabled: true },
|
|
4418
|
+
plugins: { enabled: true },
|
|
4419
|
+
memory: { enabled: true },
|
|
4420
|
+
beads: { enabled: true },
|
|
4421
|
+
antiHallucination: { enabled: true }
|
|
4422
|
+
};
|
|
4423
|
+
await writeFile9(
|
|
4424
|
+
join12(configDir, "aikit.json"),
|
|
4425
|
+
JSON.stringify(defaultConfig, null, 2)
|
|
4426
|
+
);
|
|
4427
|
+
const agentsMd = `# AIKit Agent Rules
|
|
4428
|
+
|
|
4429
|
+
## Build Commands
|
|
4430
|
+
- \`npm run build\` - Build the project
|
|
4431
|
+
- \`npm run test\` - Run tests
|
|
4432
|
+
- \`npm run lint\` - Run linting
|
|
4433
|
+
|
|
4434
|
+
## Code Style
|
|
4435
|
+
- Use 2 spaces for indentation
|
|
4436
|
+
- Use single quotes for strings
|
|
4437
|
+
- Add trailing commas
|
|
4438
|
+
|
|
4439
|
+
## Naming Conventions
|
|
4440
|
+
- Variables: camelCase
|
|
4441
|
+
- Components: PascalCase
|
|
4442
|
+
- Files: kebab-case
|
|
4443
|
+
|
|
4444
|
+
## Project-Specific Rules
|
|
4445
|
+
Add your project-specific rules here.
|
|
4446
|
+
`;
|
|
4447
|
+
await writeFile9(join12(configDir, "AGENTS.md"), agentsMd);
|
|
4448
|
+
}
|
|
4449
|
+
async function configureMcpServer(projectPath) {
|
|
4450
|
+
const { mkdir: mkdir9, writeFile: writeFile9, readFile: readFile8 } = await import("fs/promises");
|
|
4451
|
+
const { join: join12 } = await import("path");
|
|
4452
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
4453
|
+
const { homedir: homedir2 } = await import("os");
|
|
4454
|
+
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
4455
|
+
const { dirname: dirname2 } = await import("path");
|
|
4456
|
+
const currentFile = fileURLToPath2(import.meta.url);
|
|
4457
|
+
const currentDir = dirname2(currentFile);
|
|
4458
|
+
const aikitPath = join12(currentDir, "..");
|
|
4459
|
+
const mcpServerPath = join12(aikitPath, "dist", "mcp-server.js");
|
|
4460
|
+
const configLocations = [
|
|
4461
|
+
// Global config (most common)
|
|
4462
|
+
join12(homedir2(), ".config", "opencode", "opencode.json"),
|
|
4463
|
+
// Project-level config
|
|
4464
|
+
join12(projectPath, ".opencode", "opencode.json"),
|
|
4465
|
+
// Alternative global location
|
|
4466
|
+
join12(homedir2(), ".opencode", "opencode.json")
|
|
4467
|
+
];
|
|
4468
|
+
const mcpServerConfig = {
|
|
4469
|
+
type: "local",
|
|
4470
|
+
command: ["node", mcpServerPath],
|
|
4471
|
+
environment: {}
|
|
4472
|
+
};
|
|
4473
|
+
for (const configPath of configLocations) {
|
|
4474
|
+
try {
|
|
4475
|
+
const configDir = join12(configPath, "..");
|
|
4476
|
+
await mkdir9(configDir, { recursive: true });
|
|
4477
|
+
let config = {};
|
|
4478
|
+
if (existsSync4(configPath)) {
|
|
4479
|
+
try {
|
|
4480
|
+
const existing = await readFile8(configPath, "utf-8");
|
|
4481
|
+
config = JSON.parse(existing);
|
|
4482
|
+
} catch {
|
|
4483
|
+
config = {};
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
if (!config.mcp) {
|
|
4487
|
+
config.mcp = {};
|
|
4488
|
+
}
|
|
4489
|
+
config.mcp.aikit = mcpServerConfig;
|
|
4490
|
+
await writeFile9(configPath, JSON.stringify(config, null, 2));
|
|
4491
|
+
logger.success(`
|
|
4492
|
+
\u2705 MCP server configured: ${configPath}`);
|
|
4493
|
+
logger.info(` Server: node ${mcpServerPath}`);
|
|
4494
|
+
return;
|
|
4495
|
+
} catch (error) {
|
|
4496
|
+
continue;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
const instructionsPath = join12(projectPath, ".opencode", "MCP_SETUP.md");
|
|
4500
|
+
await mkdir9(join12(projectPath, ".opencode"), { recursive: true });
|
|
4501
|
+
await writeFile9(instructionsPath, `# AIKit MCP Server Configuration
|
|
4502
|
+
|
|
4503
|
+
## Automatic Setup Failed
|
|
4504
|
+
|
|
4505
|
+
Please manually configure the MCP server in OpenCode.
|
|
4506
|
+
|
|
4507
|
+
## Configuration
|
|
4508
|
+
|
|
4509
|
+
Add this to your OpenCode configuration file (\`~/.config/opencode/opencode.json\`):
|
|
4510
|
+
|
|
4511
|
+
\`\`\`json
|
|
4512
|
+
{
|
|
4513
|
+
"mcpServers": {
|
|
4514
|
+
"aikit": {
|
|
4515
|
+
"command": "node",
|
|
4516
|
+
"args": ["${mcpServerPath}"],
|
|
4517
|
+
"env": {}
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
\`\`\`
|
|
4522
|
+
|
|
4523
|
+
## After Configuration
|
|
4524
|
+
|
|
4525
|
+
1. Restart OpenCode completely
|
|
4526
|
+
2. OpenCode will automatically start the MCP server
|
|
4527
|
+
3. Tools will be available via MCP protocol
|
|
4528
|
+
4. You can use tools like \`tool_read_figma_design\` directly
|
|
4529
|
+
|
|
4530
|
+
## Verify
|
|
4531
|
+
|
|
4532
|
+
After restarting OpenCode, check:
|
|
4533
|
+
- MCP server is running (check OpenCode settings)
|
|
4534
|
+
- Tools are discoverable (OpenCode should list them)
|
|
4535
|
+
- You can call tools via MCP protocol
|
|
4536
|
+
`);
|
|
4537
|
+
logger.warn(`
|
|
4538
|
+
\u26A0\uFE0F Could not auto-configure MCP server. See: ${instructionsPath}`);
|
|
4539
|
+
}
|
|
4540
|
+
async function installToOpenCode(_opencodePath) {
|
|
4541
|
+
const { mkdir: mkdir9, writeFile: writeFile9, access: access5 } = await import("fs/promises");
|
|
4542
|
+
const { join: join12 } = await import("path");
|
|
4543
|
+
const projectPath = process.cwd();
|
|
4544
|
+
const opencodeCommandDir = join12(projectPath, ".opencode", "command");
|
|
4545
|
+
const aikitDir = join12(projectPath, ".aikit");
|
|
4546
|
+
const opencodeAgentDir = join12(paths.opencodeConfig(), "agent");
|
|
4547
|
+
await mkdir9(opencodeCommandDir, { recursive: true });
|
|
4548
|
+
await mkdir9(join12(aikitDir, "skills"), { recursive: true });
|
|
4549
|
+
await mkdir9(opencodeAgentDir, { recursive: true });
|
|
4550
|
+
const agentFiles = {
|
|
4551
|
+
agent: `---
|
|
4552
|
+
description: General-purpose default agent (OpenCode compatibility).
|
|
4553
|
+
mode: subagent
|
|
4554
|
+
tools:
|
|
4555
|
+
"*": true
|
|
4556
|
+
---
|
|
4557
|
+
|
|
4558
|
+
Use for quick tasks when no specialized agent is needed.`,
|
|
4559
|
+
planner: `---
|
|
4560
|
+
description: Strategic planner; breaks down work and coordinates specialist agents.
|
|
4561
|
+
mode: subagent
|
|
4562
|
+
tools:
|
|
4563
|
+
"*": true
|
|
4564
|
+
---
|
|
4565
|
+
|
|
4566
|
+
Use when the task is complex or multi-step. Delegates to @build for implementation,
|
|
4567
|
+
@scout for research, @review for code review/security, @explore for codebase navigation,
|
|
4568
|
+
and @vision for visual analysis.`,
|
|
4569
|
+
build: `---
|
|
4570
|
+
description: Primary builder; writes code, tests, and implements features.
|
|
4571
|
+
mode: subagent
|
|
4572
|
+
tools:
|
|
4573
|
+
"*": true
|
|
4574
|
+
---
|
|
4575
|
+
|
|
4576
|
+
Use for feature implementation, refactors, and bug fixes. Prefer TDD, small steps,
|
|
4577
|
+
and run checks after changes. Delegates to @review for audits and @explore for context.`,
|
|
4578
|
+
rush: `---
|
|
4579
|
+
description: Fast execution for small/urgent changes with minimal planning.
|
|
4580
|
+
mode: subagent
|
|
4581
|
+
tools:
|
|
4582
|
+
"*": true
|
|
4583
|
+
---
|
|
4584
|
+
|
|
4585
|
+
Use for quick fixes, hotfixes, or tiny edits. Keep scope minimal and verify quickly.`,
|
|
4586
|
+
review: `---
|
|
4587
|
+
description: Code review and quality/security auditing agent.
|
|
4588
|
+
mode: subagent
|
|
4589
|
+
tools:
|
|
4590
|
+
"*": true
|
|
4591
|
+
---
|
|
4592
|
+
|
|
4593
|
+
Use to review correctness, security, performance, maintainability, and tests. Be specific
|
|
4594
|
+
about issues and suggest concrete fixes.`,
|
|
4595
|
+
scout: `---
|
|
4596
|
+
description: Research agent for external docs, patterns, and references.
|
|
4597
|
+
mode: subagent
|
|
4598
|
+
tools:
|
|
4599
|
+
"*": true
|
|
4600
|
+
---
|
|
4601
|
+
|
|
4602
|
+
Use to look up docs, examples, best practices. Summarize findings concisely and cite sources.`,
|
|
4603
|
+
explore: `---
|
|
4604
|
+
description: Codebase navigation agent (search, grep, structure understanding).
|
|
4605
|
+
mode: subagent
|
|
4606
|
+
tools:
|
|
4607
|
+
"*": true
|
|
4608
|
+
---
|
|
4609
|
+
|
|
4610
|
+
Use to locate files, patterns, dependencies, and gather quick context in the repo.`,
|
|
4611
|
+
vision: `---
|
|
4612
|
+
description: Visual analysis agent for mockups, screenshots, PDFs, diagrams.
|
|
4613
|
+
mode: subagent
|
|
4614
|
+
tools:
|
|
4615
|
+
"*": true
|
|
4616
|
+
---
|
|
4617
|
+
|
|
4618
|
+
Use to interpret visual assets (components, layout, colors, typography) and translate to tasks.`
|
|
4619
|
+
};
|
|
4620
|
+
for (const [name, content] of Object.entries(agentFiles)) {
|
|
4621
|
+
const filePath = join12(opencodeAgentDir, `${name}.md`);
|
|
4622
|
+
try {
|
|
4623
|
+
await access5(filePath);
|
|
4624
|
+
} catch {
|
|
4625
|
+
await writeFile9(filePath, content, "utf8");
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
const config = await loadConfig();
|
|
4629
|
+
const skillEngine = new SkillEngine(config);
|
|
4630
|
+
const commandRunner = new CommandRunner(config);
|
|
4631
|
+
const skills = await skillEngine.listSkills();
|
|
4632
|
+
const commands = await commandRunner.listCommands();
|
|
4633
|
+
const opencodeCommands = {};
|
|
4634
|
+
const skillsList = skills.map((s) => `| \`/${s.name.replace(/\s+/g, "-")}\` | ${s.description} |`).join("\n");
|
|
4635
|
+
opencodeCommands["skills"] = `List all available AIKit skills and how to use them.
|
|
4636
|
+
|
|
4637
|
+
READ .aikit/AGENTS.md
|
|
4638
|
+
|
|
4639
|
+
## Available Skills
|
|
4640
|
+
|
|
4641
|
+
| Command | Description |
|
|
4642
|
+
|---------|-------------|
|
|
4643
|
+
${skillsList}
|
|
4644
|
+
|
|
4645
|
+
Type any command to use that skill. For example: \`/test-driven-development\` or \`/tdd\`.`;
|
|
4646
|
+
for (const skill of skills) {
|
|
4647
|
+
const commandName = skill.name.replace(/\s+/g, "-").toLowerCase();
|
|
4648
|
+
const skillPath = skill.filePath;
|
|
4649
|
+
const relativePath = skillPath.startsWith(projectPath) ? skillPath.replace(projectPath, "").replace(/\\/g, "/").replace(/^\//, "") : `.aikit/skills/${skill.name.replace(/\s+/g, "-").toLowerCase()}.md`;
|
|
4650
|
+
const useWhen = skill.useWhen || `The user asks you to ${skill.name}`;
|
|
4651
|
+
opencodeCommands[commandName] = `Use the **${skill.name} skill** ${useWhen.toLowerCase()}.
|
|
4652
|
+
|
|
4653
|
+
READ ${relativePath}
|
|
4654
|
+
|
|
4655
|
+
## Description
|
|
4656
|
+
${skill.description}
|
|
4657
|
+
|
|
4658
|
+
## When to Use
|
|
4659
|
+
${useWhen}
|
|
4660
|
+
|
|
4661
|
+
## Workflow
|
|
4662
|
+
${skill.content.split("\n").slice(0, 20).join("\n")}${skill.content.split("\n").length > 20 ? "\n\n... (see full skill file for complete workflow)" : ""}
|
|
4663
|
+
|
|
4664
|
+
**IMPORTANT**: Follow this skill's workflow step by step. Do not skip steps.
|
|
4665
|
+
Complete the checklist at the end of the skill.`;
|
|
4666
|
+
}
|
|
4667
|
+
for (const cmd of commands) {
|
|
4668
|
+
if (opencodeCommands[cmd.name]) continue;
|
|
4669
|
+
const commandName = cmd.name.replace(/\//g, "").replace(/\s+/g, "-");
|
|
4670
|
+
const examples = cmd.examples.map((e) => `- \`${e}\``).join("\n");
|
|
4671
|
+
if (cmd.name === "analyze-figma") {
|
|
4672
|
+
opencodeCommands[commandName] = `# Command: /analyze-figma
|
|
4673
|
+
|
|
4674
|
+
## Description
|
|
4675
|
+
${cmd.description}
|
|
4676
|
+
|
|
4677
|
+
## Usage
|
|
4678
|
+
\`${cmd.usage}\`
|
|
4679
|
+
|
|
4680
|
+
## Examples
|
|
4681
|
+
${examples}
|
|
4682
|
+
|
|
4683
|
+
## \u26A0\uFE0F CRITICAL: Extract URL FIRST!
|
|
4684
|
+
|
|
4685
|
+
**BEFORE ANYTHING ELSE**: Look at the user's FULL input message (all lines) and find the Figma URL. It's ALWAYS there - never ask for it!
|
|
4686
|
+
|
|
4687
|
+
**The URL pattern**: Look for text containing \`figma.com/design/\` anywhere in the user's message.
|
|
4688
|
+
|
|
4689
|
+
**Example of what user input looks like**:
|
|
4690
|
+
\`\`\`
|
|
4691
|
+
/analyze-figma https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/
|
|
4692
|
+
Online-Education-Website-Free-Template--Community-?t=7G5yzTiEtJlIZBtY-0
|
|
4693
|
+
\`\`\`
|
|
4694
|
+
|
|
4695
|
+
**Extract the complete URL** (combine if split):
|
|
4696
|
+
\`https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?t=7G5yzTiEtJlIZBtY-0\`
|
|
4697
|
+
|
|
4698
|
+
## Workflow
|
|
4699
|
+
|
|
4700
|
+
**IMPORTANT**: When user provides a Figma URL, you MUST immediately:
|
|
4701
|
+
|
|
4702
|
+
**Step 1: Extract URL from User Input**
|
|
4703
|
+
|
|
4704
|
+
**CRITICAL**: The URL is ALWAYS in the user's input message! DO NOT ask for it - just extract it!
|
|
4705
|
+
|
|
4706
|
+
**MANDATORY**: You MUST extract the URL before proceeding. This is not optional!
|
|
4707
|
+
|
|
4708
|
+
**How to Extract**:
|
|
4709
|
+
1. **Read the ENTIRE user input message** - look at ALL lines, not just the first line
|
|
4710
|
+
2. **Search for ANY text containing** \`figma.com/design/\` - this is the URL
|
|
4711
|
+
3. **URL may appear in different formats**:
|
|
4712
|
+
- On same line: \`/analyze-figma https://www.figma.com/design/...\`
|
|
4713
|
+
- Split across lines:
|
|
4714
|
+
\`\`\`
|
|
4715
|
+
/analyze-figma https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/
|
|
4716
|
+
Online-Education-Website-Free-Template--Community-?t=7G5yzTiEtJlIZBtY-0
|
|
4717
|
+
\`\`\`
|
|
4718
|
+
- Just the URL: \`https://www.figma.com/design/...\`
|
|
4719
|
+
4. **Extract the COMPLETE URL**:
|
|
4720
|
+
- Start from \`https://\` or \`http://\`
|
|
4721
|
+
- Include everything until the end of the line or next whitespace
|
|
4722
|
+
- If URL is split, combine ALL parts into one complete URL
|
|
4723
|
+
5. **Include ALL query parameters**: \`?node-id=...\`, \`&t=...\`, etc.
|
|
4724
|
+
|
|
4725
|
+
**REAL EXAMPLE**:
|
|
4726
|
+
\`\`\`
|
|
4727
|
+
User input:
|
|
4728
|
+
/analyze-figma https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/
|
|
4729
|
+
Online-Education-Website-Free-Template--Community-?t=7G5yzTiEtJlIZBtY-0
|
|
4730
|
+
\`\`\`
|
|
4731
|
+
|
|
4732
|
+
**Extract as**:
|
|
4733
|
+
\`https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?t=7G5yzTiEtJlIZBtY-0\`
|
|
4734
|
+
|
|
4735
|
+
**CRITICAL RULES**:
|
|
4736
|
+
- \u2705 DO: Read the ENTIRE user message (all lines)
|
|
4737
|
+
- \u2705 DO: Look for \`figma.com/design/\` anywhere in the message
|
|
4738
|
+
- \u2705 DO: Combine split lines into one URL
|
|
4739
|
+
- \u274C DO NOT: Ask user for URL - it's ALWAYS in the input
|
|
4740
|
+
- \u274C DO NOT: Skip this step - URL extraction is MANDATORY
|
|
4741
|
+
- \u274C DO NOT: Proceed without extracting URL first
|
|
4742
|
+
|
|
4743
|
+
**If you think URL is not found**:
|
|
4744
|
+
1. Re-read the user's message line by line
|
|
4745
|
+
2. Look for ANY mention of "figma.com"
|
|
4746
|
+
3. Check if URL is split across multiple lines
|
|
4747
|
+
4. The URL is definitely there - find it!
|
|
4748
|
+
- If URL not found in current message, check previous messages
|
|
4749
|
+
|
|
4750
|
+
**Step 2: Check Tool Configuration**
|
|
4751
|
+
|
|
4752
|
+
Before calling the tool, verify that Figma tool is configured:
|
|
4753
|
+
- If not configured, inform user to run: \`aikit skills figma-analysis config\`
|
|
4754
|
+
- The tool requires a Figma Personal Access Token
|
|
4755
|
+
|
|
4756
|
+
**Step 3: Call MCP Tool read_figma_design**
|
|
4757
|
+
|
|
4758
|
+
Use the MCP tool \`read_figma_design\` (or \`tool_read_figma_design\` via MCP) with the extracted URL:
|
|
4759
|
+
\`\`\`
|
|
4760
|
+
Use tool: read_figma_design
|
|
4761
|
+
Arguments: { "url": "[extracted URL]" }
|
|
4762
|
+
\`\`\`
|
|
4763
|
+
|
|
4764
|
+
**This tool will automatically**:
|
|
4765
|
+
1. Validate the Figma URL format
|
|
4766
|
+
2. Check if Figma tool is configured
|
|
4767
|
+
3. Call Figma API to fetch design data
|
|
4768
|
+
4. Extract design tokens:
|
|
4769
|
+
- Colors (from fills and strokes, converted to hex)
|
|
4770
|
+
- Typography (font families, sizes, weights, line heights)
|
|
4771
|
+
- Spacing system (8px grid detection)
|
|
4772
|
+
- Components (from Figma components)
|
|
4773
|
+
- Screens/Frames (dimensions and names)
|
|
4774
|
+
- Breakpoints (common responsive breakpoints)
|
|
4775
|
+
5. Return formatted markdown with all extracted tokens
|
|
4776
|
+
|
|
4777
|
+
**Step 4: Format and Save**
|
|
4778
|
+
|
|
4779
|
+
Format extracted tokens as structured markdown and save using memory-update tool:
|
|
4780
|
+
\`\`\`
|
|
4781
|
+
Use tool: memory-update
|
|
4782
|
+
Arguments: {
|
|
4783
|
+
"key": "research/figma-analysis",
|
|
4784
|
+
"content": "[formatted markdown with all tokens]"
|
|
4785
|
+
}
|
|
4786
|
+
\`\`\`
|
|
4787
|
+
|
|
4788
|
+
**Step 5: Report Results**
|
|
4789
|
+
|
|
4790
|
+
Report what was extracted:
|
|
4791
|
+
- Number of screens found
|
|
4792
|
+
- Number of colors in palette
|
|
4793
|
+
- Typography styles found
|
|
4794
|
+
- Components identified
|
|
4795
|
+
- Confirm save location: \`memory/research/figma-analysis.md\`
|
|
4796
|
+
|
|
4797
|
+
## Critical Instructions
|
|
4798
|
+
|
|
4799
|
+
- **DO NOT** ask user to "share the Figma URL" - they already provided it in the command
|
|
4800
|
+
- **DO NOT** wait for confirmation - just start analyzing immediately
|
|
4801
|
+
- **DO** extract URL from full user input message
|
|
4802
|
+
- **DO** call MCP tool \`read_figma_design\` immediately
|
|
4803
|
+
- **DO** use browser MCP to navigate and snapshot
|
|
4804
|
+
- **DO** extract everything automatically without asking
|
|
4805
|
+
- **DO** save to memory automatically
|
|
4806
|
+
|
|
4807
|
+
## Error Handling
|
|
4808
|
+
|
|
4809
|
+
If the tool returns an error:
|
|
4810
|
+
1. **If "needs config"**: Guide user to run \`aikit skills figma-analysis config\`
|
|
4811
|
+
2. **If API error**:
|
|
4812
|
+
- Verify the Figma URL is correct and accessible
|
|
4813
|
+
- Ensure your API token has access to the file
|
|
4814
|
+
- Check if the file is public or you have permission to access it
|
|
4815
|
+
3. **If URL invalid**: Re-check the extracted URL format
|
|
4816
|
+
|
|
4817
|
+
## How to Parse URL from Command
|
|
4818
|
+
|
|
4819
|
+
**CRITICAL**: The Figma URL is provided in the SAME message as the command!
|
|
4820
|
+
|
|
4821
|
+
Example:
|
|
4822
|
+
- User input: \`/analyze-figma https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?node-id=0-1&t=70yZa7w5wSyjDhYj-1\`
|
|
4823
|
+
- The URL is: \`https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?node-id=0-1&t=70yZa7w5wSyjDhYj-1\`
|
|
4824
|
+
|
|
4825
|
+
**Extract the URL** from the command input:
|
|
4826
|
+
- Everything after \`/analyze-figma \` (note the space) is the URL
|
|
4827
|
+
- The URL starts with \`https://\` or \`http://\`
|
|
4828
|
+
- Extract the ENTIRE URL including all query parameters
|
|
4829
|
+
- Check the FULL user message, not just command name
|
|
4830
|
+
|
|
4831
|
+
## Example Usage
|
|
4832
|
+
|
|
4833
|
+
User input: \`/analyze-figma https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?node-id=0-1&t=70yZa7w5wSyjDhYj-1\`
|
|
4834
|
+
|
|
4835
|
+
**Step 1: Extract URL**
|
|
4836
|
+
- Check full user input message
|
|
4837
|
+
- Extract: \`https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?node-id=0-1&t=70yZa7w5wSyjDhYj-1\`
|
|
4838
|
+
|
|
4839
|
+
**Step 2: Call MCP Tool**
|
|
4840
|
+
\`\`\`
|
|
4841
|
+
Use tool: read_figma_design
|
|
4842
|
+
Arguments: { "url": "https://www.figma.com/design/lC34qpTSy2MYalTIOsj8S2/Online-Education-Website-Free-Template--Community-?node-id=0-1&t=70yZa7w5wSyjDhYj-1" }
|
|
4843
|
+
\`\`\`
|
|
4844
|
+
|
|
4845
|
+
**Step 3: Tool automatically extracts tokens via Figma API**
|
|
4846
|
+
|
|
4847
|
+
**Step 4: Save to memory using memory-update tool**
|
|
4848
|
+
|
|
4849
|
+
**Step 5: Report results to user**
|
|
4850
|
+
|
|
4851
|
+
## Important Reminders
|
|
4852
|
+
|
|
4853
|
+
- The URL is ALREADY in the command input - extract it from full message!
|
|
4854
|
+
- URL may be split across multiple lines - combine them into one complete URL
|
|
4855
|
+
- Do NOT ask for URL again - it's always in the input
|
|
4856
|
+
- Do NOT wait - start immediately
|
|
4857
|
+
- Use MCP tool \`read_figma_design\` which uses Figma API directly`;
|
|
4858
|
+
} else {
|
|
4859
|
+
opencodeCommands[commandName] = `# Command: /${cmd.name}
|
|
4860
|
+
|
|
4861
|
+
## Description
|
|
4862
|
+
${cmd.description}
|
|
4863
|
+
|
|
4864
|
+
## Usage
|
|
4865
|
+
\`${cmd.usage}\`
|
|
4866
|
+
|
|
4867
|
+
## Examples
|
|
4868
|
+
${examples}
|
|
4869
|
+
|
|
4870
|
+
## Workflow
|
|
4871
|
+
${cmd.content}
|
|
4872
|
+
|
|
4873
|
+
**Category**: ${cmd.category}`;
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
let count = 0;
|
|
4877
|
+
for (const [name, content] of Object.entries(opencodeCommands)) {
|
|
4878
|
+
const filePath = join12(opencodeCommandDir, `${name}.md`);
|
|
4879
|
+
await writeFile9(filePath, content.trim());
|
|
4880
|
+
logger.info(` \u2713 Created /${name} command`);
|
|
4881
|
+
count++;
|
|
4882
|
+
}
|
|
4883
|
+
logger.success(`
|
|
4884
|
+
Created ${count} OpenCode commands in .opencode/command/`);
|
|
4885
|
+
await configureMcpServer(projectPath);
|
|
4886
|
+
logger.info("\nUsage in OpenCode:");
|
|
4887
|
+
logger.info(" Press Ctrl+K to open command picker");
|
|
4888
|
+
logger.info(" Or type /skills to see all available skills");
|
|
4889
|
+
logger.info(` Available: ${skills.length} skills, ${commands.length} commands`);
|
|
4890
|
+
logger.info(" MCP server configured - tools available via MCP protocol");
|
|
4891
|
+
}
|
|
4892
|
+
function groupBy(array, keyFn) {
|
|
4893
|
+
return array.reduce((acc, item) => {
|
|
4894
|
+
const key = keyFn(item);
|
|
4895
|
+
if (!acc[key]) acc[key] = [];
|
|
4896
|
+
acc[key].push(item);
|
|
4897
|
+
return acc;
|
|
4898
|
+
}, {});
|
|
4899
|
+
}
|
|
4900
|
+
program.parse();
|
|
4901
|
+
//# sourceMappingURL=cli.js.map
|