capman 0.4.1 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/CODEBASE.md +391 -0
  3. package/README.md +76 -97
  4. package/bin/capman.js +21 -405
  5. package/bin/lib/cmd-demo.js +180 -0
  6. package/bin/lib/cmd-explain.js +72 -0
  7. package/bin/lib/cmd-generate.js +280 -0
  8. package/bin/lib/cmd-help.js +26 -0
  9. package/bin/lib/cmd-init.js +19 -0
  10. package/bin/lib/cmd-inspect.js +33 -0
  11. package/bin/lib/cmd-run.js +71 -0
  12. package/bin/lib/cmd-validate.js +32 -0
  13. package/bin/lib/shared.js +70 -0
  14. package/dist/cjs/engine.d.ts +58 -1
  15. package/dist/cjs/engine.d.ts.map +1 -1
  16. package/dist/cjs/engine.js +307 -12
  17. package/dist/cjs/engine.js.map +1 -1
  18. package/dist/cjs/generator.d.ts.map +1 -1
  19. package/dist/cjs/generator.js +4 -0
  20. package/dist/cjs/generator.js.map +1 -1
  21. package/dist/cjs/index.d.ts +13 -17
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/index.js +12 -7
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/matcher.d.ts.map +1 -1
  26. package/dist/cjs/matcher.js +19 -25
  27. package/dist/cjs/matcher.js.map +1 -1
  28. package/dist/cjs/parser.d.ts +11 -0
  29. package/dist/cjs/parser.d.ts.map +1 -0
  30. package/dist/cjs/parser.js +304 -0
  31. package/dist/cjs/parser.js.map +1 -0
  32. package/dist/cjs/types.d.ts +27 -0
  33. package/dist/cjs/types.d.ts.map +1 -1
  34. package/dist/cjs/version.d.ts +1 -1
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/esm/cache.d.ts +49 -0
  37. package/dist/esm/engine.d.ts +138 -0
  38. package/dist/esm/engine.js +307 -12
  39. package/dist/esm/generator.d.ts +7 -0
  40. package/dist/esm/generator.js +4 -0
  41. package/dist/esm/index.d.ts +47 -0
  42. package/dist/esm/index.js +6 -4
  43. package/dist/esm/learning.d.ts +55 -0
  44. package/dist/esm/logger.d.ts +21 -0
  45. package/dist/esm/matcher.d.ts +6 -0
  46. package/dist/esm/matcher.js +19 -25
  47. package/dist/esm/parser.d.ts +10 -0
  48. package/dist/esm/parser.js +267 -0
  49. package/dist/esm/resolver.d.ts +21 -0
  50. package/dist/esm/schema.d.ts +740 -0
  51. package/dist/esm/types.d.ts +136 -0
  52. package/dist/esm/version.d.ts +1 -0
  53. package/dist/esm/version.js +1 -1
  54. package/package.json +5 -3
