@apitap/core 1.6.3 → 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.
@@ -1,13 +1,15 @@
1
1
  // src/skill/search.ts
2
- import { listSkillFiles, readSkillFile } from './store.js';
2
+ import { ensureIndex } from './index.js';
3
3
  /**
4
4
  * Search skill files for endpoints matching a query.
5
+ * Uses the search index for sub-second results.
5
6
  * Matches against domain names, endpoint IDs, and endpoint paths.
6
7
  * Query terms are matched case-insensitively.
7
8
  */
8
9
  export async function searchSkills(query, skillsDir) {
9
- const summaries = await listSkillFiles(skillsDir);
10
- if (summaries.length === 0) {
10
+ const index = await ensureIndex(skillsDir);
11
+ const domainCount = Object.keys(index.domains).length;
12
+ if (domainCount === 0) {
11
13
  return {
12
14
  found: false,
13
15
  suggestion: 'No skill files found. Run `apitap capture <url>` to capture API traffic first.',
@@ -15,32 +17,28 @@ export async function searchSkills(query, skillsDir) {
15
17
  }
16
18
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
17
19
  const results = [];
18
- for (const summary of summaries) {
19
- const skill = await readSkillFile(summary.domain, skillsDir, { trustUnsigned: true });
20
- if (!skill)
21
- continue;
22
- const domainLower = skill.domain.toLowerCase();
23
- for (const ep of skill.endpoints) {
20
+ for (const [domain, entry] of Object.entries(index.domains)) {
21
+ const domainLower = domain.toLowerCase();
22
+ for (const ep of entry.endpoints) {
24
23
  const endpointIdLower = ep.id.toLowerCase();
25
24
  const pathLower = ep.path.toLowerCase();
26
25
  const methodLower = ep.method.toLowerCase();
27
- // Check if all query terms match against the combined searchable text
28
26
  const searchText = `${domainLower} ${endpointIdLower} ${pathLower} ${methodLower}`;
29
27
  const allMatch = terms.every(term => searchText.includes(term));
30
28
  if (allMatch) {
31
29
  results.push({
32
- domain: skill.domain,
30
+ domain,
33
31
  endpointId: ep.id,
34
32
  method: ep.method,
35
33
  path: ep.path,
36
- tier: ep.replayability?.tier ?? 'unknown',
37
- verified: ep.replayability?.verified ?? false,
34
+ tier: ep.tier ?? 'unknown',
35
+ verified: ep.verified ?? false,
38
36
  });
39
37
  }
40
38
  }
41
39
  }
42
40
  if (results.length === 0) {
43
- const domains = summaries.map(s => s.domain).join(', ');
41
+ const domains = Object.keys(index.domains).join(', ');
44
42
  return {
45
43
  found: false,
46
44
  suggestion: `No matches for "${query}". Available domains: ${domains}`,
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/skill/search.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAiB3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,SAAkB;IAElB,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,SAAS,CAAC,CAAC;IAClD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,gFAAgF;SAC7F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAmB,EAAE,CAAC;IAEnC,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,IAAI,CAAC,KAAK;YAAE,SAAS;QAErB,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAE/C,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,MAAM,eAAe,GAAG,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;YAC5C,MAAM,SAAS,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACxC,MAAM,WAAW,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAE5C,sEAAsE;YACtE,MAAM,UAAU,GAAG,GAAG,WAAW,IAAI,eAAe,IAAI,SAAS,IAAI,WAAW,EAAE,CAAC;YACnF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAEhE,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,UAAU,EAAE,EAAE,CAAC,EAAE;oBACjB,MAAM,EAAE,EAAE,CAAC,MAAM;oBACjB,IAAI,EAAE,EAAE,CAAC,IAAI;oBACb,IAAI,EAAE,EAAE,CAAC,aAAa,EAAE,IAAI,IAAI,SAAS;oBACzC,QAAQ,EAAE,EAAE,CAAC,aAAa,EAAE,QAAQ,IAAI,KAAK;iBAC9C,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,mBAAmB,KAAK,yBAAyB,OAAO,EAAE;SACvE,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/skill/search.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAiBzC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,SAAkB;IAElB,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,SAAS,CAAC,CAAC;IAE3C,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACtD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,gFAAgF;SAC7F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAmB,EAAE,CAAC;IAEnC,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QAEzC,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,MAAM,eAAe,GAAG,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;YAC5C,MAAM,SAAS,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACxC,MAAM,WAAW,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAE5C,MAAM,UAAU,GAAG,GAAG,WAAW,IAAI,eAAe,IAAI,SAAS,IAAI,WAAW,EAAE,CAAC;YACnF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAEhE,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM;oBACN,UAAU,EAAE,EAAE,CAAC,EAAE;oBACjB,MAAM,EAAE,EAAE,CAAC,MAAM;oBACjB,IAAI,EAAE,EAAE,CAAC,IAAI;oBACb,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,SAAS;oBAC1B,QAAQ,EAAE,EAAE,CAAC,QAAQ,IAAI,KAAK;iBAC/B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,mBAAmB,KAAK,yBAAyB,OAAO,EAAE;SACvE,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC"}
@@ -6,4 +6,11 @@ export declare function readSkillFile(domain: string, skillsDir?: string, option
6
6
  /** Allow loading unsigned files without throwing. Tampered signed files still reject. */
7
7
  trustUnsigned?: boolean;
8
8
  }): Promise<SkillFile | null>;
9
+ /**
10
+ * Fault-tolerant wrapper around readSkillFile — returns null instead of
11
+ * throwing on validation errors, bad signatures, etc. ENOENT (missing
12
+ * file) also returns null. Use this when iterating many files where one
13
+ * bad file should not abort the whole operation.
14
+ */
15
+ export declare function safeReadSkillFile(domain: string, skillsDir?: string, options?: Parameters<typeof readSkillFile>[2]): Promise<SkillFile | null>;
9
16
  export declare function listSkillFiles(skillsDir?: string): Promise<SkillSummary[]>;
@@ -1,8 +1,9 @@
1
1
  // src/skill/store.ts
2
- import { readFile, writeFile, mkdir, readdir, access, rename } from 'node:fs/promises';
2
+ import { readFile, writeFile, mkdir, access, rename } from 'node:fs/promises';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { validateSkillFile } from './validate.js';
6
+ import { updateIndex, ensureIndex } from './index.js';
6
7
  const DEFAULT_SKILLS_DIR = join(homedir(), '.apitap', 'skills');
7
8
  const BASE_GITIGNORE = `# ApiTap — prevent accidental credential commits
8
9
  auth.enc
@@ -28,6 +29,8 @@ async function ensureGitignore(skillsDir) {
28
29
  }
29
30
  }
30
31
  export async function writeSkillFile(skill, skillsDir = DEFAULT_SKILLS_DIR) {
32
+ // Validate before writing — catch bad data at the source, not on read
33
+ validateSkillFile(skill);
31
34
  await mkdir(skillsDir, { recursive: true, mode: 0o700 });
32
35
  await ensureGitignore(skillsDir);
33
36
  const filePath = skillPath(skill.domain, skillsDir);
@@ -35,6 +38,19 @@ export async function writeSkillFile(skill, skillsDir = DEFAULT_SKILLS_DIR) {
35
38
  const content = JSON.stringify(skill, null, 2) + '\n';
36
39
  await writeFile(tmpPath, content, { mode: 0o600 });
37
40
  await rename(tmpPath, filePath);
41
+ // Incrementally update the search index
42
+ try {
43
+ await updateIndex(skill.domain, skill.endpoints.map(ep => ({
44
+ id: ep.id,
45
+ method: ep.method,
46
+ path: ep.path,
47
+ ...(ep.replayability?.tier ? { tier: ep.replayability.tier } : {}),
48
+ ...(ep.replayability?.verified ? { verified: true } : {}),
49
+ })), skill.provenance ?? 'unsigned', skillsDir, skill.capturedAt);
50
+ }
51
+ catch {
52
+ // Index update failure should not block writes
53
+ }
38
54
  return filePath;
39
55
  }
40
56
  export async function readSkillFile(domain, skillsDir = DEFAULT_SKILLS_DIR, options) {
@@ -90,34 +106,31 @@ export async function readSkillFile(domain, skillsDir = DEFAULT_SKILLS_DIR, opti
90
106
  throw e;
91
107
  }
92
108
  }
93
- export async function listSkillFiles(skillsDir = DEFAULT_SKILLS_DIR) {
94
- let files;
109
+ /**
110
+ * Fault-tolerant wrapper around readSkillFile — returns null instead of
111
+ * throwing on validation errors, bad signatures, etc. ENOENT (missing
112
+ * file) also returns null. Use this when iterating many files where one
113
+ * bad file should not abort the whole operation.
114
+ */
115
+ export async function safeReadSkillFile(domain, skillsDir = DEFAULT_SKILLS_DIR, options) {
95
116
  try {
96
- files = await readdir(skillsDir);
117
+ return await readSkillFile(domain, skillsDir, options);
97
118
  }
98
- catch (err) {
99
- if (err.code === 'ENOENT')
100
- return [];
101
- throw err;
119
+ catch {
120
+ return null;
102
121
  }
122
+ }
123
+ export async function listSkillFiles(skillsDir = DEFAULT_SKILLS_DIR) {
124
+ const index = await ensureIndex(skillsDir);
103
125
  const summaries = [];
104
- const DOMAIN_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
105
- for (const file of files) {
106
- if (!file.endsWith('.json'))
107
- continue;
108
- const domain = file.replace(/\.json$/, '');
109
- if (!DOMAIN_RE.test(domain))
110
- continue; // skip non-conforming filenames
111
- const skill = await readSkillFile(domain, skillsDir, { trustUnsigned: true });
112
- if (skill) {
113
- summaries.push({
114
- domain: skill.domain,
115
- skillFile: join(skillsDir, file),
116
- endpointCount: skill.endpoints.length,
117
- capturedAt: skill.capturedAt,
118
- provenance: skill.provenance ?? 'unsigned',
119
- });
120
- }
126
+ for (const [domain, entry] of Object.entries(index.domains)) {
127
+ summaries.push({
128
+ domain,
129
+ skillFile: join(skillsDir, `${domain}.json`),
130
+ endpointCount: entry.endpointCount,
131
+ capturedAt: entry.capturedAt || index.builtAt,
132
+ provenance: entry.provenance,
133
+ });
121
134
  }
122
135
  return summaries;
123
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/skill/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/skill/store.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAW,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AAEhE,MAAM,cAAc,GAAG;;;CAGtB,CAAC;AAEF,SAAS,SAAS,CAAC,MAAc,EAAE,SAAiB;IAClD,IAAI,CAAC,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,SAAiB;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACnC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAC5B,+BAA+B;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;QAChC,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,MAAM,SAAS,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAgB,EAChB,YAAoB,kBAAkB;IAEtC,sEAAsE;IACtE,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAEzB,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IACjD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACtD,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,MAAM,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAEhC,wCAAwC;IACxC,IAAI,CAAC;QACH,MAAM,WAAW,CACf,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACzB,EAAE,EAAE,EAAE,CAAC,EAAE;YACT,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,GAAG,CAAC,EAAE,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,EAAE,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1D,CAAC,CAAC,EACH,KAAK,CAAC,UAAU,IAAI,UAAU,EAC9B,SAAS,EACT,KAAK,CAAC,UAAU,CACjB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,YAAoB,kBAAkB,EACtC,OAKC;IAED,iFAAiF;IACjF,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;QAErC,mDAAmD;QACnD,MAAM,YAAY,GAAG,OAAO,EAAE,eAAe,KAAK,KAAK,CAAC;QACxD,IAAI,YAAY,EAAE,CAAC;YACjB,0CAA0C;YAC1C,IAAI,UAAU,GAAG,OAAO,EAAE,UAAU,CAAC;YACrC,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;gBAC/D,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;gBAC5D,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAC;gBACvC,UAAU,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAC3C,CAAC;YAED,IAAI,KAAK,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;gBACpC,+DAA+D;YACjE,CAAC;iBAAM,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;gBAC5B,0DAA0D;gBAC1D,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,CAAC;oBAC5B,MAAM,IAAI,KAAK,CACb,kBAAkB,MAAM,uCAAuC;wBAC/D,6EAA6E,CAC9E,CAAC;gBACJ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;gBACzD,IAAI,QAAQ,GAAG,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;gBAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,2EAA2E;oBAC3E,kFAAkF;oBAClF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;oBACxD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;oBAC5D,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAC;oBACvC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;oBACvC,QAAQ,GAAG,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;gBAC/C,CAAC;gBACD,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,MAAM,IAAI,KAAK,CAAC,gDAAgD,MAAM,yBAAyB,CAAC,CAAC;gBACnG,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,OAAO,CAAU,EAAE,CAAC;QACpB,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAChE,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAc,EACd,YAAoB,kBAAkB,EACtC,OAA6C;IAE7C,IAAI,CAAC;QACH,OAAO,MAAM,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,YAAoB,kBAAkB;IAEtC,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAmB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,SAAS,CAAC,IAAI,CAAC;YACb,MAAM;YACN,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,OAAO,CAAC;YAC5C,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,OAAO;YAC7C,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apitap/core",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "description": "Intercept web API traffic during browsing. Generate portable skill files so AI agents can call APIs directly instead of scraping.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -30,6 +30,7 @@ import { attach, parseDomainPatterns } from './capture/cdp-attach.js';
30
30
  import { isOpenAPISpec, convertOpenAPISpec } from './skill/openapi-converter.js';
31
31
  import { mergeSkillFile } from './skill/merge.js';
32
32
  import { fetchApisGuruList, filterEntries, fetchSpec } from './skill/apis-guru.js';
33
+ import { buildIndex, removeFromIndex } from './skill/index.js';
33
34
 
34
35
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
35
36
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -92,6 +93,7 @@ function printUsage(): void {
92
93
  apitap audit Audit stored skill files and credentials
93
94
  apitap forget <domain> Remove skill file and credentials for a domain
94
95
  apitap stats Show token savings report
96
+ apitap index build Rebuild search index (run after manual edits)
95
97
  apitap extension install Register native messaging host for Chrome
96
98
 
97
99
  Discover options:
@@ -367,7 +369,8 @@ async function handleShow(positional: string[], flags: Record<string, string | b
367
369
  process.exit(1);
368
370
  }
369
371
 
370
- const skill = await readSkillFile(domain, SKILLS_DIR, { trustUnsigned: true });
372
+ // Intentionally skip HMAC for browse-only verification happens at replay time
373
+ const skill = await readSkillFile(domain, SKILLS_DIR, { verifySignature: false });
371
374
  if (!skill) {
372
375
  console.error(`Error: No skill file found for "${domain}". Run \`apitap capture\` first.`);
373
376
  process.exit(1);
@@ -1599,6 +1602,7 @@ async function handleForget(positional: string[]): Promise<void> {
1599
1602
  notFound = false;
1600
1603
  await unlink(skillFilePath);
1601
1604
  skillRemoved = true;
1605
+ try { await removeFromIndex(domain, skillsDir); } catch {}
1602
1606
  } catch {}
1603
1607
 
1604
1608
  // Remove stored auth
@@ -1628,6 +1632,22 @@ async function handleForget(positional: string[]): Promise<void> {
1628
1632
  console.log(`Forgot ${domain} — ${parts.join(' and ')} removed`);
1629
1633
  }
1630
1634
 
1635
+ async function handleIndex(positional: string[]): Promise<void> {
1636
+ const subcommand = positional[0];
1637
+ if (subcommand !== 'build') {
1638
+ console.error('Usage: apitap index build');
1639
+ console.error(' Force rebuild the search index from all skill files on disk.');
1640
+ console.error(' Run this after manually editing skill files outside of apitap commands.');
1641
+ process.exit(1);
1642
+ }
1643
+
1644
+ const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
1645
+ console.log('\n Rebuilding search index...');
1646
+ const index = await buildIndex(skillsDir);
1647
+ const endpointCount = Object.values(index.domains).reduce((sum, d) => sum + d.endpointCount, 0);
1648
+ console.log(` Done: ${Object.keys(index.domains).length} domains, ${endpointCount.toLocaleString()} endpoints indexed\n`);
1649
+ }
1650
+
1631
1651
  async function handleExtension(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
1632
1652
  const subcommand = positional[0];
1633
1653
 
@@ -1754,6 +1774,9 @@ async function main(): Promise<void> {
1754
1774
  case 'attach':
1755
1775
  await handleAttach(positional, flags);
1756
1776
  break;
1777
+ case 'index':
1778
+ await handleIndex(positional);
1779
+ break;
1757
1780
  default:
1758
1781
  printUsage();
1759
1782
  }
@@ -0,0 +1,238 @@
1
+ // src/skill/index.ts
2
+ import { readFile, writeFile, readdir, rename } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+
6
+ // --- Types ---
7
+
8
+ export interface IndexEndpoint {
9
+ id: string;
10
+ method: string;
11
+ path: string;
12
+ tier?: string;
13
+ verified?: boolean;
14
+ }
15
+
16
+ export interface IndexDomain {
17
+ endpointCount: number;
18
+ provenance: 'self' | 'imported-signed' | 'imported' | 'unsigned';
19
+ capturedAt: string;
20
+ endpoints: IndexEndpoint[];
21
+ }
22
+
23
+ export interface IndexFile {
24
+ version: number;
25
+ fileCount: number;
26
+ builtAt: string;
27
+ domains: Record<string, IndexDomain>;
28
+ }
29
+
30
+ // --- Paths ---
31
+
32
+ const DEFAULT_SKILLS_DIR = join(homedir(), '.apitap', 'skills');
33
+ const INDEX_VERSION = 1;
34
+
35
+ function indexPath(skillsDir: string): string {
36
+ return join(skillsDir, '..', 'index.json');
37
+ }
38
+
39
+ // --- Read ---
40
+
41
+ /**
42
+ * Read the index file. Returns null if missing or unparseable.
43
+ */
44
+ export async function readIndex(skillsDir: string = DEFAULT_SKILLS_DIR): Promise<IndexFile | null> {
45
+ try {
46
+ const content = await readFile(indexPath(skillsDir), 'utf-8');
47
+ const parsed = JSON.parse(content);
48
+ if (parsed.version !== INDEX_VERSION) return null;
49
+ return parsed as IndexFile;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // --- Stale detection ---
56
+
57
+ const STALE_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
58
+
59
+ async function countSkillFiles(skillsDir: string): Promise<number> {
60
+ try {
61
+ const files = await readdir(skillsDir);
62
+ return files.filter(f => f.endsWith('.json')).length;
63
+ } catch {
64
+ return 0;
65
+ }
66
+ }
67
+
68
+ export interface StaleCheck {
69
+ stale: boolean;
70
+ reason?: 'missing' | 'filecount-mismatch' | 'version-mismatch';
71
+ ageWarning?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Check if the index is stale. Returns { stale, reason, ageWarning }.
76
+ * ageWarning is true if the index is >24h old but fileCount matches (soft signal).
77
+ */
78
+ export async function checkStale(
79
+ index: IndexFile | null,
80
+ skillsDir: string = DEFAULT_SKILLS_DIR,
81
+ ): Promise<StaleCheck> {
82
+ if (!index) return { stale: true, reason: 'missing' };
83
+ if (index.version !== INDEX_VERSION) return { stale: true, reason: 'version-mismatch' };
84
+
85
+ const diskCount = await countSkillFiles(skillsDir);
86
+ if (diskCount !== index.fileCount) return { stale: true, reason: 'filecount-mismatch' };
87
+
88
+ const age = Date.now() - new Date(index.builtAt).getTime();
89
+ if (age > STALE_AGE_MS) return { stale: false, ageWarning: true };
90
+
91
+ return { stale: false };
92
+ }
93
+
94
+ // --- Build (full rebuild) ---
95
+
96
+ /**
97
+ * Full rebuild of the index from all skill files on disk.
98
+ * No validation or HMAC checks — index is a read-only metadata cache.
99
+ */
100
+ export async function buildIndex(skillsDir: string = DEFAULT_SKILLS_DIR): Promise<IndexFile> {
101
+ let files: string[];
102
+ try {
103
+ files = await readdir(skillsDir);
104
+ } catch {
105
+ files = [];
106
+ }
107
+
108
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
109
+ const domains: Record<string, IndexDomain> = {};
110
+
111
+ for (const file of jsonFiles) {
112
+ try {
113
+ const content = await readFile(join(skillsDir, file), 'utf-8');
114
+ const skill = JSON.parse(content);
115
+ const domain = file.replace(/\.json$/, '');
116
+
117
+ if (!skill.endpoints || !Array.isArray(skill.endpoints)) continue;
118
+
119
+ domains[domain] = {
120
+ endpointCount: skill.endpoints.length,
121
+ provenance: skill.provenance ?? 'unsigned',
122
+ capturedAt: skill.capturedAt ?? '',
123
+ endpoints: skill.endpoints.map((ep: any) => ({
124
+ id: ep.id ?? '',
125
+ method: ep.method ?? 'GET',
126
+ path: ep.path ?? '/',
127
+ ...(ep.replayability?.tier ? { tier: ep.replayability.tier } : {}),
128
+ ...(ep.replayability?.verified ? { verified: true } : {}),
129
+ })),
130
+ };
131
+ } catch {
132
+ // Skip unparseable files
133
+ }
134
+ }
135
+
136
+ const index: IndexFile = {
137
+ version: INDEX_VERSION,
138
+ fileCount: jsonFiles.length,
139
+ builtAt: new Date().toISOString(),
140
+ domains,
141
+ };
142
+
143
+ await writeIndexAtomic(index, skillsDir);
144
+ return index;
145
+ }
146
+
147
+ // --- Incremental update ---
148
+
149
+ /**
150
+ * Update a single domain entry in the index after writeSkillFile().
151
+ * Increments fileCount only for genuinely new domains.
152
+ */
153
+ export async function updateIndex(
154
+ domain: string,
155
+ endpoints: IndexEndpoint[],
156
+ provenance: string,
157
+ skillsDir: string = DEFAULT_SKILLS_DIR,
158
+ capturedAt: string = '',
159
+ ): Promise<void> {
160
+ const existing = await readIndex(skillsDir);
161
+ const index: IndexFile = existing ?? {
162
+ version: INDEX_VERSION,
163
+ fileCount: 0,
164
+ builtAt: '',
165
+ domains: {},
166
+ };
167
+
168
+ const isNew = !(domain in index.domains);
169
+
170
+ index.domains[domain] = {
171
+ endpointCount: endpoints.length,
172
+ provenance: (provenance ?? 'unsigned') as IndexDomain['provenance'],
173
+ capturedAt,
174
+ endpoints,
175
+ };
176
+
177
+ if (isNew) {
178
+ index.fileCount += 1;
179
+ }
180
+ index.builtAt = new Date().toISOString();
181
+
182
+ await writeIndexAtomic(index, skillsDir);
183
+ }
184
+
185
+ // --- Remove from index ---
186
+
187
+ /**
188
+ * Remove a domain from the index after forgetSkillFile().
189
+ */
190
+ export async function removeFromIndex(
191
+ domain: string,
192
+ skillsDir: string = DEFAULT_SKILLS_DIR,
193
+ ): Promise<void> {
194
+ const existing = await readIndex(skillsDir);
195
+ if (!existing) return;
196
+
197
+ if (domain in existing.domains) {
198
+ delete existing.domains[domain];
199
+ existing.fileCount -= 1;
200
+ existing.builtAt = new Date().toISOString();
201
+ await writeIndexAtomic(existing, skillsDir);
202
+ }
203
+ }
204
+
205
+ // --- Ensure index (read with stale check + auto-rebuild) ---
206
+
207
+ /**
208
+ * Read the index, rebuilding if stale or missing.
209
+ * Logs warnings to stderr for observability.
210
+ */
211
+ export async function ensureIndex(skillsDir: string = DEFAULT_SKILLS_DIR): Promise<IndexFile> {
212
+ let index = await readIndex(skillsDir);
213
+ const staleCheck = await checkStale(index, skillsDir);
214
+
215
+ if (staleCheck.stale) {
216
+ if (staleCheck.reason === 'missing') {
217
+ process.stderr.write('Search index not found — rebuilding (this may take a moment)...\n');
218
+ } else {
219
+ process.stderr.write('Search index is stale — rebuilding...\n');
220
+ }
221
+ index = await buildIndex(skillsDir);
222
+ } else if (staleCheck.ageWarning) {
223
+ process.stderr.write(
224
+ "Search index is over 24h old — run 'apitap index build' if you've edited skill files manually\n",
225
+ );
226
+ }
227
+
228
+ return index!;
229
+ }
230
+
231
+ // --- Internal ---
232
+
233
+ async function writeIndexAtomic(index: IndexFile, skillsDir: string): Promise<void> {
234
+ const path = indexPath(skillsDir);
235
+ const tmpPath = `${path}.${process.pid}.tmp`;
236
+ await writeFile(tmpPath, JSON.stringify(index));
237
+ await rename(tmpPath, path);
238
+ }
@@ -140,10 +140,14 @@ export function mergeSkillFile(
140
140
 
141
141
  // --- Case: no existing file — create a new SkillFile from imported endpoints ---
142
142
  if (existing === null) {
143
- const endpoints = imported.map(ep => ({
143
+ let endpoints = imported.map(ep => ({
144
144
  ...ep,
145
145
  normalizedPath: normalizePath(ep.path),
146
146
  }));
147
+ if (endpoints.length > 500) {
148
+ endpoints.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
149
+ endpoints = endpoints.slice(0, 500);
150
+ }
147
151
 
148
152
  const skillFile: SkillFile = {
149
153
  version: '1.2',
@@ -306,6 +310,24 @@ export function mergeSkillFile(
306
310
  }
307
311
  }
308
312
 
313
+ // Cap at MAX_ENDPOINTS after merge — keep captured/high-confidence first
314
+ const MAX_ENDPOINTS = 500;
315
+ if (resultEndpoints.length > MAX_ENDPOINTS) {
316
+ const overflow = resultEndpoints.length - MAX_ENDPOINTS;
317
+ resultEndpoints.sort((a, b) => {
318
+ // Captured endpoints always win over imported
319
+ const aIsCaptured = !a.endpointProvenance || a.endpointProvenance === 'captured';
320
+ const bIsCaptured = !b.endpointProvenance || b.endpointProvenance === 'captured';
321
+ if (aIsCaptured !== bIsCaptured) return aIsCaptured ? -1 : 1;
322
+ // Then by confidence descending
323
+ return (b.confidence ?? 0) - (a.confidence ?? 0);
324
+ });
325
+ resultEndpoints.length = MAX_ENDPOINTS;
326
+ process.stderr.write(
327
+ `[openapi-import] Warning: merged result has ${MAX_ENDPOINTS + overflow} endpoints, truncated to ${MAX_ENDPOINTS}\n`,
328
+ );
329
+ }
330
+
309
331
  // Build updated import history
310
332
  const prevHistory = existing.metadata.importHistory ?? [];
311
333
  const newHistoryEntry = {
@@ -1,5 +1,5 @@
1
1
  // src/skill/search.ts
2
- import { listSkillFiles, readSkillFile } from './store.js';
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 summaries = await listSkillFiles(skillsDir);
29
- if (summaries.length === 0) {
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 summary of summaries) {
40
- const skill = await readSkillFile(summary.domain, skillsDir, { trustUnsigned: true });
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 skill.endpoints) {
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: skill.domain,
55
+ domain,
57
56
  endpointId: ep.id,
58
57
  method: ep.method,
59
58
  path: ep.path,
60
- tier: ep.replayability?.tier ?? 'unknown',
61
- verified: ep.replayability?.verified ?? false,
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 = summaries.map(s => s.domain).join(', ');
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}`,