@loom-framework/core 0.1.0-alpha.8 → 0.1.0-alpha.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-base.d.ts +29 -0
- package/dist/adapter-base.d.ts.map +1 -0
- package/dist/adapter-base.js +62 -0
- package/dist/adapter-base.js.map +1 -0
- package/dist/adapter-factory.d.ts +8 -0
- package/dist/adapter-factory.d.ts.map +1 -0
- package/dist/adapter-factory.js +25 -0
- package/dist/adapter-factory.js.map +1 -0
- package/dist/adapter-filesystem.d.ts +6 -11
- package/dist/adapter-filesystem.d.ts.map +1 -1
- package/dist/adapter-filesystem.js +56 -41
- package/dist/adapter-filesystem.js.map +1 -1
- package/dist/adapter-sqlite.d.ts +6 -23
- package/dist/adapter-sqlite.d.ts.map +1 -1
- package/dist/adapter-sqlite.js +65 -50
- package/dist/adapter-sqlite.js.map +1 -1
- package/dist/backend/ai/button-resolver.d.ts +18 -0
- package/dist/backend/ai/button-resolver.d.ts.map +1 -0
- package/dist/backend/ai/button-resolver.js +58 -0
- package/dist/backend/ai/button-resolver.js.map +1 -0
- package/dist/backend/ai/engine.d.ts +52 -0
- package/dist/backend/ai/engine.d.ts.map +1 -0
- package/dist/backend/ai/engine.js +186 -0
- package/dist/backend/ai/engine.js.map +1 -0
- package/dist/backend/ai/index.d.ts +11 -0
- package/dist/backend/ai/index.d.ts.map +1 -0
- package/dist/backend/ai/index.js +8 -0
- package/dist/backend/ai/index.js.map +1 -0
- package/dist/backend/ai/output-parser.d.ts +29 -0
- package/dist/backend/ai/output-parser.d.ts.map +1 -0
- package/dist/backend/ai/output-parser.js +247 -0
- package/dist/backend/ai/output-parser.js.map +1 -0
- package/dist/backend/ai/session-manager.d.ts +103 -0
- package/dist/backend/ai/session-manager.d.ts.map +1 -0
- package/dist/backend/ai/session-manager.js +298 -0
- package/dist/backend/ai/session-manager.js.map +1 -0
- package/dist/backend/index.d.ts +61 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +161 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/observe/index.d.ts +6 -0
- package/dist/backend/observe/index.d.ts.map +1 -0
- package/dist/backend/observe/index.js +5 -0
- package/dist/backend/observe/index.js.map +1 -0
- package/dist/backend/observe/logger.d.ts +28 -0
- package/dist/backend/observe/logger.d.ts.map +1 -0
- package/dist/backend/observe/logger.js +80 -0
- package/dist/backend/observe/logger.js.map +1 -0
- package/dist/backend/observe/types.d.ts +26 -0
- package/dist/backend/observe/types.d.ts.map +1 -0
- package/dist/backend/observe/types.js +7 -0
- package/dist/backend/observe/types.js.map +1 -0
- package/dist/backend/routes/chat.d.ts +31 -0
- package/dist/backend/routes/chat.d.ts.map +1 -0
- package/dist/backend/routes/chat.js +426 -0
- package/dist/backend/routes/chat.js.map +1 -0
- package/dist/backend/routes/data.d.ts +13 -0
- package/dist/backend/routes/data.d.ts.map +1 -0
- package/dist/backend/routes/data.js +134 -0
- package/dist/backend/routes/data.js.map +1 -0
- package/dist/backend/routes/health.d.ts +7 -0
- package/dist/backend/routes/health.d.ts.map +1 -0
- package/dist/backend/routes/health.js +15 -0
- package/dist/backend/routes/health.js.map +1 -0
- package/dist/backend/routes/index.d.ts +11 -0
- package/dist/backend/routes/index.d.ts.map +1 -0
- package/dist/backend/routes/index.js +9 -0
- package/dist/backend/routes/index.js.map +1 -0
- package/dist/backend/routes/skills.d.ts +16 -0
- package/dist/backend/routes/skills.d.ts.map +1 -0
- package/dist/backend/routes/skills.js +590 -0
- package/dist/backend/routes/skills.js.map +1 -0
- package/dist/backend/routes/upload.d.ts +24 -0
- package/dist/backend/routes/upload.d.ts.map +1 -0
- package/dist/backend/routes/upload.js +67 -0
- package/dist/backend/routes/upload.js.map +1 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/capability-generator.d.ts +21 -6
- package/dist/capability-generator.d.ts.map +1 -1
- package/dist/capability-generator.js +88 -261
- package/dist/capability-generator.js.map +1 -1
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +170 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/data.d.ts +12 -0
- package/dist/cli/commands/data.d.ts.map +1 -0
- package/dist/cli/commands/data.js +158 -0
- package/dist/cli/commands/data.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +9 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +114 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/generate-capabilities.d.ts +8 -0
- package/dist/cli/commands/generate-capabilities.d.ts.map +1 -0
- package/dist/cli/commands/generate-capabilities.js +40 -0
- package/dist/cli/commands/generate-capabilities.js.map +1 -0
- package/dist/cli/commands/generate-cli-command.d.ts +8 -0
- package/dist/cli/commands/generate-cli-command.d.ts.map +1 -0
- package/dist/cli/commands/generate-cli-command.js +64 -0
- package/dist/cli/commands/generate-cli-command.js.map +1 -0
- package/dist/cli/commands/generate-dashboard.d.ts +9 -0
- package/dist/cli/commands/generate-dashboard.d.ts.map +1 -0
- package/dist/cli/commands/generate-dashboard.js +452 -0
- package/dist/cli/commands/generate-dashboard.js.map +1 -0
- package/dist/cli/commands/generate-page.d.ts +9 -0
- package/dist/cli/commands/generate-page.d.ts.map +1 -0
- package/dist/cli/commands/generate-page.js +518 -0
- package/dist/cli/commands/generate-page.js.map +1 -0
- package/dist/cli/commands/generate-skill.d.ts +8 -0
- package/dist/cli/commands/generate-skill.d.ts.map +1 -0
- package/dist/cli/commands/generate-skill.js +75 -0
- package/dist/cli/commands/generate-skill.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +6 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +19 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +539 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/observe.d.ts +9 -0
- package/dist/cli/commands/observe.d.ts.map +1 -0
- package/dist/cli/commands/observe.js +142 -0
- package/dist/cli/commands/observe.js.map +1 -0
- package/dist/cli/commands/skill.d.ts +9 -0
- package/dist/cli/commands/skill.d.ts.map +1 -0
- package/dist/cli/commands/skill.js +186 -0
- package/dist/cli/commands/skill.js.map +1 -0
- package/dist/cli/helpers/app-tsx-wiring.d.ts +17 -0
- package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -0
- package/dist/cli/helpers/app-tsx-wiring.js +132 -0
- package/dist/cli/helpers/app-tsx-wiring.js.map +1 -0
- package/dist/cli/helpers/duration.d.ts +5 -0
- package/dist/cli/helpers/duration.d.ts.map +1 -0
- package/dist/cli/helpers/duration.js +19 -0
- package/dist/cli/helpers/duration.js.map +1 -0
- package/dist/cli/helpers/field-template.d.ts +10 -0
- package/dist/cli/helpers/field-template.d.ts.map +1 -0
- package/dist/cli/helpers/field-template.js +100 -0
- package/dist/cli/helpers/field-template.js.map +1 -0
- package/dist/cli/helpers/naming.d.ts +12 -0
- package/dist/cli/helpers/naming.d.ts.map +1 -0
- package/dist/cli/helpers/naming.js +25 -0
- package/dist/cli/helpers/naming.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +33 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils.d.ts +10 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +31 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/config.d.ts +118 -42
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -10
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/server-bin.d.ts +12 -0
- package/dist/server-bin.d.ts.map +1 -0
- package/dist/server-bin.js +75 -0
- package/dist/server-bin.js.map +1 -0
- package/dist/types.d.ts +71 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -10
- package/templates/app-skill/SKILL.md +27 -0
- package/templates/app-skill/references/data-semantics.md +44 -0
- package/templates/app-skill/references/models.md +31 -0
- package/templates/loom-skill/SKILL.md +153 -0
- package/templates/loom-skill/references/README.md +128 -0
- package/templates/loom-skill/references/dashboard.md +161 -0
- package/templates/loom-skill/references/data-model.md +78 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes - Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
export { registerDataRoutes } from './data.js';
|
|
5
|
+
export { registerHealthRoute } from './health.js';
|
|
6
|
+
export { registerUploadRoutes, saveUploadedFile } from './upload.js';
|
|
7
|
+
export { registerChatRoutes } from './chat.js';
|
|
8
|
+
export type { ChatRouteOptions } from './chat.js';
|
|
9
|
+
export { registerSkillRoutes } from './skills.js';
|
|
10
|
+
export type { SkillRouteOptions } from './skills.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/backend/routes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes - Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
export { registerDataRoutes } from './data.js';
|
|
5
|
+
export { registerHealthRoute } from './health.js';
|
|
6
|
+
export { registerUploadRoutes, saveUploadedFile } from './upload.js';
|
|
7
|
+
export { registerChatRoutes } from './chat.js';
|
|
8
|
+
export { registerSkillRoutes } from './skills.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/backend/routes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Management Routes - REST API for managing skills in .claude/skills/
|
|
3
|
+
*
|
|
4
|
+
* POST /api/v1/skills — Create a new skill
|
|
5
|
+
* GET /api/v1/skills — List all skills with metadata
|
|
6
|
+
* GET /api/v1/skills/:skillName — Get skill details
|
|
7
|
+
* DELETE /api/v1/skills/:skillName — Delete a skill
|
|
8
|
+
* GET /api/v1/skills/:skillName/file/:filePath — Get a file's content
|
|
9
|
+
* POST /api/v1/skills/:skillName/file — Upload/replace a file in a skill
|
|
10
|
+
*/
|
|
11
|
+
import type { FastifyInstance } from 'fastify';
|
|
12
|
+
export interface SkillRouteOptions {
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function registerSkillRoutes(fastify: FastifyInstance, options: SkillRouteOptions): void;
|
|
16
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/backend/routes/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA2X/C,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,iBAAiB,GACzB,IAAI,CAoSN"}
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Management Routes - REST API for managing skills in .claude/skills/
|
|
3
|
+
*
|
|
4
|
+
* POST /api/v1/skills — Create a new skill
|
|
5
|
+
* GET /api/v1/skills — List all skills with metadata
|
|
6
|
+
* GET /api/v1/skills/:skillName — Get skill details
|
|
7
|
+
* DELETE /api/v1/skills/:skillName — Delete a skill
|
|
8
|
+
* GET /api/v1/skills/:skillName/file/:filePath — Get a file's content
|
|
9
|
+
* POST /api/v1/skills/:skillName/file — Upload/replace a file in a skill
|
|
10
|
+
*/
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import AdmZip from 'adm-zip';
|
|
14
|
+
// ── Schemas ──
|
|
15
|
+
const skillNameParamSchema = {
|
|
16
|
+
type: 'object',
|
|
17
|
+
required: ['skillName'],
|
|
18
|
+
properties: {
|
|
19
|
+
skillName: { type: 'string', minLength: 1 },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const createSkillBodySchema = {
|
|
23
|
+
type: 'object',
|
|
24
|
+
required: ['name'],
|
|
25
|
+
properties: {
|
|
26
|
+
name: { type: 'string', minLength: 1, maxLength: 64 },
|
|
27
|
+
skillMd: { type: 'string' },
|
|
28
|
+
references: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
required: ['name', 'content'],
|
|
33
|
+
properties: {
|
|
34
|
+
name: { type: 'string' },
|
|
35
|
+
content: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
scripts: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: ['name', 'content'],
|
|
44
|
+
properties: {
|
|
45
|
+
name: { type: 'string' },
|
|
46
|
+
content: { type: 'string' },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
files: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
required: ['path', 'content'],
|
|
55
|
+
properties: {
|
|
56
|
+
path: { type: 'string' },
|
|
57
|
+
content: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const parseArchiveBodySchema = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
required: ['data'],
|
|
66
|
+
properties: {
|
|
67
|
+
data: { type: 'string', minLength: 1 },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const uploadFileBodySchema = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
required: ['filename', 'content'],
|
|
73
|
+
properties: {
|
|
74
|
+
filename: { type: 'string', minLength: 1 },
|
|
75
|
+
content: { type: 'string' },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
// ── Helpers ─
|
|
79
|
+
/**
|
|
80
|
+
* Parse YAML frontmatter from markdown content using simple regex.
|
|
81
|
+
*/
|
|
82
|
+
function parseFrontmatter(content) {
|
|
83
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
84
|
+
if (!match)
|
|
85
|
+
return { metadata: {}, body: content };
|
|
86
|
+
try {
|
|
87
|
+
const metadata = {};
|
|
88
|
+
const lines = match[1].split('\n');
|
|
89
|
+
let currentKey = '';
|
|
90
|
+
let currentValues = [];
|
|
91
|
+
let inArray = false;
|
|
92
|
+
let inBlock = false;
|
|
93
|
+
let blockLines = [];
|
|
94
|
+
const flushBlock = () => {
|
|
95
|
+
if (inBlock && currentKey) {
|
|
96
|
+
metadata[currentKey] = blockLines.join('\n').trim();
|
|
97
|
+
}
|
|
98
|
+
inBlock = false;
|
|
99
|
+
blockLines = [];
|
|
100
|
+
};
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (inBlock) {
|
|
103
|
+
if (/^[ \t]+/.test(line) || (line === '' && blockLines.length > 0)) {
|
|
104
|
+
blockLines.push(line.replace(/^[ \t]+/, ''));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
flushBlock();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const trimmed = line.trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
continue;
|
|
114
|
+
const arrayMatch = trimmed.match(/^-\s+(.+)$/);
|
|
115
|
+
if (inArray && arrayMatch) {
|
|
116
|
+
currentValues.push(arrayMatch[1].replace(/^['"]|['"]$/g, '').trim());
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (inArray && currentKey) {
|
|
120
|
+
metadata[currentKey] = currentValues;
|
|
121
|
+
inArray = false;
|
|
122
|
+
currentValues = [];
|
|
123
|
+
}
|
|
124
|
+
const kvMatch = trimmed.match(/^([a-zA-Z_-][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
125
|
+
if (kvMatch) {
|
|
126
|
+
currentKey = kvMatch[1];
|
|
127
|
+
let val = kvMatch[2].trim();
|
|
128
|
+
if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) {
|
|
129
|
+
val = val.slice(1, -1);
|
|
130
|
+
}
|
|
131
|
+
if (val === '|' || val === '>') {
|
|
132
|
+
inBlock = true;
|
|
133
|
+
blockLines = [];
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (val === '[') {
|
|
137
|
+
inArray = true;
|
|
138
|
+
currentValues = [];
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
142
|
+
metadata[currentKey] = val.slice(1, -1).split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
|
|
143
|
+
currentKey = '';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
metadata[currentKey] = val;
|
|
147
|
+
currentKey = '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (inArray && currentKey) {
|
|
151
|
+
metadata[currentKey] = currentValues;
|
|
152
|
+
}
|
|
153
|
+
flushBlock();
|
|
154
|
+
return { metadata, body: match[2] || '' };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { metadata: {}, body: content };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Recursively scan a directory for files, returning relative paths.
|
|
162
|
+
* Excludes SKILL.md (handled separately).
|
|
163
|
+
*/
|
|
164
|
+
async function scanDirFiles(dir, base) {
|
|
165
|
+
const results = [];
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
const subFiles = await scanDirFiles(path.join(dir, entry.name), base);
|
|
176
|
+
results.push(...subFiles);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const relPath = path.relative(base, path.join(dir, entry.name));
|
|
180
|
+
results.push(relPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Safely resolve a file path within a directory. Rejects '..' traversal.
|
|
187
|
+
*/
|
|
188
|
+
function safeResolve(baseDir, filePath) {
|
|
189
|
+
if (filePath.includes('..'))
|
|
190
|
+
return null;
|
|
191
|
+
const resolved = path.resolve(baseDir, filePath);
|
|
192
|
+
if (!resolved.startsWith(baseDir))
|
|
193
|
+
return null;
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Validate skill name.
|
|
198
|
+
*/
|
|
199
|
+
function isValidSkillName(name) {
|
|
200
|
+
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Text file extensions that should be kept when parsing zip archives.
|
|
204
|
+
*/
|
|
205
|
+
const TEXT_EXTENSIONS = new Set([
|
|
206
|
+
'.md', '.json', '.yaml', '.yml', '.txt', '.ts', '.js', '.tsx', '.jsx',
|
|
207
|
+
'.xml', '.html', '.css', '.sh', '.py', '.rb', '.go', '.rs', '.java',
|
|
208
|
+
'.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.toml', '.ini', '.cfg',
|
|
209
|
+
'.env', '.gitignore', '.prettierrc', '.eslintrc', '.conf', '.properties',
|
|
210
|
+
]);
|
|
211
|
+
/**
|
|
212
|
+
* Check if a file path looks like a text file based on extension.
|
|
213
|
+
*/
|
|
214
|
+
function isTextFile(filePath) {
|
|
215
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
216
|
+
if (TEXT_EXTENSIONS.has(ext))
|
|
217
|
+
return true;
|
|
218
|
+
// Files with no extension but common names
|
|
219
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
220
|
+
if (['readme', 'license', 'makefile', 'dockerfile', '.gitkeep', '.gitignore'].includes(basename))
|
|
221
|
+
return true;
|
|
222
|
+
if (basename.startsWith('.git'))
|
|
223
|
+
return true;
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parse a zip archive buffer and extract skill structure.
|
|
228
|
+
*/
|
|
229
|
+
function parseZipArchive(buffer) {
|
|
230
|
+
const zip = new AdmZip(buffer);
|
|
231
|
+
const entries = zip.getEntries();
|
|
232
|
+
// Collect valid file entries
|
|
233
|
+
const validEntries = entries.filter((entry) => {
|
|
234
|
+
if (entry.isDirectory)
|
|
235
|
+
return false;
|
|
236
|
+
const entryPath = entry.entryName;
|
|
237
|
+
// Skip macOS metadata
|
|
238
|
+
if (entryPath.startsWith('__MACOSX/'))
|
|
239
|
+
return false;
|
|
240
|
+
// Skip .DS_Store and hidden files in root
|
|
241
|
+
const parts = entryPath.split('/');
|
|
242
|
+
if (parts.some((p) => p.startsWith('.') && p !== '.gitkeep' && p !== '.gitignore'))
|
|
243
|
+
return false;
|
|
244
|
+
return true;
|
|
245
|
+
});
|
|
246
|
+
if (validEntries.length === 0) {
|
|
247
|
+
throw new Error('压缩包中没有任何有效文件');
|
|
248
|
+
}
|
|
249
|
+
// Detect common top-level directory prefix
|
|
250
|
+
let prefix = '';
|
|
251
|
+
const firstEntry = validEntries[0].entryName;
|
|
252
|
+
const slashIdx = firstEntry.indexOf('/');
|
|
253
|
+
if (slashIdx > 0) {
|
|
254
|
+
const candidate = firstEntry.substring(0, slashIdx + 1);
|
|
255
|
+
if (validEntries.every((e) => e.entryName.startsWith(candidate))) {
|
|
256
|
+
prefix = candidate;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Strip prefix and collect files
|
|
260
|
+
const allFiles = [];
|
|
261
|
+
for (const entry of validEntries) {
|
|
262
|
+
let relPath = entry.entryName;
|
|
263
|
+
if (prefix && relPath.startsWith(prefix)) {
|
|
264
|
+
relPath = relPath.substring(prefix.length);
|
|
265
|
+
}
|
|
266
|
+
// Skip if empty after stripping prefix
|
|
267
|
+
if (!relPath)
|
|
268
|
+
continue;
|
|
269
|
+
// Only keep text files
|
|
270
|
+
if (!isTextFile(relPath))
|
|
271
|
+
continue;
|
|
272
|
+
const content = entry.getData().toString('utf-8');
|
|
273
|
+
allFiles.push({ path: relPath, content });
|
|
274
|
+
}
|
|
275
|
+
// Find SKILL.md
|
|
276
|
+
const skillMdEntry = allFiles.find((f) => f.path === 'SKILL.md');
|
|
277
|
+
if (!skillMdEntry) {
|
|
278
|
+
throw new Error('压缩包中未找到 SKILL.md 文件');
|
|
279
|
+
}
|
|
280
|
+
// Check for duplicate SKILL.md in subdirectories
|
|
281
|
+
const skillMdCount = allFiles.filter((f) => path.basename(f.path) === 'SKILL.md').length;
|
|
282
|
+
if (skillMdCount > 1) {
|
|
283
|
+
throw new Error('压缩包中包含多个 SKILL.md 文件,请确保只有一个');
|
|
284
|
+
}
|
|
285
|
+
// Extract skill name from frontmatter or directory name
|
|
286
|
+
const { metadata } = parseFrontmatter(skillMdEntry.content);
|
|
287
|
+
let skillName = metadata.name;
|
|
288
|
+
if (!skillName || typeof skillName !== 'string') {
|
|
289
|
+
// Fallback to directory name from prefix
|
|
290
|
+
if (prefix) {
|
|
291
|
+
skillName = prefix.replace(/\/$/, '');
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
throw new Error('无法确定 Skill 名称,请在 SKILL.md 的 frontmatter 中指定 name 字段');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Validate skill name
|
|
298
|
+
if (!isValidSkillName(skillName)) {
|
|
299
|
+
throw new Error(`Skill 名称 "${skillName}" 不合法,只能包含字母、数字、中划线和下划线,且以字母开头`);
|
|
300
|
+
}
|
|
301
|
+
// Separate SKILL.md from other files
|
|
302
|
+
const files = allFiles.filter((f) => f.path !== 'SKILL.md');
|
|
303
|
+
return { skillName, skillMd: skillMdEntry.content, files };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get MIME type for a file extension.
|
|
307
|
+
*/
|
|
308
|
+
function getContentType(filePath) {
|
|
309
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
310
|
+
const types = {
|
|
311
|
+
'.md': 'text/markdown',
|
|
312
|
+
'.txt': 'text/plain',
|
|
313
|
+
'.json': 'application/json',
|
|
314
|
+
'.xml': 'application/xml',
|
|
315
|
+
'.yaml': 'text/yaml',
|
|
316
|
+
'.yml': 'text/yaml',
|
|
317
|
+
'.ts': 'text/typescript',
|
|
318
|
+
'.js': 'text/javascript',
|
|
319
|
+
'.html': 'text/html',
|
|
320
|
+
'.css': 'text/css',
|
|
321
|
+
};
|
|
322
|
+
return types[ext] || 'text/plain';
|
|
323
|
+
}
|
|
324
|
+
export function registerSkillRoutes(fastify, options) {
|
|
325
|
+
const skillsDir = path.join(options.projectRoot, '.claude', 'skills');
|
|
326
|
+
const ensureSkillsDir = async () => {
|
|
327
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
328
|
+
};
|
|
329
|
+
// ── GET /api/v1/skills ──
|
|
330
|
+
fastify.get('/api/v1/skills', async (_request, reply) => {
|
|
331
|
+
await ensureSkillsDir();
|
|
332
|
+
let entries;
|
|
333
|
+
try {
|
|
334
|
+
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return reply.send({ data: [] });
|
|
338
|
+
}
|
|
339
|
+
const skills = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (!entry.isDirectory())
|
|
342
|
+
continue;
|
|
343
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
344
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
345
|
+
let metadata = {};
|
|
346
|
+
try {
|
|
347
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
348
|
+
metadata = parseFrontmatter(content).metadata;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// SKILL.md doesn't exist or is unreadable
|
|
352
|
+
}
|
|
353
|
+
// Scan subdirectories dynamically (any folder name, not just 'references')
|
|
354
|
+
let subDirs = [];
|
|
355
|
+
try {
|
|
356
|
+
const dirEntries = await fs.readdir(skillDir, { withFileTypes: true });
|
|
357
|
+
subDirs = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// ignore
|
|
361
|
+
}
|
|
362
|
+
skills.push({ name: entry.name, metadata, subDirs });
|
|
363
|
+
}
|
|
364
|
+
return reply.send({ data: skills });
|
|
365
|
+
});
|
|
366
|
+
// ── POST /api/v1/skills/parse-archive ──
|
|
367
|
+
fastify.post('/api/v1/skills/parse-archive', {
|
|
368
|
+
schema: { body: parseArchiveBodySchema },
|
|
369
|
+
}, async (request, reply) => {
|
|
370
|
+
const body = request.body;
|
|
371
|
+
let buffer;
|
|
372
|
+
try {
|
|
373
|
+
buffer = Buffer.from(body.data, 'base64');
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return reply.status(400).send({ error: '无效的 base64 编码' });
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const result = parseZipArchive(buffer);
|
|
380
|
+
return reply.send({ data: result });
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const msg = err instanceof Error ? err.message : '解析压缩包失败';
|
|
384
|
+
return reply.status(400).send({ error: msg });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ── GET /api/v1/skills/:skillName ──
|
|
388
|
+
fastify.get('/api/v1/skills/:skillName', {
|
|
389
|
+
schema: { params: skillNameParamSchema },
|
|
390
|
+
}, async (request, reply) => {
|
|
391
|
+
const { skillName } = request.params;
|
|
392
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
393
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
394
|
+
if (!exists) {
|
|
395
|
+
return reply.status(404).send({ error: `Skill "${skillName}" not found` });
|
|
396
|
+
}
|
|
397
|
+
let skillMd = '';
|
|
398
|
+
let metadata = {};
|
|
399
|
+
try {
|
|
400
|
+
skillMd = await fs.readFile(path.join(skillDir, 'SKILL.md'), 'utf-8');
|
|
401
|
+
metadata = parseFrontmatter(skillMd).metadata;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// SKILL.md doesn't exist
|
|
405
|
+
}
|
|
406
|
+
// Dynamically scan all files in subdirectories
|
|
407
|
+
const allFiles = await scanDirFiles(skillDir, skillDir);
|
|
408
|
+
const files = allFiles.map((p) => ({
|
|
409
|
+
name: path.basename(p),
|
|
410
|
+
path: p,
|
|
411
|
+
}));
|
|
412
|
+
return reply.send({ data: { name: skillName, metadata, skillMd, files } });
|
|
413
|
+
});
|
|
414
|
+
// ── GET /api/v1/skills/:skillName/archive ──
|
|
415
|
+
fastify.get('/api/v1/skills/:skillName/archive', {
|
|
416
|
+
schema: { params: skillNameParamSchema },
|
|
417
|
+
}, async (request, reply) => {
|
|
418
|
+
const { skillName } = request.params;
|
|
419
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
420
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
421
|
+
if (!exists) {
|
|
422
|
+
return reply.status(404).send({ error: `Skill "${skillName}" not found` });
|
|
423
|
+
}
|
|
424
|
+
const zip = new AdmZip();
|
|
425
|
+
// Add SKILL.md
|
|
426
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
427
|
+
try {
|
|
428
|
+
const skillMd = await fs.readFile(skillMdPath, 'utf-8');
|
|
429
|
+
zip.addFile('SKILL.md', Buffer.from(skillMd, 'utf-8'));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// SKILL.md doesn't exist, skip
|
|
433
|
+
}
|
|
434
|
+
// Add all other files, preserving directory structure
|
|
435
|
+
const allFiles = await scanDirFiles(skillDir, skillDir);
|
|
436
|
+
for (const filePath of allFiles) {
|
|
437
|
+
if (filePath === 'SKILL.md')
|
|
438
|
+
continue;
|
|
439
|
+
const resolvedPath = safeResolve(skillDir, filePath);
|
|
440
|
+
if (!resolvedPath)
|
|
441
|
+
continue;
|
|
442
|
+
try {
|
|
443
|
+
const content = await fs.readFile(resolvedPath);
|
|
444
|
+
// Store under skillName/ prefix for proper extraction
|
|
445
|
+
zip.addFile(`${skillName}/${filePath}`, content);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// skip unreadable files
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Also put SKILL.md under the prefix
|
|
452
|
+
zip.deleteFile('SKILL.md');
|
|
453
|
+
try {
|
|
454
|
+
const skillMd = await fs.readFile(skillMdPath, 'utf-8');
|
|
455
|
+
zip.addFile(`${skillName}/SKILL.md`, Buffer.from(skillMd, 'utf-8'));
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// skip
|
|
459
|
+
}
|
|
460
|
+
const zipBuffer = zip.toBuffer();
|
|
461
|
+
reply.header('Content-Type', 'application/zip');
|
|
462
|
+
reply.header('Content-Disposition', `attachment; filename="${skillName}.zip"`);
|
|
463
|
+
return reply.send(zipBuffer);
|
|
464
|
+
});
|
|
465
|
+
// ── GET /api/v1/skills/:skillName/file/* ──
|
|
466
|
+
fastify.get('/api/v1/skills/:skillName/file/*', async (request, reply) => {
|
|
467
|
+
const { skillName } = request.params;
|
|
468
|
+
const filePath = request.params['*'];
|
|
469
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
470
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
471
|
+
if (!exists) {
|
|
472
|
+
return reply.status(404).send({ error: `Skill "${skillName}" not found` });
|
|
473
|
+
}
|
|
474
|
+
const resolvedPath = safeResolve(skillDir, filePath);
|
|
475
|
+
if (!resolvedPath) {
|
|
476
|
+
return reply.status(400).send({ error: 'Invalid file path' });
|
|
477
|
+
}
|
|
478
|
+
const fileExists = await fs.access(resolvedPath).then(() => true, () => false);
|
|
479
|
+
if (!fileExists) {
|
|
480
|
+
return reply.status(404).send({ error: `File "${filePath}" not found` });
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
484
|
+
return reply.send({ data: { content, path: filePath } });
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
return reply.status(500).send({ error: `Failed to read file: ${err}` });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// ── POST /api/v1/skills ──
|
|
491
|
+
fastify.post('/api/v1/skills', {
|
|
492
|
+
schema: { body: createSkillBodySchema },
|
|
493
|
+
}, async (request, reply) => {
|
|
494
|
+
const body = request.body;
|
|
495
|
+
const { name, skillMd, references, scripts: scriptFiles, files: archiveFiles } = body;
|
|
496
|
+
if (!isValidSkillName(name)) {
|
|
497
|
+
return reply.status(400).send({ error: 'Invalid skill name. Use alphanumeric, hyphens, and underscores only.' });
|
|
498
|
+
}
|
|
499
|
+
const skillDir = path.join(skillsDir, name);
|
|
500
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
501
|
+
if (exists) {
|
|
502
|
+
return reply.status(409).send({ error: `Skill "${name}" already exists` });
|
|
503
|
+
}
|
|
504
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
505
|
+
const contentToWrite = skillMd || `---
|
|
506
|
+
name: ${name}
|
|
507
|
+
description: ''
|
|
508
|
+
version: 1.0.0
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
# ${name}
|
|
512
|
+
|
|
513
|
+
## Overview
|
|
514
|
+
|
|
515
|
+
[Describe what this skill does]
|
|
516
|
+
|
|
517
|
+
## Usage Scenarios
|
|
518
|
+
|
|
519
|
+
- "User prompt" → [what the skill does]
|
|
520
|
+
`;
|
|
521
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), contentToWrite, 'utf-8');
|
|
522
|
+
if (references && references.length > 0) {
|
|
523
|
+
const refDir = path.join(skillDir, 'references');
|
|
524
|
+
await fs.mkdir(refDir, { recursive: true });
|
|
525
|
+
for (const ref of references) {
|
|
526
|
+
const safePath = safeResolve(refDir, ref.name);
|
|
527
|
+
if (safePath) {
|
|
528
|
+
await fs.mkdir(path.dirname(safePath), { recursive: true });
|
|
529
|
+
await fs.writeFile(safePath, ref.content, 'utf-8');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (scriptFiles && scriptFiles.length > 0) {
|
|
534
|
+
const scriptsDir = path.join(skillDir, 'scripts');
|
|
535
|
+
await fs.mkdir(scriptsDir, { recursive: true });
|
|
536
|
+
for (const script of scriptFiles) {
|
|
537
|
+
const safePath = safeResolve(scriptsDir, script.name);
|
|
538
|
+
if (safePath) {
|
|
539
|
+
await fs.mkdir(path.dirname(safePath), { recursive: true });
|
|
540
|
+
await fs.writeFile(safePath, script.content, 'utf-8');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (archiveFiles && archiveFiles.length > 0) {
|
|
545
|
+
for (const file of archiveFiles) {
|
|
546
|
+
const safePath = safeResolve(skillDir, file.path);
|
|
547
|
+
if (safePath) {
|
|
548
|
+
await fs.mkdir(path.dirname(safePath), { recursive: true });
|
|
549
|
+
await fs.writeFile(safePath, file.content, 'utf-8');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const metadata = parseFrontmatter(contentToWrite).metadata;
|
|
554
|
+
return reply.code(201).send({ data: { name, metadata } });
|
|
555
|
+
});
|
|
556
|
+
// ── DELETE /api/v1/skills/:skillName ──
|
|
557
|
+
fastify.delete('/api/v1/skills/:skillName', {
|
|
558
|
+
schema: { params: skillNameParamSchema },
|
|
559
|
+
}, async (request, reply) => {
|
|
560
|
+
const { skillName } = request.params;
|
|
561
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
562
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
563
|
+
if (!exists) {
|
|
564
|
+
return reply.status(404).send({ error: `Skill "${skillName}" not found` });
|
|
565
|
+
}
|
|
566
|
+
await fs.rm(skillDir, { recursive: true, force: true });
|
|
567
|
+
return reply.send({ success: true });
|
|
568
|
+
});
|
|
569
|
+
// ── POST /api/v1/skills/:skillName/file ──
|
|
570
|
+
fastify.post('/api/v1/skills/:skillName/file', {
|
|
571
|
+
schema: { params: skillNameParamSchema, body: uploadFileBodySchema },
|
|
572
|
+
}, async (request, reply) => {
|
|
573
|
+
const { skillName } = request.params;
|
|
574
|
+
const body = request.body;
|
|
575
|
+
const { filename, content } = body;
|
|
576
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
577
|
+
const exists = await fs.access(skillDir).then(() => true, () => false);
|
|
578
|
+
if (!exists) {
|
|
579
|
+
return reply.status(404).send({ error: `Skill "${skillName}" not found` });
|
|
580
|
+
}
|
|
581
|
+
const resolvedPath = safeResolve(skillDir, filename);
|
|
582
|
+
if (!resolvedPath) {
|
|
583
|
+
return reply.status(400).send({ error: 'Invalid file path' });
|
|
584
|
+
}
|
|
585
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
586
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
587
|
+
return reply.send({ success: true, path: filename });
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
//# sourceMappingURL=skills.js.map
|