@@ -0,0 +1,180 @@
1
+ 'use strict'
2
+
3
+ const { header, c, requireSrc } = require('./shared')
4
+
5
+ module.exports = function cmdDemo() {
6
+ header()
7
+ const { generate, match } = requireSrc()
8
+
9
+ console.log(`${c.bold} Turn natural language into reliable, explainable backend actions.${c.reset}`)
10
+ console.log(`${c.gray} ─────────────────────────────────────────${c.reset}\n`)
11
+
12
+ const config = {
13
+ app: 'demo-store',
14
+ baseUrl: 'https://api.demo-store.com',
15
+ capabilities: [
16
+ {
17
+ id: 'check_product_availability',
18
+ name: 'Check product availability',
19
+ description: 'Check stock and pricing for a product by name or ID.',
20
+ examples: [
21
+ 'Is the blue jacket in stock?',
22
+ 'Check stock for blue jacket',
23
+ 'Check availability for blue jacket',
24
+ 'Product availability for jacket',
25
+ ],
26
+ params: [
27
+ { name: 'product', description: 'Product name or ID', required: true, source: 'user_query' },
28
+ ],
29
+ returns: ['stock', 'price', 'variants'],
30
+ resolver: { type: 'api', endpoints: [{ method: 'GET', path: '/products/{product}/availability' }] },
31
+ privacy: { level: 'public' },
32
+ },
33
+ {
34
+ id: 'get_order_status',
35
+ name: 'Get order status',
36
+ description: 'Retrieve the current status and tracking info for an order by order ID.',
37
+ examples: [
38
+ 'Where is my order?',
39
+ 'Track order 1234',
40
+ 'What is the status of my purchase?',
41
+ ],
42
+ params: [
43
+ { name: 'order_id', description: 'Order ID', required: true, source: 'user_query' },
44
+ ],
45
+ returns: ['status', 'tracking', 'estimated_delivery'],
46
+ resolver: { type: 'api', endpoints: [{ method: 'GET', path: '/orders/{order_id}' }] },
47
+ privacy: { level: 'user_owned' },
48
+ },
49
+ {
50
+ id: 'navigate_to_screen',
51
+ name: 'Navigate to screen',
52
+ description: 'Route the user to a specific page in the store.',
53
+ examples: [
54
+ 'Take me to cart',
55
+ 'Open cart',
56
+ 'Go to checkout',
57
+ 'Navigate to account',
58
+ ],
59
+ params: [
60
+ { name: 'destination', description: 'Target screen', required: true, source: 'user_query' },
61
+ ],
62
+ returns: ['deep_link'],
63
+ resolver: { type: 'nav', destination: '/{destination}' },
64
+ privacy: { level: 'public' },
65
+ },
66
+ ],
67
+ }
68
+
69
+ const manifest = generate(config)
70
+
71
+ console.log(`${c.gray} app:${c.reset} ${c.bold}${config.app}${c.reset}`)
72
+ console.log(`${c.gray} capabilities:${c.reset} ${manifest.capabilities.length}`)
73
+ console.log(`${c.gray} matcher:${c.reset} keyword (no LLM, no API key needed)\n`)
74
+
75
+ const queries = [
76
+ { text: 'Check availability for blue jacket', expectMatch: true },
77
+ { text: 'Track order 1234', expectMatch: true },
78
+ { text: 'Go to cart', expectMatch: true },
79
+ { text: 'Is the website down?', expectMatch: false },
80
+ ]
81
+
82
+ let passed = 0
83
+ let outOfScope = 0
84
+
85
+ for (const q of queries) {
86
+ const t0 = Date.now()
87
+ const result = match(q.text, manifest)
88
+ const ms = Date.now() - t0
89
+
90
+ console.log(`${c.gray} ────────────────────────────────────────${c.reset}`)
91
+ console.log()
92
+ console.log(` ${c.bold}QUERY${c.reset}`)
93
+ console.log(` "${c.bold}${q.text}${c.reset}"\n`)
94
+
95
+ if (!result.capability) {
96
+ outOfScope++
97
+ console.log(` ${c.bold}MATCH${c.reset}`)
98
+ console.log(` ${c.yellow}○ OUT_OF_SCOPE${c.reset} — no capability handles this query\n`)
99
+ console.log(` ${c.bold}EXECUTION${c.reset}`)
100
+ console.log(` ${c.gray}[1] keyword_match no match ${ms}ms${c.reset}\n`)
101
+ console.log(` ${c.bold}RESULT${c.reset}`)
102
+ console.log(` ${c.yellow}No action taken — query is outside manifest scope${c.reset}\n`)
103
+ console.log(` ${c.bold}EXPLANATION${c.reset}`)
104
+ if (result.candidates.length) {
105
+ const best = result.candidates.sort((a, b) => b.score - a.score)[0]
106
+ console.log(` ${c.gray}Closest capability was "${best.capabilityId}" (${best.score}%) —`)
107
+ console.log(` below the 50% confidence threshold. Correctly rejected.${c.reset}`)
108
+ }
109
+ console.log()
110
+ continue
111
+ }
112
+
113
+ passed++
114
+
115
+ let actionLine = ''
116
+ if (result.capability.resolver.type === 'api') {
117
+ const endpoint = result.capability.resolver.endpoints[0]
118
+ let p = endpoint.path
119
+ for (const [k, v] of Object.entries(result.extractedParams)) {
120
+ if (v) p = p.replace(`{${k}}`, String(v))
121
+ }
122
+ actionLine = `${endpoint.method} ${config.baseUrl}${p}`
123
+ } else if (result.capability.resolver.type === 'nav') {
124
+ let dest = result.capability.resolver.destination
125
+ for (const [k, v] of Object.entries(result.extractedParams)) {
126
+ if (v) dest = dest.replace(`{${k}}`, String(v))
127
+ }
128
+ actionLine = `navigate → ${dest}`
129
+ }
130
+
131
+ const sorted = [...result.candidates].sort((a, b) => b.score - a.score)
132
+ const winner = sorted[0]
133
+ const runners = sorted.slice(1).filter(r => r.score > 0)
134
+ const paramEntries = Object.entries(result.extractedParams).filter(([, v]) => v !== null)
135
+
136
+ console.log(` ${c.bold}MATCH${c.reset}`)
137
+ console.log(` ${c.green}✓ ${result.capability.id}${c.reset}`)
138
+ console.log(` ${c.gray}intent: ${c.reset}${result.intent}`)
139
+ console.log(` ${c.gray}confidence: ${c.reset}${c.bold}${result.confidence}%${c.reset}`)
140
+ console.log(` ${c.gray}privacy: ${c.reset}${result.capability.privacy.level}`)
141
+ if (paramEntries.length) {
142
+ console.log(` ${c.gray}params: ${c.reset}${paramEntries.map(([k, v]) => `${k}=${v}`).join(', ')}`)
143
+ }
144
+ console.log()
145
+
146
+ console.log(` ${c.bold}EXECUTION${c.reset}`)
147
+ console.log(` ${c.gray}[1] cache_check miss 0ms${c.reset}`)
148
+ console.log(` ${c.gray}[2]${c.reset} keyword_match ${c.green}pass${c.reset} ${ms}ms confidence: ${result.confidence}%`)
149
+ console.log(` ${c.gray}[3] privacy_check pass 0ms level: ${result.capability.privacy.level}${c.reset}`)
150
+ console.log(` ${c.gray}[4] resolve pass ${ms}ms via ${result.capability.resolver.type}${c.reset}`)
151
+ console.log()
152
+
153
+ console.log(` ${c.bold}RESULT${c.reset}`)
154
+ console.log(` ${c.green}${actionLine}${c.reset}`)
155
+ console.log()
156
+
157
+ console.log(` ${c.bold}EXPLANATION${c.reset}`)
158
+ console.log(` ${c.gray}Why "${winner.capabilityId}"?${c.reset}`)
159
+ console.log(` ${c.gray} scored ${winner.score}% — highest match against examples and description${c.reset}`)
160
+ if (runners.length) {
161
+ console.log(` ${c.gray} rejected: ${runners.map(r => `${r.capabilityId} (${r.score}%)`).join(', ')}${c.reset}`)
162
+ }
163
+ if (paramEntries.length) {
164
+ console.log(` ${c.gray} extracted from query: ${paramEntries.map(([k, v]) => `${k}="${v}"`).join(', ')}${c.reset}`)
165
+ }
166
+ console.log()
167
+ }
168
+
169
+ console.log(`${c.gray} ────────────────────────────────────────${c.reset}\n`)
170
+ 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`)
171
+ console.log(` ${c.bold}Every query above is fully traced.${c.reset}`)
172
+ console.log(` ${c.gray}You saw what matched, why it matched, how it executed, what it called,`)
173
+ console.log(` and which alternatives were considered and rejected.${c.reset}`)
174
+ console.log(` ${c.gray}No black box. No guessing. Full control.\n${c.reset}`)
175
+ console.log(` ${c.gray}Next steps:${c.reset}`)
176
+ console.log(` ${c.teal}npx capman init${c.reset} ${c.gray}→ define your app's capabilities${c.reset}`)
177
+ console.log(` ${c.teal}npx capman generate${c.reset} ${c.gray}→ generate manifest.json${c.reset}`)
178
+ console.log(` ${c.teal}npx capman run "your query" --debug${c.reset} ${c.gray}→ trace any query live${c.reset}`)
179
+ console.log(` ${c.teal}npm install capman${c.reset} ${c.gray}→ use in your AI agent${c.reset}\n`)
180
+ }
@@ -0,0 +1,72 @@
1
+ 'use strict'
2
+
3
+ const { header, log, c, args, getFlag, requireSrc } = require('./shared')
4
+
5
+ module.exports = async function cmdExplain() {
6
+ header()
7
+ const query = args[1]
8
+ const manifestPath = getFlag('--manifest') ?? 'manifest.json'
9
+
10
+ if (!query) {
11
+ log.error('Please provide a query.')
12
+ console.log(` Example: npx capman explain "show me articles"\n`)
13
+ process.exit(1)
14
+ }
15
+
16
+ const { readManifest, CapmanEngine } = requireSrc()
17
+
18
+ let manifest
19
+ try {
20
+ manifest = readManifest(manifestPath)
21
+ } catch (e) {
22
+ log.error(e.message)
23
+ process.exit(1)
24
+ }
25
+
26
+ const engine = new CapmanEngine({ manifest, cache: false, learning: false, mode: 'cheap' })
27
+ const result = await engine.explain(query)
28
+
29
+ console.log(`\n ${c.bold}QUERY${c.reset}`)
30
+ console.log(` "${c.bold}${query}${c.reset}"\n`)
31
+
32
+ // ── Match ────────────────────────────────────────────────────────────────
33
+ console.log(` ${c.bold}MATCH${c.reset}`)
34
+ if (result.matched.capability) {
35
+ console.log(` ${c.green}✓ ${result.matched.capability.id}${c.reset}`)
36
+ console.log(` ${c.gray}confidence:${c.reset} ${result.matched.confidence}%`)
37
+ console.log(` ${c.gray}intent:${c.reset} ${result.matched.intent}`)
38
+ } else {
39
+ console.log(` ${c.yellow}○ OUT_OF_SCOPE${c.reset} — no capability matched\n`)
40
+ }
41
+ console.log()
42
+
43
+ // ── Reasoning ────────────────────────────────────────────────────────────
44
+ console.log(` ${c.bold}REASONING${c.reset}`)
45
+ result.matched.reasoning.forEach(r => {
46
+ console.log(` ${c.gray}•${c.reset} ${r}`)
47
+ })
48
+ console.log()
49
+
50
+ // ── All candidates ────────────────────────────────────────────────────────
51
+ console.log(` ${c.bold}ALL CANDIDATES${c.reset}`)
52
+ result.candidates.forEach(cand => {
53
+ const marker = cand.matched ? `${c.green}✓${c.reset}` : `${c.gray}○${c.reset}`
54
+ const scoreColor = cand.score >= 50 ? c.green : c.gray
55
+ console.log(` ${marker} ${cand.capabilityId}`)
56
+ console.log(` ${scoreColor}${cand.score}%${c.reset} ${c.gray}${cand.explanation}${c.reset}`)
57
+ })
58
+ console.log()
59
+
60
+ // ── Would execute ─────────────────────────────────────────────────────────
61
+ console.log(` ${c.bold}WOULD EXECUTE${c.reset}`)
62
+ if (result.wouldExecute.blocked) {
63
+ console.log(` ${c.yellow}✗ Blocked — ${result.wouldExecute.blocked}${c.reset}`)
64
+ } else if (result.wouldExecute.action) {
65
+ console.log(` ${c.green}✓ ${result.wouldExecute.action}${c.reset}`)
66
+ console.log(` ${c.gray}privacy: ${result.wouldExecute.privacy}${c.reset}`)
67
+ } else {
68
+ console.log(` ${c.yellow}○ No action — query is out of scope${c.reset}`)
69
+ }
70
+ console.log()
71
+ console.log(` ${c.gray}${result.durationMs}ms · via ${result.resolvedVia}${c.reset}\n`)
72
+ }
@@ -0,0 +1,280 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const { header, log, c, flags, getFlag, requireSrc } = require('./shared')
5
+
6
+ // ─── AI prompt builder ────────────────────────────────────────────────────────
7
+
8
+ function buildAIPrompt(description) {
9
+ return `You are helping generate a capman capability manifest config.
10
+
11
+ The user's app description:
12
+ ${JSON.stringify({ app_description: description })}
13
+
14
+ Generate a valid capman config as a JSON object (not module.exports, just the raw JSON object).
15
+
16
+ The config must follow this exact structure:
17
+ {
18
+ "app": "app-name-in-kebab-case",
19
+ "baseUrl": "https://api.your-app.com",
20
+ "capabilities": [
21
+ {
22
+ "id": "snake_case_id",
23
+ "name": "Human readable name",
24
+ "description": "What this capability does (min 10 chars)",
25
+ "examples": ["Example query 1", "Example query 2", "Example query 3"],
26
+ "params": [
27
+ {
28
+ "name": "param_name",
29
+ "description": "What this param is",
30
+ "required": true,
31
+ "source": "user_query"
32
+ }
33
+ ],
34
+ "returns": ["resource_name"],
35
+ "resolver": {
36
+ "type": "api",
37
+ "endpoints": [{ "method": "GET", "path": "/resource/{param_name}" }]
38
+ },
39
+ "privacy": { "level": "public" }
40
+ }
41
+ ]
42
+ }
43
+
44
+ Rules:
45
+ - privacy.level must be "public", "user_owned", or "admin"
46
+ - resolver.type must be "api", "nav", or "hybrid"
47
+ - method must be "GET", "POST", "PUT", "PATCH", or "DELETE"
48
+ - source must be "user_query", "session", "context", or "static"
49
+ - Generate 3-8 capabilities based on the description
50
+ - Each capability needs at least 2 examples
51
+ - Respond ONLY with the raw JSON object — no markdown, no explanation`
52
+ }
53
+
54
+ // ─── LLM caller ───────────────────────────────────────────────────────────────
55
+
56
+ async function callLLM(provider, apiKey, prompt) {
57
+ if (provider === 'anthropic') {
58
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'x-api-key': apiKey,
63
+ 'anthropic-version': '2023-06-01',
64
+ },
65
+ body: JSON.stringify({
66
+ model: 'claude-sonnet-4-20250514',
67
+ max_tokens: 4000,
68
+ messages: [{ role: 'user', content: prompt }],
69
+ }),
70
+ })
71
+ const data = await res.json()
72
+ if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
73
+ return data.content[0].text
74
+ }
75
+
76
+ if (provider === 'openai') {
77
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'Authorization': `Bearer ${apiKey}`,
82
+ },
83
+ body: JSON.stringify({
84
+ model: 'gpt-4o-mini',
85
+ max_tokens: 4000,
86
+ messages: [{ role: 'user', content: prompt }],
87
+ }),
88
+ })
89
+ const data = await res.json()
90
+ if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
91
+ return data.choices[0].message.content
92
+ }
93
+
94
+ if (provider === 'openrouter') {
95
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ 'Authorization': `Bearer ${apiKey}`,
100
+ 'HTTP-Referer': 'https://github.com/Hobbydefiningdoctory/capman',
101
+ },
102
+ body: JSON.stringify({
103
+ model: 'openai/gpt-oss-120b:free',
104
+ max_tokens: 4000,
105
+ messages: [{ role: 'user', content: prompt }],
106
+ provider: { order: ['open-inference'], allow_fallbacks: true },
107
+ }),
108
+ })
109
+ const data = await res.json()
110
+ if (!res.ok) throw new Error(data.error?.message ?? res.statusText)
111
+ return data.choices[0].message.content
112
+ }
113
+
114
+ throw new Error(`Unknown provider: ${provider}`)
115
+ }
116
+
117
+ // ─── Command ──────────────────────────────────────────────────────────────────
118
+
119
+ module.exports = async function cmdGenerate() {
120
+ header()
121
+ const { generate, loadConfig, writeManifest, validate, parseOpenAPI } = requireSrc()
122
+
123
+ const fromFlag = getFlag('--from')
124
+ const aiFlag = flags.includes('--ai')
125
+ const outPath = getFlag('--out') ?? 'manifest.json'
126
+ const configOut = getFlag('--config-out') ?? 'capman.config.js'
127
+
128
+ // ── Path 1: OpenAPI parser ───────────────────────────────────────────────
129
+ if (fromFlag) {
130
+ log.info(`Parsing OpenAPI spec: ${fromFlag}`)
131
+ let result
132
+ try {
133
+ result = await parseOpenAPI(fromFlag)
134
+ } catch (e) {
135
+ log.error(e.message)
136
+ process.exit(1)
137
+ }
138
+
139
+ const { config, stats } = result
140
+
141
+ log.success(`Parsed ${stats.total} capabilities from spec`)
142
+ if (stats.skipped > 0) log.info(`Skipped ${stats.skipped} operations (insufficient info)`)
143
+ if (stats.warnings.length) stats.warnings.forEach(w => log.warn(w))
144
+
145
+ const configContent = `// Auto-generated by capman from OpenAPI spec
146
+ // Review and adjust before committing
147
+
148
+ module.exports = ${JSON.stringify(config, null, 2)}
149
+ `
150
+ fs.writeFileSync(configOut, configContent)
151
+ log.success(`Config written to ${configOut}`)
152
+
153
+ try {
154
+ const manifest = generate(config)
155
+ writeManifest(manifest, outPath)
156
+ log.success(`Manifest written to ${outPath}`)
157
+ log.info(`${manifest.capabilities.length} capabilities registered`)
158
+ } catch (e) {
159
+ log.warn(`Could not generate manifest: ${e.message}`)
160
+ }
161
+
162
+ console.log()
163
+ console.log(` ${c.gray}Next steps:${c.reset}`)
164
+ console.log(` 1. Review ${c.teal}${configOut}${c.reset} — adjust descriptions and examples`)
165
+ console.log(` 2. Run ${c.teal}npx capman validate${c.reset} to check your manifest`)
166
+ console.log(` 3. Run ${c.teal}npx capman demo${c.reset} to see it in action`)
167
+ console.log()
168
+ return
169
+ }
170
+
171
+ // ── Path 2: LLM-assisted ─────────────────────────────────────────────────
172
+ if (aiFlag) {
173
+ log.info('LLM-assisted manifest generation')
174
+ console.log()
175
+
176
+ const readline = require('readline')
177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
178
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve))
179
+
180
+ console.log(` ${c.gray}Describe your app and its main capabilities.${c.reset}`)
181
+ console.log(` ${c.gray}Example: "A CRM app. Users can create contacts, log calls,`)
182
+ console.log(` view pipeline stages. Admins can manage teams and billing."${c.reset}\n`)
183
+
184
+ const description = await ask(` ${c.teal}>${c.reset} `)
185
+ rl.close()
186
+
187
+ if (!description.trim()) {
188
+ log.error('No description provided.')
189
+ process.exit(1)
190
+ }
191
+
192
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.OPENROUTER_API_KEY
193
+ const provider =
194
+ process.env.ANTHROPIC_API_KEY ? 'anthropic' :
195
+ process.env.OPENAI_API_KEY ? 'openai' :
196
+ process.env.OPENROUTER_API_KEY ? 'openrouter' : null
197
+
198
+ if (!apiKey || !provider) {
199
+ log.error('No LLM API key found.')
200
+ 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}`)
201
+ process.exit(1)
202
+ }
203
+
204
+ log.info(`Using ${provider} to generate manifest...`)
205
+
206
+ let raw
207
+ try {
208
+ raw = await callLLM(provider, apiKey, buildAIPrompt(description))
209
+ } catch (e) {
210
+ log.error(`LLM call failed: ${e.message}`)
211
+ process.exit(1)
212
+ }
213
+
214
+ let config
215
+ try {
216
+ const clean = raw.replace(/```json|```javascript|```js|```/g, '').trim()
217
+ const match = clean.match(/module\.exports\s*=\s*(\{[\s\S]+\})/)
218
+ config = JSON.parse(match ? match[1] : clean)
219
+ } catch (e) {
220
+ log.error('Could not parse LLM response. Try again or use --from with an OpenAPI spec.')
221
+ console.log(`\n Raw response:\n${raw.slice(0, 500)}`)
222
+ process.exit(1)
223
+ }
224
+
225
+ const configContent = `// Auto-generated by capman AI\n// Review before committing\n\nmodule.exports = ${JSON.stringify(config, null, 2)}\n`
226
+ fs.writeFileSync(configOut, configContent)
227
+ log.success(`Config written to ${configOut}`)
228
+
229
+ try {
230
+ const manifest = generate(config)
231
+ writeManifest(manifest, outPath)
232
+ log.success(`Manifest written to ${outPath}`)
233
+ log.info(`${manifest.capabilities.length} capabilities generated`)
234
+ } catch (e) {
235
+ log.warn(`Manifest generation failed — review your config: ${e.message}`)
236
+ }
237
+
238
+ console.log()
239
+ console.log(` ${c.gray}Review ${configOut} — the AI may have missed things.${c.reset}`)
240
+ console.log(` ${c.teal}npx capman validate${c.reset} ${c.gray}→ check for errors${c.reset}`)
241
+ console.log(` ${c.teal}npx capman inspect${c.reset} ${c.gray}→ see all capabilities${c.reset}`)
242
+ console.log()
243
+ return
244
+ }
245
+
246
+ // ── Path 3: Manual ───────────────────────────────────────────────────────
247
+ const configPath = getFlag('--config')
248
+ log.info('Loading config...')
249
+ let config
250
+ try {
251
+ config = loadConfig(configPath)
252
+ } catch (e) {
253
+ log.error(e.message)
254
+ process.exit(1)
255
+ }
256
+
257
+ log.info(`Generating manifest for ${config.app}...`)
258
+
259
+ let manifest
260
+ try {
261
+ manifest = generate(config)
262
+ } catch (e) {
263
+ log.error(`Generation failed: ${e.message}`)
264
+ process.exit(1)
265
+ }
266
+
267
+ const validation = validate(manifest)
268
+ if (!validation.valid) {
269
+ validation.errors.forEach(e => log.error(e))
270
+ process.exit(1)
271
+ }
272
+ if (validation.warnings.length) {
273
+ validation.warnings.forEach(w => log.warn(w))
274
+ }
275
+
276
+ writeManifest(manifest, outPath)
277
+ log.success(`Manifest written to ${outPath}`)
278
+ log.info(`${manifest.capabilities.length} capabilities registered`)
279
+ console.log()
280
+ }
@@ -0,0 +1,26 @@
1
+ 'use strict'
2
+
3
+ const { header, c } = require('./shared')
4
+
5
+ module.exports = function cmdHelp() {
6
+ header()
7
+ console.log(`${c.bold} Usage:${c.reset} capman <command>`)
8
+ console.log()
9
+ console.log(`${c.bold} Commands:${c.reset}`)
10
+ console.log(` ${c.teal}init${c.reset} Create a starter capman.config.js`)
11
+ console.log(` ${c.teal}generate${c.reset} Generate manifest from capman.config.js`)
12
+ console.log(` ${c.teal}generate --from <path|url>${c.reset} Generate from OpenAPI/Swagger spec`)
13
+ console.log(` ${c.teal}generate --ai${c.reset} Generate manifest using AI`)
14
+ console.log(` ${c.teal}validate${c.reset} Validate an existing manifest.json`)
15
+ console.log(` ${c.teal}inspect${c.reset} Print all capabilities in manifest`)
16
+ console.log(` ${c.teal}demo${c.reset} Run a live demo with sample queries`)
17
+ console.log(` ${c.teal}run "query"${c.reset} Run a query against your manifest`)
18
+ console.log(` ${c.teal}run "query" --debug${c.reset} Run with full candidate scoring`)
19
+ console.log(` ${c.teal}explain "query"${c.reset} Explain what would match without executing`)
20
+ console.log()
21
+ console.log(`${c.bold} Options:${c.reset}`)
22
+ console.log(` ${c.gray}--config Path to config file (default: capman.config.js)${c.reset}`)
23
+ console.log(` ${c.gray}--out Output path (default: manifest.json)${c.reset}`)
24
+ console.log(` ${c.gray}--manifest Manifest to read (default: manifest.json)${c.reset}`)
25
+ console.log()
26
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+ const { header, log, c, requireSrc } = require('./shared')
6
+
7
+ module.exports = function cmdInit() {
8
+ header()
9
+ const outPath = path.resolve(process.cwd(), 'capman.config.js')
10
+ if (fs.existsSync(outPath)) {
11
+ log.warn('capman.config.js already exists — not overwriting.')
12
+ process.exit(0)
13
+ }
14
+ const { generateStarterConfig } = requireSrc()
15
+ fs.writeFileSync(outPath, generateStarterConfig())
16
+ log.success(`Created ${c.bold}capman.config.js${c.reset}`)
17
+ log.info(`Edit it with your app's capabilities, then run:`)
18
+ console.log(`\n npx capman generate\n`)
19
+ }
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+
3
+ const { header, log, c, getFlag, requireSrc } = require('./shared')
4
+
5
+ module.exports = function cmdInspect() {
6
+ header()
7
+ const { readManifest } = requireSrc()
8
+
9
+ const manifestPath = getFlag('--manifest') ?? 'manifest.json'
10
+ let manifest
11
+ try {
12
+ manifest = readManifest(manifestPath)
13
+ } catch (e) {
14
+ log.error(e.message)
15
+ process.exit(1)
16
+ }
17
+
18
+ console.log(`${c.bold} App:${c.reset} ${manifest.app}`)
19
+ console.log(`${c.bold} Generated:${c.reset} ${manifest.generatedAt}`)
20
+ console.log(`${c.bold} Capabilities:${c.reset} ${manifest.capabilities.length}`)
21
+ console.log()
22
+
23
+ for (const cap of manifest.capabilities) {
24
+ const col = cap.resolver.type === 'hybrid' ? c.yellow : c.teal
25
+ console.log(` ${c.bold}${cap.name}${c.reset} ${col}[${cap.resolver.type}]${c.reset} ${c.gray}${cap.privacy.level}${c.reset}`)
26
+ console.log(` ${c.gray}id: ${cap.id}${c.reset}`)
27
+ console.log(` ${cap.description}`)
28
+ if (cap.examples?.length) {
29
+ console.log(` ${c.gray}e.g. "${cap.examples[0]}"${c.reset}`)
30
+ }
31
+ console.log()
32
+ }
33
+ }
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ const { header, log, c, args, flags, getFlag, requireSrc } = require('./shared')
4
+
5
+ module.exports = function cmdRun() {
6
+ header()
7
+ const query = args[1]
8
+ const debug = flags.includes('--debug')
9
+ const manifestPath = getFlag('--manifest') ?? 'manifest.json'
10
+
11
+ if (!query) {
12
+ log.error('Please provide a query.')
13
+ console.log(` Example: npx capman run "show me articles"\n`)
14
+ process.exit(1)
15
+ }
16
+
17
+ const { readManifest, match } = requireSrc()
18
+
19
+ let manifest
20
+ try {
21
+ manifest = readManifest(manifestPath)
22
+ } catch (e) {
23
+ log.error(e.message)
24
+ process.exit(1)
25
+ }
26
+
27
+ log.info(`Query: "${query}"`)
28
+ log.blank()
29
+
30
+ const result = match(query, manifest)
31
+
32
+ if (result.capability) {
33
+ console.log(` ${c.green}✓${c.reset} Matched: ${c.bold}${result.capability.id}${c.reset}`)
34
+ console.log(` Intent: ${result.intent}`)
35
+ console.log(` Confidence: ${result.confidence}%`)
36
+ console.log(` Resolver: ${result.capability.resolver.type}`)
37
+
38
+ if (Object.keys(result.extractedParams).length > 0) {
39
+ const params = Object.entries(result.extractedParams)
40
+ .map(([k, v]) => `${k}=${v}`)
41
+ .join(', ')
42
+ console.log(` Params: ${params}`)
43
+ }
44
+
45
+ if (debug && result.candidates.length) {
46
+ log.blank()
47
+ console.log(` ${c.gray}── All candidates:${c.reset}`)
48
+ result.candidates
49
+ .sort((a, b) => b.score - a.score)
50
+ .forEach(c2 => {
51
+ const marker = c2.matched ? c.green + '✓' : c.gray + '○'
52
+ console.log(` ${marker}${c.reset} ${c2.capabilityId}: ${c2.score}%`)
53
+ })
54
+ }
55
+ } else {
56
+ console.log(` ${c.yellow}○${c.reset} OUT_OF_SCOPE — no capability matched`)
57
+ console.log(` ${c.gray}${result.reasoning}${c.reset}`)
58
+
59
+ if (debug && result.candidates.length) {
60
+ log.blank()
61
+ console.log(` ${c.gray}── All candidates:${c.reset}`)
62
+ result.candidates
63
+ .sort((a, b) => b.score - a.score)
64
+ .slice(0, 5)
65
+ .forEach(c2 => {
66
+ console.log(` ${c.gray}○ ${c2.capabilityId}: ${c2.score}%${c.reset}`)
67
+ })
68
+ }
69
+ }
70
+ console.log()
71
+ }