@codihaus/claude-skills 1.0.0 → 1.2.0

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.
@@ -20,7 +20,9 @@ import {
20
20
  import {
21
21
  getAvailableSkills,
22
22
  getInstalledSkills,
23
- copySkillsToProject
23
+ copySkillsToProject,
24
+ copyKnowledgeToProject,
25
+ copyScriptsToProject
24
26
  } from '../utils/skills.js';
25
27
  import {
26
28
  setupSettings,
@@ -28,6 +30,11 @@ import {
28
30
  setupClaudeMd,
29
31
  updateGitignore
30
32
  } from '../utils/config.js';
33
+ import {
34
+ runProjectSetup,
35
+ detectEnvExample,
36
+ hasEnvFile
37
+ } from '../utils/project-setup.js';
31
38
 
32
39
  export async function init(options) {
33
40
  const projectPath = process.cwd();
@@ -158,7 +165,41 @@ export async function init(options) {
158
165
  process.exit(1);
159
166
  }
160
167
 
161
- // Step 5: Set up settings
168
+ // Step 5: Copy knowledge (stacks, domains)
169
+ const knowledgeSpinner = ora('Installing knowledge base...').start();
170
+
171
+ try {
172
+ const { copied: knowledgeCopied, errors: knowledgeErrors } = await copyKnowledgeToProject(projectPath);
173
+
174
+ if (knowledgeErrors.length > 0) {
175
+ knowledgeSpinner.warn(`Installed ${knowledgeCopied.length} knowledge folders with ${knowledgeErrors.length} errors`);
176
+ } else if (knowledgeCopied.length > 0) {
177
+ knowledgeSpinner.succeed(`Installed knowledge: ${knowledgeCopied.join(', ')}`);
178
+ } else {
179
+ knowledgeSpinner.info('No knowledge to install');
180
+ }
181
+ } catch (e) {
182
+ knowledgeSpinner.warn('Failed to install knowledge');
183
+ }
184
+
185
+ // Step 6: Copy scripts (graph.py, etc.)
186
+ const scriptsSpinner = ora('Installing scripts...').start();
187
+
188
+ try {
189
+ const { copied: scriptsCopied, errors: scriptsErrors } = await copyScriptsToProject(projectPath);
190
+
191
+ if (scriptsErrors.length > 0) {
192
+ scriptsSpinner.warn(`Installed ${scriptsCopied.length} scripts with ${scriptsErrors.length} errors`);
193
+ } else if (scriptsCopied.length > 0) {
194
+ scriptsSpinner.succeed(`Installed scripts: ${scriptsCopied.join(', ')}`);
195
+ } else {
196
+ scriptsSpinner.info('No scripts to install');
197
+ }
198
+ } catch (e) {
199
+ scriptsSpinner.warn('Failed to install scripts');
200
+ }
201
+
202
+ // Step 7: Set up settings
162
203
  const settingsSpinner = ora('Setting up Claude Code configuration...').start();
163
204
 
164
205
  try {
@@ -168,7 +209,7 @@ export async function init(options) {
168
209
  settingsSpinner.warn('Failed to set up settings');
169
210
  }
170
211
 
171
- // Step 6: Set up hooks
212
+ // Step 8: Set up hooks
172
213
  if (!options.noHooks) {
173
214
  const hooksSpinner = ora('Setting up hooks...').start();
174
215
 
@@ -184,7 +225,7 @@ export async function init(options) {
184
225
  }
185
226
  }
186
227
 
187
- // Step 7: Set up CLAUDE.md
228
+ // Step 9: Set up CLAUDE.md
188
229
  const claudeSpinner = ora('Setting up CLAUDE.md...').start();
189
230
 
190
231
  try {
@@ -194,7 +235,7 @@ export async function init(options) {
194
235
  claudeSpinner.warn('Failed to set up CLAUDE.md');
195
236
  }
196
237
 
197
- // Step 8: Update .gitignore
238
+ // Step 10: Update .gitignore
198
239
  const gitSpinner = ora('Updating .gitignore...').start();
199
240
 
200
241
  try {
@@ -204,15 +245,21 @@ export async function init(options) {
204
245
  gitSpinner.warn('Failed to update .gitignore');
205
246
  }
206
247
 
207
- // Step 9: Check project dependencies
248
+ // Step 11: Project setup (package manager, .env, node version)
249
+ console.log('\n' + chalk.cyan('─'.repeat(40)));
250
+ console.log(chalk.bold('📦 Project Setup\n'));
251
+
252
+ const setupResult = await runProjectSetup(projectPath, { yes: options.yes });
253
+
254
+ // Step 12: Check skill-specific project dependencies
208
255
  if (!options.noDeps) {
209
256
  console.log('');
210
- const projectSpinner = ora('Checking project dependencies...').start();
257
+ const projectSpinner = ora('Checking skill dependencies...').start();
211
258
  const projectDeps = await checkProjectDeps(projectPath);
212
259
  projectSpinner.stop();
213
260
 
214
261
  if (projectDeps.missing.length > 0) {
215
- console.log(chalk.yellow('\n⚠️ Some optional dependencies are missing:\n'));
262
+ console.log(chalk.yellow('\n⚠️ Some optional dependencies for skills are missing:\n'));
216
263
 
217
264
  for (const dep of projectDeps.missing) {
218
265
  const cmd = dep.type === 'pip'
@@ -226,7 +273,7 @@ export async function init(options) {
226
273
  const { install } = await inquirer.prompt([{
227
274
  type: 'confirm',
228
275
  name: 'install',
229
- message: 'Install missing dependencies now?',
276
+ message: 'Install missing skill dependencies now?',
230
277
  default: false
231
278
  }]);
232
279
 
@@ -240,11 +287,41 @@ export async function init(options) {
240
287
  // Done!
241
288
  console.log(chalk.green('\n✅ Claude Skills initialized successfully!\n'));
242
289
 
290
+ // Show startup commands if detected
291
+ if (setupResult.startupCommands && setupResult.startupCommands.length > 0) {
292
+ console.log(chalk.bold('Available commands:'));
293
+ console.log('');
294
+ for (const cmd of setupResult.startupCommands) {
295
+ const pm = setupResult.packageManager?.name || 'npm';
296
+ console.log(` ${chalk.cyan(`${pm} run ${cmd.name}`)} - ${chalk.gray(cmd.command)}`);
297
+ }
298
+ console.log('');
299
+ }
300
+
243
301
  console.log(chalk.bold('Next steps:'));
244
302
  console.log('');
245
- console.log(' 1. Start Claude Code in this project');
246
- console.log(' 2. Try a skill: ' + chalk.cyan('/debrief "your project idea"'));
247
- console.log(' 3. See all skills: ' + chalk.cyan('/help'));
303
+
304
+ // Dynamic next steps based on setup
305
+ let stepNum = 1;
306
+
307
+ if (!setupResult.depsInstalled && setupResult.packageManager) {
308
+ console.log(` ${stepNum}. Install dependencies: ${chalk.cyan(setupResult.packageManager.install)}`);
309
+ stepNum++;
310
+ }
311
+
312
+ if (!setupResult.envCreated && setupResult.packageManager) {
313
+ const envExample = detectEnvExample(projectPath);
314
+ if (envExample && !hasEnvFile(projectPath)) {
315
+ console.log(` ${stepNum}. Set up environment: ${chalk.cyan(`cp ${envExample.file} .env`)} and fill in values`);
316
+ stepNum++;
317
+ }
318
+ }
319
+
320
+ console.log(` ${stepNum}. Start Claude Code in this project`);
321
+ stepNum++;
322
+ console.log(` ${stepNum}. Try a skill: ${chalk.cyan('/debrief "your project idea"')}`);
323
+ stepNum++;
324
+ console.log(` ${stepNum}. See all skills: ${chalk.cyan('/help')}`);
248
325
  console.log('');
249
326
  console.log(chalk.gray('Docs: .claude/skills/_registry.md'));
250
327
  console.log('');
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Project Setup Utilities
3
+ *
4
+ * Detects and helps set up the development environment.
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import chalk from 'chalk';
11
+ import inquirer from 'inquirer';
12
+
13
+ /**
14
+ * Detect package manager from lockfile
15
+ */
16
+ export function detectPackageManager(projectPath) {
17
+ const lockfiles = {
18
+ 'bun.lockb': { name: 'bun', install: 'bun install' },
19
+ 'pnpm-lock.yaml': { name: 'pnpm', install: 'pnpm install' },
20
+ 'yarn.lock': { name: 'yarn', install: 'yarn install' },
21
+ 'package-lock.json': { name: 'npm', install: 'npm install' }
22
+ };
23
+
24
+ for (const [file, pm] of Object.entries(lockfiles)) {
25
+ if (fs.existsSync(path.join(projectPath, file))) {
26
+ return pm;
27
+ }
28
+ }
29
+
30
+ // Default to npm if package.json exists
31
+ if (fs.existsSync(path.join(projectPath, 'package.json'))) {
32
+ return { name: 'npm', install: 'npm install' };
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Check if node_modules exists
40
+ */
41
+ export function hasNodeModules(projectPath) {
42
+ return fs.existsSync(path.join(projectPath, 'node_modules'));
43
+ }
44
+
45
+ /**
46
+ * Install project dependencies
47
+ */
48
+ export async function installDeps(projectPath, pm) {
49
+ try {
50
+ execSync(pm.install, {
51
+ cwd: projectPath,
52
+ stdio: 'inherit'
53
+ });
54
+ return true;
55
+ } catch (e) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Detect node version requirement
62
+ */
63
+ export function detectNodeVersion(projectPath) {
64
+ const versionFiles = ['.nvmrc', '.node-version', '.tool-versions'];
65
+
66
+ for (const file of versionFiles) {
67
+ const filePath = path.join(projectPath, file);
68
+ if (fs.existsSync(filePath)) {
69
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
70
+
71
+ // Handle .tool-versions format (asdf)
72
+ if (file === '.tool-versions') {
73
+ const match = content.match(/nodejs\s+(\S+)/);
74
+ if (match) {
75
+ return { file, version: match[1] };
76
+ }
77
+ } else {
78
+ // .nvmrc or .node-version - just the version
79
+ const version = content.replace(/^v/, '');
80
+ return { file, version };
81
+ }
82
+ }
83
+ }
84
+
85
+ // Check package.json engines
86
+ const pkgPath = path.join(projectPath, 'package.json');
87
+ if (fs.existsSync(pkgPath)) {
88
+ const pkg = fs.readJsonSync(pkgPath);
89
+ if (pkg.engines?.node) {
90
+ return { file: 'package.json (engines)', version: pkg.engines.node };
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Get current node version
99
+ */
100
+ export function getCurrentNodeVersion() {
101
+ try {
102
+ return process.version.replace(/^v/, '');
103
+ } catch (e) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check if current node version satisfies requirement
110
+ */
111
+ export function checkNodeVersion(required, current) {
112
+ // Simple check - just compare major versions for now
113
+ const reqMajor = parseInt(required.split('.')[0]);
114
+ const curMajor = parseInt(current.split('.')[0]);
115
+
116
+ if (isNaN(reqMajor)) {
117
+ // Could be a range like >=18, just warn
118
+ return { ok: true, warning: `Required: ${required}, Current: ${current}` };
119
+ }
120
+
121
+ if (curMajor < reqMajor) {
122
+ return { ok: false, warning: `Required: ${required}, Current: ${current}` };
123
+ }
124
+
125
+ return { ok: true };
126
+ }
127
+
128
+ /**
129
+ * Detect .env.example and parse variables
130
+ */
131
+ export function detectEnvExample(projectPath) {
132
+ const envExampleNames = ['.env.example', '.env.sample', '.env.template', 'example.env'];
133
+
134
+ for (const name of envExampleNames) {
135
+ const filePath = path.join(projectPath, name);
136
+ if (fs.existsSync(filePath)) {
137
+ const content = fs.readFileSync(filePath, 'utf-8');
138
+ const vars = parseEnvFile(content);
139
+ return { file: name, vars };
140
+ }
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Check if .env exists
148
+ */
149
+ export function hasEnvFile(projectPath) {
150
+ return fs.existsSync(path.join(projectPath, '.env'));
151
+ }
152
+
153
+ /**
154
+ * Parse env file content into variables
155
+ */
156
+ export function parseEnvFile(content) {
157
+ const vars = [];
158
+ const lines = content.split('\n');
159
+
160
+ for (const line of lines) {
161
+ const trimmed = line.trim();
162
+
163
+ // Skip empty lines and comments
164
+ if (!trimmed || trimmed.startsWith('#')) {
165
+ continue;
166
+ }
167
+
168
+ // Parse KEY=value
169
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/i);
170
+ if (match) {
171
+ const [, key, value] = match;
172
+ const cleanValue = value.replace(/^["']|["']$/g, ''); // Remove quotes
173
+
174
+ vars.push({
175
+ key,
176
+ defaultValue: cleanValue || null,
177
+ hasDefault: cleanValue !== '',
178
+ isSecret: isSecretVar(key)
179
+ });
180
+ }
181
+ }
182
+
183
+ return vars;
184
+ }
185
+
186
+ /**
187
+ * Detect if variable is likely a secret
188
+ */
189
+ function isSecretVar(key) {
190
+ const secretPatterns = [
191
+ /KEY$/i,
192
+ /SECRET$/i,
193
+ /TOKEN$/i,
194
+ /PASSWORD$/i,
195
+ /PASS$/i,
196
+ /AUTH/i,
197
+ /PRIVATE/i,
198
+ /CREDENTIAL/i
199
+ ];
200
+
201
+ return secretPatterns.some(pattern => pattern.test(key));
202
+ }
203
+
204
+ /**
205
+ * Prompt user for env values
206
+ */
207
+ export async function promptEnvValues(vars, existingEnv = {}) {
208
+ const values = { ...existingEnv };
209
+ const questions = [];
210
+
211
+ for (const v of vars) {
212
+ // Skip if already has value
213
+ if (existingEnv[v.key]) {
214
+ continue;
215
+ }
216
+
217
+ const defaultVal = v.hasDefault ? v.defaultValue : undefined;
218
+
219
+ questions.push({
220
+ type: v.isSecret ? 'password' : 'input',
221
+ name: v.key,
222
+ message: v.isSecret
223
+ ? `${v.key}: (secret)`
224
+ : `${v.key}:`,
225
+ default: defaultVal,
226
+ transformer: (input) => {
227
+ if (v.isSecret && input) {
228
+ return '*'.repeat(input.length);
229
+ }
230
+ return input;
231
+ }
232
+ });
233
+ }
234
+
235
+ if (questions.length === 0) {
236
+ return values;
237
+ }
238
+
239
+ console.log(chalk.cyan('\nEnvironment variables needed:\n'));
240
+
241
+ const answers = await inquirer.prompt(questions);
242
+
243
+ return { ...values, ...answers };
244
+ }
245
+
246
+ /**
247
+ * Write .env file
248
+ */
249
+ export function writeEnvFile(projectPath, values) {
250
+ const lines = [];
251
+
252
+ for (const [key, value] of Object.entries(values)) {
253
+ // Quote values with spaces or special chars
254
+ const needsQuotes = /[\s#"'`$]/.test(value);
255
+ const formattedValue = needsQuotes ? `"${value}"` : value;
256
+ lines.push(`${key}=${formattedValue}`);
257
+ }
258
+
259
+ const content = lines.join('\n') + '\n';
260
+ fs.writeFileSync(path.join(projectPath, '.env'), content);
261
+
262
+ return lines.length;
263
+ }
264
+
265
+ /**
266
+ * Read existing .env file
267
+ */
268
+ export function readEnvFile(projectPath) {
269
+ const envPath = path.join(projectPath, '.env');
270
+ if (!fs.existsSync(envPath)) {
271
+ return {};
272
+ }
273
+
274
+ const content = fs.readFileSync(envPath, 'utf-8');
275
+ const vars = parseEnvFile(content);
276
+
277
+ const result = {};
278
+ for (const v of vars) {
279
+ result[v.key] = v.defaultValue || '';
280
+ }
281
+
282
+ return result;
283
+ }
284
+
285
+ /**
286
+ * Get startup commands from package.json
287
+ */
288
+ export function getStartupCommands(projectPath) {
289
+ const pkgPath = path.join(projectPath, 'package.json');
290
+ if (!fs.existsSync(pkgPath)) {
291
+ return null;
292
+ }
293
+
294
+ const pkg = fs.readJsonSync(pkgPath);
295
+ const scripts = pkg.scripts || {};
296
+
297
+ const startupScripts = [];
298
+ const commonStartScripts = ['dev', 'start', 'serve', 'develop', 'start:dev'];
299
+
300
+ for (const name of commonStartScripts) {
301
+ if (scripts[name]) {
302
+ startupScripts.push({ name, command: scripts[name] });
303
+ }
304
+ }
305
+
306
+ // Also include build if exists
307
+ if (scripts.build) {
308
+ startupScripts.push({ name: 'build', command: scripts.build });
309
+ }
310
+
311
+ return startupScripts.length > 0 ? startupScripts : null;
312
+ }
313
+
314
+ /**
315
+ * Run full project setup
316
+ */
317
+ export async function runProjectSetup(projectPath, options = {}) {
318
+ const results = {
319
+ packageManager: null,
320
+ depsInstalled: false,
321
+ nodeVersion: null,
322
+ envCreated: false,
323
+ startupCommands: null
324
+ };
325
+
326
+ // 1. Detect package manager
327
+ const pm = detectPackageManager(projectPath);
328
+ if (!pm) {
329
+ return results; // Not a Node.js project
330
+ }
331
+ results.packageManager = pm;
332
+
333
+ // 2. Check node version
334
+ const nodeReq = detectNodeVersion(projectPath);
335
+ if (nodeReq) {
336
+ const currentNode = getCurrentNodeVersion();
337
+ const check = checkNodeVersion(nodeReq.version, currentNode);
338
+ results.nodeVersion = {
339
+ required: nodeReq.version,
340
+ current: currentNode,
341
+ file: nodeReq.file,
342
+ ok: check.ok,
343
+ warning: check.warning
344
+ };
345
+
346
+ if (!check.ok) {
347
+ console.log(chalk.yellow(`\n⚠️ Node version mismatch`));
348
+ console.log(chalk.yellow(` Required: ${nodeReq.version} (from ${nodeReq.file})`));
349
+ console.log(chalk.yellow(` Current: ${currentNode}`));
350
+ console.log(chalk.gray(` Consider using nvm: nvm install ${nodeReq.version}\n`));
351
+ }
352
+ }
353
+
354
+ // 3. Install dependencies if needed
355
+ if (!hasNodeModules(projectPath)) {
356
+ if (!options.yes) {
357
+ const { install } = await inquirer.prompt([{
358
+ type: 'confirm',
359
+ name: 'install',
360
+ message: `No node_modules found. Run ${chalk.cyan(pm.install)}?`,
361
+ default: true
362
+ }]);
363
+
364
+ if (install) {
365
+ console.log(chalk.cyan(`\nRunning ${pm.install}...\n`));
366
+ results.depsInstalled = await installDeps(projectPath, pm);
367
+ }
368
+ } else {
369
+ console.log(chalk.cyan(`\nRunning ${pm.install}...\n`));
370
+ results.depsInstalled = await installDeps(projectPath, pm);
371
+ }
372
+ } else {
373
+ results.depsInstalled = true; // Already installed
374
+ }
375
+
376
+ // 4. Setup .env from example
377
+ // Always prompt for env values since they're required to run the app
378
+ const envExample = detectEnvExample(projectPath);
379
+ if (envExample) {
380
+ const hasEnv = hasEnvFile(projectPath);
381
+
382
+ if (!hasEnv) {
383
+ console.log(chalk.cyan(`\nFound ${envExample.file} with ${envExample.vars.length} variables`));
384
+
385
+ // Check if there are any vars that need user input (no default or secrets)
386
+ const needsInput = envExample.vars.some(v => !v.hasDefault || v.isSecret);
387
+
388
+ if (needsInput) {
389
+ const { setupEnv } = await inquirer.prompt([{
390
+ type: 'confirm',
391
+ name: 'setupEnv',
392
+ message: 'Set up .env file now? (required to run the app)',
393
+ default: true
394
+ }]);
395
+
396
+ if (setupEnv) {
397
+ const values = await promptEnvValues(envExample.vars);
398
+ const count = writeEnvFile(projectPath, values);
399
+ console.log(chalk.green(`\n✓ Created .env with ${count} variables`));
400
+ results.envCreated = true;
401
+ }
402
+ } else {
403
+ // All vars have defaults, auto-create .env
404
+ const values = {};
405
+ for (const v of envExample.vars) {
406
+ values[v.key] = v.defaultValue || '';
407
+ }
408
+ const count = writeEnvFile(projectPath, values);
409
+ console.log(chalk.green(`✓ Created .env with ${count} variables (using defaults)`));
410
+ results.envCreated = true;
411
+ }
412
+ } else {
413
+ // Check for missing vars in existing .env
414
+ const existingVars = readEnvFile(projectPath);
415
+ const missingVars = envExample.vars.filter(v => !existingVars[v.key]);
416
+
417
+ if (missingVars.length > 0) {
418
+ console.log(chalk.yellow(`\n⚠️ .env exists but missing ${missingVars.length} variables from ${envExample.file}`));
419
+
420
+ const { addMissing } = await inquirer.prompt([{
421
+ type: 'confirm',
422
+ name: 'addMissing',
423
+ message: 'Add missing variables?',
424
+ default: true
425
+ }]);
426
+
427
+ if (addMissing) {
428
+ const newValues = await promptEnvValues(missingVars, existingVars);
429
+ const count = writeEnvFile(projectPath, newValues);
430
+ console.log(chalk.green(`\n✓ Updated .env with ${count} variables`));
431
+ results.envCreated = true;
432
+ }
433
+ } else {
434
+ console.log(chalk.green(`✓ .env already configured`));
435
+ results.envCreated = true;
436
+ }
437
+ }
438
+ }
439
+
440
+ // 5. Get startup commands
441
+ results.startupCommands = getStartupCommands(projectPath);
442
+
443
+ return results;
444
+ }
@@ -11,8 +11,10 @@ import { fileURLToPath } from 'url';
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = path.dirname(__filename);
13
13
 
14
- // Path to bundled skills (in npm package)
14
+ // Paths to bundled content (in npm package)
15
15
  const SKILLS_SOURCE = path.join(__dirname, '../../skills');
16
+ const KNOWLEDGE_SOURCE = path.join(__dirname, '../../knowledge');
17
+ const SCRIPTS_SOURCE = path.join(__dirname, '../../project-scripts');
16
18
 
17
19
  /**
18
20
  * Get list of all available skills
@@ -251,3 +253,87 @@ export async function checkForUpdates(projectPath) {
251
253
 
252
254
  return updates;
253
255
  }
256
+
257
+ /**
258
+ * Copy knowledge (stacks, domains) to project
259
+ */
260
+ export async function copyKnowledgeToProject(projectPath) {
261
+ const targetPath = path.join(projectPath, '.claude', 'knowledge');
262
+
263
+ // Check if source exists
264
+ if (!await fs.pathExists(KNOWLEDGE_SOURCE)) {
265
+ return { copied: [], errors: [{ name: 'knowledge', error: 'Source not found' }] };
266
+ }
267
+
268
+ // Remove existing and copy fresh
269
+ await fs.remove(targetPath);
270
+ await fs.ensureDir(targetPath);
271
+
272
+ const copied = [];
273
+ const errors = [];
274
+
275
+ try {
276
+ const items = await fs.readdir(KNOWLEDGE_SOURCE);
277
+
278
+ for (const item of items) {
279
+ // Skip hidden files
280
+ if (item.startsWith('.')) continue;
281
+
282
+ const sourcePath = path.join(KNOWLEDGE_SOURCE, item);
283
+ const targetItemPath = path.join(targetPath, item);
284
+
285
+ try {
286
+ await fs.copy(sourcePath, targetItemPath);
287
+ copied.push(item);
288
+ } catch (e) {
289
+ errors.push({ name: item, error: e.message });
290
+ }
291
+ }
292
+ } catch (e) {
293
+ errors.push({ name: 'knowledge', error: e.message });
294
+ }
295
+
296
+ return { copied, errors };
297
+ }
298
+
299
+ /**
300
+ * Copy scripts (graph.py, etc.) to project
301
+ */
302
+ export async function copyScriptsToProject(projectPath) {
303
+ const targetPath = path.join(projectPath, '.claude', 'scripts');
304
+
305
+ // Check if source exists
306
+ if (!await fs.pathExists(SCRIPTS_SOURCE)) {
307
+ return { copied: [], errors: [{ name: 'scripts', error: 'Source not found' }] };
308
+ }
309
+
310
+ // Remove existing and copy fresh
311
+ await fs.remove(targetPath);
312
+ await fs.ensureDir(targetPath);
313
+
314
+ const copied = [];
315
+ const errors = [];
316
+
317
+ try {
318
+ const items = await fs.readdir(SCRIPTS_SOURCE);
319
+
320
+ for (const item of items) {
321
+ // Skip hidden files
322
+ if (item.startsWith('.')) continue;
323
+
324
+ const sourcePath = path.join(SCRIPTS_SOURCE, item);
325
+ const targetItemPath = path.join(targetPath, item);
326
+
327
+ try {
328
+ await fs.copy(sourcePath, targetItemPath);
329
+ copied.push(item);
330
+ } catch (e) {
331
+ errors.push({ name: item, error: e.message });
332
+ }
333
+ }
334
+ } catch (e) {
335
+ errors.push({ name: 'scripts', error: e.message });
336
+ }
337
+
338
+ return { copied, errors };
339
+ }