capman 0.1.0 → 0.3.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.
Files changed (78) hide show
  1. package/bin/capman.js +152 -6
  2. package/dist/cjs/cache.d.ts +42 -0
  3. package/dist/cjs/cache.d.ts.map +1 -0
  4. package/dist/cjs/cache.js +181 -0
  5. package/dist/cjs/cache.js.map +1 -0
  6. package/dist/cjs/engine.d.ts +82 -0
  7. package/dist/cjs/engine.d.ts.map +1 -0
  8. package/dist/cjs/engine.js +154 -0
  9. package/dist/cjs/engine.js.map +1 -0
  10. package/dist/cjs/generator.d.ts.map +1 -0
  11. package/dist/{generator.js → cjs/generator.js} +2 -1
  12. package/dist/cjs/generator.js.map +1 -0
  13. package/dist/{index.d.ts → cjs/index.d.ts} +18 -1
  14. package/dist/cjs/index.d.ts.map +1 -0
  15. package/dist/cjs/index.js +77 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/learning.d.ts +56 -0
  18. package/dist/cjs/learning.d.ts.map +1 -0
  19. package/dist/cjs/learning.js +184 -0
  20. package/dist/cjs/learning.js.map +1 -0
  21. package/dist/cjs/logger.d.ts.map +1 -0
  22. package/dist/cjs/logger.js.map +1 -0
  23. package/dist/cjs/matcher.d.ts.map +1 -0
  24. package/dist/{matcher.js → cjs/matcher.js} +27 -5
  25. package/dist/cjs/matcher.js.map +1 -0
  26. package/dist/cjs/resolver.d.ts.map +1 -0
  27. package/dist/{resolver.js → cjs/resolver.js} +36 -4
  28. package/dist/cjs/resolver.js.map +1 -0
  29. package/dist/{schema.d.ts → cjs/schema.d.ts} +10 -10
  30. package/dist/{schema.d.ts.map → cjs/schema.d.ts.map} +1 -1
  31. package/dist/cjs/schema.js.map +1 -0
  32. package/dist/{types.d.ts → cjs/types.d.ts} +12 -5
  33. package/dist/cjs/types.d.ts.map +1 -0
  34. package/dist/cjs/types.js.map +1 -0
  35. package/dist/cjs/version.d.ts +2 -0
  36. package/dist/cjs/version.d.ts.map +1 -0
  37. package/dist/cjs/version.js +6 -0
  38. package/dist/cjs/version.js.map +1 -0
  39. package/dist/esm/cache.js +141 -0
  40. package/dist/esm/engine.js +149 -0
  41. package/dist/esm/generator.js +183 -0
  42. package/dist/esm/generator.js.map +1 -0
  43. package/dist/esm/index.js +56 -0
  44. package/dist/esm/index.js.map +1 -0
  45. package/dist/esm/learning.js +145 -0
  46. package/dist/esm/logger.js +47 -0
  47. package/dist/esm/logger.js.map +1 -0
  48. package/dist/esm/matcher.js +211 -0
  49. package/dist/esm/matcher.js.map +1 -0
  50. package/dist/esm/resolver.js +192 -0
  51. package/dist/esm/resolver.js.map +1 -0
  52. package/dist/esm/schema.js +88 -0
  53. package/dist/esm/schema.js.map +1 -0
  54. package/dist/esm/types.js +2 -0
  55. package/dist/esm/types.js.map +1 -0
  56. package/dist/esm/version.js +2 -0
  57. package/package.json +27 -11
  58. package/dist/generator.d.ts.map +0 -1
  59. package/dist/generator.js.map +0 -1
  60. package/dist/index.d.ts.map +0 -1
  61. package/dist/index.js +0 -42
  62. package/dist/index.js.map +0 -1
  63. package/dist/logger.d.ts.map +0 -1
  64. package/dist/logger.js.map +0 -1
  65. package/dist/matcher.d.ts.map +0 -1
  66. package/dist/matcher.js.map +0 -1
  67. package/dist/resolver.d.ts.map +0 -1
  68. package/dist/resolver.js.map +0 -1
  69. package/dist/schema.js.map +0 -1
  70. package/dist/types.d.ts.map +0 -1
  71. package/dist/types.js.map +0 -1
  72. /package/dist/{generator.d.ts → cjs/generator.d.ts} +0 -0
  73. /package/dist/{logger.d.ts → cjs/logger.d.ts} +0 -0
  74. /package/dist/{logger.js → cjs/logger.js} +0 -0
  75. /package/dist/{matcher.d.ts → cjs/matcher.d.ts} +0 -0
  76. /package/dist/{resolver.d.ts → cjs/resolver.d.ts} +0 -0
  77. /package/dist/{schema.js → cjs/schema.js} +0 -0
  78. /package/dist/{types.js → cjs/types.js} +0 -0
