cli4ai 0.8.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.
- package/README.md +275 -0
- package/package.json +49 -0
- package/src/bin.ts +120 -0
- package/src/cli.ts +256 -0
- package/src/commands/add.ts +530 -0
- package/src/commands/browse.ts +449 -0
- package/src/commands/config.ts +126 -0
- package/src/commands/info.ts +102 -0
- package/src/commands/init.test.ts +163 -0
- package/src/commands/init.ts +560 -0
- package/src/commands/list.ts +89 -0
- package/src/commands/mcp-config.ts +59 -0
- package/src/commands/remove.ts +72 -0
- package/src/commands/routines.ts +393 -0
- package/src/commands/run.ts +45 -0
- package/src/commands/search.ts +148 -0
- package/src/commands/secrets.ts +273 -0
- package/src/commands/start.ts +40 -0
- package/src/commands/update.ts +218 -0
- package/src/core/config.test.ts +188 -0
- package/src/core/config.ts +649 -0
- package/src/core/execute.ts +507 -0
- package/src/core/link.test.ts +238 -0
- package/src/core/link.ts +190 -0
- package/src/core/lockfile.test.ts +337 -0
- package/src/core/lockfile.ts +308 -0
- package/src/core/manifest.test.ts +327 -0
- package/src/core/manifest.ts +319 -0
- package/src/core/routine-engine.test.ts +139 -0
- package/src/core/routine-engine.ts +725 -0
- package/src/core/routines.ts +111 -0
- package/src/core/secrets.test.ts +79 -0
- package/src/core/secrets.ts +430 -0
- package/src/lib/cli.ts +234 -0
- package/src/mcp/adapter.test.ts +132 -0
- package/src/mcp/adapter.ts +123 -0
- package/src/mcp/config-gen.test.ts +214 -0
- package/src/mcp/config-gen.ts +106 -0
- package/src/mcp/server.ts +363 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai init - Create a new tool project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import { resolve, basename } from 'path';
|
|
7
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
8
|
+
import { createManifest, MANIFEST_FILENAME, type Manifest } from '../core/manifest.js';
|
|
9
|
+
|
|
10
|
+
interface InitOptions {
|
|
11
|
+
template?: string;
|
|
12
|
+
runtime?: 'bun' | 'node';
|
|
13
|
+
yes?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function initCommand(name: string | undefined, options: InitOptions): Promise<void> {
|
|
17
|
+
const projectName = name || basename(process.cwd());
|
|
18
|
+
const targetDir = name ? resolve(process.cwd(), name) : process.cwd();
|
|
19
|
+
const manifestPath = resolve(targetDir, MANIFEST_FILENAME);
|
|
20
|
+
const templateName = options.template || 'basic';
|
|
21
|
+
const runtime = options.runtime || 'bun';
|
|
22
|
+
|
|
23
|
+
// Check if cli4ai.json already exists
|
|
24
|
+
if (existsSync(manifestPath)) {
|
|
25
|
+
outputError('INVALID_INPUT', `${MANIFEST_FILENAME} already exists`, {
|
|
26
|
+
path: manifestPath,
|
|
27
|
+
hint: 'Remove it first or use a different directory'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create directory if needed
|
|
32
|
+
if (name && !existsSync(targetDir)) {
|
|
33
|
+
mkdirSync(targetDir, { recursive: true });
|
|
34
|
+
log(`Created directory: ${targetDir}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const manifest = createManifest(projectName, {
|
|
38
|
+
...getBaseManifest(templateName, runtime),
|
|
39
|
+
...getTemplateManifest(templateName, runtime),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Write manifest
|
|
43
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
44
|
+
log(`Created ${MANIFEST_FILENAME}`);
|
|
45
|
+
|
|
46
|
+
// Create entry file if it doesn't exist
|
|
47
|
+
const entryPath = resolve(targetDir, manifest.entry);
|
|
48
|
+
if (!existsSync(entryPath)) {
|
|
49
|
+
const template = getTemplate(templateName, runtime, manifest);
|
|
50
|
+
writeFileSync(entryPath, template);
|
|
51
|
+
log(`Created ${manifest.entry}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create helpful extras (non-destructive)
|
|
55
|
+
const readmePath = resolve(targetDir, 'README.md');
|
|
56
|
+
if (!existsSync(readmePath)) {
|
|
57
|
+
writeFileSync(readmePath, getReadmeTemplate(templateName, runtime, manifest));
|
|
58
|
+
log('Created README.md');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const gitignorePath = resolve(targetDir, '.gitignore');
|
|
62
|
+
if (!existsSync(gitignorePath)) {
|
|
63
|
+
writeFileSync(gitignorePath, getGitignoreTemplate());
|
|
64
|
+
log('Created .gitignore');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pkgJsonPath = resolve(targetDir, 'package.json');
|
|
68
|
+
if (!existsSync(pkgJsonPath)) {
|
|
69
|
+
const pkgJson = getDevPackageJson(templateName, runtime, manifest);
|
|
70
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
71
|
+
log('Created package.json');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Template-specific extras
|
|
75
|
+
if (templateName === 'api') {
|
|
76
|
+
const envExamplePath = resolve(targetDir, '.env.example');
|
|
77
|
+
if (!existsSync(envExamplePath)) {
|
|
78
|
+
writeFileSync(envExamplePath, 'API_KEY=\n');
|
|
79
|
+
log('Created .env.example');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nextSteps = getNextSteps(name, targetDir, templateName, runtime, manifest);
|
|
84
|
+
|
|
85
|
+
output({
|
|
86
|
+
created: {
|
|
87
|
+
manifest: manifestPath,
|
|
88
|
+
entry: entryPath,
|
|
89
|
+
readme: readmePath,
|
|
90
|
+
gitignore: gitignorePath,
|
|
91
|
+
packageJson: existsSync(resolve(targetDir, 'package.json')) ? resolve(targetDir, 'package.json') : undefined,
|
|
92
|
+
},
|
|
93
|
+
name: manifest.name,
|
|
94
|
+
version: manifest.version,
|
|
95
|
+
runtime: manifest.runtime,
|
|
96
|
+
hint: nextSteps[0]?.command,
|
|
97
|
+
nextSteps
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function guessAuthor(): string | undefined {
|
|
102
|
+
const candidates = [
|
|
103
|
+
process.env.GIT_AUTHOR_NAME,
|
|
104
|
+
process.env.GIT_COMMITTER_NAME,
|
|
105
|
+
process.env.USER,
|
|
106
|
+
process.env.USERNAME,
|
|
107
|
+
].filter((v): v is string => Boolean(v));
|
|
108
|
+
|
|
109
|
+
return candidates[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getBaseManifest(templateName: string, runtime: 'bun' | 'node'): Partial<Manifest> {
|
|
113
|
+
const author = guessAuthor();
|
|
114
|
+
const keywords = Array.from(new Set([
|
|
115
|
+
'cli4ai',
|
|
116
|
+
'cli4ai',
|
|
117
|
+
templateName,
|
|
118
|
+
]));
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
entry: runtime === 'node' ? 'run.mjs' : 'run.ts',
|
|
122
|
+
runtime,
|
|
123
|
+
description: `${templateName === 'basic' ? 'CLI' : templateName} tool`,
|
|
124
|
+
author,
|
|
125
|
+
license: 'MIT',
|
|
126
|
+
keywords,
|
|
127
|
+
mcp: { enabled: true, transport: 'stdio' },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getTemplateManifest(templateName: string, runtime: 'bun' | 'node'): Partial<Manifest> {
|
|
132
|
+
switch (templateName) {
|
|
133
|
+
case 'api':
|
|
134
|
+
return {
|
|
135
|
+
commands: {
|
|
136
|
+
fetch: {
|
|
137
|
+
description: 'Fetch JSON from an API endpoint',
|
|
138
|
+
args: [{ name: 'endpoint', required: true }],
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
env: {
|
|
142
|
+
API_KEY: {
|
|
143
|
+
required: true,
|
|
144
|
+
description: 'API key for the service you are calling (stored via `cli4ai secrets`)',
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
dependencies: getDefaultDependencies(templateName, runtime),
|
|
148
|
+
};
|
|
149
|
+
case 'browser':
|
|
150
|
+
return {
|
|
151
|
+
commands: {
|
|
152
|
+
screenshot: {
|
|
153
|
+
description: 'Take a screenshot of a webpage',
|
|
154
|
+
args: [
|
|
155
|
+
{ name: 'url', required: true },
|
|
156
|
+
{ name: 'output', required: false },
|
|
157
|
+
],
|
|
158
|
+
options: [
|
|
159
|
+
{ name: 'full-page', type: 'boolean', description: 'Capture full page' },
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
dependencies: getDefaultDependencies(templateName, runtime),
|
|
164
|
+
};
|
|
165
|
+
default:
|
|
166
|
+
return {
|
|
167
|
+
commands: {
|
|
168
|
+
hello: {
|
|
169
|
+
description: 'Say hello',
|
|
170
|
+
args: [{ name: 'name', required: false }]
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
dependencies: getDefaultDependencies(templateName, runtime),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getDefaultDependencies(
|
|
179
|
+
templateName: string,
|
|
180
|
+
runtime: 'bun' | 'node'
|
|
181
|
+
): Record<string, string> | undefined {
|
|
182
|
+
const deps: Record<string, string> = {};
|
|
183
|
+
|
|
184
|
+
// Prefer shared SDK helpers when running on bun (or node with TS support, if that lands later).
|
|
185
|
+
if (runtime === 'bun') {
|
|
186
|
+
deps['@cli4ai/lib'] = '^1.0.0';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
deps['commander'] = '^14.0.0';
|
|
190
|
+
|
|
191
|
+
if (templateName === 'browser') {
|
|
192
|
+
deps['puppeteer'] = '^24.0.0';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return deps;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getTemplate(templateName: string, runtime: 'bun' | 'node', manifest: Manifest): string {
|
|
199
|
+
if (runtime === 'node') {
|
|
200
|
+
return getNodeTemplate(templateName, manifest);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
switch (templateName) {
|
|
204
|
+
case 'api':
|
|
205
|
+
return getBunApiTemplate(manifest);
|
|
206
|
+
case 'browser':
|
|
207
|
+
return getBunBrowserTemplate(manifest);
|
|
208
|
+
default:
|
|
209
|
+
return getBunBasicTemplate(manifest);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getBunBasicTemplate(manifest: Manifest): string {
|
|
214
|
+
return `#!/usr/bin/env bun
|
|
215
|
+
/**
|
|
216
|
+
* ${manifest.name} - ${manifest.description || 'A cli4ai tool'}
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
import { cli, output } from '@cli4ai/lib/cli.ts';
|
|
220
|
+
|
|
221
|
+
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
222
|
+
|
|
223
|
+
program
|
|
224
|
+
.command('hello [name]')
|
|
225
|
+
.description('Say hello')
|
|
226
|
+
.action((name?: string) => {
|
|
227
|
+
const who = name?.trim() || 'world';
|
|
228
|
+
output({ message: \`Hello, \${who}!\` });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
program.parse();
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getBunApiTemplate(manifest: Manifest): string {
|
|
236
|
+
return `#!/usr/bin/env bun
|
|
237
|
+
/**
|
|
238
|
+
* ${manifest.name} - ${manifest.description || 'API wrapper tool'}
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
import { cli, env, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
|
|
242
|
+
|
|
243
|
+
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
244
|
+
|
|
245
|
+
program
|
|
246
|
+
.command('fetch <endpoint>')
|
|
247
|
+
.description('Fetch data from API')
|
|
248
|
+
.action(withErrorHandling(async (endpoint: string) => {
|
|
249
|
+
// Use \`cli4ai secrets set API_KEY\` to store this securely (cli4ai injects it at runtime).
|
|
250
|
+
const apiKey = env('API_KEY');
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(\`https://api.example.com/\${endpoint}\`, {
|
|
254
|
+
headers: { 'Authorization': \`Bearer \${apiKey}\` }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
outputError('API_ERROR', \`HTTP \${res.status}: \${res.statusText}\`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
output(await res.json());
|
|
262
|
+
} catch (err) {
|
|
263
|
+
outputError('NETWORK_ERROR', err instanceof Error ? err.message : String(err));
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
program.parse();
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getBunBrowserTemplate(manifest: Manifest): string {
|
|
272
|
+
return `#!/usr/bin/env bun
|
|
273
|
+
/**
|
|
274
|
+
* ${manifest.name} - ${manifest.description || 'Browser automation tool'}
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
import puppeteer from 'puppeteer';
|
|
278
|
+
import { cli, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
|
|
279
|
+
|
|
280
|
+
const program = cli('${manifest.name}', '${manifest.version}', '${manifest.description || manifest.name}');
|
|
281
|
+
|
|
282
|
+
program
|
|
283
|
+
.command('screenshot <url>')
|
|
284
|
+
.description('Take a screenshot of a webpage')
|
|
285
|
+
.option('-o, --output <file>', 'Output file', 'screenshot.png')
|
|
286
|
+
.option('--full-page', 'Capture full page')
|
|
287
|
+
.action(withErrorHandling(async (url: string, options: { output: string; fullPage?: boolean }) => {
|
|
288
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const page = await browser.newPage();
|
|
292
|
+
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
293
|
+
await page.screenshot({ path: options.output, fullPage: options.fullPage });
|
|
294
|
+
|
|
295
|
+
output({
|
|
296
|
+
url,
|
|
297
|
+
screenshot: options.output,
|
|
298
|
+
timestamp: new Date().toISOString()
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
302
|
+
} finally {
|
|
303
|
+
await browser.close();
|
|
304
|
+
}
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
program.parse();
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getNodeTemplate(templateName: string, manifest: Manifest): string {
|
|
312
|
+
switch (templateName) {
|
|
313
|
+
case 'api':
|
|
314
|
+
return getNodeApiTemplate(manifest);
|
|
315
|
+
case 'browser':
|
|
316
|
+
return getNodeBrowserTemplate(manifest);
|
|
317
|
+
default:
|
|
318
|
+
return getNodeBasicTemplate(manifest);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getNodeBasicTemplate(manifest: Manifest): string {
|
|
323
|
+
return `#!/usr/bin/env node
|
|
324
|
+
/**
|
|
325
|
+
* ${manifest.name} - ${manifest.description || 'A cli4ai tool'}
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
import { Command } from 'commander';
|
|
329
|
+
|
|
330
|
+
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
331
|
+
const outputError = (code, message, details) => {
|
|
332
|
+
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const withErrorHandling = (fn) => async (...args) => {
|
|
337
|
+
try {
|
|
338
|
+
await fn(...args);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const program = new Command()
|
|
345
|
+
.name('${manifest.name}')
|
|
346
|
+
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
347
|
+
.description('${manifest.description || manifest.name}');
|
|
348
|
+
|
|
349
|
+
program
|
|
350
|
+
.command('hello [name]')
|
|
351
|
+
.description('Say hello')
|
|
352
|
+
.action(withErrorHandling(async (name) => {
|
|
353
|
+
const who = (name || '').trim() || 'world';
|
|
354
|
+
output({ message: \`Hello, \${who}!\` });
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
program.parse();
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getNodeApiTemplate(manifest: Manifest): string {
|
|
362
|
+
return `#!/usr/bin/env node
|
|
363
|
+
/**
|
|
364
|
+
* ${manifest.name} - ${manifest.description || 'API wrapper tool'}
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
import { Command } from 'commander';
|
|
368
|
+
|
|
369
|
+
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
370
|
+
const outputError = (code, message, details) => {
|
|
371
|
+
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
372
|
+
process.exit(1);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const withErrorHandling = (fn) => async (...args) => {
|
|
376
|
+
try {
|
|
377
|
+
await fn(...args);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const env = (name) => {
|
|
384
|
+
const value = process.env[name];
|
|
385
|
+
if (!value) outputError('ENV_MISSING', \`Missing required environment variable: \${name}\`);
|
|
386
|
+
return value;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const program = new Command()
|
|
390
|
+
.name('${manifest.name}')
|
|
391
|
+
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
392
|
+
.description('${manifest.description || manifest.name}');
|
|
393
|
+
|
|
394
|
+
program
|
|
395
|
+
.command('fetch <endpoint>')
|
|
396
|
+
.description('Fetch JSON from an API endpoint')
|
|
397
|
+
.action(withErrorHandling(async (endpoint) => {
|
|
398
|
+
// Use \`cli4ai secrets set API_KEY\` to store this securely (cli4ai injects it at runtime).
|
|
399
|
+
const apiKey = env('API_KEY');
|
|
400
|
+
const res = await fetch(\`https://api.example.com/\${endpoint}\`, {
|
|
401
|
+
headers: { Authorization: \`Bearer \${apiKey}\` },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!res.ok) {
|
|
405
|
+
outputError('API_ERROR', \`HTTP \${res.status}: \${res.statusText}\`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
output(await res.json());
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
program.parse();
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getNodeBrowserTemplate(manifest: Manifest): string {
|
|
416
|
+
return `#!/usr/bin/env node
|
|
417
|
+
/**
|
|
418
|
+
* ${manifest.name} - ${manifest.description || 'Browser automation tool'}
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
import { Command } from 'commander';
|
|
422
|
+
import puppeteer from 'puppeteer';
|
|
423
|
+
|
|
424
|
+
const output = (data) => console.log(JSON.stringify(data, null, 2));
|
|
425
|
+
const outputError = (code, message, details) => {
|
|
426
|
+
console.error(JSON.stringify({ error: code, message, ...(details || {}) }));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const withErrorHandling = (fn) => async (...args) => {
|
|
431
|
+
try {
|
|
432
|
+
await fn(...args);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
outputError('API_ERROR', err instanceof Error ? err.message : String(err));
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const program = new Command()
|
|
439
|
+
.name('${manifest.name}')
|
|
440
|
+
.version('${manifest.version}', '-v, --version', 'Show version')
|
|
441
|
+
.description('${manifest.description || manifest.name}');
|
|
442
|
+
|
|
443
|
+
program
|
|
444
|
+
.command('screenshot <url>')
|
|
445
|
+
.description('Take a screenshot of a webpage')
|
|
446
|
+
.option('-o, --output <file>', 'Output file', 'screenshot.png')
|
|
447
|
+
.option('--full-page', 'Capture full page')
|
|
448
|
+
.action(withErrorHandling(async (url, options) => {
|
|
449
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
450
|
+
try {
|
|
451
|
+
const page = await browser.newPage();
|
|
452
|
+
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
453
|
+
await page.screenshot({ path: options.output, fullPage: options.fullPage });
|
|
454
|
+
output({ url, screenshot: options.output, timestamp: new Date().toISOString() });
|
|
455
|
+
} finally {
|
|
456
|
+
await browser.close();
|
|
457
|
+
}
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
program.parse();
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function getReadmeTemplate(templateName: string, runtime: 'bun' | 'node', manifest: Manifest): string {
|
|
465
|
+
const runCmd =
|
|
466
|
+
runtime === 'node' ? `node ${manifest.entry}` :
|
|
467
|
+
`bun run ${manifest.entry}`;
|
|
468
|
+
|
|
469
|
+
const exampleCmd =
|
|
470
|
+
templateName === 'api' ? 'fetch users' :
|
|
471
|
+
templateName === 'browser' ? 'screenshot https://example.com' :
|
|
472
|
+
'hello world';
|
|
473
|
+
|
|
474
|
+
return `# ${manifest.name}
|
|
475
|
+
|
|
476
|
+
${manifest.description || ''}
|
|
477
|
+
|
|
478
|
+
## Run directly
|
|
479
|
+
|
|
480
|
+
\`\`\`bash
|
|
481
|
+
${runCmd} ${exampleCmd}
|
|
482
|
+
\`\`\`
|
|
483
|
+
|
|
484
|
+
## Install with cli4ai (project-scoped)
|
|
485
|
+
|
|
486
|
+
\`\`\`bash
|
|
487
|
+
# From the project you want to use this tool in:
|
|
488
|
+
cli4ai add --local /path/to/${manifest.name} -y
|
|
489
|
+
cli4ai run ${manifest.name} ${exampleCmd}
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
## MCP
|
|
493
|
+
|
|
494
|
+
\`\`\`bash
|
|
495
|
+
cli4ai start ${manifest.name}
|
|
496
|
+
\`\`\`
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getGitignoreTemplate(): string {
|
|
501
|
+
return `node_modules
|
|
502
|
+
.cli4ai
|
|
503
|
+
.env
|
|
504
|
+
.env.*
|
|
505
|
+
dist
|
|
506
|
+
.DS_Store
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getDevPackageJson(
|
|
511
|
+
templateName: string,
|
|
512
|
+
runtime: 'bun' | 'node',
|
|
513
|
+
manifest: Manifest
|
|
514
|
+
): Record<string, unknown> {
|
|
515
|
+
const deps = manifest.dependencies || {};
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
name: manifest.name,
|
|
519
|
+
version: manifest.version,
|
|
520
|
+
private: true,
|
|
521
|
+
type: 'module',
|
|
522
|
+
bin: {
|
|
523
|
+
[manifest.name]: `./${manifest.entry}`,
|
|
524
|
+
},
|
|
525
|
+
dependencies: deps,
|
|
526
|
+
scripts: {
|
|
527
|
+
dev: runtime === 'node' ? `node ${manifest.entry}` : `bun run ${manifest.entry}`,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function getNextSteps(
|
|
533
|
+
nameArg: string | undefined,
|
|
534
|
+
targetDir: string,
|
|
535
|
+
templateName: string,
|
|
536
|
+
runtime: 'bun' | 'node',
|
|
537
|
+
manifest: Manifest
|
|
538
|
+
): Array<{ description: string; command: string }> {
|
|
539
|
+
const exampleArgs =
|
|
540
|
+
templateName === 'api' ? 'fetch users' :
|
|
541
|
+
templateName === 'browser' ? 'screenshot https://example.com' :
|
|
542
|
+
'hello world';
|
|
543
|
+
|
|
544
|
+
const runDirect =
|
|
545
|
+
runtime === 'node' ? `node ${manifest.entry} ${exampleArgs}` :
|
|
546
|
+
`bun run ${manifest.entry} ${exampleArgs}`;
|
|
547
|
+
|
|
548
|
+
const installPath = nameArg ? `./${basename(targetDir)}` : '.';
|
|
549
|
+
|
|
550
|
+
return [
|
|
551
|
+
{
|
|
552
|
+
description: 'Run the tool directly in this folder',
|
|
553
|
+
command: runDirect,
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
description: 'Install into a project and run via cli4ai',
|
|
557
|
+
command: `cli4ai add --local ${installPath} -y && cli4ai run ${manifest.name} ${exampleArgs}`,
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai list - Show installed packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { output } from '../lib/cli.js';
|
|
6
|
+
import {
|
|
7
|
+
getGlobalPackages,
|
|
8
|
+
getLocalPackages,
|
|
9
|
+
getNpmGlobalPackages,
|
|
10
|
+
type InstalledPackage
|
|
11
|
+
} from '../core/config.js';
|
|
12
|
+
|
|
13
|
+
interface ListOptions {
|
|
14
|
+
global?: boolean;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PackageWithScope = InstalledPackage & { scope: 'local' | 'cli4ai' | 'npm' };
|
|
19
|
+
|
|
20
|
+
export async function listCommand(options: ListOptions): Promise<void> {
|
|
21
|
+
let packages: PackageWithScope[];
|
|
22
|
+
|
|
23
|
+
// Get all package sources
|
|
24
|
+
const local = getLocalPackages(process.cwd());
|
|
25
|
+
const cli4aiGlobal = getGlobalPackages();
|
|
26
|
+
const npmGlobal = getNpmGlobalPackages();
|
|
27
|
+
|
|
28
|
+
if (options.global) {
|
|
29
|
+
// Only global packages (cli4ai + npm)
|
|
30
|
+
const cli4aiNames = new Set(cli4aiGlobal.map(p => p.name));
|
|
31
|
+
packages = [
|
|
32
|
+
...cli4aiGlobal.map(p => ({ ...p, scope: 'cli4ai' as const })),
|
|
33
|
+
...npmGlobal.filter(p => !cli4aiNames.has(p.name)).map(p => ({ ...p, scope: 'npm' as const }))
|
|
34
|
+
];
|
|
35
|
+
} else {
|
|
36
|
+
// All packages: local > cli4ai global > npm global
|
|
37
|
+
const seenNames = new Set<string>();
|
|
38
|
+
|
|
39
|
+
packages = [];
|
|
40
|
+
|
|
41
|
+
for (const pkg of local) {
|
|
42
|
+
packages.push({ ...pkg, scope: 'local' as const });
|
|
43
|
+
seenNames.add(pkg.name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const pkg of cli4aiGlobal) {
|
|
47
|
+
if (!seenNames.has(pkg.name)) {
|
|
48
|
+
packages.push({ ...pkg, scope: 'cli4ai' as const });
|
|
49
|
+
seenNames.add(pkg.name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const pkg of npmGlobal) {
|
|
54
|
+
if (!seenNames.has(pkg.name)) {
|
|
55
|
+
packages.push({ ...pkg, scope: 'npm' as const });
|
|
56
|
+
seenNames.add(pkg.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options.json || !process.stdout.isTTY) {
|
|
62
|
+
output({
|
|
63
|
+
packages: packages.map(p => ({
|
|
64
|
+
name: p.name,
|
|
65
|
+
version: p.version,
|
|
66
|
+
path: p.path,
|
|
67
|
+
source: p.source,
|
|
68
|
+
scope: p.scope
|
|
69
|
+
})),
|
|
70
|
+
count: packages.length
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
// Human-readable output
|
|
74
|
+
if (packages.length === 0) {
|
|
75
|
+
console.log('No packages installed');
|
|
76
|
+
console.log('\nRun "cli4ai browse" to find and install packages');
|
|
77
|
+
console.log('Or: npm install -g @cli4ai/<package>');
|
|
78
|
+
} else {
|
|
79
|
+
console.log(`\nInstalled packages (${packages.length}):\n`);
|
|
80
|
+
for (const pkg of packages) {
|
|
81
|
+
const scopeTag = pkg.scope === 'local' ? '' :
|
|
82
|
+
pkg.scope === 'npm' ? ' (npm)' : ' (cli4ai)';
|
|
83
|
+
console.log(` ${pkg.name}@${pkg.version}${scopeTag}`);
|
|
84
|
+
console.log(` ${pkg.path}`);
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai mcp-config - Generate MCP configuration for Claude Code
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { outputError } from '../lib/cli.js';
|
|
6
|
+
import {
|
|
7
|
+
generateClaudeCodeConfig,
|
|
8
|
+
formatClaudeCodeConfig,
|
|
9
|
+
generateConfigSnippet
|
|
10
|
+
} from '../mcp/config-gen.js';
|
|
11
|
+
import { findPackage } from '../core/config.js';
|
|
12
|
+
import { tryLoadManifest } from '../core/manifest.js';
|
|
13
|
+
|
|
14
|
+
interface McpConfigOptions {
|
|
15
|
+
global?: boolean;
|
|
16
|
+
package?: string;
|
|
17
|
+
snippet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function mcpConfigCommand(options: McpConfigOptions): Promise<void> {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
// Single package snippet mode
|
|
24
|
+
if (options.snippet && options.package) {
|
|
25
|
+
const pkg = findPackage(options.package, cwd);
|
|
26
|
+
if (!pkg) {
|
|
27
|
+
outputError('NOT_FOUND', `Package not found: ${options.package}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const manifest = tryLoadManifest(pkg!.path);
|
|
31
|
+
if (!manifest) {
|
|
32
|
+
outputError('MANIFEST_ERROR', `Could not load manifest for ${options.package}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!manifest!.mcp?.enabled) {
|
|
36
|
+
outputError('INVALID_INPUT', `Package ${options.package} does not have MCP enabled`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const snippet = generateConfigSnippet(manifest!, pkg!.path);
|
|
40
|
+
console.log(snippet);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Generate full config
|
|
45
|
+
const packages = options.package ? [options.package] : undefined;
|
|
46
|
+
const config = generateClaudeCodeConfig(cwd, {
|
|
47
|
+
global: options.global,
|
|
48
|
+
packages
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
52
|
+
outputError('NOT_FOUND', 'No MCP-enabled packages found', {
|
|
53
|
+
hint: 'Install packages with "cli4ai add <package>" first'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Output formatted config
|
|
58
|
+
console.log(formatClaudeCodeConfig(config));
|
|
59
|
+
}
|