toolpack-cli 0.1.0-SNAPSHOT
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/LICENSE +201 -0
- package/README.md +131 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +29 -0
- package/dist/commands/clear.d.ts +3 -0
- package/dist/commands/clear.js +15 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +29 -0
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.js +16 -0
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.js +24 -0
- package/dist/commands/mode.d.ts +3 -0
- package/dist/commands/mode.js +51 -0
- package/dist/commands/model.d.ts +3 -0
- package/dist/commands/model.js +14 -0
- package/dist/commands/registry.d.ts +32 -0
- package/dist/commands/registry.js +86 -0
- package/dist/commands/tool-log.d.ts +3 -0
- package/dist/commands/tool-log.js +17 -0
- package/dist/commands/tool-search.d.ts +3 -0
- package/dist/commands/tool-search.js +57 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +45 -0
- package/dist/commands/types.d.ts +25 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.js +25 -0
- package/dist/components/AppInfo.d.ts +1 -0
- package/dist/components/AppInfo.js +10 -0
- package/dist/components/HomeInput.d.ts +11 -0
- package/dist/components/HomeInput.js +328 -0
- package/dist/components/Logo.d.ts +1 -0
- package/dist/components/Logo.js +15 -0
- package/dist/components/Markdown.d.ts +5 -0
- package/dist/components/Markdown.js +121 -0
- package/dist/components/ProviderBar.d.ts +12 -0
- package/dist/components/ProviderBar.js +32 -0
- package/dist/components/ShimmerText.d.ts +8 -0
- package/dist/components/ShimmerText.js +20 -0
- package/dist/components/ToolLogPopup.d.ts +7 -0
- package/dist/components/ToolLogPopup.js +87 -0
- package/dist/components/common/HistorySelect.d.ts +6 -0
- package/dist/components/common/HistorySelect.js +57 -0
- package/dist/components/common/Modal.d.ts +10 -0
- package/dist/components/common/Modal.js +13 -0
- package/dist/components/common/ModeSelect.d.ts +6 -0
- package/dist/components/common/ModeSelect.js +13 -0
- package/dist/components/common/ModelSelect.d.ts +9 -0
- package/dist/components/common/ModelSelect.js +45 -0
- package/dist/context/ConversationContext.d.ts +44 -0
- package/dist/context/ConversationContext.js +113 -0
- package/dist/context/ToolpackContext.d.ts +55 -0
- package/dist/context/ToolpackContext.js +221 -0
- package/dist/custom-providers/AnthropicCustomAdapter.d.ts +49 -0
- package/dist/custom-providers/AnthropicCustomAdapter.js +297 -0
- package/dist/custom-providers/XAIAdapter.d.ts +40 -0
- package/dist/custom-providers/XAIAdapter.js +295 -0
- package/dist/custom-tools/skill-tools/index.d.ts +33 -0
- package/dist/custom-tools/skill-tools/index.js +63 -0
- package/dist/custom-tools/skill-tools/tools/create/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/create/index.js +93 -0
- package/dist/custom-tools/skill-tools/tools/create/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/create/schema.js +41 -0
- package/dist/custom-tools/skill-tools/tools/list/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/list/index.js +113 -0
- package/dist/custom-tools/skill-tools/tools/list/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/list/schema.js +19 -0
- package/dist/custom-tools/skill-tools/tools/read/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/read/index.js +124 -0
- package/dist/custom-tools/skill-tools/tools/read/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/read/schema.js +27 -0
- package/dist/custom-tools/skill-tools/tools/search/bm25.d.ts +71 -0
- package/dist/custom-tools/skill-tools/tools/search/bm25.js +305 -0
- package/dist/custom-tools/skill-tools/tools/search/index.d.ts +8 -0
- package/dist/custom-tools/skill-tools/tools/search/index.js +63 -0
- package/dist/custom-tools/skill-tools/tools/search/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/search/schema.js +19 -0
- package/dist/custom-tools/skill-tools/tools/search/skill-index.d.ts +54 -0
- package/dist/custom-tools/skill-tools/tools/search/skill-index.js +251 -0
- package/dist/custom-tools/skill-tools/tools/update/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/update/index.js +115 -0
- package/dist/custom-tools/skill-tools/tools/update/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/update/schema.js +41 -0
- package/dist/screens/ChatScreen.d.ts +1 -0
- package/dist/screens/ChatScreen.js +327 -0
- package/dist/screens/HomeScreen.d.ts +1 -0
- package/dist/screens/HomeScreen.js +68 -0
- package/dist/screens/SettingsScreen.d.ts +1 -0
- package/dist/screens/SettingsScreen.js +35 -0
- package/dist/services/db.d.ts +31 -0
- package/dist/services/db.js +108 -0
- package/dist/theme/ThemeContext.d.ts +11 -0
- package/dist/theme/ThemeContext.js +31 -0
- package/dist/theme/theme.d.ts +17 -0
- package/dist/theme/theme.js +82 -0
- package/package.json +101 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { name, displayName, description, parameters, category, } from './schema.js';
|
|
4
|
+
const SKILLS_DIR = '.skills';
|
|
5
|
+
function parseSkillFile(content) {
|
|
6
|
+
// Parse frontmatter
|
|
7
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8
|
+
const metadata = {
|
|
9
|
+
name: '',
|
|
10
|
+
title: '',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
created: '',
|
|
13
|
+
updated: '',
|
|
14
|
+
tags: [],
|
|
15
|
+
};
|
|
16
|
+
if (frontmatterMatch && frontmatterMatch[1]) {
|
|
17
|
+
const frontmatter = frontmatterMatch[1];
|
|
18
|
+
const lines = frontmatter.split('\n');
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const [key, ...valueParts] = line.split(':');
|
|
21
|
+
const value = valueParts.join(':').trim();
|
|
22
|
+
if (key === 'name')
|
|
23
|
+
metadata.name = value;
|
|
24
|
+
if (key === 'title')
|
|
25
|
+
metadata.title = value;
|
|
26
|
+
if (key === 'version')
|
|
27
|
+
metadata.version = value;
|
|
28
|
+
if (key === 'created')
|
|
29
|
+
metadata.created = value;
|
|
30
|
+
if (key === 'updated')
|
|
31
|
+
metadata.updated = value;
|
|
32
|
+
if (key === 'tags') {
|
|
33
|
+
const tagsMatch = value.match(/\[(.*)\]/);
|
|
34
|
+
if (tagsMatch) {
|
|
35
|
+
metadata.tags = (tagsMatch[1] || '')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map(t => t.trim().replace(/"/g, ''))
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Parse sections
|
|
44
|
+
const descMatch = content.match(/## Description\n\n([\s\S]*?)(?=\n## |$)/);
|
|
45
|
+
const triggersMatch = content.match(/## Triggers\n\n([\s\S]*?)(?=\n## |$)/);
|
|
46
|
+
const instructionsMatch = content.match(/## Instructions\n\n([\s\S]*?)(?=\n## |$)/);
|
|
47
|
+
const examplesMatch = content.match(/## Examples\n\n([\s\S]*?)(?=\n## |$)/);
|
|
48
|
+
const triggers = [];
|
|
49
|
+
if (triggersMatch && triggersMatch[1]) {
|
|
50
|
+
const triggerLines = triggersMatch[1]
|
|
51
|
+
.split('\n')
|
|
52
|
+
.filter(l => l.startsWith('- '));
|
|
53
|
+
for (const line of triggerLines) {
|
|
54
|
+
const match = line.match(/- "(.*)"/);
|
|
55
|
+
if (match && match[1])
|
|
56
|
+
triggers.push(match[1]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const examples = [];
|
|
60
|
+
if (examplesMatch && examplesMatch[1]) {
|
|
61
|
+
const exampleBlocks = (examplesMatch[1] || '')
|
|
62
|
+
.split(/### Example \d+\n\n/)
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
examples.push(...exampleBlocks.map(e => e.trim()));
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
metadata,
|
|
68
|
+
description: descMatch && descMatch[1] ? descMatch[1].trim() : '',
|
|
69
|
+
triggers,
|
|
70
|
+
instructions: instructionsMatch && instructionsMatch[1]
|
|
71
|
+
? instructionsMatch[1].trim()
|
|
72
|
+
: '',
|
|
73
|
+
examples,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function execute(args) {
|
|
77
|
+
const skillName = args['name'];
|
|
78
|
+
const section = args['section'] || 'all';
|
|
79
|
+
const skillPath = path.join(process.cwd(), SKILLS_DIR, `${skillName}.skill.md`);
|
|
80
|
+
// Check if skill exists
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(skillPath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
error: 'skill_not_found',
|
|
87
|
+
message: `Skill "${skillName}" not found. Use skill.list to see available skills.`,
|
|
88
|
+
}, null, 2);
|
|
89
|
+
}
|
|
90
|
+
// Read and parse the skill file
|
|
91
|
+
const content = await fs.readFile(skillPath, 'utf-8');
|
|
92
|
+
const parsed = parseSkillFile(content);
|
|
93
|
+
// Return requested section
|
|
94
|
+
if (section === 'metadata') {
|
|
95
|
+
return JSON.stringify({ skill: skillName, metadata: parsed.metadata }, null, 2);
|
|
96
|
+
}
|
|
97
|
+
else if (section === 'description') {
|
|
98
|
+
return JSON.stringify({ skill: skillName, description: parsed.description }, null, 2);
|
|
99
|
+
}
|
|
100
|
+
else if (section === 'triggers') {
|
|
101
|
+
return JSON.stringify({ skill: skillName, triggers: parsed.triggers }, null, 2);
|
|
102
|
+
}
|
|
103
|
+
else if (section === 'instructions') {
|
|
104
|
+
return JSON.stringify({ skill: skillName, instructions: parsed.instructions }, null, 2);
|
|
105
|
+
}
|
|
106
|
+
else if (section === 'examples') {
|
|
107
|
+
return JSON.stringify({ skill: skillName, examples: parsed.examples }, null, 2);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
skill: skillName,
|
|
112
|
+
path: skillPath,
|
|
113
|
+
...parsed,
|
|
114
|
+
}, null, 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const skillReadTool = {
|
|
118
|
+
name,
|
|
119
|
+
displayName,
|
|
120
|
+
description,
|
|
121
|
+
parameters,
|
|
122
|
+
category,
|
|
123
|
+
execute,
|
|
124
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ToolParameters } from 'toolpack-sdk';
|
|
2
|
+
export declare const name = "skill.read";
|
|
3
|
+
export declare const displayName = "Read Skill";
|
|
4
|
+
export declare const description = "Read and parse a SKILL.md file to retrieve skill documentation including metadata, triggers, instructions, and examples.";
|
|
5
|
+
export declare const category = "productivity";
|
|
6
|
+
export declare const parameters: ToolParameters;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const name = 'skill.read';
|
|
2
|
+
export const displayName = 'Read Skill';
|
|
3
|
+
export const description = 'Read and parse a SKILL.md file to retrieve skill documentation including metadata, triggers, instructions, and examples.';
|
|
4
|
+
export const category = 'productivity';
|
|
5
|
+
export const parameters = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
name: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'The skill name to read (e.g., "code-review")',
|
|
11
|
+
},
|
|
12
|
+
section: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Optional: specific section to retrieve ("metadata", "description", "triggers", "instructions", "examples"). Default: returns all.',
|
|
15
|
+
enum: [
|
|
16
|
+
'metadata',
|
|
17
|
+
'description',
|
|
18
|
+
'triggers',
|
|
19
|
+
'instructions',
|
|
20
|
+
'examples',
|
|
21
|
+
'all',
|
|
22
|
+
],
|
|
23
|
+
default: 'all',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['name'],
|
|
27
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 (Best Matching 25) Search Implementation
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, zero-dependency implementation of the BM25 ranking algorithm
|
|
5
|
+
* for searching skill documents. BM25 is a bag-of-words retrieval function that
|
|
6
|
+
* ranks documents based on query term frequency and inverse document frequency.
|
|
7
|
+
*/
|
|
8
|
+
export interface BM25Document {
|
|
9
|
+
id: string;
|
|
10
|
+
content: string;
|
|
11
|
+
fields: Record<string, string | string[]>;
|
|
12
|
+
}
|
|
13
|
+
export interface BM25SearchResult {
|
|
14
|
+
id: string;
|
|
15
|
+
score: number;
|
|
16
|
+
fields: Record<string, string | string[]>;
|
|
17
|
+
}
|
|
18
|
+
export interface BM25Options {
|
|
19
|
+
k1?: number;
|
|
20
|
+
b?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class BM25Index {
|
|
23
|
+
private documents;
|
|
24
|
+
private termFrequencies;
|
|
25
|
+
private documentLengths;
|
|
26
|
+
private avgDocLength;
|
|
27
|
+
private k1;
|
|
28
|
+
private b;
|
|
29
|
+
constructor(options?: BM25Options);
|
|
30
|
+
/**
|
|
31
|
+
* Tokenize text into searchable terms.
|
|
32
|
+
* Handles camelCase, snake_case, kebab-case, and common word boundaries.
|
|
33
|
+
*/
|
|
34
|
+
private tokenize;
|
|
35
|
+
/**
|
|
36
|
+
* Common stop words to filter out
|
|
37
|
+
*/
|
|
38
|
+
private isStopWord;
|
|
39
|
+
/**
|
|
40
|
+
* Add a document to the index
|
|
41
|
+
*/
|
|
42
|
+
addDocument(doc: BM25Document): void;
|
|
43
|
+
/**
|
|
44
|
+
* Remove a document from the index
|
|
45
|
+
*/
|
|
46
|
+
removeDocument(docId: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Clear the entire index
|
|
49
|
+
*/
|
|
50
|
+
clear(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Update average document length
|
|
53
|
+
*/
|
|
54
|
+
private updateAvgDocLength;
|
|
55
|
+
/**
|
|
56
|
+
* Calculate IDF (Inverse Document Frequency) for a term
|
|
57
|
+
*/
|
|
58
|
+
private idf;
|
|
59
|
+
/**
|
|
60
|
+
* Search the index with a query
|
|
61
|
+
*/
|
|
62
|
+
search(query: string, limit?: number): BM25SearchResult[];
|
|
63
|
+
/**
|
|
64
|
+
* Get the number of indexed documents
|
|
65
|
+
*/
|
|
66
|
+
get size(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Check if a document exists in the index
|
|
69
|
+
*/
|
|
70
|
+
has(docId: string): boolean;
|
|
71
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 (Best Matching 25) Search Implementation
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, zero-dependency implementation of the BM25 ranking algorithm
|
|
5
|
+
* for searching skill documents. BM25 is a bag-of-words retrieval function that
|
|
6
|
+
* ranks documents based on query term frequency and inverse document frequency.
|
|
7
|
+
*/
|
|
8
|
+
export class BM25Index {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
Object.defineProperty(this, "documents", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: new Map()
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "termFrequencies", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: new Map()
|
|
21
|
+
}); // term -> docId -> frequency
|
|
22
|
+
Object.defineProperty(this, "documentLengths", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: new Map()
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "avgDocLength", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "k1", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "b", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: void 0
|
|
45
|
+
});
|
|
46
|
+
this.k1 = options.k1 ?? 1.5;
|
|
47
|
+
this.b = options.b ?? 0.75;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Tokenize text into searchable terms.
|
|
51
|
+
* Handles camelCase, snake_case, kebab-case, and common word boundaries.
|
|
52
|
+
*/
|
|
53
|
+
tokenize(text) {
|
|
54
|
+
if (!text)
|
|
55
|
+
return [];
|
|
56
|
+
// Convert to lowercase
|
|
57
|
+
let normalized = text.toLowerCase();
|
|
58
|
+
// Split camelCase and PascalCase
|
|
59
|
+
normalized = normalized.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
60
|
+
// Replace non-alphanumeric with spaces
|
|
61
|
+
normalized = normalized.replace(/[^a-z0-9]/g, ' ');
|
|
62
|
+
// Split and filter
|
|
63
|
+
const tokens = normalized
|
|
64
|
+
.split(/\s+/)
|
|
65
|
+
.filter(token => token.length > 1) // Remove single chars
|
|
66
|
+
.filter(token => !this.isStopWord(token));
|
|
67
|
+
return tokens;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Common stop words to filter out
|
|
71
|
+
*/
|
|
72
|
+
isStopWord(word) {
|
|
73
|
+
const stopWords = new Set([
|
|
74
|
+
'a',
|
|
75
|
+
'an',
|
|
76
|
+
'the',
|
|
77
|
+
'and',
|
|
78
|
+
'or',
|
|
79
|
+
'but',
|
|
80
|
+
'in',
|
|
81
|
+
'on',
|
|
82
|
+
'at',
|
|
83
|
+
'to',
|
|
84
|
+
'for',
|
|
85
|
+
'of',
|
|
86
|
+
'with',
|
|
87
|
+
'by',
|
|
88
|
+
'from',
|
|
89
|
+
'as',
|
|
90
|
+
'is',
|
|
91
|
+
'was',
|
|
92
|
+
'are',
|
|
93
|
+
'were',
|
|
94
|
+
'been',
|
|
95
|
+
'be',
|
|
96
|
+
'have',
|
|
97
|
+
'has',
|
|
98
|
+
'had',
|
|
99
|
+
'do',
|
|
100
|
+
'does',
|
|
101
|
+
'did',
|
|
102
|
+
'will',
|
|
103
|
+
'would',
|
|
104
|
+
'could',
|
|
105
|
+
'should',
|
|
106
|
+
'may',
|
|
107
|
+
'might',
|
|
108
|
+
'must',
|
|
109
|
+
'shall',
|
|
110
|
+
'can',
|
|
111
|
+
'need',
|
|
112
|
+
'it',
|
|
113
|
+
'its',
|
|
114
|
+
'this',
|
|
115
|
+
'that',
|
|
116
|
+
'these',
|
|
117
|
+
'those',
|
|
118
|
+
'i',
|
|
119
|
+
'you',
|
|
120
|
+
'he',
|
|
121
|
+
'she',
|
|
122
|
+
'we',
|
|
123
|
+
'they',
|
|
124
|
+
'what',
|
|
125
|
+
'which',
|
|
126
|
+
'who',
|
|
127
|
+
'when',
|
|
128
|
+
'where',
|
|
129
|
+
'why',
|
|
130
|
+
'how',
|
|
131
|
+
'all',
|
|
132
|
+
'each',
|
|
133
|
+
'every',
|
|
134
|
+
'both',
|
|
135
|
+
'few',
|
|
136
|
+
'more',
|
|
137
|
+
'most',
|
|
138
|
+
'other',
|
|
139
|
+
'some',
|
|
140
|
+
'such',
|
|
141
|
+
'no',
|
|
142
|
+
'nor',
|
|
143
|
+
'not',
|
|
144
|
+
'only',
|
|
145
|
+
'own',
|
|
146
|
+
'same',
|
|
147
|
+
'so',
|
|
148
|
+
'than',
|
|
149
|
+
'too',
|
|
150
|
+
'very',
|
|
151
|
+
'just',
|
|
152
|
+
'also',
|
|
153
|
+
'now',
|
|
154
|
+
'here',
|
|
155
|
+
'there',
|
|
156
|
+
'then',
|
|
157
|
+
]);
|
|
158
|
+
return stopWords.has(word);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Add a document to the index
|
|
162
|
+
*/
|
|
163
|
+
addDocument(doc) {
|
|
164
|
+
this.documents.set(doc.id, doc);
|
|
165
|
+
// Build searchable content from all fields
|
|
166
|
+
let fullContent = doc.content;
|
|
167
|
+
for (const [, value] of Object.entries(doc.fields)) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
fullContent += ' ' + value.join(' ');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
fullContent += ' ' + value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const tokens = this.tokenize(fullContent);
|
|
176
|
+
this.documentLengths.set(doc.id, tokens.length);
|
|
177
|
+
// Count term frequencies for this document
|
|
178
|
+
const termCounts = {};
|
|
179
|
+
for (const token of tokens) {
|
|
180
|
+
termCounts[token] = (termCounts[token] || 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
// Update global term frequencies
|
|
183
|
+
for (const term of Object.keys(termCounts)) {
|
|
184
|
+
const count = termCounts[term] || 0;
|
|
185
|
+
if (!this.termFrequencies.has(term)) {
|
|
186
|
+
this.termFrequencies.set(term, new Map());
|
|
187
|
+
}
|
|
188
|
+
this.termFrequencies.get(term).set(doc.id, count);
|
|
189
|
+
}
|
|
190
|
+
// Recalculate average document length
|
|
191
|
+
this.updateAvgDocLength();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Remove a document from the index
|
|
195
|
+
*/
|
|
196
|
+
removeDocument(docId) {
|
|
197
|
+
if (!this.documents.has(docId))
|
|
198
|
+
return;
|
|
199
|
+
// Remove from term frequencies
|
|
200
|
+
const termsToDelete = [];
|
|
201
|
+
this.termFrequencies.forEach((docFreqs, term) => {
|
|
202
|
+
docFreqs.delete(docId);
|
|
203
|
+
if (docFreqs.size === 0) {
|
|
204
|
+
termsToDelete.push(term);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
for (const term of termsToDelete) {
|
|
208
|
+
this.termFrequencies.delete(term);
|
|
209
|
+
}
|
|
210
|
+
this.documents.delete(docId);
|
|
211
|
+
this.documentLengths.delete(docId);
|
|
212
|
+
this.updateAvgDocLength();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Clear the entire index
|
|
216
|
+
*/
|
|
217
|
+
clear() {
|
|
218
|
+
this.documents.clear();
|
|
219
|
+
this.termFrequencies.clear();
|
|
220
|
+
this.documentLengths.clear();
|
|
221
|
+
this.avgDocLength = 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Update average document length
|
|
225
|
+
*/
|
|
226
|
+
updateAvgDocLength() {
|
|
227
|
+
if (this.documentLengths.size === 0) {
|
|
228
|
+
this.avgDocLength = 0;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let total = 0;
|
|
232
|
+
this.documentLengths.forEach(length => {
|
|
233
|
+
total += length;
|
|
234
|
+
});
|
|
235
|
+
this.avgDocLength = total / this.documentLengths.size;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Calculate IDF (Inverse Document Frequency) for a term
|
|
239
|
+
*/
|
|
240
|
+
idf(term) {
|
|
241
|
+
const N = this.documents.size;
|
|
242
|
+
const docFreq = this.termFrequencies.get(term)?.size || 0;
|
|
243
|
+
if (docFreq === 0)
|
|
244
|
+
return 0;
|
|
245
|
+
// Standard BM25 IDF formula
|
|
246
|
+
return Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Search the index with a query
|
|
250
|
+
*/
|
|
251
|
+
search(query, limit = 10) {
|
|
252
|
+
const queryTokens = this.tokenize(query);
|
|
253
|
+
if (queryTokens.length === 0) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const scores = {};
|
|
257
|
+
// Calculate BM25 score for each document
|
|
258
|
+
this.documents.forEach((_, docId) => {
|
|
259
|
+
let score = 0;
|
|
260
|
+
const docLength = this.documentLengths.get(docId) || 0;
|
|
261
|
+
for (const term of queryTokens) {
|
|
262
|
+
const termDocFreqs = this.termFrequencies.get(term);
|
|
263
|
+
if (!termDocFreqs)
|
|
264
|
+
continue;
|
|
265
|
+
const tf = termDocFreqs.get(docId) || 0;
|
|
266
|
+
if (tf === 0)
|
|
267
|
+
continue;
|
|
268
|
+
const idf = this.idf(term);
|
|
269
|
+
// BM25 scoring formula
|
|
270
|
+
const numerator = tf * (this.k1 + 1);
|
|
271
|
+
const denominator = tf +
|
|
272
|
+
this.k1 * (1 - this.b + this.b * (docLength / this.avgDocLength));
|
|
273
|
+
score += idf * (numerator / denominator);
|
|
274
|
+
}
|
|
275
|
+
if (score > 0) {
|
|
276
|
+
scores[docId] = score;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// Sort by score descending
|
|
280
|
+
const results = [];
|
|
281
|
+
for (const docId of Object.keys(scores)) {
|
|
282
|
+
const score = scores[docId] || 0;
|
|
283
|
+
const doc = this.documents.get(docId);
|
|
284
|
+
results.push({
|
|
285
|
+
id: docId,
|
|
286
|
+
score,
|
|
287
|
+
fields: doc.fields,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
results.sort((a, b) => b.score - a.score);
|
|
291
|
+
return results.slice(0, limit);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get the number of indexed documents
|
|
295
|
+
*/
|
|
296
|
+
get size() {
|
|
297
|
+
return this.documents.size;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if a document exists in the index
|
|
301
|
+
*/
|
|
302
|
+
has(docId) {
|
|
303
|
+
return this.documents.has(docId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Search Tool
|
|
3
|
+
*
|
|
4
|
+
* Uses BM25 search to find relevant skills based on a natural language query.
|
|
5
|
+
* Searches across skill names, titles, descriptions, tags, keywords, and triggers.
|
|
6
|
+
*/
|
|
7
|
+
import { ToolDefinition } from 'toolpack-sdk';
|
|
8
|
+
export declare const skillSearchTool: ToolDefinition;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Search Tool
|
|
3
|
+
*
|
|
4
|
+
* Uses BM25 search to find relevant skills based on a natural language query.
|
|
5
|
+
* Searches across skill names, titles, descriptions, tags, keywords, and triggers.
|
|
6
|
+
*/
|
|
7
|
+
import { name, displayName, description, parameters, category, } from './schema.js';
|
|
8
|
+
import { skillIndex } from './skill-index.js';
|
|
9
|
+
async function execute(args) {
|
|
10
|
+
const query = args['query'];
|
|
11
|
+
const limit = args['limit'] || 5;
|
|
12
|
+
if (!query || query.trim().length === 0) {
|
|
13
|
+
return JSON.stringify({
|
|
14
|
+
error: 'invalid_query',
|
|
15
|
+
message: 'Search query cannot be empty.',
|
|
16
|
+
}, null, 2);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const results = await skillIndex.search(query, limit);
|
|
20
|
+
if (results.length === 0) {
|
|
21
|
+
return JSON.stringify({
|
|
22
|
+
query,
|
|
23
|
+
results: [],
|
|
24
|
+
count: 0,
|
|
25
|
+
message: 'No skills found matching your query. Try different keywords or use skill.list to see all available skills.',
|
|
26
|
+
}, null, 2);
|
|
27
|
+
}
|
|
28
|
+
// Format results for AI consumption
|
|
29
|
+
const formattedResults = results.map((result, index) => ({
|
|
30
|
+
rank: index + 1,
|
|
31
|
+
name: result.skill.name,
|
|
32
|
+
title: result.skill.title,
|
|
33
|
+
score: Math.round(result.score * 100) / 100,
|
|
34
|
+
description: result.skill.description.length > 200
|
|
35
|
+
? result.skill.description.substring(0, 200) + '...'
|
|
36
|
+
: result.skill.description,
|
|
37
|
+
tags: result.skill.tags,
|
|
38
|
+
triggers: result.skill.triggers.slice(0, 3), // Limit triggers shown
|
|
39
|
+
path: result.skill.path,
|
|
40
|
+
}));
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
query,
|
|
43
|
+
results: formattedResults,
|
|
44
|
+
count: results.length,
|
|
45
|
+
indexed_skills: skillIndex.size,
|
|
46
|
+
hint: 'Use skill.read with the skill name to get full details including instructions and examples.',
|
|
47
|
+
}, null, 2);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
error: 'search_failed',
|
|
52
|
+
message: error.message || 'Failed to search skills.',
|
|
53
|
+
}, null, 2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const skillSearchTool = {
|
|
57
|
+
name,
|
|
58
|
+
displayName,
|
|
59
|
+
description,
|
|
60
|
+
parameters,
|
|
61
|
+
category,
|
|
62
|
+
execute,
|
|
63
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ToolParameters } from 'toolpack-sdk';
|
|
2
|
+
export declare const name = "skill.search";
|
|
3
|
+
export declare const displayName = "Search Skills";
|
|
4
|
+
export declare const description = "Search for relevant skills using BM25 text search. Searches across skill names, titles, descriptions, tags, keywords, and triggers to find the most relevant skills for a given query.";
|
|
5
|
+
export declare const category = "productivity";
|
|
6
|
+
export declare const parameters: ToolParameters;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const name = 'skill.search';
|
|
2
|
+
export const displayName = 'Search Skills';
|
|
3
|
+
export const description = 'Search for relevant skills using BM25 text search. Searches across skill names, titles, descriptions, tags, keywords, and triggers to find the most relevant skills for a given query.';
|
|
4
|
+
export const category = 'productivity';
|
|
5
|
+
export const parameters = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
query: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'The search query to find relevant skills. Can be natural language like "how to deploy to AWS" or keywords like "react testing".',
|
|
11
|
+
},
|
|
12
|
+
limit: {
|
|
13
|
+
type: 'number',
|
|
14
|
+
description: 'Maximum number of results to return. Default: 5',
|
|
15
|
+
default: 5,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['query'],
|
|
19
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Index Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the BM25 index for skill files, handling indexing, caching,
|
|
5
|
+
* and automatic re-indexing when skills change.
|
|
6
|
+
*/
|
|
7
|
+
import { BM25SearchResult } from './bm25.js';
|
|
8
|
+
export interface SkillDocument {
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
triggers: string[];
|
|
14
|
+
keywords: string[];
|
|
15
|
+
path: string;
|
|
16
|
+
lastModified: number;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillSearchResult extends BM25SearchResult {
|
|
19
|
+
skill: SkillDocument;
|
|
20
|
+
}
|
|
21
|
+
declare class SkillIndexManager {
|
|
22
|
+
private index;
|
|
23
|
+
private skillCache;
|
|
24
|
+
private lastIndexTime;
|
|
25
|
+
private indexedDir;
|
|
26
|
+
constructor();
|
|
27
|
+
/**
|
|
28
|
+
* Parse a skill file and extract all searchable content
|
|
29
|
+
*/
|
|
30
|
+
private parseSkillFile;
|
|
31
|
+
/**
|
|
32
|
+
* Build searchable content for BM25 indexing
|
|
33
|
+
* Weights different fields by repeating important terms
|
|
34
|
+
*/
|
|
35
|
+
private buildSearchableContent;
|
|
36
|
+
/**
|
|
37
|
+
* Index all skills in the .skills directory
|
|
38
|
+
*/
|
|
39
|
+
indexSkills(workspaceRoot?: string): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Search for skills matching the query
|
|
42
|
+
*/
|
|
43
|
+
search(query: string, limit?: number, workspaceRoot?: string): Promise<SkillSearchResult[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Get the number of indexed skills
|
|
46
|
+
*/
|
|
47
|
+
get size(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Force a full re-index
|
|
50
|
+
*/
|
|
51
|
+
reindex(workspaceRoot?: string): Promise<number>;
|
|
52
|
+
}
|
|
53
|
+
export declare const skillIndex: SkillIndexManager;
|
|
54
|
+
export {};
|