@@ -0,0 +1,183 @@
1
+ import { VERSION } from './version';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { validateConfig, validateManifest } from './schema';
5
+ import { logger } from './logger';
6
+ export function generate(config) {
7
+ return {
8
+ version: VERSION,
9
+ app: config.app,
10
+ generatedAt: new Date().toISOString(),
11
+ capabilities: config.capabilities,
12
+ };
13
+ }
14
+ export function loadConfig(configPath) {
15
+ const candidates = configPath
16
+ ? [configPath]
17
+ : ['capman.config.js', 'capman.config.json'];
18
+ // If a specific path was given but doesn't exist — clear error
19
+ if (configPath) {
20
+ const resolved = path.resolve(process.cwd(), configPath);
21
+ if (!fs.existsSync(resolved)) {
22
+ throw new Error(`Config file not found at: ${resolved}\n` +
23
+ `Check the path and try again.`);
24
+ }
25
+ }
26
+ for (const candidate of candidates) {
27
+ const resolved = path.resolve(process.cwd(), candidate);
28
+ if (fs.existsSync(resolved)) {
29
+ let raw;
30
+ // Catch syntax errors in config file
31
+ try {
32
+ const mod = require(resolved);
33
+ raw = mod.default ?? mod;
34
+ }
35
+ catch (err) {
36
+ throw new Error(`Failed to load config at ${resolved}:\n` +
37
+ ` ${err instanceof Error ? err.message : String(err)}\n\n` +
38
+ `Check your config file for syntax errors.`);
39
+ }
40
+ // Catch invalid config structure
41
+ const check = validateConfig(raw);
42
+ if (!check.valid) {
43
+ throw new Error(`Invalid capman config at ${resolved}:\n` +
44
+ check.errors.map(e => ` • ${e}`).join('\n') + '\n\n' +
45
+ `Run: node bin/capman.js init to see a valid example config.`);
46
+ }
47
+ return raw;
48
+ }
49
+ }
50
+ // No config found at all
51
+ throw new Error(`No capman config file found.\n\n` +
52
+ `Expected one of:\n` +
53
+ candidates.map(c => ` • ${c}`).join('\n') + '\n\n' +
54
+ `Run: node bin/capman.js init to create one.`);
55
+ }
56
+ export function writeManifest(manifest, outputPath = 'manifest.json') {
57
+ const resolved = path.resolve(process.cwd(), outputPath);
58
+ fs.writeFileSync(resolved, JSON.stringify(manifest, null, 2));
59
+ return resolved;
60
+ }
61
+ export function readManifest(manifestPath = 'manifest.json') {
62
+ const resolved = path.resolve(process.cwd(), manifestPath);
63
+ if (!fs.existsSync(resolved)) {
64
+ throw new Error(`No manifest found at ${resolved}. Run: node bin/capman.js generate`);
65
+ }
66
+ const raw = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
67
+ const check = validateManifest(raw);
68
+ if (!check.valid) {
69
+ throw new Error(`Invalid manifest at ${resolved}:\n` +
70
+ check.errors.map(e => ` • ${e}`).join('\n'));
71
+ }
72
+ return raw;
73
+ }
74
+ export function validate(manifest) {
75
+ const errors = [];
76
+ const warnings = [];
77
+ // Delegate error checking to Zod
78
+ const zodResult = validateManifest(manifest);
79
+ errors.push(...zodResult.errors);
80
+ // Warnings that Zod doesn't cover
81
+ for (const cap of manifest.capabilities ?? []) {
82
+ if (!cap.examples?.length) {
83
+ const msg = `Capability "${cap.id}" has no examples — adding examples improves matching`;
84
+ warnings.push(msg);
85
+ logger.warn(msg);
86
+ }
87
+ if (!cap.returns?.length) {
88
+ const msg = `Capability "${cap.id}" has no "returns" declaration`;
89
+ warnings.push(msg);
90
+ logger.warn(msg);
91
+ }
92
+ }
93
+ if (errors.length > 0) {
94
+ logger.error(`Manifest validation failed — ${errors.length} error(s)`);
95
+ errors.forEach(e => logger.error(e));
96
+ }
97
+ return { valid: errors.length === 0, errors, warnings };
98
+ }
99
+ export function generateStarterConfig() {
100
+ return `// capman.config.js
101
+ // Define what your app can do for AI agents.
102
+ // Replace the examples below with your own app's capabilities.
103
+
104
+ module.exports = {
105
+ app: 'your-app-name',
106
+ baseUrl: 'https://api.your-app.com',
107
+
108
+ capabilities: [
109
+ {
110
+ id: 'get_resource',
111
+ name: 'Get a resource',
112
+ description: 'Fetch a specific resource by name, ID, or filter from the app.',
113
+ examples: [
114
+ 'Show me the resource details',
115
+ 'Find resource by ID',
116
+ 'Look up resource by name',
117
+ ],
118
+ params: [
119
+ {
120
+ name: 'resource_id',
121
+ description: 'The ID or name of the resource to fetch',
122
+ required: true,
123
+ source: 'user_query',
124
+ },
125
+ ],
126
+ returns: ['resource', 'metadata'],
127
+ resolver: {
128
+ type: 'api',
129
+ endpoints: [{ method: 'GET', path: '/resources/{resource_id}' }],
130
+ },
131
+ privacy: { level: 'public', note: 'No auth required' },
132
+ },
133
+
134
+ {
135
+ id: 'navigate_to_screen',
136
+ name: 'Navigate to a screen',
137
+ description: 'Route the user to a specific page or section in the app.',
138
+ examples: [
139
+ 'Take me to the dashboard',
140
+ 'Open settings',
141
+ 'Go to my profile',
142
+ ],
143
+ params: [
144
+ {
145
+ name: 'destination',
146
+ description: 'The screen or page to navigate to',
147
+ required: true,
148
+ source: 'user_query',
149
+ },
150
+ ],
151
+ returns: ['deep_link'],
152
+ resolver: { type: 'nav', destination: '{destination}' },
153
+ privacy: { level: 'public' },
154
+ },
155
+
156
+ {
157
+ id: 'get_user_data',
158
+ name: 'Get user data',
159
+ description: 'Retrieve data belonging to the currently authenticated user.',
160
+ examples: [
161
+ 'Show my account details',
162
+ 'What is my current plan?',
163
+ 'Show my recent activity',
164
+ ],
165
+ params: [
166
+ {
167
+ name: 'user_id',
168
+ description: 'Current user ID',
169
+ required: true,
170
+ source: 'session',
171
+ },
172
+ ],
173
+ returns: ['user_data'],
174
+ resolver: {
175
+ type: 'api',
176
+ endpoints: [{ method: 'GET', path: '/users/{user_id}' }],
177
+ },
178
+ privacy: { level: 'user_owned', note: 'Requires auth — scoped to current user only' },
179
+ },
180
+ ],
181
+ }
182
+ `;
183
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ export { setLogLevel } from './logger';
2
+ import { logger } from './logger';
3
+ export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
4
+ export { match, matchWithLLM, } from './matcher';
5
+ export { resolve } from './resolver';
6
+ // ─── Convenience: ask() — match + resolve in one call ────────────────────────
7
+ import { match as _match, matchWithLLM as _matchWithLLM } from './matcher';
8
+ import { resolve as _resolve } from './resolver';
9
+ // ─── Engine (recommended API) ─────────────────────────────────────────────────
10
+ export { CapmanEngine } from './engine';
11
+ // ─── Cache ────────────────────────────────────────────────────────────────────
12
+ export { MemoryCache, FileCache, ComboCache } from './cache';
13
+ // ─── Learning ─────────────────────────────────────────────────────────────────
14
+ export { FileLearningStore, MemoryLearningStore } from './learning';
15
+ /**
16
+ * One-shot convenience: match + resolve in a single call.
17
+ *
18
+ * @example
19
+ * const result = await ask("show me the dashboard", manifest, {
20
+ * baseUrl: 'https://api.your-app.com',
21
+ * })
22
+ */
23
+ export async function ask(query, manifest, options = {}) {
24
+ const { llm, mode = 'balanced', ...resolveOptions } = options;
25
+ let matchResult;
26
+ switch (mode) {
27
+ case 'cheap': {
28
+ // Keyword only — never calls LLM
29
+ matchResult = _match(query, manifest);
30
+ break;
31
+ }
32
+ case 'accurate': {
33
+ // LLM first — falls back to keyword if LLM fails or no llm provided
34
+ if (llm) {
35
+ matchResult = await _matchWithLLM(query, manifest, { llm });
36
+ }
37
+ else {
38
+ logger.warn('ask() mode is "accurate" but no llm function was provided — falling back to keyword matching');
39
+ matchResult = _match(query, manifest);
40
+ }
41
+ break;
42
+ }
43
+ case 'balanced':
44
+ default: {
45
+ // Keyword first — LLM fallback if confidence below threshold
46
+ const keywordResult = _match(query, manifest);
47
+ const THRESHOLD = 50;
48
+ matchResult = (keywordResult.confidence >= THRESHOLD || !llm)
49
+ ? keywordResult
50
+ : await _matchWithLLM(query, manifest, { llm });
51
+ break;
52
+ }
53
+ }
54
+ const resolution = await _resolve(matchResult, matchResult.extractedParams, resolveOptions);
55
+ return { match: matchResult, resolution };
56
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAmBjC,OAAO,EACL,QAAQ,EACR,UAAU,EACV,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,qBAAqB,GACtB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,EACL,YAAY,GACb,MAAM,WAAW,CAAA;AAGlB,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAGpC,gFAAgF;AAEhF,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,YAAY,IAAI,aAAa,EAAE,MAAM,WAAW,CAAA;AAC1E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAA;AA0BhD;;;;;;;GAOG;AAEH,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,KAAa,EACb,QAAkB,EAClB,UAAsB,EAAE;IAExB,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,UAAU,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,CAAA;IAE7D,IAAI,WAAwB,CAAA;IAE5B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,iCAAiC;YACjC,WAAW,GAAG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;YACrC,MAAK;QACP,CAAC;QAED,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,oEAAoE;YACpE,IAAI,GAAG,EAAE,CAAC;gBACR,WAAW,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;YAC7D,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,8FAA8F,CAAC,CAAA;gBAC3G,WAAW,GAAG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;YACvC,CAAC;YACD,MAAK;QACP,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,OAAO,CAAC,CAAC,CAAC;YACR,6DAA6D;YAC7D,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;YAC7C,MAAM,SAAS,GAAG,EAAE,CAAA;YACpB,WAAW,GAAG,CAAC,aAAa,CAAC,UAAU,IAAI,SAAS,IAAI,CAAC,GAAG,CAAC;gBAC3D,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,MAAM,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;YACjD,MAAK;QACP,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,QAAQ,CAC/B,WAAW,EACX,WAAW,CAAC,eAA0C,EACtD,cAAc,CACf,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,CAAA;AAC3C,CAAC"}
@@ -0,0 +1,145 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { logger } from './logger';
4
+ // ─── File Learning Store ──────────────────────────────────────────────────────
5
+ export class FileLearningStore {
6
+ constructor(filePath = '.capman/learning.json') {
7
+ this.entries = [];
8
+ this.loaded = false;
9
+ this.filePath = path.resolve(process.cwd(), filePath);
10
+ }
11
+ load() {
12
+ if (this.loaded)
13
+ return;
14
+ try {
15
+ if (fs.existsSync(this.filePath)) {
16
+ const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
17
+ this.entries = raw.entries ?? [];
18
+ logger.debug(`Learning store loaded: ${this.entries.length} entries`);
19
+ }
20
+ }
21
+ catch {
22
+ logger.warn(`Failed to load learning store at ${this.filePath}`);
23
+ }
24
+ this.loaded = true;
25
+ }
26
+ save() {
27
+ try {
28
+ const dir = path.dirname(this.filePath);
29
+ if (!fs.existsSync(dir))
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ fs.writeFileSync(this.filePath, JSON.stringify({
32
+ entries: this.entries,
33
+ updatedAt: new Date().toISOString(),
34
+ }, null, 2));
35
+ }
36
+ catch {
37
+ logger.warn(`Failed to save learning store to ${this.filePath}`);
38
+ }
39
+ }
40
+ async record(entry) {
41
+ this.load();
42
+ this.entries.push(entry);
43
+ this.save();
44
+ logger.debug(`Learning recorded: "${entry.query}" → ${entry.capabilityId ?? 'OUT_OF_SCOPE'} via ${entry.resolvedVia}`);
45
+ }
46
+ async getStats() {
47
+ this.load();
48
+ const index = {};
49
+ let totalQueries = 0;
50
+ let llmQueries = 0;
51
+ let cacheHits = 0;
52
+ let outOfScope = 0;
53
+ for (const entry of this.entries) {
54
+ totalQueries++;
55
+ if (entry.resolvedVia === 'llm')
56
+ llmQueries++;
57
+ if (entry.resolvedVia === 'cache')
58
+ cacheHits++;
59
+ if (!entry.capabilityId)
60
+ outOfScope++;
61
+ if (entry.capabilityId) {
62
+ // Index each word of the query against the matched capability
63
+ const words = entry.query.toLowerCase()
64
+ .split(/\W+/)
65
+ .filter(w => w.length > 2);
66
+ for (const word of words) {
67
+ if (!index[word])
68
+ index[word] = {};
69
+ index[word][entry.capabilityId] =
70
+ (index[word][entry.capabilityId] ?? 0) + 1;
71
+ }
72
+ }
73
+ }
74
+ return { index, totalQueries, llmQueries, cacheHits, outOfScope };
75
+ }
76
+ async getTopCapabilities(limit = 5) {
77
+ this.load();
78
+ const counts = {};
79
+ for (const entry of this.entries) {
80
+ if (entry.capabilityId) {
81
+ counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
82
+ }
83
+ }
84
+ return Object.entries(counts)
85
+ .sort(([, a], [, b]) => b - a)
86
+ .slice(0, limit)
87
+ .map(([id, hits]) => ({ id, hits }));
88
+ }
89
+ async clear() {
90
+ this.entries = [];
91
+ this.save();
92
+ }
93
+ }
94
+ // ─── Memory Learning Store (for testing) ─────────────────────────────────────
95
+ export class MemoryLearningStore {
96
+ constructor() {
97
+ this.entries = [];
98
+ }
99
+ async record(entry) {
100
+ this.entries.push(entry);
101
+ }
102
+ async getStats() {
103
+ const index = {};
104
+ let totalQueries = 0;
105
+ let llmQueries = 0;
106
+ let cacheHits = 0;
107
+ let outOfScope = 0;
108
+ for (const entry of this.entries) {
109
+ totalQueries++;
110
+ if (entry.resolvedVia === 'llm')
111
+ llmQueries++;
112
+ if (entry.resolvedVia === 'cache')
113
+ cacheHits++;
114
+ if (!entry.capabilityId)
115
+ outOfScope++;
116
+ if (entry.capabilityId) {
117
+ const words = entry.query.toLowerCase()
118
+ .split(/\W+/)
119
+ .filter(w => w.length > 2);
120
+ for (const word of words) {
121
+ if (!index[word])
122
+ index[word] = {};
123
+ index[word][entry.capabilityId] =
124
+ (index[word][entry.capabilityId] ?? 0) + 1;
125
+ }
126
+ }
127
+ }
128
+ return { index, totalQueries, llmQueries, cacheHits, outOfScope };
129
+ }
130
+ async getTopCapabilities(limit = 5) {
131
+ const counts = {};
132
+ for (const entry of this.entries) {
133
+ if (entry.capabilityId) {
134
+ counts[entry.capabilityId] = (counts[entry.capabilityId] ?? 0) + 1;
135
+ }
136
+ }
137
+ return Object.entries(counts)
138
+ .sort(([, a], [, b]) => b - a)
139
+ .slice(0, limit)
140
+ .map(([id, hits]) => ({ id, hits }));
141
+ }
142
+ async clear() {
143
+ this.entries = [];
144
+ }
145
+ }
@@ -0,0 +1,47 @@
1
+ // ─── Log levels ───────────────────────────────────────────────────────────────
2
+ const LEVELS = {
3
+ silent: 0,
4
+ error: 1,
5
+ warn: 2,
6
+ info: 3,
7
+ debug: 4,
8
+ };
9
+ // ─── Logger ───────────────────────────────────────────────────────────────────
10
+ export class Logger {
11
+ constructor(level = 'silent') {
12
+ this.level = LEVELS[level];
13
+ }
14
+ setLevel(level) {
15
+ this.level = LEVELS[level];
16
+ }
17
+ error(msg, ...args) {
18
+ if (this.level >= LEVELS.error)
19
+ console.error(`[capman:error] ${msg}`, ...args);
20
+ }
21
+ warn(msg, ...args) {
22
+ if (this.level >= LEVELS.warn)
23
+ console.warn(`[capman:warn] ${msg}`, ...args);
24
+ }
25
+ info(msg, ...args) {
26
+ if (this.level >= LEVELS.info)
27
+ console.log(`[capman:info] ${msg}`, ...args);
28
+ }
29
+ debug(msg, ...args) {
30
+ if (this.level >= LEVELS.debug)
31
+ console.log(`[capman:debug] ${msg}`, ...args);
32
+ }
33
+ }
34
+ // ─── Global logger instance ───────────────────────────────────────────────────
35
+ export const logger = new Logger('silent');
36
+ /**
37
+ * Set the global log level for capman.
38
+ *
39
+ * @example
40
+ * import { setLogLevel } from 'capman'
41
+ * setLogLevel('debug') // see everything
42
+ * setLogLevel('info') // see key steps
43
+ * setLogLevel('silent') // no output (default)
44
+ */
45
+ export function setLogLevel(level) {
46
+ logger.setLevel(level);
47
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAIjF,MAAM,MAAM,GAA6B;IACvC,MAAM,EAAE,CAAC;IACT,KAAK,EAAG,CAAC;IACT,IAAI,EAAI,CAAC;IACT,IAAI,EAAI,CAAC;IACT,KAAK,EAAG,CAAC;CACV,CAAA;AAED,iFAAiF;AAEjF,MAAM,OAAO,MAAM;IAGjB,YAAY,QAAkB,QAAQ;QACpC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC;IAED,QAAQ,CAAC,KAAe;QACtB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAW,EAAE,GAAG,IAAe;QACnC,IAAI,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK;YAC5B,OAAO,CAAC,KAAK,CAAC,kBAAkB,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,CAAC,GAAW,EAAE,GAAG,IAAe;QAClC,IAAI,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI;YAC3B,OAAO,CAAC,IAAI,CAAC,iBAAiB,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,CAAC;IAED,IAAI,CAAC,GAAW,EAAE,GAAG,IAAe;QAClC,IAAI,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI;YAC3B,OAAO,CAAC,GAAG,CAAC,iBAAiB,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,CAAA;IAChD,CAAC;IAED,KAAK,CAAC,GAAW,EAAE,GAAG,IAAe;QACnC,IAAI,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK;YAC5B,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,CAAC;CACF;AAED,iFAAiF;AAEjF,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAA;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACxB,CAAC"}
@@ -0,0 +1,211 @@
1
+ import { logger } from './logger';
2
+ const STOPWORDS = new Set([
3
+ 'show', 'me', 'the', 'get', 'find', 'fetch', 'give', 'please',
4
+ 'can', 'you', 'i', 'want', 'to', 'a', 'an', 'my', 'our', 'your',
5
+ 'what', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
6
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
7
+ 'could', 'should', 'may', 'might', 'shall', 'and', 'or', 'but',
8
+ 'in', 'on', 'at', 'by', 'for', 'with', 'about', 'into', 'through',
9
+ 'of', 'from', 'up', 'out', 'that', 'this', 'these', 'those',
10
+ 'it', 'its', 'how', 'when', 'where', 'who', 'which', 'all',
11
+ 'just', 'some', 'any', 'there', 'their', 'them', 'they',
12
+ ]);
13
+ function filterStopwords(words) {
14
+ return words.filter(w => !STOPWORDS.has(w.toLowerCase()) && w.length > 1);
15
+ }
16
+ function scoreCapability(query, cap) {
17
+ const q = query.toLowerCase();
18
+ let score = 0;
19
+ const qWords = filterStopwords(q.split(/\W+/).filter(Boolean));
20
+ // Check examples — exact substring match is a strong signal
21
+ for (const example of cap.examples ?? []) {
22
+ const exWords = filterStopwords(example.toLowerCase().split(/\s+/));
23
+ if (exWords.length === 0)
24
+ continue;
25
+ const overlap = exWords.filter(w => qWords.includes(w)).length;
26
+ score += (overlap / exWords.length) * 60;
27
+ }
28
+ // Check description words
29
+ const descWords = filterStopwords(cap.description.toLowerCase().split(/\W+/).filter(Boolean));
30
+ if (descWords.length > 0) {
31
+ const descOverlap = descWords.filter(w => qWords.includes(w)).length;
32
+ score += (descOverlap / descWords.length) * 30;
33
+ }
34
+ // Check name words
35
+ const nameWords = filterStopwords(cap.name.toLowerCase().split(/\W+/).filter(Boolean));
36
+ if (nameWords.length > 0) {
37
+ const nameOverlap = nameWords.filter(w => qWords.includes(w)).length;
38
+ score += (nameOverlap / nameWords.length) * 10;
39
+ }
40
+ return Math.min(Math.round(score), 100);
41
+ }
42
+ function resolverToIntent(cap) {
43
+ const t = cap.resolver.type;
44
+ if (t === 'api')
45
+ return 'retrieval';
46
+ if (t === 'nav')
47
+ return 'navigation';
48
+ if (t === 'hybrid')
49
+ return 'hybrid';
50
+ return 'out_of_scope';
51
+ }
52
+ /**
53
+ * Extracts parameter values from a user query using keyword heuristics.
54
+ *
55
+ * Known limits:
56
+ * - Extracts single tokens only — "jane smith" would extract "jane"
57
+ * - Keyword matching is positional — "articles from authors I follow"
58
+ * may extract "authors" instead of nothing, since "from" is a keyword
59
+ * - For complex or ambiguous queries, use matchWithLLM() which handles
60
+ * param extraction more accurately via the LLM prompt
61
+ */
62
+ function extractParams(query, cap) {
63
+ const result = {};
64
+ const q = query.toLowerCase();
65
+ for (const param of cap.params) {
66
+ // Session params come from auth context, not query
67
+ if (param.source === 'session') {
68
+ result[param.name] = '[from_session]';
69
+ continue;
70
+ }
71
+ if (param.source !== 'user_query') {
72
+ result[param.name] = null;
73
+ continue;
74
+ }
75
+ // Try to extract value after known keywords
76
+ // e.g. "profile for johndoe" → johndoe
77
+ // "articles by jane" → jane
78
+ // "tag javascript" → javascript
79
+ // Use param name and description as hints for what to look for
80
+ const paramHints = [param.name, ...param.description.toLowerCase().split(/\s+/)]
81
+ .filter(w => w.length > 2);
82
+ // Try keyword-based extraction first
83
+ const keywords = [
84
+ `for `, `by `, `about `, `named `, `called `,
85
+ `tag `, `user `, `author `, `slug `, `id `,
86
+ `from `, `with `,
87
+ ];
88
+ // For nav params — look for destination after navigation verbs
89
+ const navKeywords = [`to `, `open `, `show `];
90
+ const isNavParam = param.name === 'destination' ||
91
+ param.description.toLowerCase().includes('screen') ||
92
+ param.description.toLowerCase().includes('page');
93
+ const activeKeywords = isNavParam
94
+ ? [...navKeywords, ...keywords]
95
+ : keywords;
96
+ let extracted = null;
97
+ for (const kw of activeKeywords) {
98
+ const idx = q.indexOf(kw);
99
+ if (idx !== -1) {
100
+ const after = query.slice(idx + kw.length).trim();
101
+ // Get remaining words, filter stopwords, take first meaningful one
102
+ const tokens = after.split(/\s+/)
103
+ .map(t => t.replace(/[^a-zA-Z0-9-_@.]/g, ''))
104
+ .filter(t => t.length > 1 && !STOPWORDS.has(t.toLowerCase()));
105
+ if (tokens.length > 0) {
106
+ // For IDs and numbers — single token is correct
107
+ const isIdParam = param.name === 'id' ||
108
+ param.name.endsWith('_id') ||
109
+ param.name.endsWith('Id') ||
110
+ /^\s*\w+\s+id\b/i.test(param.description) ||
111
+ /^id\b/i.test(param.description);
112
+ // For names, products, destinations — grab multi-word phrase
113
+ extracted = (isIdParam || isNavParam) ? tokens[0] : tokens.join('-').toLowerCase();
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ // Fallback — grab last meaningful word in the query
119
+ if (!extracted) {
120
+ const words = query.trim().split(/\s+/);
121
+ const meaningful = words.filter(w => !STOPWORDS.has(w.toLowerCase()));
122
+ extracted = meaningful[meaningful.length - 1] ?? null;
123
+ }
124
+ result[param.name] = extracted;
125
+ }
126
+ return result;
127
+ }
128
+ export function match(query, manifest) {
129
+ if (!query?.trim()) {
130
+ logger.warn('Empty query received');
131
+ return {
132
+ capability: null,
133
+ confidence: 0,
134
+ intent: 'out_of_scope',
135
+ extractedParams: {},
136
+ reasoning: 'Empty query',
137
+ };
138
+ }
139
+ logger.info(`Matching query: "${query}"`);
140
+ logger.debug(`Manifest has ${manifest.capabilities.length} capabilities`);
141
+ let best = null;
142
+ let bestScore = 0;
143
+ for (const cap of manifest.capabilities) {
144
+ const score = scoreCapability(query, cap);
145
+ logger.debug(` scored "${cap.id}": ${score}%`);
146
+ if (score > bestScore) {
147
+ bestScore = score;
148
+ best = cap;
149
+ }
150
+ }
151
+ if (!best || bestScore < 50) {
152
+ logger.info(`No match above threshold (best: ${bestScore}% for "${best?.id ?? 'none'}")`);
153
+ return {
154
+ capability: null,
155
+ confidence: bestScore,
156
+ intent: 'out_of_scope',
157
+ extractedParams: {},
158
+ reasoning: `No capability matched with sufficient confidence (best score: ${bestScore})`,
159
+ };
160
+ }
161
+ const params = extractParams(query, best);
162
+ logger.info(`Matched "${best.id}" at ${bestScore}% confidence`);
163
+ logger.debug(`Extracted params: ${JSON.stringify(params)}`);
164
+ return {
165
+ capability: best,
166
+ confidence: bestScore,
167
+ intent: resolverToIntent(best),
168
+ extractedParams: params,
169
+ reasoning: `Matched "${best.id}" via keyword scoring (score: ${bestScore})`,
170
+ };
171
+ }
172
+ export async function matchWithLLM(query, manifest, options) {
173
+ const manifestSummary = manifest.capabilities.map(c => `- ${c.id} (${c.resolver.type}): ${c.description}${c.examples?.length ? `\n examples: ${c.examples.slice(0, 2).join(', ')}` : ''}`).join('\n');
174
+ const prompt = `You are an intent matcher for an AI agent system.
175
+
176
+ App: ${manifest.app}
177
+
178
+ Available capabilities:
179
+ ${manifestSummary}
180
+
181
+ User query: "${query}"
182
+
183
+ Respond ONLY in valid JSON (no markdown):
184
+ {
185
+ "matched_capability": "<capability_id or OUT_OF_SCOPE>",
186
+ "confidence": <0-100>,
187
+ "intent": "<navigation|retrieval|hybrid|out_of_scope>",
188
+ "reasoning": "<one sentence>",
189
+ "extracted_params": { "<param_name>": "<value or null>" }
190
+ }`;
191
+ try {
192
+ const raw = await options.llm(prompt);
193
+ const clean = raw.replace(/```json|```/g, '').trim();
194
+ const parsed = JSON.parse(clean);
195
+ const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
196
+ const capability = isOOS
197
+ ? null
198
+ : manifest.capabilities.find(c => c.id === parsed.matched_capability) ?? null;
199
+ return {
200
+ capability,
201
+ confidence: parsed.confidence,
202
+ intent: isOOS ? 'out_of_scope' : parsed.intent,
203
+ extractedParams: parsed.extracted_params ?? {},
204
+ reasoning: parsed.reasoning,
205
+ };
206
+ }
207
+ catch (err) {
208
+ logger.warn(`LLM match failed, falling back to keyword matcher: ${err}`);
209
+ return match(query, manifest);
210
+ }
211
+ }