capman 0.4.2 → 0.4.3
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 +127 -0
- package/CODEBASE.md +391 -0
- package/README.md +1 -1
- package/bin/capman.js +11 -724
- package/bin/lib/cmd-demo.js +180 -0
- package/bin/lib/cmd-explain.js +72 -0
- package/bin/lib/cmd-generate.js +280 -0
- package/bin/lib/cmd-help.js +26 -0
- package/bin/lib/cmd-init.js +19 -0
- package/bin/lib/cmd-inspect.js +33 -0
- package/bin/lib/cmd-run.js +71 -0
- package/bin/lib/cmd-validate.js +32 -0
- package/bin/lib/shared.js +70 -0
- package/dist/cjs/engine.d.ts +58 -1
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +307 -12
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/generator.d.ts.map +1 -1
- package/dist/cjs/generator.js +4 -0
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +19 -25
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/types.d.ts +27 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +49 -0
- package/dist/esm/engine.d.ts +138 -0
- package/dist/esm/engine.js +307 -12
- package/dist/esm/generator.d.ts +7 -0
- package/dist/esm/generator.js +4 -0
- package/dist/esm/index.d.ts +47 -0
- package/dist/esm/learning.d.ts +55 -0
- package/dist/esm/logger.d.ts +21 -0
- package/dist/esm/matcher.d.ts +6 -0
- package/dist/esm/matcher.js +19 -25
- package/dist/esm/parser.d.ts +10 -0
- package/dist/esm/resolver.d.ts +21 -0
- package/dist/esm/schema.d.ts +740 -0
- package/dist/esm/types.d.ts +136 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +1 -1
- package/package.json +5 -3
package/bin/capman.js
CHANGED
|
@@ -1,737 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const fs = require('fs')
|
|
4
|
+
const { command, header, log, c } = require('./lib/shared')
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
const command = args[0]
|
|
9
|
-
const flags = args.slice(1)
|
|
10
|
-
|
|
11
|
-
const getFlag = (name) => {
|
|
12
|
-
const i = flags.indexOf(name)
|
|
13
|
-
return i !== -1 ? flags[i + 1] : undefined
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const c = {
|
|
17
|
-
reset: '\x1b[0m',
|
|
18
|
-
bold: '\x1b[1m',
|
|
19
|
-
teal: '\x1b[36m',
|
|
20
|
-
yellow: '\x1b[33m',
|
|
21
|
-
red: '\x1b[31m',
|
|
22
|
-
green: '\x1b[32m',
|
|
23
|
-
gray: '\x1b[90m',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const log = {
|
|
27
|
-
info: (...a) => console.log(`${c.teal}i${c.reset}`, ...a),
|
|
28
|
-
success: (...a) => console.log(`${c.green}✓${c.reset}`, ...a),
|
|
29
|
-
warn: (...a) => console.log(`${c.yellow}⚠${c.reset}`, ...a),
|
|
30
|
-
error: (...a) => console.error(`${c.red}✗${c.reset}`, ...a),
|
|
31
|
-
blank: () => console.log(),
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function header() {
|
|
35
|
-
const pkg = require(path.join(__dirname, '..', 'package.json'))
|
|
36
|
-
console.log()
|
|
37
|
-
console.log(`${c.bold}${c.teal} capman${c.reset} ${c.gray}v${pkg.version} — Capability Manifest Engine${c.reset}`)
|
|
38
|
-
console.log(`${c.gray} ─────────────────────────────────────────${c.reset}`)
|
|
39
|
-
console.log()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function requireSrc() {
|
|
43
|
-
const distPath = path.join(__dirname, '..', 'dist', 'cjs', 'index.js')
|
|
44
|
-
if (fs.existsSync(distPath)) return require(distPath)
|
|
45
|
-
|
|
46
|
-
// dist not built — try to build automatically
|
|
47
|
-
log.info('dist/cjs not found — running build...')
|
|
48
|
-
try {
|
|
49
|
-
require('child_process').execSync('npm run build', {
|
|
50
|
-
cwd: path.join(__dirname, '..'),
|
|
51
|
-
stdio: 'inherit'
|
|
52
|
-
})
|
|
53
|
-
if (fs.existsSync(distPath)) return require(distPath)
|
|
54
|
-
} catch {
|
|
55
|
-
// build failed
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
log.error('Cannot find dist/cjs/. Run: pnpm run build')
|
|
59
|
-
process.exit(1)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function cmdHelp() {
|
|
63
|
-
header()
|
|
64
|
-
console.log(`${c.bold} Usage:${c.reset} node bin/capman.js <command>`)
|
|
65
|
-
console.log()
|
|
66
|
-
console.log(`${c.bold} Commands:${c.reset}`)
|
|
67
|
-
console.log(` ${c.teal}init${c.reset} Create a starter capman.config.js`)
|
|
68
|
-
console.log(` ${c.teal}generate${c.reset} Generate manifest from capman.config.js`)
|
|
69
|
-
console.log(` ${c.teal}generate --from <path|url>${c.reset} Generate from OpenAPI/Swagger spec`)
|
|
70
|
-
console.log(` ${c.teal}generate --ai${c.reset} Generate manifest using AI (needs API key)`)
|
|
71
|
-
console.log(` ${c.teal}validate${c.reset} Validate an existing manifest.json`)
|
|
72
|
-
console.log(` ${c.teal}inspect${c.reset} Print all capabilities in manifest`)
|
|
73
|
-
console.log(` ${c.teal}demo${c.reset} Run a live demo with sample queries`)
|
|
74
|
-
console.log(` ${c.teal}run${c.reset} Run a query against your manifest`)
|
|
75
|
-
console.log()
|
|
76
|
-
console.log(`${c.bold} Options:${c.reset}`)
|
|
77
|
-
console.log(` ${c.gray}--config Path to config file (default: capman.config.js)${c.reset}`)
|
|
78
|
-
console.log(` ${c.gray}--out Output path (default: manifest.json)${c.reset}`)
|
|
79
|
-
console.log(` ${c.gray}--manifest Manifest to read (default: manifest.json)${c.reset}`)
|
|
80
|
-
console.log(` ${c.gray}Options: --debug (show all candidates)${c.reset}`)
|
|
81
|
-
console.log()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function cmdInit() {
|
|
85
|
-
header()
|
|
86
|
-
const outPath = path.resolve(process.cwd(), 'capman.config.js')
|
|
87
|
-
if (fs.existsSync(outPath)) {
|
|
88
|
-
log.warn('capman.config.js already exists — not overwriting.')
|
|
89
|
-
process.exit(0)
|
|
90
|
-
}
|
|
91
|
-
const { generateStarterConfig } = requireSrc()
|
|
92
|
-
fs.writeFileSync(outPath, generateStarterConfig())
|
|
93
|
-
log.success(`Created ${c.bold}capman.config.js${c.reset}`)
|
|
94
|
-
log.info(`Edit it with your app's capabilities, then run:`)
|
|
95
|
-
console.log(`\n node bin/capman.js generate\n`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function cmdGenerate() {
|
|
99
|
-
header()
|
|
100
|
-
const { generate, loadConfig, writeManifest, validate, generateStarterConfig, parseOpenAPI } = requireSrc()
|
|
101
|
-
|
|
102
|
-
const fromFlag = getFlag('--from')
|
|
103
|
-
const aiFlag = flags.includes('--ai')
|
|
104
|
-
const outPath = getFlag('--out') ?? 'manifest.json'
|
|
105
|
-
const configOut = getFlag('--config-out') ?? 'capman.config.js'
|
|
106
|
-
|
|
107
|
-
// ── Path 1: OpenAPI parser ─────────────────────────────────────────────────
|
|
108
|
-
if (fromFlag) {
|
|
109
|
-
log.info(`Parsing OpenAPI spec: ${fromFlag}`)
|
|
110
|
-
let result
|
|
111
|
-
try {
|
|
112
|
-
result = await parseOpenAPI(fromFlag)
|
|
113
|
-
} catch (e) {
|
|
114
|
-
log.error(e.message)
|
|
115
|
-
process.exit(1)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const { config, stats } = result
|
|
119
|
-
|
|
120
|
-
log.success(`Parsed ${stats.total} capabilities from spec`)
|
|
121
|
-
if (stats.skipped > 0) {
|
|
122
|
-
log.info(`Skipped ${stats.skipped} operations (insufficient info)`)
|
|
123
|
-
}
|
|
124
|
-
if (stats.warnings.length) {
|
|
125
|
-
stats.warnings.forEach(w => log.warn(w))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Write capman.config.js
|
|
129
|
-
const configContent = `// Auto-generated by capman from OpenAPI spec
|
|
130
|
-
// Review and adjust before committing
|
|
131
|
-
|
|
132
|
-
module.exports = ${JSON.stringify(config, null, 2)
|
|
133
|
-
.replace(/"([^"]+)":/g, '$1:')
|
|
134
|
-
.replace(/"/g, "'")
|
|
135
|
-
}
|
|
136
|
-
`
|
|
137
|
-
const fs = require('fs')
|
|
138
|
-
fs.writeFileSync(configOut, configContent)
|
|
139
|
-
log.success(`Config written to ${configOut}`)
|
|
140
|
-
|
|
141
|
-
// Also generate manifest.json
|
|
142
|
-
try {
|
|
143
|
-
const manifest = generate(config)
|
|
144
|
-
writeManifest(manifest, outPath)
|
|
145
|
-
log.success(`Manifest written to ${outPath}`)
|
|
146
|
-
log.info(`${manifest.capabilities.length} capabilities registered`)
|
|
147
|
-
} catch (e) {
|
|
148
|
-
log.warn(`Could not generate manifest: ${e.message}`)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log()
|
|
152
|
-
console.log(` ${c.gray}Next steps:${c.reset}`)
|
|
153
|
-
console.log(` 1. Review ${c.teal}${configOut}${c.reset} — adjust descriptions and examples`)
|
|
154
|
-
console.log(` 2. Run ${c.teal}npx capman validate${c.reset} to check your manifest`)
|
|
155
|
-
console.log(` 3. Run ${c.teal}npx capman demo${c.reset} to see it in action`)
|
|
156
|
-
console.log()
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Path 2: LLM-assisted ───────────────────────────────────────────────────
|
|
161
|
-
if (aiFlag) {
|
|
162
|
-
log.info('LLM-assisted manifest generation')
|
|
163
|
-
console.log()
|
|
164
|
-
|
|
165
|
-
const readline = require('readline')
|
|
166
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
167
|
-
|
|
168
|
-
const ask = (q) => new Promise(resolve => rl.question(q, resolve))
|
|
169
|
-
|
|
170
|
-
console.log(` ${c.gray}Describe your app and its main capabilities.${c.reset}`)
|
|
171
|
-
console.log(` ${c.gray}Example: "A CRM app. Users can create contacts, log calls,`)
|
|
172
|
-
console.log(` view pipeline stages. Admins can manage teams and billing."${c.reset}\n`)
|
|
173
|
-
|
|
174
|
-
const description = await ask(` ${c.teal}>${c.reset} `)
|
|
175
|
-
rl.close()
|
|
176
|
-
|
|
177
|
-
if (!description.trim()) {
|
|
178
|
-
log.error('No description provided.')
|
|
179
|
-
process.exit(1)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Detect LLM provider from env
|
|
183
|
-
const apiKey = process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.OPENROUTER_API_KEY
|
|
184
|
-
const provider =
|
|
185
|
-
process.env.ANTHROPIC_API_KEY ? 'anthropic' :
|
|
186
|
-
process.env.OPENAI_API_KEY ? 'openai' :
|
|
187
|
-
process.env.OPENROUTER_API_KEY ? 'openrouter' : null
|
|
188
|
-
|
|
189
|
-
if (!apiKey || !provider) {
|
|
190
|
-
log.error('No LLM API key found.')
|
|
191
|
-
console.log(` Set one of: ${c.teal}ANTHROPIC_API_KEY${c.reset}, ${c.teal}OPENAI_API_KEY${c.reset}, or ${c.teal}OPENROUTER_API_KEY${c.reset}`)
|
|
192
|
-
process.exit(1)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
log.info(`Using ${provider} to generate manifest...`)
|
|
196
|
-
|
|
197
|
-
const prompt = buildAIPrompt(description)
|
|
198
|
-
let raw
|
|
199
|
-
try {
|
|
200
|
-
raw = await callLLM(provider, apiKey, prompt)
|
|
201
|
-
} catch (e) {
|
|
202
|
-
log.error(`LLM call failed: ${e.message}`)
|
|
203
|
-
process.exit(1)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Parse response
|
|
207
|
-
let config
|
|
208
|
-
try {
|
|
209
|
-
const clean = raw.replace(/```json|```javascript|```js|```/g, '').trim()
|
|
210
|
-
const match = clean.match(/module\.exports\s*=\s*(\{[\s\S]+\})/)
|
|
211
|
-
const jsonStr = match ? match[1] : clean
|
|
212
|
-
config = JSON.parse(jsonStr)
|
|
213
|
-
} catch (e) {
|
|
214
|
-
log.error('Could not parse LLM response. Try again or use --from with an OpenAPI spec.')
|
|
215
|
-
console.log(`\n Raw response:\n${raw.slice(0, 500)}`)
|
|
216
|
-
process.exit(1)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Write config
|
|
220
|
-
const fs = require('fs')
|
|
221
|
-
const configContent = `// Auto-generated by capman AI\n// Review before committing\n\nmodule.exports = ${JSON.stringify(config, null, 2)}\n`
|
|
222
|
-
fs.writeFileSync(configOut, configContent)
|
|
223
|
-
log.success(`Config written to ${configOut}`)
|
|
224
|
-
|
|
225
|
-
// Generate manifest
|
|
226
|
-
try {
|
|
227
|
-
const manifest = generate(config)
|
|
228
|
-
writeManifest(manifest, outPath)
|
|
229
|
-
log.success(`Manifest written to ${outPath}`)
|
|
230
|
-
log.info(`${manifest.capabilities.length} capabilities generated`)
|
|
231
|
-
} catch (e) {
|
|
232
|
-
log.warn(`Manifest generation failed — review your config: ${e.message}`)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
console.log()
|
|
236
|
-
console.log(` ${c.gray}Review ${configOut} — the AI may have missed things.${c.reset}`)
|
|
237
|
-
console.log(` ${c.teal}npx capman validate${c.reset} ${c.gray}→ check for errors${c.reset}`)
|
|
238
|
-
console.log(` ${c.teal}npx capman inspect${c.reset} ${c.gray}→ see all capabilities${c.reset}`)
|
|
239
|
-
console.log()
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ── Path 3: Manual (existing behaviour) ───────────────────────────────────
|
|
244
|
-
const configPath = getFlag('--config')
|
|
245
|
-
log.info('Loading config...')
|
|
246
|
-
let config
|
|
247
|
-
try {
|
|
248
|
-
config = loadConfig(configPath)
|
|
249
|
-
} catch (e) {
|
|
250
|
-
log.error(e.message)
|
|
251
|
-
process.exit(1)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const outManifestPath = getFlag('--out') ?? 'manifest.json'
|
|
255
|
-
log.info(`Generating manifest for ${config.app}...`)
|
|
256
|
-
|
|
257
|
-
let manifest
|
|
258
|
-
try {
|
|
259
|
-
manifest = generate(config)
|
|
260
|
-
} catch (e) {
|
|
261
|
-
log.error(`Generation failed: ${e.message}`)
|
|
262
|
-
process.exit(1)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const validation = validate(manifest)
|
|
266
|
-
if (!validation.valid) {
|
|
267
|
-
validation.errors.forEach(e => log.error(e))
|
|
268
|
-
process.exit(1)
|
|
269
|
-
}
|
|
270
|
-
if (validation.warnings.length) {
|
|
271
|
-
validation.warnings.forEach(w => log.warn(w))
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
writeManifest(manifest, outManifestPath)
|
|
275
|
-
log.success(`Manifest written to ${outManifestPath}`)
|
|
276
|
-
log.info(`${manifest.capabilities.length} capabilities registered`)
|
|
277
|
-
console.log()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ─── AI prompt builder ─────────────────────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
function buildAIPrompt(description) {
|
|
283
|
-
return `You are helping generate a capman capability manifest config.
|
|
284
|
-
|
|
285
|
-
The user's app description:
|
|
286
|
-
${JSON.stringify({ app_description: description })}
|
|
287
|
-
|
|
288
|
-
Generate a valid capman config as a JSON object (not module.exports, just the raw JSON object).
|
|
289
|
-
|
|
290
|
-
The config must follow this exact structure:
|
|
291
|
-
{
|
|
292
|
-
"app": "app-name-in-kebab-case",
|
|
293
|
-
"baseUrl": "https://api.your-app.com",
|
|
294
|
-
"capabilities": [
|
|
295
|
-
{
|
|
296
|
-
"id": "snake_case_id",
|
|
297
|
-
"name": "Human readable name",
|
|
298
|
-
"description": "What this capability does (min 10 chars)",
|
|
299
|
-
"examples": ["Example query 1", "Example query 2", "Example query 3"],
|
|
300
|
-
"params": [
|
|
301
|
-
{
|
|
302
|
-
"name": "param_name",
|
|
303
|
-
"description": "What this param is",
|
|
304
|
-
"required": true,
|
|
305
|
-
"source": "user_query"
|
|
306
|
-
}
|
|
307
|
-
],
|
|
308
|
-
"returns": ["resource_name"],
|
|
309
|
-
"resolver": {
|
|
310
|
-
"type": "api",
|
|
311
|
-
"endpoints": [{ "method": "GET", "path": "/resource/{param_name}" }]
|
|
312
|
-
},
|
|
313
|
-
"privacy": { "level": "public" }
|
|
314
|
-
}
|
|
315
|
-
]
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
Rules:
|
|
319
|
-
- privacy.level must be "public", "user_owned", or "admin"
|
|
320
|
-
- resolver.type must be "api", "nav", or "hybrid"
|
|
321
|
-
- method must be "GET", "POST", "PUT", "PATCH", or "DELETE"
|
|
322
|
-
- source must be "user_query", "session", "context", or "static"
|
|
323
|
-
- Generate 3-8 capabilities based on the description
|
|
324
|
-
- Each capability needs at least 2 examples
|
|
325
|
-
- Respond ONLY with the raw JSON object — no markdown, no explanation`
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ─── LLM caller ───────────────────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
async function callLLM(provider, apiKey, prompt) {
|
|
331
|
-
if (provider === 'anthropic') {
|
|
332
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
333
|
-
method: 'POST',
|
|
334
|
-
headers: {
|
|
335
|
-
'Content-Type': 'application/json',
|
|
336
|
-
'x-api-key': apiKey,
|
|
337
|
-
'anthropic-version': '2023-06-01',
|
|
338
|
-
},
|
|
339
|
-
body: JSON.stringify({
|
|
340
|
-
model: 'claude-sonnet-4-20250514',
|
|
341
|
-
max_tokens: 4000,
|
|
342
|
-
messages: [{ role: 'user', content: prompt }],
|
|
343
|
-
}),
|
|
344
|
-
})
|
|
345
|
-
const data = await res.json()
|
|
346
|
-
if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
|
|
347
|
-
return data.content[0].text
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (provider === 'openai') {
|
|
351
|
-
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
352
|
-
method: 'POST',
|
|
353
|
-
headers: {
|
|
354
|
-
'Content-Type': 'application/json',
|
|
355
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
356
|
-
},
|
|
357
|
-
body: JSON.stringify({
|
|
358
|
-
model: 'gpt-4o-mini',
|
|
359
|
-
max_tokens: 4000,
|
|
360
|
-
messages: [{ role: 'user', content: prompt }],
|
|
361
|
-
}),
|
|
362
|
-
})
|
|
363
|
-
const data = await res.json()
|
|
364
|
-
if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
|
|
365
|
-
return data.choices[0].message.content
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (provider === 'openrouter') {
|
|
369
|
-
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
370
|
-
method: 'POST',
|
|
371
|
-
headers: {
|
|
372
|
-
'Content-Type': 'application/json',
|
|
373
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
374
|
-
'HTTP-Referer': 'https://github.com/Hobbydefiningdoctory/capman',
|
|
375
|
-
},
|
|
376
|
-
body: JSON.stringify({
|
|
377
|
-
model: 'openai/gpt-oss-120b:free',
|
|
378
|
-
max_tokens: 4000,
|
|
379
|
-
messages: [{ role: 'user', content: prompt }],
|
|
380
|
-
provider: { order: ['open-inference'], allow_fallbacks: true },
|
|
381
|
-
}),
|
|
382
|
-
})
|
|
383
|
-
const data = await res.json()
|
|
384
|
-
if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
|
|
385
|
-
return data.choices[0].message.content
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
throw new Error(`Unknown provider: ${provider}`)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function cmdValidate() {
|
|
392
|
-
header()
|
|
393
|
-
const { readManifest, validate } = requireSrc()
|
|
394
|
-
|
|
395
|
-
const manifestPath = getFlag('--manifest') ?? 'manifest.json'
|
|
396
|
-
let manifest
|
|
397
|
-
try {
|
|
398
|
-
manifest = readManifest(manifestPath)
|
|
399
|
-
} catch (e) {
|
|
400
|
-
log.error(e.message)
|
|
401
|
-
process.exit(1)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
log.info(`Validating ${c.bold}${manifestPath}${c.reset}...`)
|
|
405
|
-
const result = validate(manifest)
|
|
406
|
-
log.blank()
|
|
407
|
-
|
|
408
|
-
for (const w of result.warnings) log.warn(w)
|
|
409
|
-
for (const e of result.errors) log.error(e)
|
|
410
|
-
|
|
411
|
-
if (result.valid) {
|
|
412
|
-
log.success(`${manifest.capabilities.length} capabilities — all valid`)
|
|
413
|
-
} else {
|
|
414
|
-
log.error(`${result.errors.length} error(s) found.`)
|
|
415
|
-
process.exit(1)
|
|
416
|
-
}
|
|
417
|
-
console.log()
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function cmdInspect() {
|
|
421
|
-
header()
|
|
422
|
-
const { readManifest } = requireSrc()
|
|
423
|
-
|
|
424
|
-
const manifestPath = getFlag('--manifest') ?? 'manifest.json'
|
|
425
|
-
let manifest
|
|
426
|
-
try {
|
|
427
|
-
manifest = readManifest(manifestPath)
|
|
428
|
-
} catch (e) {
|
|
429
|
-
log.error(e.message)
|
|
430
|
-
process.exit(1)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
console.log(`${c.bold} App:${c.reset} ${manifest.app}`)
|
|
434
|
-
console.log(`${c.bold} Generated:${c.reset} ${manifest.generatedAt}`)
|
|
435
|
-
console.log(`${c.bold} Capabilities:${c.reset} ${manifest.capabilities.length}`)
|
|
436
|
-
console.log()
|
|
437
|
-
|
|
438
|
-
for (const cap of manifest.capabilities) {
|
|
439
|
-
const col = cap.resolver.type === 'api' ? c.teal : cap.resolver.type === 'nav' ? c.teal : c.yellow
|
|
440
|
-
console.log(` ${c.bold}${cap.name}${c.reset} ${col}[${cap.resolver.type}]${c.reset} ${c.gray}${cap.privacy.level}${c.reset}`)
|
|
441
|
-
console.log(` ${c.gray}id: ${cap.id}${c.reset}`)
|
|
442
|
-
console.log(` ${cap.description}`)
|
|
443
|
-
if (cap.examples?.length) {
|
|
444
|
-
console.log(` ${c.gray}e.g. "${cap.examples[0]}"${c.reset}`)
|
|
445
|
-
}
|
|
446
|
-
console.log()
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function cmdDemo() {
|
|
451
|
-
header()
|
|
452
|
-
const { generate, match } = requireSrc()
|
|
453
|
-
|
|
454
|
-
// ── Title ──────────────────────────────────────────────────────────────────
|
|
455
|
-
console.log(`${c.bold} Turn natural language into reliable, explainable backend actions.${c.reset}`)
|
|
456
|
-
console.log(`${c.gray} ─────────────────────────────────────────${c.reset}\n`)
|
|
457
|
-
|
|
458
|
-
// ── Manifest ───────────────────────────────────────────────────────────────
|
|
459
|
-
const config = {
|
|
460
|
-
app: 'demo-store',
|
|
461
|
-
baseUrl: 'https://api.demo-store.com',
|
|
462
|
-
capabilities: [
|
|
463
|
-
{
|
|
464
|
-
id: 'check_product_availability',
|
|
465
|
-
name: 'Check product availability',
|
|
466
|
-
description: 'Check stock and pricing for a product by name or ID.',
|
|
467
|
-
examples: [
|
|
468
|
-
'Is the blue jacket in stock?',
|
|
469
|
-
'Check stock for blue jacket',
|
|
470
|
-
'Check availability for blue jacket',
|
|
471
|
-
'Product availability for jacket',
|
|
472
|
-
],
|
|
473
|
-
params: [
|
|
474
|
-
{ name: 'product', description: 'Product name or ID', required: true, source: 'user_query' }
|
|
475
|
-
],
|
|
476
|
-
returns: ['stock', 'price', 'variants'],
|
|
477
|
-
resolver: { type: 'api', endpoints: [{ method: 'GET', path: '/products/{product}/availability' }] },
|
|
478
|
-
privacy: { level: 'public' },
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
id: 'get_order_status',
|
|
482
|
-
name: 'Get order status',
|
|
483
|
-
description: 'Retrieve the current status and tracking info for an order by order ID.',
|
|
484
|
-
examples: [
|
|
485
|
-
'Where is my order?',
|
|
486
|
-
'Track order 1234',
|
|
487
|
-
'What is the status of my purchase?',
|
|
488
|
-
],
|
|
489
|
-
params: [
|
|
490
|
-
{ name: 'order_id', description: 'Order ID', required: true, source: 'user_query' }
|
|
491
|
-
],
|
|
492
|
-
returns: ['status', 'tracking', 'estimated_delivery'],
|
|
493
|
-
resolver: { type: 'api', endpoints: [{ method: 'GET', path: '/orders/{order_id}' }] },
|
|
494
|
-
privacy: { level: 'user_owned' },
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
id: 'navigate_to_screen',
|
|
498
|
-
name: 'Navigate to screen',
|
|
499
|
-
description: 'Route the user to a specific page in the store.',
|
|
500
|
-
examples: [
|
|
501
|
-
'Take me to cart',
|
|
502
|
-
'Open cart',
|
|
503
|
-
'Go to checkout',
|
|
504
|
-
'Navigate to account',
|
|
505
|
-
],
|
|
506
|
-
params: [
|
|
507
|
-
{ name: 'destination', description: 'Target screen', required: true, source: 'user_query' }
|
|
508
|
-
],
|
|
509
|
-
returns: ['deep_link'],
|
|
510
|
-
resolver: { type: 'nav', destination: '/{destination}' },
|
|
511
|
-
privacy: { level: 'public' },
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const manifest = generate(config)
|
|
517
|
-
|
|
518
|
-
console.log(`${c.gray} app:${c.reset} ${c.bold}${config.app}${c.reset}`)
|
|
519
|
-
console.log(`${c.gray} capabilities:${c.reset} ${manifest.capabilities.length}`)
|
|
520
|
-
console.log(`${c.gray} matcher:${c.reset} keyword (no LLM, no API key needed)\n`)
|
|
521
|
-
|
|
522
|
-
// ── Queries ────────────────────────────────────────────────────────────────
|
|
523
|
-
const queries = [
|
|
524
|
-
{ text: 'Check availability for blue jacket', expectMatch: true },
|
|
525
|
-
{ text: 'Track order 1234', expectMatch: true },
|
|
526
|
-
{ text: 'Go to cart', expectMatch: true },
|
|
527
|
-
{ text: 'Is the website down?', expectMatch: false },
|
|
528
|
-
]
|
|
529
|
-
|
|
530
|
-
let passed = 0
|
|
531
|
-
let outOfScope = 0
|
|
532
|
-
|
|
533
|
-
for (const q of queries) {
|
|
534
|
-
const t0 = Date.now()
|
|
535
|
-
const result = match(q.text, manifest)
|
|
536
|
-
const ms = Date.now() - t0
|
|
537
|
-
|
|
538
|
-
console.log(`${c.gray} ────────────────────────────────────────${c.reset}`)
|
|
539
|
-
console.log()
|
|
540
|
-
|
|
541
|
-
// ── 1. QUERY ──────────────────────────────────────────────────────────
|
|
542
|
-
console.log(` ${c.bold}QUERY${c.reset}`)
|
|
543
|
-
console.log(` "${c.bold}${q.text}${c.reset}"\n`)
|
|
544
|
-
|
|
545
|
-
if (!result.capability) {
|
|
546
|
-
outOfScope++
|
|
547
|
-
|
|
548
|
-
// ── MATCH (out of scope) ─────────────────────────────────────────
|
|
549
|
-
console.log(` ${c.bold}MATCH${c.reset}`)
|
|
550
|
-
console.log(` ${c.yellow}○ OUT_OF_SCOPE${c.reset} — no capability handles this query\n`)
|
|
551
|
-
|
|
552
|
-
// ── EXECUTION ────────────────────────────────────────────────────
|
|
553
|
-
console.log(` ${c.bold}EXECUTION${c.reset}`)
|
|
554
|
-
console.log(` ${c.gray}[1] keyword_match no match ${ms}ms${c.reset}\n`)
|
|
555
|
-
|
|
556
|
-
// ── RESULT ───────────────────────────────────────────────────────
|
|
557
|
-
console.log(` ${c.bold}RESULT${c.reset}`)
|
|
558
|
-
console.log(` ${c.yellow}No action taken — query is outside manifest scope${c.reset}\n`)
|
|
559
|
-
|
|
560
|
-
// ── EXPLANATION ──────────────────────────────────────────────────
|
|
561
|
-
console.log(` ${c.bold}EXPLANATION${c.reset}`)
|
|
562
|
-
if (result.candidates.length) {
|
|
563
|
-
const best = result.candidates.sort((a, b) => b.score - a.score)[0]
|
|
564
|
-
console.log(` ${c.gray}Closest capability was "${best.capabilityId}" (${best.score}%) —`)
|
|
565
|
-
console.log(` below the 50% confidence threshold. Correctly rejected.${c.reset}`)
|
|
566
|
-
}
|
|
567
|
-
console.log()
|
|
568
|
-
continue
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
passed++
|
|
572
|
-
|
|
573
|
-
// Build API call or nav target
|
|
574
|
-
let actionLine = ''
|
|
575
|
-
if (result.capability.resolver.type === 'api') {
|
|
576
|
-
const endpoint = result.capability.resolver.endpoints[0]
|
|
577
|
-
let path = endpoint.path
|
|
578
|
-
for (const [k, v] of Object.entries(result.extractedParams)) {
|
|
579
|
-
if (v) path = path.replace(`{${k}}`, String(v))
|
|
580
|
-
}
|
|
581
|
-
actionLine = `${endpoint.method} ${config.baseUrl}${path}`
|
|
582
|
-
} else if (result.capability.resolver.type === 'nav') {
|
|
583
|
-
let dest = result.capability.resolver.destination
|
|
584
|
-
for (const [k, v] of Object.entries(result.extractedParams)) {
|
|
585
|
-
if (v) dest = dest.replace(`{${k}}`, String(v))
|
|
586
|
-
}
|
|
587
|
-
actionLine = `navigate → ${dest}`
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Candidates
|
|
591
|
-
const sorted = [...result.candidates].sort((a, b) => b.score - a.score)
|
|
592
|
-
const winner = sorted[0]
|
|
593
|
-
const runners = sorted.slice(1).filter(r => r.score > 0)
|
|
594
|
-
|
|
595
|
-
// Extracted params
|
|
596
|
-
const paramEntries = Object.entries(result.extractedParams).filter(([, v]) => v !== null)
|
|
597
|
-
|
|
598
|
-
// ── 2. MATCH ──────────────────────────────────────────────────────────
|
|
599
|
-
console.log(` ${c.bold}MATCH${c.reset}`)
|
|
600
|
-
console.log(` ${c.green}✓ ${result.capability.id}${c.reset}`)
|
|
601
|
-
console.log(` ${c.gray}intent: ${c.reset}${result.intent}`)
|
|
602
|
-
console.log(` ${c.gray}confidence: ${c.reset}${c.bold}${result.confidence}%${c.reset}`)
|
|
603
|
-
console.log(` ${c.gray}privacy: ${c.reset}${result.capability.privacy.level}`)
|
|
604
|
-
if (paramEntries.length) {
|
|
605
|
-
const pStr = paramEntries.map(([k, v]) => `${k}=${v}`).join(', ')
|
|
606
|
-
console.log(` ${c.gray}params: ${c.reset}${pStr}`)
|
|
607
|
-
}
|
|
608
|
-
console.log()
|
|
609
|
-
|
|
610
|
-
// ── 3. EXECUTION ──────────────────────────────────────────────────────
|
|
611
|
-
console.log(` ${c.bold}EXECUTION${c.reset}`)
|
|
612
|
-
console.log(` ${c.gray}[1] cache_check miss 0ms${c.reset}`)
|
|
613
|
-
console.log(` ${c.gray}[2]${c.reset} keyword_match ${c.green}pass${c.reset} ${ms}ms confidence: ${result.confidence}%`)
|
|
614
|
-
console.log(` ${c.gray}[3] privacy_check pass 0ms level: ${result.capability.privacy.level}${c.reset}`)
|
|
615
|
-
console.log(` ${c.gray}[4] resolve pass ${ms}ms via ${result.capability.resolver.type}${c.reset}`)
|
|
616
|
-
console.log()
|
|
617
|
-
|
|
618
|
-
// ── 4. RESULT ─────────────────────────────────────────────────────────
|
|
619
|
-
console.log(` ${c.bold}RESULT${c.reset}`)
|
|
620
|
-
console.log(` ${c.green}${actionLine}${c.reset}`)
|
|
621
|
-
console.log()
|
|
622
|
-
|
|
623
|
-
// ── 5. EXPLANATION ────────────────────────────────────────────────────
|
|
624
|
-
console.log(` ${c.bold}EXPLANATION${c.reset}`)
|
|
625
|
-
console.log(` ${c.gray}Why "${winner.capabilityId}"?${c.reset}`)
|
|
626
|
-
console.log(` ${c.gray} scored ${winner.score}% — highest match against examples and description${c.reset}`)
|
|
627
|
-
if (runners.length) {
|
|
628
|
-
const rStr = runners.map(r => `${r.capabilityId} (${r.score}%)`).join(', ')
|
|
629
|
-
console.log(` ${c.gray} rejected: ${rStr}${c.reset}`)
|
|
630
|
-
}
|
|
631
|
-
if (paramEntries.length) {
|
|
632
|
-
const pStr = paramEntries.map(([k, v]) => `${k}="${v}"`).join(', ')
|
|
633
|
-
console.log(` ${c.gray} extracted from query: ${pStr}${c.reset}`)
|
|
634
|
-
}
|
|
635
|
-
console.log()
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// ── Summary ────────────────────────────────────────────────────────────────
|
|
639
|
-
console.log(`${c.gray} ────────────────────────────────────────${c.reset}\n`)
|
|
640
|
-
console.log(` ${c.green}${passed} matched${c.reset} ${c.gray}·${c.reset} ${c.yellow}${outOfScope} out of scope${c.reset} ${c.gray}·${c.reset} ${manifest.capabilities.length} capabilities ${c.gray}·${c.reset} no LLM required\n`)
|
|
641
|
-
console.log(` ${c.bold}Every query above is fully traced.${c.reset}`)
|
|
642
|
-
console.log(` ${c.gray}You saw what matched, why it matched, how it executed, what it called,`)
|
|
643
|
-
console.log(` and which alternatives were considered and rejected.${c.reset}`)
|
|
644
|
-
console.log(` ${c.gray}No black box. No guessing. Full control.\n${c.reset}`)
|
|
645
|
-
console.log(` ${c.gray}Next steps:${c.reset}`)
|
|
646
|
-
console.log(` ${c.teal}npx capman init${c.reset} ${c.gray}→ define your app's capabilities${c.reset}`)
|
|
647
|
-
console.log(` ${c.teal}npx capman generate${c.reset} ${c.gray}→ generate manifest.json${c.reset}`)
|
|
648
|
-
console.log(` ${c.teal}npx capman run "your query" --debug${c.reset} ${c.gray}→ trace any query live${c.reset}`)
|
|
649
|
-
console.log(` ${c.teal}npm install capman${c.reset} ${c.gray}→ use in your AI agent${c.reset}\n`)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function cmdRun() {
|
|
653
|
-
header()
|
|
654
|
-
const query = args[1]
|
|
655
|
-
const debug = flags.includes('--debug')
|
|
656
|
-
const manifestPath = getFlag('--manifest') ?? 'manifest.json'
|
|
657
|
-
|
|
658
|
-
if (!query) {
|
|
659
|
-
log.error('Please provide a query. Example: node bin/capman.js run "show me articles"')
|
|
660
|
-
process.exit(1)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const { readManifest, match } = requireSrc()
|
|
664
|
-
|
|
665
|
-
let manifest
|
|
666
|
-
try {
|
|
667
|
-
manifest = readManifest(manifestPath)
|
|
668
|
-
} catch (e) {
|
|
669
|
-
log.error(e.message)
|
|
670
|
-
process.exit(1)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
log.info(`Query: "${query}"`)
|
|
674
|
-
log.blank()
|
|
675
|
-
|
|
676
|
-
const result = match(query, manifest)
|
|
677
|
-
|
|
678
|
-
if (result.capability) {
|
|
679
|
-
console.log(` ${c.green}✓${c.reset} Matched: ${c.bold}${result.capability.id}${c.reset}`)
|
|
680
|
-
console.log(` Intent: ${result.intent}`)
|
|
681
|
-
console.log(` Confidence: ${result.confidence}%`)
|
|
682
|
-
console.log(` Resolver: ${result.capability.resolver.type}`)
|
|
683
|
-
|
|
684
|
-
if (Object.keys(result.extractedParams).length > 0) {
|
|
685
|
-
const params = Object.entries(result.extractedParams)
|
|
686
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
687
|
-
.join(', ')
|
|
688
|
-
console.log(` Params: ${params}`)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (debug && result.candidates?.length) {
|
|
692
|
-
log.blank()
|
|
693
|
-
console.log(` ${c.gray}── All candidates:${c.reset}`)
|
|
694
|
-
result.candidates
|
|
695
|
-
.sort((a, b) => b.score - a.score)
|
|
696
|
-
.forEach(c2 => {
|
|
697
|
-
const marker = c2.matched ? c.green + '✓' : c.gray + '○'
|
|
698
|
-
console.log(` ${marker}${c.reset} ${c2.capabilityId}: ${c2.score}%`)
|
|
699
|
-
})
|
|
700
|
-
}
|
|
701
|
-
} else {
|
|
702
|
-
console.log(` ${c.yellow}○${c.reset} OUT_OF_SCOPE — no capability matched`)
|
|
703
|
-
console.log(` ${c.gray}${result.reasoning}${c.reset}`)
|
|
704
|
-
|
|
705
|
-
if (debug && result.candidates?.length) {
|
|
706
|
-
log.blank()
|
|
707
|
-
console.log(` ${c.gray}── All candidates:${c.reset}`)
|
|
708
|
-
result.candidates
|
|
709
|
-
.sort((a, b) => b.score - a.score)
|
|
710
|
-
.slice(0, 5)
|
|
711
|
-
.forEach(c2 => {
|
|
712
|
-
console.log(` ${c.gray}○ ${c2.capabilityId}: ${c2.score}%${c.reset}`)
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
console.log()
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
(async () => {
|
|
6
|
+
;(async () => {
|
|
720
7
|
switch (command) {
|
|
721
|
-
case 'init':
|
|
722
|
-
case 'generate': await
|
|
723
|
-
case 'validate':
|
|
724
|
-
case 'inspect':
|
|
725
|
-
case 'demo':
|
|
726
|
-
case 'run':
|
|
8
|
+
case 'init': require('./lib/cmd-init')(); break
|
|
9
|
+
case 'generate': await require('./lib/cmd-generate')(); break
|
|
10
|
+
case 'validate': require('./lib/cmd-validate')(); break
|
|
11
|
+
case 'inspect': require('./lib/cmd-inspect')(); break
|
|
12
|
+
case 'demo': require('./lib/cmd-demo')(); break
|
|
13
|
+
case 'run': require('./lib/cmd-run')(); break
|
|
14
|
+
case 'explain': await require('./lib/cmd-explain')(); break
|
|
727
15
|
case undefined:
|
|
728
16
|
case '--help':
|
|
729
|
-
case '-h':
|
|
17
|
+
case '-h': require('./lib/cmd-help')(); break
|
|
730
18
|
default:
|
|
731
19
|
header()
|
|
732
20
|
log.error(`Unknown command: ${command}`)
|
|
733
|
-
console.log(` Run:
|
|
21
|
+
console.log(` Run: ${c.teal}capman --help${c.reset}\n`)
|
|
734
22
|
process.exit(1)
|
|
735
23
|
}
|
|
736
24
|
})()
|
|
737
|
-
|