@nahisaho/shikigami 2.0.4 → 2.0.6
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 +42 -0
- package/README.md +132 -136
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/build-cowork-plugin.js +859 -0
- package/scripts/cli.js +6 -6
- package/scripts/init.js +4 -15
- package/scripts/setup-cowork.js +0 -367
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SHIKIGAMI Cowork Plugin Builder
|
|
4
|
+
*
|
|
5
|
+
* Builds a Microsoft 365 Copilot Cowork plugin package (.zip)
|
|
6
|
+
* from SHIKIGAMI's Agent Skills following the official "Build from scratch" guide.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx shikigami cowork [options]
|
|
9
|
+
* --out <dir> Output directory (default: ./dist)
|
|
10
|
+
* --check Dry-run validation only
|
|
11
|
+
* --help Show help
|
|
12
|
+
*
|
|
13
|
+
* Reference: https://learn.microsoft.com/microsoft-365/copilot/cowork/cowork-plugin-development
|
|
14
|
+
*
|
|
15
|
+
* Cross-platform: The same SKILL.md files work in GitHub Copilot CLI,
|
|
16
|
+
* VS Code Copilot, Claude Code, Gemini CLI, and Copilot Cowork.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execFileSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const MAX_COMPANIONS_PER_SKILL = 20;
|
|
26
|
+
const MAX_COMPANION_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
27
|
+
const MAX_COMPANION_TOTAL = 10 * 1024 * 1024; // 10 MB per skill
|
|
28
|
+
const MAX_SKILLS = 20;
|
|
29
|
+
const MAX_FOLDER_PATH = 256;
|
|
30
|
+
const RECOMMENDED_BODY_WORDS = 3000;
|
|
31
|
+
|
|
32
|
+
const PACKAGE_NAME = 'com.nahisaho.shikigami';
|
|
33
|
+
const APP_ID = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d';
|
|
34
|
+
const ACCENT_COLOR = '#6B46C1';
|
|
35
|
+
|
|
36
|
+
const WINDOWS_RESERVED = new Set([
|
|
37
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
38
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
39
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const SAFE_FILENAME_RE = /^[a-zA-Z0-9\-_. !]+$/;
|
|
43
|
+
|
|
44
|
+
// Cowork-specific frontmatter defaults per skill
|
|
45
|
+
const COWORK_DEFAULTS = {
|
|
46
|
+
'shikigami-planner': { category: 'Research', icon: 'Lightbulb' },
|
|
47
|
+
'shikigami-deep-research': { category: 'Research', icon: 'Search' },
|
|
48
|
+
'shikigami-consulting-framework': { category: 'Analysis', icon: 'Briefcase' },
|
|
49
|
+
'shikigami-writing': { category: 'Writing', icon: 'DocumentBulletList' },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── Minimal PNG generator (no dependencies) ───────────────────────────────
|
|
53
|
+
|
|
54
|
+
function createMinimalPNG(width, height, r, g, b) {
|
|
55
|
+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
56
|
+
|
|
57
|
+
function crc32(buf) {
|
|
58
|
+
let crc = 0xFFFFFFFF;
|
|
59
|
+
const table = new Int32Array(256);
|
|
60
|
+
for (let i = 0; i < 256; i++) {
|
|
61
|
+
let c = i;
|
|
62
|
+
for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
63
|
+
table[i] = c;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < buf.length; i++) {
|
|
66
|
+
crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
|
67
|
+
}
|
|
68
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeChunk(type, data) {
|
|
72
|
+
const len = Buffer.alloc(4);
|
|
73
|
+
len.writeUInt32BE(data.length);
|
|
74
|
+
const typeAndData = Buffer.concat([Buffer.from(type), data]);
|
|
75
|
+
const crcBuf = Buffer.alloc(4);
|
|
76
|
+
crcBuf.writeUInt32BE(crc32(typeAndData));
|
|
77
|
+
return Buffer.concat([len, typeAndData, crcBuf]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ihdr = Buffer.alloc(13);
|
|
81
|
+
ihdr.writeUInt32BE(width, 0);
|
|
82
|
+
ihdr.writeUInt32BE(height, 4);
|
|
83
|
+
ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
|
84
|
+
|
|
85
|
+
const rowSize = 1 + width * 3;
|
|
86
|
+
const rawData = Buffer.alloc(rowSize * height);
|
|
87
|
+
for (let y = 0; y < height; y++) {
|
|
88
|
+
rawData[y * rowSize] = 0;
|
|
89
|
+
for (let x = 0; x < width; x++) {
|
|
90
|
+
const offset = y * rowSize + 1 + x * 3;
|
|
91
|
+
rawData[offset] = r;
|
|
92
|
+
rawData[offset + 1] = g;
|
|
93
|
+
rawData[offset + 2] = b;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const zlib = require('zlib');
|
|
98
|
+
const compressed = zlib.deflateSync(rawData);
|
|
99
|
+
|
|
100
|
+
return Buffer.concat([
|
|
101
|
+
signature,
|
|
102
|
+
makeChunk('IHDR', ihdr),
|
|
103
|
+
makeChunk('IDAT', compressed),
|
|
104
|
+
makeChunk('IEND', Buffer.alloc(0)),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── YAML Frontmatter Parser ───────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function parseFrontmatter(content) {
|
|
111
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
112
|
+
if (!match) return null;
|
|
113
|
+
|
|
114
|
+
const yaml = match[1];
|
|
115
|
+
const result = {};
|
|
116
|
+
let currentKey = null;
|
|
117
|
+
let blockLines = [];
|
|
118
|
+
let inBlock = false;
|
|
119
|
+
let currentIndent = 0;
|
|
120
|
+
let mapKey = null;
|
|
121
|
+
let mapObj = null;
|
|
122
|
+
|
|
123
|
+
function flushBlock() {
|
|
124
|
+
if (inBlock && currentKey) {
|
|
125
|
+
result[currentKey] = blockLines.join('\n').trim();
|
|
126
|
+
inBlock = false;
|
|
127
|
+
blockLines = [];
|
|
128
|
+
}
|
|
129
|
+
if (mapKey && mapObj) {
|
|
130
|
+
result[mapKey] = mapObj;
|
|
131
|
+
mapKey = null;
|
|
132
|
+
mapObj = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const line of yaml.split('\n')) {
|
|
137
|
+
// Handle block scalar continuation
|
|
138
|
+
if (inBlock) {
|
|
139
|
+
if (line.match(/^\s{2,}/) || line.trim() === '') {
|
|
140
|
+
blockLines.push(line.replace(/^\s{2}/, ''));
|
|
141
|
+
continue;
|
|
142
|
+
} else {
|
|
143
|
+
flushBlock();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle map continuation (metadata:, cowork: style)
|
|
148
|
+
if (mapKey && line.match(/^\s{2,}\w/)) {
|
|
149
|
+
const kvMatch = line.trim().match(/^(\w[\w.-]*)\s*:\s*(.*)/);
|
|
150
|
+
if (kvMatch) {
|
|
151
|
+
mapObj[kvMatch[1]] = kvMatch[2].trim().replace(/^["']|["']$/g, '');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
} else if (mapKey) {
|
|
155
|
+
flushBlock();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Top-level key: value
|
|
159
|
+
const kvMatch = line.match(/^(\w[\w.-]*)\s*:\s*(.*)/);
|
|
160
|
+
if (kvMatch) {
|
|
161
|
+
currentKey = kvMatch[1];
|
|
162
|
+
const value = kvMatch[2].trim();
|
|
163
|
+
if (value === '|' || value === '>') {
|
|
164
|
+
inBlock = true;
|
|
165
|
+
blockLines = [];
|
|
166
|
+
} else if (value === '') {
|
|
167
|
+
// Could be a map start (metadata:, cowork:)
|
|
168
|
+
mapKey = currentKey;
|
|
169
|
+
mapObj = {};
|
|
170
|
+
} else {
|
|
171
|
+
result[currentKey] = value.replace(/^["']|["']$/g, '');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
flushBlock();
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractBody(content) {
|
|
181
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
|
|
182
|
+
return match ? match[1].trim() : content;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Companion File Path Validation ────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function validateCompanionPath(relPath) {
|
|
188
|
+
const errors = [];
|
|
189
|
+
const parts = relPath.split('/');
|
|
190
|
+
|
|
191
|
+
// No absolute paths
|
|
192
|
+
if (path.isAbsolute(relPath)) {
|
|
193
|
+
errors.push(`Absolute path not allowed: ${relPath}`);
|
|
194
|
+
}
|
|
195
|
+
// No path traversal
|
|
196
|
+
if (parts.includes('..')) {
|
|
197
|
+
errors.push(`Path traversal (..) not allowed: ${relPath}`);
|
|
198
|
+
}
|
|
199
|
+
// No backslashes or null bytes
|
|
200
|
+
if (relPath.includes('\\') || relPath.includes('\0')) {
|
|
201
|
+
errors.push(`Backslash or null byte in path: ${relPath}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const part of parts) {
|
|
205
|
+
// No hidden files
|
|
206
|
+
if (part.startsWith('.')) {
|
|
207
|
+
errors.push(`Hidden file not allowed: ${relPath}`);
|
|
208
|
+
}
|
|
209
|
+
// No Windows reserved names
|
|
210
|
+
const baseName = part.replace(/\.[^.]*$/, '').toUpperCase();
|
|
211
|
+
if (WINDOWS_RESERVED.has(baseName)) {
|
|
212
|
+
errors.push(`Windows reserved name: ${part} in ${relPath}`);
|
|
213
|
+
}
|
|
214
|
+
// Safe characters only
|
|
215
|
+
if (!SAFE_FILENAME_RE.test(part)) {
|
|
216
|
+
errors.push(`Unsafe characters in filename: ${part} in ${relPath}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return errors;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Companion File Consolidation ──────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
function discoverCompanions(skillDir) {
|
|
226
|
+
const allFiles = [];
|
|
227
|
+
|
|
228
|
+
function walkDir(dir, prefix) {
|
|
229
|
+
if (!fs.existsSync(dir)) return;
|
|
230
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (entry.name.startsWith('.')) continue; // skip hidden
|
|
233
|
+
const fullPath = path.join(dir, entry.name);
|
|
234
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
235
|
+
if (entry.isDirectory()) {
|
|
236
|
+
walkDir(fullPath, relPath);
|
|
237
|
+
} else if (entry.name !== 'SKILL.md') {
|
|
238
|
+
allFiles.push({ fullPath, relPath, name: entry.name });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
walkDir(skillDir, '');
|
|
244
|
+
return allFiles;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function consolidateCompanions(allFiles, skillName) {
|
|
248
|
+
if (allFiles.length <= MAX_COMPANIONS_PER_SKILL) {
|
|
249
|
+
return allFiles.map(f => ({
|
|
250
|
+
relativePath: f.relPath,
|
|
251
|
+
content: fs.readFileSync(f.fullPath),
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const files = [];
|
|
256
|
+
|
|
257
|
+
// Group by top-level directory
|
|
258
|
+
const topGroups = {};
|
|
259
|
+
for (const f of allFiles) {
|
|
260
|
+
const topDir = f.relPath.split('/')[0] || '_root';
|
|
261
|
+
if (!topGroups[topDir]) topGroups[topDir] = [];
|
|
262
|
+
topGroups[topDir].push(f);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const [dir, dirFiles] of Object.entries(topGroups)) {
|
|
266
|
+
if (dir === '_root') {
|
|
267
|
+
for (const f of dirFiles) {
|
|
268
|
+
files.push({
|
|
269
|
+
relativePath: f.relPath,
|
|
270
|
+
content: fs.readFileSync(f.fullPath),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check if sub-directory grouping keeps within limits
|
|
277
|
+
const subGroups = {};
|
|
278
|
+
for (const f of dirFiles) {
|
|
279
|
+
const parts = f.relPath.split('/');
|
|
280
|
+
const subKey = parts.length > 1 ? parts[1] : '_direct';
|
|
281
|
+
if (!subGroups[subKey]) subGroups[subKey] = [];
|
|
282
|
+
subGroups[subKey].push(f);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// If sub-grouping fits, use it; otherwise consolidate entire directory
|
|
286
|
+
if (Object.keys(subGroups).length <= MAX_COMPANIONS_PER_SKILL - files.length) {
|
|
287
|
+
for (const [sub, subFiles] of Object.entries(subGroups)) {
|
|
288
|
+
let consolidated = `# ${dir}/${sub}\n\nConsolidated reference for ${skillName}.\n\n`;
|
|
289
|
+
for (const f of subFiles) {
|
|
290
|
+
const content = fs.readFileSync(f.fullPath, 'utf-8');
|
|
291
|
+
const basename = path.basename(f.name, path.extname(f.name));
|
|
292
|
+
consolidated += `---\n\n## ${basename}\n\n${content}\n\n`;
|
|
293
|
+
}
|
|
294
|
+
files.push({
|
|
295
|
+
relativePath: `references/${dir}-${sub}.md`,
|
|
296
|
+
content: Buffer.from(consolidated, 'utf-8'),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// Consolidate entire top-level directory into one file
|
|
301
|
+
let consolidated = `# ${dir}\n\nConsolidated reference for ${skillName}.\n\n`;
|
|
302
|
+
for (const f of dirFiles) {
|
|
303
|
+
const content = fs.readFileSync(f.fullPath, 'utf-8');
|
|
304
|
+
const basename = path.basename(f.name, path.extname(f.name));
|
|
305
|
+
consolidated += `---\n\n## ${basename}\n\n${content}\n\n`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const consolidatedBuf = Buffer.from(consolidated, 'utf-8');
|
|
309
|
+
|
|
310
|
+
// Chunk if exceeds 5MB
|
|
311
|
+
if (consolidatedBuf.length > MAX_COMPANION_SIZE) {
|
|
312
|
+
const lines = consolidated.split('\n');
|
|
313
|
+
let chunk = [];
|
|
314
|
+
let chunkSize = 0;
|
|
315
|
+
let chunkIdx = 1;
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
const lineSize = Buffer.byteLength(line + '\n', 'utf-8');
|
|
318
|
+
if (chunkSize + lineSize > MAX_COMPANION_SIZE * 0.9 && chunk.length > 0) {
|
|
319
|
+
files.push({
|
|
320
|
+
relativePath: `references/${dir}-${String(chunkIdx).padStart(2, '0')}.md`,
|
|
321
|
+
content: Buffer.from(chunk.join('\n'), 'utf-8'),
|
|
322
|
+
});
|
|
323
|
+
chunk = [];
|
|
324
|
+
chunkSize = 0;
|
|
325
|
+
chunkIdx++;
|
|
326
|
+
}
|
|
327
|
+
chunk.push(line);
|
|
328
|
+
chunkSize += lineSize;
|
|
329
|
+
}
|
|
330
|
+
if (chunk.length > 0) {
|
|
331
|
+
files.push({
|
|
332
|
+
relativePath: `references/${dir}-${String(chunkIdx).padStart(2, '0')}.md`,
|
|
333
|
+
content: Buffer.from(chunk.join('\n'), 'utf-8'),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
files.push({
|
|
338
|
+
relativePath: `references/${dir}.md`,
|
|
339
|
+
content: consolidatedBuf,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return files;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── SKILL.md Rewriter for Cowork ──────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function rewriteSkillMdForCowork(content, originalCompanions, packagedCompanions, skillName) {
|
|
351
|
+
/**
|
|
352
|
+
* Creates a Cowork-compatible version of SKILL.md:
|
|
353
|
+
* 1. Adds cowork.category and cowork.icon to frontmatter if missing
|
|
354
|
+
* 2. Adds metadata.author and metadata.version if missing
|
|
355
|
+
* 3. Updates file path references to match consolidated paths
|
|
356
|
+
* 4. Adds "Additional Resources" section listing companion files
|
|
357
|
+
*/
|
|
358
|
+
let result = content;
|
|
359
|
+
|
|
360
|
+
// Update frontmatter with Cowork fields
|
|
361
|
+
const fmMatch = result.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
362
|
+
if (fmMatch) {
|
|
363
|
+
let fm = fmMatch[2];
|
|
364
|
+
const defaults = COWORK_DEFAULTS[skillName] || {};
|
|
365
|
+
|
|
366
|
+
if (!fm.includes('cowork.category') && defaults.category) {
|
|
367
|
+
fm += `\ncowork.category: ${defaults.category}`;
|
|
368
|
+
}
|
|
369
|
+
if (!fm.includes('cowork.icon') && defaults.icon) {
|
|
370
|
+
fm += `\ncowork.icon: ${defaults.icon}`;
|
|
371
|
+
}
|
|
372
|
+
if (!fm.includes('metadata:')) {
|
|
373
|
+
fm += `\nmetadata:\n author: nahisaho\n version: "2.0"`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
result = `${fmMatch[1]}${fm}${fmMatch[3]}${result.slice(fmMatch[0].length)}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Build path mapping: original -> consolidated
|
|
380
|
+
if (originalCompanions.length > MAX_COMPANIONS_PER_SKILL) {
|
|
381
|
+
const pathMap = {};
|
|
382
|
+
for (const orig of originalCompanions) {
|
|
383
|
+
const topDir = orig.relPath.split('/')[0];
|
|
384
|
+
const consolidated = packagedCompanions.find(
|
|
385
|
+
p => p.relativePath.startsWith(`references/${topDir}`)
|
|
386
|
+
);
|
|
387
|
+
if (consolidated) {
|
|
388
|
+
pathMap[orig.relPath] = consolidated.relativePath;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Replace file path references in body
|
|
393
|
+
for (const [origPath, newPath] of Object.entries(pathMap)) {
|
|
394
|
+
result = result.replace(new RegExp(escapeRegex(origPath), 'g'), newPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Add Additional Resources section if companions exist and not already present
|
|
399
|
+
if (packagedCompanions.length > 0 && !result.includes('## Additional Resources')) {
|
|
400
|
+
let resources = '\n\n## Additional Resources\n\n';
|
|
401
|
+
for (const c of packagedCompanions) {
|
|
402
|
+
const name = path.basename(c.relativePath, path.extname(c.relativePath));
|
|
403
|
+
resources += `- **\`${c.relativePath}\`** — ${name}\n`;
|
|
404
|
+
}
|
|
405
|
+
result += resources;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function escapeRegex(str) {
|
|
412
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── ZIP Builder ───────────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
function createZip(outputPath, files) {
|
|
418
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'shikigami-cowork-'));
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
for (const file of files) {
|
|
422
|
+
const filePath = path.join(tmpDir, file.path);
|
|
423
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
424
|
+
if (Buffer.isBuffer(file.content)) {
|
|
425
|
+
fs.writeFileSync(filePath, file.content);
|
|
426
|
+
} else {
|
|
427
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const absOutput = path.resolve(outputPath);
|
|
432
|
+
try {
|
|
433
|
+
execFileSync('zip', ['-r', absOutput, '.'], { cwd: tmpDir, stdio: 'pipe' });
|
|
434
|
+
} catch {
|
|
435
|
+
// Fallback: PowerShell (Windows)
|
|
436
|
+
try {
|
|
437
|
+
execFileSync('powershell', [
|
|
438
|
+
'-Command',
|
|
439
|
+
`Compress-Archive -Path '${tmpDir}${path.sep}*' -DestinationPath '${absOutput}' -Force`
|
|
440
|
+
], { stdio: 'pipe' });
|
|
441
|
+
} catch {
|
|
442
|
+
console.error('❌ zip command not found. Install zip or use PowerShell on Windows.');
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} finally {
|
|
447
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── Manifest Generator ─────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
function generateManifest(skills, version) {
|
|
454
|
+
return {
|
|
455
|
+
'$schema': 'https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json',
|
|
456
|
+
manifestVersion: 'devPreview',
|
|
457
|
+
version: version,
|
|
458
|
+
id: APP_ID,
|
|
459
|
+
packageName: PACKAGE_NAME,
|
|
460
|
+
developer: {
|
|
461
|
+
name: 'nahisaho',
|
|
462
|
+
websiteUrl: 'https://github.com/nahisaho/SHIKIGAMI',
|
|
463
|
+
privacyUrl: 'https://github.com/nahisaho/SHIKIGAMI/blob/main/LICENSE',
|
|
464
|
+
termsOfUseUrl: 'https://github.com/nahisaho/SHIKIGAMI/blob/main/LICENSE',
|
|
465
|
+
},
|
|
466
|
+
name: {
|
|
467
|
+
short: 'SHIKIGAMI',
|
|
468
|
+
full: 'SHIKIGAMI - Deep Research & Consulting Agent Skills',
|
|
469
|
+
},
|
|
470
|
+
description: {
|
|
471
|
+
short: 'AI-powered deep research and consulting framework analysis',
|
|
472
|
+
full: 'SHIKIGAMI provides 4 specialized Agent Skills: '
|
|
473
|
+
+ 'deep research with hallucination prevention, 50+ consulting frameworks '
|
|
474
|
+
+ '(SWOT, PEST, 5Forces, BCG Matrix, etc.), structured report writing, '
|
|
475
|
+
+ 'and project planning with 5 Whys/JTBD purpose discovery. '
|
|
476
|
+
+ 'Cross-platform compatible with GitHub Copilot CLI, VS Code, Claude Code, and Gemini CLI.',
|
|
477
|
+
},
|
|
478
|
+
icons: {
|
|
479
|
+
color: 'color.png',
|
|
480
|
+
outline: 'outline.png',
|
|
481
|
+
},
|
|
482
|
+
accentColor: ACCENT_COLOR,
|
|
483
|
+
agentSkills: skills.map(s => ({ folder: `./skills/${s.name}` })),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Validation (ASKILL codes from official docs) ──────────────────────────
|
|
488
|
+
|
|
489
|
+
function validateManifest(manifest) {
|
|
490
|
+
const errors = [];
|
|
491
|
+
|
|
492
|
+
// ASKILL-M001: folder required in each agentSkills entry
|
|
493
|
+
for (const skill of manifest.agentSkills || []) {
|
|
494
|
+
if (!skill.folder) {
|
|
495
|
+
errors.push('[ASKILL-M001] `folder` is required for each agentSkills entry');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ASKILL-M002: max 20 skills
|
|
500
|
+
if ((manifest.agentSkills || []).length > MAX_SKILLS) {
|
|
501
|
+
errors.push(`[ASKILL-M002] Max ${MAX_SKILLS} agentSkills entries (found ${manifest.agentSkills.length})`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ASKILL-M003: folder path max 256 chars
|
|
505
|
+
for (const skill of manifest.agentSkills || []) {
|
|
506
|
+
if (skill.folder && skill.folder.length > MAX_FOLDER_PATH) {
|
|
507
|
+
errors.push(`[ASKILL-M003] Folder path exceeds ${MAX_FOLDER_PATH} chars: ${skill.folder}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ASKILL-P008: no duplicate folder values
|
|
512
|
+
const folders = (manifest.agentSkills || []).map(s => s.folder);
|
|
513
|
+
const dupes = folders.filter((f, i) => folders.indexOf(f) !== i);
|
|
514
|
+
if (dupes.length > 0) {
|
|
515
|
+
errors.push(`[ASKILL-P008] Duplicate folder values: ${[...new Set(dupes)].join(', ')}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// GUID format
|
|
519
|
+
if (!manifest.id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(manifest.id)) {
|
|
520
|
+
errors.push('[Manifest] Invalid GUID format for id');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return errors;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function validateSkill(skillDir, skillName) {
|
|
527
|
+
const errors = [];
|
|
528
|
+
const warnings = [];
|
|
529
|
+
|
|
530
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
531
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
532
|
+
// ASKILL-P002
|
|
533
|
+
errors.push(`[ASKILL-P002] SKILL.md not found in ${skillName}`);
|
|
534
|
+
return { errors, warnings };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
538
|
+
const frontmatter = parseFrontmatter(content);
|
|
539
|
+
|
|
540
|
+
// ASKILL-P003: valid YAML frontmatter
|
|
541
|
+
if (!frontmatter) {
|
|
542
|
+
errors.push(`[ASKILL-P003] No valid YAML frontmatter in ${skillName}/SKILL.md`);
|
|
543
|
+
return { errors, warnings };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ASKILL-P004: name field required
|
|
547
|
+
if (!frontmatter.name) {
|
|
548
|
+
errors.push(`[ASKILL-P004] Missing 'name' field in ${skillName}/SKILL.md frontmatter`);
|
|
549
|
+
} else {
|
|
550
|
+
// ASKILL-P006: name matches folder name
|
|
551
|
+
if (frontmatter.name !== skillName) {
|
|
552
|
+
errors.push(`[ASKILL-P006] Frontmatter name '${frontmatter.name}' doesn't match folder '${skillName}'`);
|
|
553
|
+
}
|
|
554
|
+
// ASKILL-P007: name is kebab-case, 1-64 chars
|
|
555
|
+
if (frontmatter.name.length > 64) {
|
|
556
|
+
errors.push(`[ASKILL-P007] Name exceeds 64 characters: ${frontmatter.name}`);
|
|
557
|
+
}
|
|
558
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(frontmatter.name)) {
|
|
559
|
+
errors.push(`[ASKILL-P007] Name must be kebab-case: ${frontmatter.name}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ASKILL-P005: description field required, 1-1024 chars
|
|
564
|
+
if (!frontmatter.description) {
|
|
565
|
+
errors.push(`[ASKILL-P005] Missing 'description' field in ${skillName}/SKILL.md frontmatter`);
|
|
566
|
+
} else if (frontmatter.description.length > 1024) {
|
|
567
|
+
errors.push(`[ASKILL-P005] Description exceeds 1024 characters (${frontmatter.description.length}) in ${skillName}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Body size warning
|
|
571
|
+
const body = extractBody(content);
|
|
572
|
+
const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
|
|
573
|
+
if (wordCount > RECOMMENDED_BODY_WORDS) {
|
|
574
|
+
warnings.push(`${skillName}: SKILL.md body is ~${wordCount} words (recommended <${RECOMMENDED_BODY_WORDS}). Consider moving details to references/`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Discover and validate companion files
|
|
578
|
+
const originalCompanions = discoverCompanions(skillDir);
|
|
579
|
+
|
|
580
|
+
// Validate companion file paths
|
|
581
|
+
for (const f of originalCompanions) {
|
|
582
|
+
const pathErrors = validateCompanionPath(f.relPath);
|
|
583
|
+
for (const e of pathErrors) {
|
|
584
|
+
errors.push(`[Companion] ${skillName}: ${e}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Consolidate companions for packaging
|
|
589
|
+
const packagedCompanions = consolidateCompanions(originalCompanions, skillName);
|
|
590
|
+
|
|
591
|
+
// Validate consolidated results
|
|
592
|
+
if (packagedCompanions.length > MAX_COMPANIONS_PER_SKILL) {
|
|
593
|
+
errors.push(`[Companion] ${skillName}: ${packagedCompanions.length} files after consolidation (max ${MAX_COMPANIONS_PER_SKILL})`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const totalSize = packagedCompanions.reduce((sum, f) => sum + f.content.length, 0);
|
|
597
|
+
if (totalSize > MAX_COMPANION_TOTAL) {
|
|
598
|
+
errors.push(`[Companion] ${skillName}: Total size ${(totalSize / 1024 / 1024).toFixed(1)}MB exceeds 10MB limit`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
for (const f of packagedCompanions) {
|
|
602
|
+
if (f.content.length > MAX_COMPANION_SIZE) {
|
|
603
|
+
errors.push(`[Companion] ${skillName}: ${f.relativePath} exceeds 5MB limit`);
|
|
604
|
+
}
|
|
605
|
+
// Validate packaged paths too
|
|
606
|
+
const pathErrors = validateCompanionPath(f.relativePath);
|
|
607
|
+
for (const e of pathErrors) {
|
|
608
|
+
errors.push(`[Companion] ${skillName}: ${e}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { errors, warnings, originalCompanions, packagedCompanions, frontmatter, content };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
function parseArgs() {
|
|
618
|
+
const args = process.argv.slice(2);
|
|
619
|
+
return {
|
|
620
|
+
out: args.includes('--out') ? args[args.indexOf('--out') + 1] : './dist',
|
|
621
|
+
check: args.includes('--check') || args.includes('--dry-run'),
|
|
622
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function showHelp() {
|
|
627
|
+
console.log(`
|
|
628
|
+
🎭 SHIKIGAMI Cowork Plugin Builder
|
|
629
|
+
|
|
630
|
+
Builds a Microsoft 365 Copilot Cowork plugin package (.zip)
|
|
631
|
+
following the official "Build a plugin from scratch" guide.
|
|
632
|
+
|
|
633
|
+
Usage: npx shikigami cowork [options]
|
|
634
|
+
|
|
635
|
+
Options:
|
|
636
|
+
--out <dir> Output directory (default: ./dist)
|
|
637
|
+
--check Validation only (dry-run)
|
|
638
|
+
-h, --help Show this help
|
|
639
|
+
|
|
640
|
+
Steps (per official docs):
|
|
641
|
+
1. Discover & validate SKILL.md files (frontmatter, naming, paths)
|
|
642
|
+
2. Consolidate companion files (max 20 per skill)
|
|
643
|
+
3. Rewrite SKILL.md for Cowork (add cowork.category, cowork.icon, metadata)
|
|
644
|
+
4. Generate manifest.json (M365 Unified App Manifest devPreview)
|
|
645
|
+
5. Generate icons (color.png 192×192, outline.png 32×32)
|
|
646
|
+
6. Create .zip package
|
|
647
|
+
|
|
648
|
+
Cross-platform:
|
|
649
|
+
Same SKILL.md files work in GitHub Copilot CLI, VS Code Copilot,
|
|
650
|
+
Claude Code, Gemini CLI, Cursor, JetBrains Junie, and Copilot Cowork.
|
|
651
|
+
|
|
652
|
+
Reference:
|
|
653
|
+
https://learn.microsoft.com/microsoft-365/copilot/cowork/cowork-plugin-development
|
|
654
|
+
`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function build() {
|
|
658
|
+
const { out, check, help } = parseArgs();
|
|
659
|
+
|
|
660
|
+
if (help) {
|
|
661
|
+
showHelp();
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
console.log('');
|
|
666
|
+
console.log('🎭 SHIKIGAMI Cowork Plugin Builder');
|
|
667
|
+
console.log('====================================');
|
|
668
|
+
console.log('');
|
|
669
|
+
|
|
670
|
+
// ── Step 0: Locate skills directory ──
|
|
671
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
672
|
+
let skillsRoot = path.join(packageDir, 'github-assets', 'skills');
|
|
673
|
+
|
|
674
|
+
if (!fs.existsSync(skillsRoot)) {
|
|
675
|
+
skillsRoot = path.join(packageDir, '.github', 'skills');
|
|
676
|
+
}
|
|
677
|
+
if (!fs.existsSync(skillsRoot)) {
|
|
678
|
+
skillsRoot = path.join(process.cwd(), '.github', 'skills');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!fs.existsSync(skillsRoot)) {
|
|
682
|
+
console.error('❌ Skills directory not found.');
|
|
683
|
+
console.error(' Run `npx shikigami init` first to copy skills to your project.');
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let version = '1.0.0';
|
|
688
|
+
try {
|
|
689
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'));
|
|
690
|
+
version = pkg.version || version;
|
|
691
|
+
} catch { /* ignore */ }
|
|
692
|
+
|
|
693
|
+
// ── Step 1: Discover & validate skills ──
|
|
694
|
+
console.log('Step 1: Discovering and validating skills...');
|
|
695
|
+
console.log(`📂 Skills root: ${skillsRoot}`);
|
|
696
|
+
console.log('');
|
|
697
|
+
|
|
698
|
+
const skillDirs = fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
699
|
+
.filter(d => d.isDirectory() && d.name.startsWith('shikigami-'))
|
|
700
|
+
.map(d => d.name);
|
|
701
|
+
|
|
702
|
+
if (skillDirs.length === 0) {
|
|
703
|
+
console.error('❌ No skills found (expected shikigami-* directories)');
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let hasErrors = false;
|
|
708
|
+
const validatedSkills = [];
|
|
709
|
+
|
|
710
|
+
for (const skillName of skillDirs) {
|
|
711
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
712
|
+
const result = validateSkill(skillDir, skillName);
|
|
713
|
+
|
|
714
|
+
if (result.errors.length > 0) {
|
|
715
|
+
hasErrors = true;
|
|
716
|
+
for (const err of result.errors) console.log(` ❌ ${err}`);
|
|
717
|
+
}
|
|
718
|
+
for (const warn of result.warnings) console.log(` ⚠️ ${warn}`);
|
|
719
|
+
|
|
720
|
+
if (result.errors.length === 0) {
|
|
721
|
+
const origCount = result.originalCompanions ? result.originalCompanions.length : 0;
|
|
722
|
+
const pkgCount = result.packagedCompanions ? result.packagedCompanions.length : 0;
|
|
723
|
+
const consolidatedNote = origCount > MAX_COMPANIONS_PER_SKILL
|
|
724
|
+
? ` (consolidated ${origCount} → ${pkgCount})`
|
|
725
|
+
: '';
|
|
726
|
+
console.log(` ✅ ${skillName} (${pkgCount} companion files${consolidatedNote})`);
|
|
727
|
+
validatedSkills.push({
|
|
728
|
+
name: skillName,
|
|
729
|
+
dir: skillDir,
|
|
730
|
+
originalCompanions: result.originalCompanions,
|
|
731
|
+
packagedCompanions: result.packagedCompanions,
|
|
732
|
+
frontmatter: result.frontmatter,
|
|
733
|
+
content: result.content,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
console.log('');
|
|
739
|
+
|
|
740
|
+
if (hasErrors) {
|
|
741
|
+
console.error('❌ Validation failed. Fix errors above before building.');
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (validatedSkills.length > MAX_SKILLS) {
|
|
746
|
+
console.error(`❌ [ASKILL-M002] Too many skills: ${validatedSkills.length} (max ${MAX_SKILLS})`);
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── Manifest validation ──
|
|
751
|
+
const manifest = generateManifest(validatedSkills, version);
|
|
752
|
+
const manifestErrors = validateManifest(manifest);
|
|
753
|
+
if (manifestErrors.length > 0) {
|
|
754
|
+
hasErrors = true;
|
|
755
|
+
for (const err of manifestErrors) console.log(` ❌ ${err}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (hasErrors) {
|
|
759
|
+
console.error('❌ Manifest validation failed.');
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (check) {
|
|
764
|
+
console.log('✅ Validation passed (dry-run mode)');
|
|
765
|
+
console.log(` ${validatedSkills.length} skills ready for packaging`);
|
|
766
|
+
console.log('');
|
|
767
|
+
console.log('Cross-platform compatibility:');
|
|
768
|
+
console.log(' ✅ GitHub Copilot CLI');
|
|
769
|
+
console.log(' ✅ VS Code Copilot');
|
|
770
|
+
console.log(' ✅ Microsoft 365 Copilot Cowork');
|
|
771
|
+
console.log(' ✅ Claude Code / Gemini CLI / Cursor');
|
|
772
|
+
process.exit(0);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ── Step 2-5: Build package ──
|
|
776
|
+
console.log('Step 2: Building Cowork plugin package...');
|
|
777
|
+
|
|
778
|
+
const zipFiles = [];
|
|
779
|
+
|
|
780
|
+
// Step 4: manifest.json
|
|
781
|
+
zipFiles.push({
|
|
782
|
+
path: 'manifest.json',
|
|
783
|
+
content: JSON.stringify(manifest, null, 2),
|
|
784
|
+
});
|
|
785
|
+
console.log(' ✅ manifest.json');
|
|
786
|
+
|
|
787
|
+
// Step 5: Icons
|
|
788
|
+
const colorPng = createMinimalPNG(192, 192, 107, 70, 193);
|
|
789
|
+
const outlinePng = createMinimalPNG(32, 32, 107, 70, 193);
|
|
790
|
+
zipFiles.push({ path: 'color.png', content: colorPng });
|
|
791
|
+
zipFiles.push({ path: 'outline.png', content: outlinePng });
|
|
792
|
+
console.log(' ✅ color.png (192×192)');
|
|
793
|
+
console.log(' ✅ outline.png (32×32)');
|
|
794
|
+
|
|
795
|
+
// Steps 1-2: Skills with Cowork-specific SKILL.md
|
|
796
|
+
for (const skill of validatedSkills) {
|
|
797
|
+
// Rewrite SKILL.md for Cowork (add metadata, category, icon, update paths)
|
|
798
|
+
const rewrittenSkillMd = rewriteSkillMdForCowork(
|
|
799
|
+
skill.content,
|
|
800
|
+
skill.originalCompanions,
|
|
801
|
+
skill.packagedCompanions,
|
|
802
|
+
skill.name
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
zipFiles.push({
|
|
806
|
+
path: `skills/${skill.name}/SKILL.md`,
|
|
807
|
+
content: rewrittenSkillMd,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Copy companion files (already consolidated)
|
|
811
|
+
for (const companion of skill.packagedCompanions) {
|
|
812
|
+
zipFiles.push({
|
|
813
|
+
path: `skills/${skill.name}/${companion.relativePath}`,
|
|
814
|
+
content: companion.content,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
console.log(` ✅ skills/${skill.name}/ (${skill.packagedCompanions.length + 1} files)`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ── Step 6: Create ZIP ──
|
|
822
|
+
console.log('');
|
|
823
|
+
console.log('Step 3: Creating ZIP package...');
|
|
824
|
+
|
|
825
|
+
const outDir = path.resolve(out);
|
|
826
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
827
|
+
const zipPath = path.join(outDir, 'shikigami-cowork-plugin.zip');
|
|
828
|
+
|
|
829
|
+
if (fs.existsSync(zipPath)) {
|
|
830
|
+
fs.unlinkSync(zipPath);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
createZip(zipPath, zipFiles);
|
|
834
|
+
|
|
835
|
+
const zipSize = fs.statSync(zipPath).size;
|
|
836
|
+
console.log('');
|
|
837
|
+
console.log('✅ Plugin package created!');
|
|
838
|
+
console.log('');
|
|
839
|
+
console.log(` 📦 ${zipPath}`);
|
|
840
|
+
console.log(` 📏 Size: ${(zipSize / 1024).toFixed(1)} KB`);
|
|
841
|
+
console.log(` 🔧 Skills: ${validatedSkills.length}`);
|
|
842
|
+
console.log(` 📋 Version: ${version}`);
|
|
843
|
+
console.log('');
|
|
844
|
+
console.log('Cross-platform compatibility:');
|
|
845
|
+
console.log(' ✅ GitHub Copilot CLI — use `npx shikigami init`');
|
|
846
|
+
console.log(' ✅ VS Code Copilot — same SKILL.md format');
|
|
847
|
+
console.log(' ✅ Copilot Cowork — upload .zip to M365 Admin Center');
|
|
848
|
+
console.log(' ✅ Claude Code — same SKILL.md format');
|
|
849
|
+
console.log('');
|
|
850
|
+
console.log('📝 Next steps (Copilot Cowork):');
|
|
851
|
+
console.log(' 1. Open M365 Admin Center > Manage Apps > Upload Custom App');
|
|
852
|
+
console.log(' 2. Upload shikigami-cowork-plugin.zip');
|
|
853
|
+
console.log(' 3. Open Cowork > Sources & Skills to verify');
|
|
854
|
+
console.log('');
|
|
855
|
+
console.log('📖 https://learn.microsoft.com/microsoft-365/copilot/cowork/cowork-plugin-development');
|
|
856
|
+
console.log('');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
build();
|