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.
- package/bin/capman.js +152 -6
- package/dist/cjs/cache.d.ts +42 -0
- package/dist/cjs/cache.d.ts.map +1 -0
- package/dist/cjs/cache.js +181 -0
- package/dist/cjs/cache.js.map +1 -0
- package/dist/cjs/engine.d.ts +82 -0
- package/dist/cjs/engine.d.ts.map +1 -0
- package/dist/cjs/engine.js +154 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/generator.d.ts.map +1 -0
- package/dist/{generator.js → cjs/generator.js} +2 -1
- package/dist/cjs/generator.js.map +1 -0
- package/dist/{index.d.ts → cjs/index.d.ts} +18 -1
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +77 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/learning.d.ts +56 -0
- package/dist/cjs/learning.d.ts.map +1 -0
- package/dist/cjs/learning.js +184 -0
- package/dist/cjs/learning.js.map +1 -0
- package/dist/cjs/logger.d.ts.map +1 -0
- package/dist/cjs/logger.js.map +1 -0
- package/dist/cjs/matcher.d.ts.map +1 -0
- package/dist/{matcher.js → cjs/matcher.js} +27 -5
- package/dist/cjs/matcher.js.map +1 -0
- package/dist/cjs/resolver.d.ts.map +1 -0
- package/dist/{resolver.js → cjs/resolver.js} +36 -4
- package/dist/cjs/resolver.js.map +1 -0
- package/dist/{schema.d.ts → cjs/schema.d.ts} +10 -10
- package/dist/{schema.d.ts.map → cjs/schema.d.ts.map} +1 -1
- package/dist/cjs/schema.js.map +1 -0
- package/dist/{types.d.ts → cjs/types.d.ts} +12 -5
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.d.ts.map +1 -0
- package/dist/cjs/version.js +6 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/esm/cache.js +141 -0
- package/dist/esm/engine.js +149 -0
- package/dist/esm/generator.js +183 -0
- package/dist/esm/generator.js.map +1 -0
- package/dist/esm/index.js +56 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/learning.js +145 -0
- package/dist/esm/logger.js +47 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/matcher.js +211 -0
- package/dist/esm/matcher.js.map +1 -0
- package/dist/esm/resolver.js +192 -0
- package/dist/esm/resolver.js.map +1 -0
- package/dist/esm/schema.js +88 -0
- package/dist/esm/schema.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/version.js +2 -0
- package/package.json +27 -11
- package/dist/generator.d.ts.map +0 -1
- package/dist/generator.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -42
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/matcher.d.ts.map +0 -1
- package/dist/matcher.js.map +0 -1
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- /package/dist/{generator.d.ts → cjs/generator.d.ts} +0 -0
- /package/dist/{logger.d.ts → cjs/logger.d.ts} +0 -0
- /package/dist/{logger.js → cjs/logger.js} +0 -0
- /package/dist/{matcher.d.ts → cjs/matcher.d.ts} +0 -0
- /package/dist/{resolver.d.ts → cjs/resolver.d.ts} +0 -0
- /package/dist/{schema.js → cjs/schema.js} +0 -0
- /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
|
+
}
|