@apitap/core 1.6.4 → 1.7.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/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 +1 -0
- package/dist/skill/search.js +12 -14
- 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 +13 -14
- 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;
|
|
@@ -18,6 +18,7 @@ export interface SearchResponse {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Search skill files for endpoints matching a query.
|
|
21
|
+
* Uses the search index for sub-second results.
|
|
21
22
|
* Matches against domain names, endpoint IDs, and endpoint paths.
|
|
22
23
|
* Query terms are matched case-insensitively.
|
|
23
24
|
*/
|
|
@@ -25,8 +26,10 @@ export async function searchSkills(
|
|
|
25
26
|
query: string,
|
|
26
27
|
skillsDir?: string,
|
|
27
28
|
): Promise<SearchResponse> {
|
|
28
|
-
const
|
|
29
|
-
|
|
29
|
+
const index = await ensureIndex(skillsDir);
|
|
30
|
+
|
|
31
|
+
const domainCount = Object.keys(index.domains).length;
|
|
32
|
+
if (domainCount === 0) {
|
|
30
33
|
return {
|
|
31
34
|
found: false,
|
|
32
35
|
suggestion: 'No skill files found. Run `apitap capture <url>` to capture API traffic first.',
|
|
@@ -36,36 +39,32 @@ export async function searchSkills(
|
|
|
36
39
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
37
40
|
const results: SearchResult[] = [];
|
|
38
41
|
|
|
39
|
-
for (const
|
|
40
|
-
const
|
|
41
|
-
if (!skill) continue;
|
|
42
|
-
|
|
43
|
-
const domainLower = skill.domain.toLowerCase();
|
|
42
|
+
for (const [domain, entry] of Object.entries(index.domains)) {
|
|
43
|
+
const domainLower = domain.toLowerCase();
|
|
44
44
|
|
|
45
|
-
for (const ep of
|
|
45
|
+
for (const ep of entry.endpoints) {
|
|
46
46
|
const endpointIdLower = ep.id.toLowerCase();
|
|
47
47
|
const pathLower = ep.path.toLowerCase();
|
|
48
48
|
const methodLower = ep.method.toLowerCase();
|
|
49
49
|
|
|
50
|
-
// Check if all query terms match against the combined searchable text
|
|
51
50
|
const searchText = `${domainLower} ${endpointIdLower} ${pathLower} ${methodLower}`;
|
|
52
51
|
const allMatch = terms.every(term => searchText.includes(term));
|
|
53
52
|
|
|
54
53
|
if (allMatch) {
|
|
55
54
|
results.push({
|
|
56
|
-
domain
|
|
55
|
+
domain,
|
|
57
56
|
endpointId: ep.id,
|
|
58
57
|
method: ep.method,
|
|
59
58
|
path: ep.path,
|
|
60
|
-
tier: ep.
|
|
61
|
-
verified: ep.
|
|
59
|
+
tier: ep.tier ?? 'unknown',
|
|
60
|
+
verified: ep.verified ?? false,
|
|
62
61
|
});
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
if (results.length === 0) {
|
|
68
|
-
const domains =
|
|
67
|
+
const domains = Object.keys(index.domains).join(', ');
|
|
69
68
|
return {
|
|
70
69
|
found: false,
|
|
71
70
|
suggestion: `No matches for "${query}". Available domains: ${domains}`,
|
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;
|