@apitap/core 1.6.4 → 1.7.1
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/dist/cli.js +25 -1
- package/dist/cli.js.map +1 -1
- package/dist/skill/index.d.ts +52 -0
- package/dist/skill/index.js +173 -0
- package/dist/skill/index.js.map +1 -0
- package/dist/skill/merge.js +21 -1
- package/dist/skill/merge.js.map +1 -1
- package/dist/skill/search.d.ts +3 -1
- package/dist/skill/search.js +34 -17
- package/dist/skill/search.js.map +1 -1
- package/dist/skill/store.d.ts +7 -0
- package/dist/skill/store.js +38 -25
- package/dist/skill/store.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +24 -1
- package/src/skill/index.ts +238 -0
- package/src/skill/merge.ts +23 -1
- package/src/skill/search.ts +36 -17
- package/src/skill/store.ts +51 -22
package/src/skill/search.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/skill/search.ts
|
|
2
|
-
import {
|
|
2
|
+
import { ensureIndex } from './index.js';
|
|
3
3
|
|
|
4
4
|
export interface SearchResult {
|
|
5
5
|
domain: string;
|
|
@@ -13,20 +13,39 @@ export interface SearchResult {
|
|
|
13
13
|
export interface SearchResponse {
|
|
14
14
|
found: boolean;
|
|
15
15
|
results?: SearchResult[];
|
|
16
|
+
summary?: string;
|
|
16
17
|
suggestion?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Check if a search term matches a target string.
|
|
22
|
+
* Supports prefix matching: "payment" matches "payments", "pay" matches "payouts".
|
|
23
|
+
*/
|
|
24
|
+
function termMatches(term: string, text: string): boolean {
|
|
25
|
+
// Check if any word in the text starts with the term (prefix match)
|
|
26
|
+
// Split on word boundaries: slashes, hyphens, underscores, dots, spaces
|
|
27
|
+
const words = text.split(/[\s/\-_.]+/);
|
|
28
|
+
for (const word of words) {
|
|
29
|
+
if (word.startsWith(term)) return true;
|
|
30
|
+
}
|
|
31
|
+
// Also check plain substring for multi-word or partial path matches
|
|
32
|
+
return text.includes(term);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
/**
|
|
20
36
|
* Search skill files for endpoints matching a query.
|
|
37
|
+
* Uses the search index for sub-second results.
|
|
21
38
|
* Matches against domain names, endpoint IDs, and endpoint paths.
|
|
22
|
-
* Query terms are matched case-insensitively.
|
|
39
|
+
* Query terms are matched case-insensitively with prefix matching.
|
|
23
40
|
*/
|
|
24
41
|
export async function searchSkills(
|
|
25
42
|
query: string,
|
|
26
43
|
skillsDir?: string,
|
|
27
44
|
): Promise<SearchResponse> {
|
|
28
|
-
const
|
|
29
|
-
|
|
45
|
+
const index = await ensureIndex(skillsDir);
|
|
46
|
+
|
|
47
|
+
const domainCount = Object.keys(index.domains).length;
|
|
48
|
+
if (domainCount === 0) {
|
|
30
49
|
return {
|
|
31
50
|
found: false,
|
|
32
51
|
suggestion: 'No skill files found. Run `apitap capture <url>` to capture API traffic first.',
|
|
@@ -35,42 +54,42 @@ export async function searchSkills(
|
|
|
35
54
|
|
|
36
55
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
37
56
|
const results: SearchResult[] = [];
|
|
57
|
+
const matchedDomains = new Set<string>();
|
|
38
58
|
|
|
39
|
-
for (const
|
|
40
|
-
const
|
|
41
|
-
if (!skill) continue;
|
|
42
|
-
|
|
43
|
-
const domainLower = skill.domain.toLowerCase();
|
|
59
|
+
for (const [domain, entry] of Object.entries(index.domains)) {
|
|
60
|
+
const domainLower = domain.toLowerCase();
|
|
44
61
|
|
|
45
|
-
for (const ep of
|
|
62
|
+
for (const ep of entry.endpoints) {
|
|
46
63
|
const endpointIdLower = ep.id.toLowerCase();
|
|
47
64
|
const pathLower = ep.path.toLowerCase();
|
|
48
65
|
const methodLower = ep.method.toLowerCase();
|
|
49
66
|
|
|
50
|
-
// Check if all query terms match against the combined searchable text
|
|
51
67
|
const searchText = `${domainLower} ${endpointIdLower} ${pathLower} ${methodLower}`;
|
|
52
|
-
const allMatch = terms.every(term =>
|
|
68
|
+
const allMatch = terms.every(term => termMatches(term, searchText));
|
|
53
69
|
|
|
54
70
|
if (allMatch) {
|
|
71
|
+
matchedDomains.add(domain);
|
|
55
72
|
results.push({
|
|
56
|
-
domain
|
|
73
|
+
domain,
|
|
57
74
|
endpointId: ep.id,
|
|
58
75
|
method: ep.method,
|
|
59
76
|
path: ep.path,
|
|
60
|
-
tier: ep.
|
|
61
|
-
verified: ep.
|
|
77
|
+
tier: ep.tier ?? 'unknown',
|
|
78
|
+
verified: ep.verified ?? false,
|
|
62
79
|
});
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
if (results.length === 0) {
|
|
68
|
-
const domains =
|
|
85
|
+
const domains = Object.keys(index.domains).join(', ');
|
|
69
86
|
return {
|
|
70
87
|
found: false,
|
|
88
|
+
summary: '0 endpoints across 0 domains',
|
|
71
89
|
suggestion: `No matches for "${query}". Available domains: ${domains}`,
|
|
72
90
|
};
|
|
73
91
|
}
|
|
74
92
|
|
|
75
|
-
|
|
93
|
+
const summary = `${results.length} endpoints across ${matchedDomains.size} domain${matchedDomains.size === 1 ? '' : 's'}`;
|
|
94
|
+
return { found: true, results, summary };
|
|
76
95
|
}
|
package/src/skill/store.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { join, dirname } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import type { SkillFile, SkillSummary } from '../types.js';
|
|
6
6
|
import { validateSkillFile } from './validate.js';
|
|
7
|
+
import { updateIndex, ensureIndex } from './index.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_SKILLS_DIR = join(homedir(), '.apitap', 'skills');
|
|
9
10
|
|
|
@@ -37,6 +38,9 @@ export async function writeSkillFile(
|
|
|
37
38
|
skill: SkillFile,
|
|
38
39
|
skillsDir: string = DEFAULT_SKILLS_DIR,
|
|
39
40
|
): Promise<string> {
|
|
41
|
+
// Validate before writing — catch bad data at the source, not on read
|
|
42
|
+
validateSkillFile(skill);
|
|
43
|
+
|
|
40
44
|
await mkdir(skillsDir, { recursive: true, mode: 0o700 });
|
|
41
45
|
await ensureGitignore(skillsDir);
|
|
42
46
|
const filePath = skillPath(skill.domain, skillsDir);
|
|
@@ -44,6 +48,26 @@ export async function writeSkillFile(
|
|
|
44
48
|
const content = JSON.stringify(skill, null, 2) + '\n';
|
|
45
49
|
await writeFile(tmpPath, content, { mode: 0o600 });
|
|
46
50
|
await rename(tmpPath, filePath);
|
|
51
|
+
|
|
52
|
+
// Incrementally update the search index
|
|
53
|
+
try {
|
|
54
|
+
await updateIndex(
|
|
55
|
+
skill.domain,
|
|
56
|
+
skill.endpoints.map(ep => ({
|
|
57
|
+
id: ep.id,
|
|
58
|
+
method: ep.method,
|
|
59
|
+
path: ep.path,
|
|
60
|
+
...(ep.replayability?.tier ? { tier: ep.replayability.tier } : {}),
|
|
61
|
+
...(ep.replayability?.verified ? { verified: true } : {}),
|
|
62
|
+
})),
|
|
63
|
+
skill.provenance ?? 'unsigned',
|
|
64
|
+
skillsDir,
|
|
65
|
+
skill.capturedAt,
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
// Index update failure should not block writes
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
return filePath;
|
|
48
72
|
}
|
|
49
73
|
|
|
@@ -111,33 +135,38 @@ export async function readSkillFile(
|
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Fault-tolerant wrapper around readSkillFile — returns null instead of
|
|
140
|
+
* throwing on validation errors, bad signatures, etc. ENOENT (missing
|
|
141
|
+
* file) also returns null. Use this when iterating many files where one
|
|
142
|
+
* bad file should not abort the whole operation.
|
|
143
|
+
*/
|
|
144
|
+
export async function safeReadSkillFile(
|
|
145
|
+
domain: string,
|
|
115
146
|
skillsDir: string = DEFAULT_SKILLS_DIR,
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
options?: Parameters<typeof readSkillFile>[2],
|
|
148
|
+
): Promise<SkillFile | null> {
|
|
118
149
|
try {
|
|
119
|
-
|
|
120
|
-
} catch
|
|
121
|
-
|
|
122
|
-
throw err;
|
|
150
|
+
return await readSkillFile(domain, skillsDir, options);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
123
153
|
}
|
|
154
|
+
}
|
|
124
155
|
|
|
156
|
+
export async function listSkillFiles(
|
|
157
|
+
skillsDir: string = DEFAULT_SKILLS_DIR,
|
|
158
|
+
): Promise<SkillSummary[]> {
|
|
159
|
+
const index = await ensureIndex(skillsDir);
|
|
125
160
|
const summaries: SkillSummary[] = [];
|
|
126
|
-
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
skillFile: join(skillsDir, file),
|
|
136
|
-
endpointCount: skill.endpoints.length,
|
|
137
|
-
capturedAt: skill.capturedAt,
|
|
138
|
-
provenance: skill.provenance ?? 'unsigned',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
161
|
+
|
|
162
|
+
for (const [domain, entry] of Object.entries(index.domains)) {
|
|
163
|
+
summaries.push({
|
|
164
|
+
domain,
|
|
165
|
+
skillFile: join(skillsDir, `${domain}.json`),
|
|
166
|
+
endpointCount: entry.endpointCount,
|
|
167
|
+
capturedAt: entry.capturedAt || index.builtAt,
|
|
168
|
+
provenance: entry.provenance,
|
|
169
|
+
});
|
|
141
170
|
}
|
|
142
171
|
|
|
143
172
|
return summaries;
|