@apitap/core 1.0.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/LICENSE +60 -0
- package/README.md +362 -0
- package/SKILL.md +270 -0
- package/dist/auth/crypto.d.ts +31 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/handoff.d.ts +29 -0
- package/dist/auth/handoff.js +180 -0
- package/dist/auth/handoff.js.map +1 -0
- package/dist/auth/manager.d.ts +46 -0
- package/dist/auth/manager.js +127 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth-refresh.d.ts +16 -0
- package/dist/auth/oauth-refresh.js +91 -0
- package/dist/auth/oauth-refresh.js.map +1 -0
- package/dist/auth/refresh.d.ts +43 -0
- package/dist/auth/refresh.js +217 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/capture/anti-bot.d.ts +15 -0
- package/dist/capture/anti-bot.js +43 -0
- package/dist/capture/anti-bot.js.map +1 -0
- package/dist/capture/blocklist.d.ts +6 -0
- package/dist/capture/blocklist.js +70 -0
- package/dist/capture/blocklist.js.map +1 -0
- package/dist/capture/body-diff.d.ts +8 -0
- package/dist/capture/body-diff.js +102 -0
- package/dist/capture/body-diff.js.map +1 -0
- package/dist/capture/body-variables.d.ts +13 -0
- package/dist/capture/body-variables.js +142 -0
- package/dist/capture/body-variables.js.map +1 -0
- package/dist/capture/domain.d.ts +8 -0
- package/dist/capture/domain.js +34 -0
- package/dist/capture/domain.js.map +1 -0
- package/dist/capture/entropy.d.ts +33 -0
- package/dist/capture/entropy.js +100 -0
- package/dist/capture/entropy.js.map +1 -0
- package/dist/capture/filter.d.ts +11 -0
- package/dist/capture/filter.js +49 -0
- package/dist/capture/filter.js.map +1 -0
- package/dist/capture/graphql.d.ts +21 -0
- package/dist/capture/graphql.js +99 -0
- package/dist/capture/graphql.js.map +1 -0
- package/dist/capture/idle.d.ts +23 -0
- package/dist/capture/idle.js +44 -0
- package/dist/capture/idle.js.map +1 -0
- package/dist/capture/monitor.d.ts +26 -0
- package/dist/capture/monitor.js +183 -0
- package/dist/capture/monitor.js.map +1 -0
- package/dist/capture/oauth-detector.d.ts +18 -0
- package/dist/capture/oauth-detector.js +96 -0
- package/dist/capture/oauth-detector.js.map +1 -0
- package/dist/capture/pagination.d.ts +9 -0
- package/dist/capture/pagination.js +40 -0
- package/dist/capture/pagination.js.map +1 -0
- package/dist/capture/parameterize.d.ts +17 -0
- package/dist/capture/parameterize.js +63 -0
- package/dist/capture/parameterize.js.map +1 -0
- package/dist/capture/scrubber.d.ts +5 -0
- package/dist/capture/scrubber.js +38 -0
- package/dist/capture/scrubber.js.map +1 -0
- package/dist/capture/session.d.ts +46 -0
- package/dist/capture/session.js +445 -0
- package/dist/capture/session.js.map +1 -0
- package/dist/capture/token-detector.d.ts +16 -0
- package/dist/capture/token-detector.js +62 -0
- package/dist/capture/token-detector.js.map +1 -0
- package/dist/capture/verifier.d.ts +17 -0
- package/dist/capture/verifier.js +147 -0
- package/dist/capture/verifier.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +930 -0
- package/dist/cli.js.map +1 -0
- package/dist/discovery/auth.d.ts +17 -0
- package/dist/discovery/auth.js +81 -0
- package/dist/discovery/auth.js.map +1 -0
- package/dist/discovery/fetch.d.ts +17 -0
- package/dist/discovery/fetch.js +59 -0
- package/dist/discovery/fetch.js.map +1 -0
- package/dist/discovery/frameworks.d.ts +11 -0
- package/dist/discovery/frameworks.js +249 -0
- package/dist/discovery/frameworks.js.map +1 -0
- package/dist/discovery/index.d.ts +21 -0
- package/dist/discovery/index.js +219 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/openapi.d.ts +13 -0
- package/dist/discovery/openapi.js +175 -0
- package/dist/discovery/openapi.js.map +1 -0
- package/dist/discovery/probes.d.ts +9 -0
- package/dist/discovery/probes.js +70 -0
- package/dist/discovery/probes.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/report.d.ts +52 -0
- package/dist/inspect/report.js +191 -0
- package/dist/inspect/report.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +526 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestration/browse.d.ts +38 -0
- package/dist/orchestration/browse.js +198 -0
- package/dist/orchestration/browse.js.map +1 -0
- package/dist/orchestration/cache.d.ts +15 -0
- package/dist/orchestration/cache.js +24 -0
- package/dist/orchestration/cache.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +158 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/decoders/deepwiki.d.ts +2 -0
- package/dist/read/decoders/deepwiki.js +148 -0
- package/dist/read/decoders/deepwiki.js.map +1 -0
- package/dist/read/decoders/grokipedia.d.ts +2 -0
- package/dist/read/decoders/grokipedia.js +210 -0
- package/dist/read/decoders/grokipedia.js.map +1 -0
- package/dist/read/decoders/hackernews.d.ts +2 -0
- package/dist/read/decoders/hackernews.js +168 -0
- package/dist/read/decoders/hackernews.js.map +1 -0
- package/dist/read/decoders/index.d.ts +2 -0
- package/dist/read/decoders/index.js +12 -0
- package/dist/read/decoders/index.js.map +1 -0
- package/dist/read/decoders/reddit.d.ts +2 -0
- package/dist/read/decoders/reddit.js +142 -0
- package/dist/read/decoders/reddit.js.map +1 -0
- package/dist/read/decoders/twitter.d.ts +12 -0
- package/dist/read/decoders/twitter.js +187 -0
- package/dist/read/decoders/twitter.js.map +1 -0
- package/dist/read/decoders/wikipedia.d.ts +2 -0
- package/dist/read/decoders/wikipedia.js +66 -0
- package/dist/read/decoders/wikipedia.js.map +1 -0
- package/dist/read/decoders/youtube.d.ts +2 -0
- package/dist/read/decoders/youtube.js +69 -0
- package/dist/read/decoders/youtube.js.map +1 -0
- package/dist/read/extract.d.ts +25 -0
- package/dist/read/extract.js +320 -0
- package/dist/read/extract.js.map +1 -0
- package/dist/read/index.d.ts +14 -0
- package/dist/read/index.js +66 -0
- package/dist/read/index.js.map +1 -0
- package/dist/read/peek.d.ts +9 -0
- package/dist/read/peek.js +137 -0
- package/dist/read/peek.js.map +1 -0
- package/dist/read/types.d.ts +44 -0
- package/dist/read/types.js +3 -0
- package/dist/read/types.js.map +1 -0
- package/dist/replay/engine.d.ts +53 -0
- package/dist/replay/engine.js +441 -0
- package/dist/replay/engine.js.map +1 -0
- package/dist/replay/truncate.d.ts +16 -0
- package/dist/replay/truncate.js +92 -0
- package/dist/replay/truncate.js.map +1 -0
- package/dist/serve.d.ts +31 -0
- package/dist/serve.js +149 -0
- package/dist/serve.js.map +1 -0
- package/dist/skill/generator.d.ts +44 -0
- package/dist/skill/generator.js +419 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/importer.d.ts +26 -0
- package/dist/skill/importer.js +80 -0
- package/dist/skill/importer.js.map +1 -0
- package/dist/skill/search.d.ts +19 -0
- package/dist/skill/search.js +51 -0
- package/dist/skill/search.js.map +1 -0
- package/dist/skill/signing.d.ts +16 -0
- package/dist/skill/signing.js +34 -0
- package/dist/skill/signing.js.map +1 -0
- package/dist/skill/ssrf.d.ts +27 -0
- package/dist/skill/ssrf.js +210 -0
- package/dist/skill/ssrf.js.map +1 -0
- package/dist/skill/store.d.ts +7 -0
- package/dist/skill/store.js +93 -0
- package/dist/skill/store.js.map +1 -0
- package/dist/stats/report.d.ts +26 -0
- package/dist/stats/report.js +157 -0
- package/dist/stats/report.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/auth/crypto.ts +92 -0
- package/src/auth/handoff.ts +229 -0
- package/src/auth/manager.ts +140 -0
- package/src/auth/oauth-refresh.ts +120 -0
- package/src/auth/refresh.ts +300 -0
- package/src/capture/anti-bot.ts +63 -0
- package/src/capture/blocklist.ts +75 -0
- package/src/capture/body-diff.ts +109 -0
- package/src/capture/body-variables.ts +156 -0
- package/src/capture/domain.ts +34 -0
- package/src/capture/entropy.ts +121 -0
- package/src/capture/filter.ts +56 -0
- package/src/capture/graphql.ts +124 -0
- package/src/capture/idle.ts +45 -0
- package/src/capture/monitor.ts +224 -0
- package/src/capture/oauth-detector.ts +106 -0
- package/src/capture/pagination.ts +49 -0
- package/src/capture/parameterize.ts +68 -0
- package/src/capture/scrubber.ts +49 -0
- package/src/capture/session.ts +502 -0
- package/src/capture/token-detector.ts +76 -0
- package/src/capture/verifier.ts +171 -0
- package/src/cli.ts +1031 -0
- package/src/discovery/auth.ts +99 -0
- package/src/discovery/fetch.ts +85 -0
- package/src/discovery/frameworks.ts +231 -0
- package/src/discovery/index.ts +256 -0
- package/src/discovery/openapi.ts +230 -0
- package/src/discovery/probes.ts +76 -0
- package/src/index.ts +26 -0
- package/src/inspect/report.ts +247 -0
- package/src/mcp.ts +618 -0
- package/src/orchestration/browse.ts +250 -0
- package/src/orchestration/cache.ts +37 -0
- package/src/plugin.ts +188 -0
- package/src/read/decoders/deepwiki.ts +180 -0
- package/src/read/decoders/grokipedia.ts +246 -0
- package/src/read/decoders/hackernews.ts +198 -0
- package/src/read/decoders/index.ts +15 -0
- package/src/read/decoders/reddit.ts +158 -0
- package/src/read/decoders/twitter.ts +211 -0
- package/src/read/decoders/wikipedia.ts +75 -0
- package/src/read/decoders/youtube.ts +75 -0
- package/src/read/extract.ts +396 -0
- package/src/read/index.ts +78 -0
- package/src/read/peek.ts +175 -0
- package/src/read/types.ts +37 -0
- package/src/replay/engine.ts +559 -0
- package/src/replay/truncate.ts +116 -0
- package/src/serve.ts +189 -0
- package/src/skill/generator.ts +473 -0
- package/src/skill/importer.ts +107 -0
- package/src/skill/search.ts +76 -0
- package/src/skill/signing.ts +36 -0
- package/src/skill/ssrf.ts +238 -0
- package/src/skill/store.ts +107 -0
- package/src/stats/report.ts +208 -0
- package/src/types.ts +233 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { SkillFile, SkillEndpoint } from '../types.js';
|
|
2
|
+
import { readSkillFile } from '../skill/store.js';
|
|
3
|
+
import { replayEndpoint } from '../replay/engine.js';
|
|
4
|
+
import { SessionCache } from './cache.js';
|
|
5
|
+
import { read } from '../read/index.js';
|
|
6
|
+
|
|
7
|
+
export interface BrowseOptions {
|
|
8
|
+
skillsDir?: string;
|
|
9
|
+
cache?: SessionCache;
|
|
10
|
+
task?: string;
|
|
11
|
+
skipDiscovery?: boolean;
|
|
12
|
+
/** Maximum response size in bytes. Default: 50000 */
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
/** @internal Skip SSRF check — for testing only */
|
|
15
|
+
_skipSsrfCheck?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BrowseSuccess {
|
|
19
|
+
success: true;
|
|
20
|
+
data: unknown;
|
|
21
|
+
status: number;
|
|
22
|
+
domain: string;
|
|
23
|
+
endpointId: string;
|
|
24
|
+
tier: string;
|
|
25
|
+
fromCache: boolean;
|
|
26
|
+
capturedAt: string;
|
|
27
|
+
task?: string;
|
|
28
|
+
truncated?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BrowseGuidance {
|
|
32
|
+
success: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
discoveryConfidence?: string;
|
|
35
|
+
suggestion: string;
|
|
36
|
+
domain: string;
|
|
37
|
+
url: string;
|
|
38
|
+
task?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type BrowseResult = BrowseSuccess | BrowseGuidance;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* High-level browse: check cache → disk → discover → replay.
|
|
45
|
+
* Auto-escalates cheap steps. Returns guidance for expensive ones.
|
|
46
|
+
*/
|
|
47
|
+
export async function browse(
|
|
48
|
+
url: string,
|
|
49
|
+
options: BrowseOptions = {},
|
|
50
|
+
): Promise<BrowseResult> {
|
|
51
|
+
const { cache, skillsDir, task, skipDiscovery, maxBytes = 50_000 } = options;
|
|
52
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
53
|
+
|
|
54
|
+
let domain: string;
|
|
55
|
+
let urlPath: string;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(fullUrl);
|
|
58
|
+
domain = parsed.hostname;
|
|
59
|
+
urlPath = parsed.pathname;
|
|
60
|
+
} catch {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
reason: 'invalid_url',
|
|
64
|
+
suggestion: 'provide_valid_url',
|
|
65
|
+
domain: '',
|
|
66
|
+
url: fullUrl,
|
|
67
|
+
task,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 1: Check session cache
|
|
72
|
+
let skill: SkillFile | null = null;
|
|
73
|
+
let source: 'disk' | 'discovered' | 'captured' = 'disk';
|
|
74
|
+
|
|
75
|
+
if (cache?.has(domain)) {
|
|
76
|
+
skill = cache.get(domain)!.skillFile;
|
|
77
|
+
source = cache.get(domain)!.source;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 2: Check disk
|
|
81
|
+
if (!skill) {
|
|
82
|
+
skill = await readSkillFile(domain, skillsDir);
|
|
83
|
+
if (skill) {
|
|
84
|
+
source = 'disk';
|
|
85
|
+
cache?.set(domain, skill, 'disk');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 3: Try discovery
|
|
90
|
+
if (!skill && !skipDiscovery) {
|
|
91
|
+
try {
|
|
92
|
+
const { discover } = await import('../discovery/index.js');
|
|
93
|
+
const discovery = await discover(fullUrl);
|
|
94
|
+
|
|
95
|
+
if (discovery.skillFile && discovery.skillFile.endpoints.length > 0 &&
|
|
96
|
+
(discovery.confidence === 'high' || discovery.confidence === 'medium')) {
|
|
97
|
+
skill = discovery.skillFile;
|
|
98
|
+
source = 'discovered';
|
|
99
|
+
|
|
100
|
+
// Save to disk
|
|
101
|
+
const { writeSkillFile: writeSF } = await import('../skill/store.js');
|
|
102
|
+
await writeSF(skill, skillsDir);
|
|
103
|
+
cache?.set(domain, skill, 'discovered');
|
|
104
|
+
} else {
|
|
105
|
+
// Discovery didn't produce usable endpoints — try text-mode read
|
|
106
|
+
try {
|
|
107
|
+
const readResult = await read(fullUrl, { maxBytes });
|
|
108
|
+
if (readResult && readResult.content.trim().length > 0 && readResult.metadata.source !== 'spa-shell') {
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
data: readResult,
|
|
112
|
+
status: 200,
|
|
113
|
+
domain,
|
|
114
|
+
endpointId: 'read',
|
|
115
|
+
tier: 'green',
|
|
116
|
+
fromCache: false,
|
|
117
|
+
capturedAt: new Date().toISOString(),
|
|
118
|
+
task,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Read failed — fall through to capture_needed
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
reason: 'no_replayable_endpoints',
|
|
127
|
+
discoveryConfidence: discovery.confidence,
|
|
128
|
+
suggestion: 'capture_needed',
|
|
129
|
+
domain,
|
|
130
|
+
url: fullUrl,
|
|
131
|
+
task,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Discovery failed — fall through to guidance
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// No skill file at all — try text-mode read before giving up
|
|
140
|
+
if (!skill) {
|
|
141
|
+
if (!skipDiscovery) {
|
|
142
|
+
try {
|
|
143
|
+
const readResult = await read(fullUrl, { maxBytes });
|
|
144
|
+
if (readResult && readResult.content.trim().length > 0 && readResult.metadata.source !== 'spa-shell') {
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
data: readResult,
|
|
148
|
+
status: 200,
|
|
149
|
+
domain,
|
|
150
|
+
endpointId: 'read',
|
|
151
|
+
tier: 'green',
|
|
152
|
+
fromCache: false,
|
|
153
|
+
capturedAt: new Date().toISOString(),
|
|
154
|
+
task,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Read failed — fall through to capture_needed
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
reason: 'no_skill_file',
|
|
164
|
+
suggestion: 'capture_needed',
|
|
165
|
+
domain,
|
|
166
|
+
url: fullUrl,
|
|
167
|
+
task,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 4: Pick best endpoint
|
|
172
|
+
const endpoint = pickEndpoint(skill, urlPath);
|
|
173
|
+
if (!endpoint) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
reason: 'no_replayable_endpoints',
|
|
177
|
+
suggestion: 'capture_needed',
|
|
178
|
+
domain,
|
|
179
|
+
url: fullUrl,
|
|
180
|
+
task,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 5: Replay
|
|
185
|
+
try {
|
|
186
|
+
const result = await replayEndpoint(skill, endpoint.id, { maxBytes, _skipSsrfCheck: options._skipSsrfCheck });
|
|
187
|
+
const fromCache = source === 'disk';
|
|
188
|
+
|
|
189
|
+
// Check content-type: HTML responses are not usable API data
|
|
190
|
+
const contentType = result.headers['content-type'] ?? '';
|
|
191
|
+
if (contentType.includes('text/html')) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
reason: 'non_api_response',
|
|
195
|
+
discoveryConfidence: source === 'discovered' ? 'medium' : undefined,
|
|
196
|
+
suggestion: 'capture_needed',
|
|
197
|
+
domain,
|
|
198
|
+
url: fullUrl,
|
|
199
|
+
task,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
data: result.data,
|
|
206
|
+
status: result.status,
|
|
207
|
+
domain,
|
|
208
|
+
endpointId: endpoint.id,
|
|
209
|
+
tier: endpoint.replayability?.tier ?? 'unknown',
|
|
210
|
+
fromCache,
|
|
211
|
+
capturedAt: skill.capturedAt,
|
|
212
|
+
task,
|
|
213
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
214
|
+
};
|
|
215
|
+
} catch {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
reason: 'replay_failed',
|
|
219
|
+
suggestion: 'capture_needed',
|
|
220
|
+
domain,
|
|
221
|
+
url: fullUrl,
|
|
222
|
+
task,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const REPLAYABLE_TIERS = new Set(['green', 'yellow', 'unknown']);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Pick the best endpoint to replay. Prefers:
|
|
231
|
+
* 1. GET endpoints with green/yellow/unknown tier
|
|
232
|
+
* 2. Path overlap with the input URL
|
|
233
|
+
* 3. First match as fallback
|
|
234
|
+
*/
|
|
235
|
+
function pickEndpoint(skill: SkillFile, urlPath: string): SkillEndpoint | null {
|
|
236
|
+
const candidates = skill.endpoints.filter(ep =>
|
|
237
|
+
ep.method === 'GET' &&
|
|
238
|
+
REPLAYABLE_TIERS.has(ep.replayability?.tier ?? 'unknown'),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (candidates.length === 0) return null;
|
|
242
|
+
|
|
243
|
+
// Prefer path overlap
|
|
244
|
+
if (urlPath && urlPath !== '/') {
|
|
245
|
+
const match = candidates.find(ep => urlPath.includes(ep.path) || ep.path.includes(urlPath));
|
|
246
|
+
if (match) return match;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return candidates[0];
|
|
250
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SkillFile } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export interface CacheEntry {
|
|
4
|
+
domain: string;
|
|
5
|
+
skillFile: SkillFile;
|
|
6
|
+
discoveredAt: number;
|
|
7
|
+
source: 'disk' | 'discovered' | 'captured';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SessionCache {
|
|
11
|
+
private entries = new Map<string, CacheEntry>();
|
|
12
|
+
|
|
13
|
+
set(domain: string, skillFile: SkillFile, source: CacheEntry['source']): void {
|
|
14
|
+
this.entries.set(domain, {
|
|
15
|
+
domain,
|
|
16
|
+
skillFile,
|
|
17
|
+
discoveredAt: Date.now(),
|
|
18
|
+
source,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(domain: string): CacheEntry | null {
|
|
23
|
+
return this.entries.get(domain) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
has(domain: string): boolean {
|
|
27
|
+
return this.entries.has(domain);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
invalidate(domain: string): void {
|
|
31
|
+
this.entries.delete(domain);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
domains(): string[] {
|
|
35
|
+
return [...this.entries.keys()];
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { searchSkills } from './skill/search.js';
|
|
3
|
+
import { readSkillFile } from './skill/store.js';
|
|
4
|
+
import { replayEndpoint } from './replay/engine.js';
|
|
5
|
+
import { AuthManager, getMachineId } from './auth/manager.js';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface ToolDefinition {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: Record<string, unknown>;
|
|
13
|
+
execute: (args: Record<string, unknown>) => Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Plugin {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
tools: ToolDefinition[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PluginOptions {
|
|
23
|
+
skillsDir?: string;
|
|
24
|
+
/** @internal Skip SSRF check — for testing only */
|
|
25
|
+
_skipSsrfCheck?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const APITAP_DIR = join(homedir(), '.apitap');
|
|
29
|
+
|
|
30
|
+
export function createPlugin(options: PluginOptions = {}): Plugin {
|
|
31
|
+
const skillsDir = options.skillsDir;
|
|
32
|
+
|
|
33
|
+
const searchTool: ToolDefinition = {
|
|
34
|
+
name: 'apitap_search',
|
|
35
|
+
description:
|
|
36
|
+
'Search available API skill files for a domain or endpoint. ' +
|
|
37
|
+
'Use this FIRST to check if ApiTap has captured a site\'s API before trying to replay. ' +
|
|
38
|
+
'Returns matching endpoints with replayability tiers: ' +
|
|
39
|
+
'green = safe to replay directly, ' +
|
|
40
|
+
'yellow = needs auth credentials, ' +
|
|
41
|
+
'orange = fragile (CSRF/session-bound), ' +
|
|
42
|
+
'red = needs browser (anti-bot). ' +
|
|
43
|
+
'If not found, use apitap_capture to capture the site first.',
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
query: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Search query — domain name, endpoint path, or keyword (e.g. "polymarket", "events", "get-markets")',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['query'],
|
|
53
|
+
},
|
|
54
|
+
execute: async (args) => {
|
|
55
|
+
const query = args.query as string;
|
|
56
|
+
return searchSkills(query, skillsDir);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const replayTool: ToolDefinition = {
|
|
61
|
+
name: 'apitap_replay',
|
|
62
|
+
description:
|
|
63
|
+
'Replay a captured API endpoint to get live data. ' +
|
|
64
|
+
'Check the endpoint tier first with apitap_search: ' +
|
|
65
|
+
'green = will work, yellow = needs auth, orange/red = may fail. ' +
|
|
66
|
+
'Returns { status, data } with the API response.',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
domain: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Domain of the API (e.g. "gamma-api.polymarket.com")',
|
|
73
|
+
},
|
|
74
|
+
endpointId: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Endpoint ID from search results (e.g. "get-events")',
|
|
77
|
+
},
|
|
78
|
+
params: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
description: 'Optional key-value parameters for path substitution or query params (e.g. { "id": "123", "limit": "10" })',
|
|
81
|
+
additionalProperties: { type: 'string' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ['domain', 'endpointId'],
|
|
85
|
+
},
|
|
86
|
+
execute: async (args) => {
|
|
87
|
+
const domain = args.domain as string;
|
|
88
|
+
const endpointId = args.endpointId as string;
|
|
89
|
+
const params = args.params as Record<string, string> | undefined;
|
|
90
|
+
|
|
91
|
+
const skill = await readSkillFile(domain, skillsDir);
|
|
92
|
+
if (!skill) {
|
|
93
|
+
return {
|
|
94
|
+
error: `No skill file found for "${domain}". Use apitap_capture to capture it first.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Inject stored auth if available
|
|
99
|
+
const endpoint = skill.endpoints.find(e => e.id === endpointId);
|
|
100
|
+
if (!endpoint) {
|
|
101
|
+
return {
|
|
102
|
+
error: `Endpoint "${endpointId}" not found. Available: ${skill.endpoints.map(e => e.id).join(', ')}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasStoredPlaceholder = Object.values(endpoint.headers).some(v => v === '[stored]');
|
|
107
|
+
if (hasStoredPlaceholder) {
|
|
108
|
+
try {
|
|
109
|
+
const machineId = await getMachineId();
|
|
110
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
111
|
+
const storedAuth = await authManager.retrieve(domain);
|
|
112
|
+
if (storedAuth) {
|
|
113
|
+
endpoint.headers[storedAuth.header] = storedAuth.value;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Auth retrieval failed — proceed without it
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await replayEndpoint(skill, endpointId, {
|
|
122
|
+
params,
|
|
123
|
+
_skipSsrfCheck: options._skipSsrfCheck,
|
|
124
|
+
});
|
|
125
|
+
return { status: result.status, data: result.data };
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
return { error: err.message };
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const captureTool: ToolDefinition = {
|
|
133
|
+
name: 'apitap_capture',
|
|
134
|
+
description:
|
|
135
|
+
'Capture a website\'s API traffic by browsing it with an instrumented browser. ' +
|
|
136
|
+
'Use this when apitap_search returns no results for a site. ' +
|
|
137
|
+
'Launches a browser, navigates to the URL, captures API calls for the specified duration, ' +
|
|
138
|
+
'and generates a skill file for future replay. ' +
|
|
139
|
+
'Returns { domains, endpoints, skillFiles } summary.',
|
|
140
|
+
parameters: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
url: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'URL to capture (e.g. "https://polymarket.com")',
|
|
146
|
+
},
|
|
147
|
+
duration: {
|
|
148
|
+
type: 'number',
|
|
149
|
+
description: 'Capture duration in seconds (default: 30)',
|
|
150
|
+
},
|
|
151
|
+
allDomains: {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
description: 'Capture all domains, not just the target domain (default: false)',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ['url'],
|
|
157
|
+
},
|
|
158
|
+
execute: async (args) => {
|
|
159
|
+
const url = args.url as string;
|
|
160
|
+
const duration = (args.duration as number) ?? 30;
|
|
161
|
+
const allDomains = (args.allDomains as boolean) ?? false;
|
|
162
|
+
|
|
163
|
+
// Shell out to CLI for capture (it handles browser lifecycle, signing, etc.)
|
|
164
|
+
const { execFile } = await import('node:child_process');
|
|
165
|
+
const { promisify } = await import('node:util');
|
|
166
|
+
const execFileAsync = promisify(execFile);
|
|
167
|
+
|
|
168
|
+
const cliArgs = ['--import', 'tsx', 'src/cli.ts', 'capture', url, '--duration', String(duration), '--json', '--no-verify'];
|
|
169
|
+
if (allDomains) cliArgs.push('--all-domains');
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const { stdout } = await execFileAsync('node', cliArgs, {
|
|
173
|
+
timeout: (duration + 30) * 1000,
|
|
174
|
+
env: { ...process.env, ...(skillsDir ? { APITAP_SKILLS_DIR: skillsDir } : {}) },
|
|
175
|
+
});
|
|
176
|
+
return JSON.parse(stdout);
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
return { error: `Capture failed: ${err.message}` };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: 'apitap',
|
|
185
|
+
version: '0.4.0',
|
|
186
|
+
tools: [searchTool, replayTool, captureTool],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/read/decoders/deepwiki.ts
|
|
2
|
+
import type { Decoder, ReadResult } from '../types.js';
|
|
3
|
+
|
|
4
|
+
function estimateTokens(text: string): number {
|
|
5
|
+
return Math.ceil(text.length / 4);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DeepWiki decoder — extracts wiki content from deepwiki.com
|
|
10
|
+
*
|
|
11
|
+
* DeepWiki (by Devin/Cognition) auto-generates documentation wikis from GitHub repos.
|
|
12
|
+
* It's a Next.js app that serves content via React Server Components (RSC).
|
|
13
|
+
*
|
|
14
|
+
* Trick: Send `RSC: 1` header → get full markdown content in the RSC payload
|
|
15
|
+
* instead of the JS-heavy SPA shell. No auth required.
|
|
16
|
+
*
|
|
17
|
+
* URL patterns:
|
|
18
|
+
* deepwiki.com/{org}/{repo} → overview page
|
|
19
|
+
* deepwiki.com/{org}/{repo}/{page-slug} → specific wiki page
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const DEEPWIKI_PATTERN = /^https?:\/\/(www\.)?deepwiki\.com\/([^/]+)\/([^/]+)(\/.*)?$/;
|
|
23
|
+
|
|
24
|
+
export const deepwikiDecoder: Decoder = {
|
|
25
|
+
name: 'deepwiki',
|
|
26
|
+
patterns: [
|
|
27
|
+
/^https?:\/\/(www\.)?deepwiki\.com\/[^/]+\/[^/]+/,
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
async decode(url: string, options: { skipSsrf?: boolean; [key: string]: any } = {}): Promise<ReadResult | null> {
|
|
31
|
+
const match = url.match(DEEPWIKI_PATTERN);
|
|
32
|
+
if (!match) return null;
|
|
33
|
+
|
|
34
|
+
const org = match[2];
|
|
35
|
+
const repo = match[3];
|
|
36
|
+
const pagePath = match[4] || '';
|
|
37
|
+
|
|
38
|
+
// Construct the path for the RSC request
|
|
39
|
+
const fullPath = `/${org}/${repo}${pagePath}`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
'RSC': '1',
|
|
45
|
+
'Next-Url': fullPath,
|
|
46
|
+
'User-Agent': 'Mozilla/5.0 (compatible; ApiTap/1.0)',
|
|
47
|
+
},
|
|
48
|
+
signal: AbortSignal.timeout(10_000),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rscPayload = await response.text();
|
|
56
|
+
|
|
57
|
+
// Extract markdown content from RSC text nodes
|
|
58
|
+
// Format: {id}:T{hexLength},{content}
|
|
59
|
+
const contentBlocks: string[] = [];
|
|
60
|
+
const lines = rscPayload.split('\n');
|
|
61
|
+
|
|
62
|
+
let currentBlock: string | null = null;
|
|
63
|
+
let expectedLength = 0;
|
|
64
|
+
let collectedBytes = 0;
|
|
65
|
+
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
// Start of a new text block: {id}:T{hexLength},{content...}
|
|
68
|
+
const blockMatch = line.match(/^[0-9a-f]+:T([0-9a-f]+),(.*)$/);
|
|
69
|
+
|
|
70
|
+
if (blockMatch) {
|
|
71
|
+
// Save previous block if exists
|
|
72
|
+
if (currentBlock !== null) {
|
|
73
|
+
contentBlocks.push(currentBlock);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expectedLength = parseInt(blockMatch[1], 16);
|
|
77
|
+
const content = blockMatch[2];
|
|
78
|
+
currentBlock = content;
|
|
79
|
+
collectedBytes = Buffer.byteLength(content, 'utf-8');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If we're inside a block, keep collecting lines
|
|
84
|
+
if (currentBlock !== null) {
|
|
85
|
+
// Check if this line starts a new RSC record (not a continuation)
|
|
86
|
+
if (/^[0-9a-f]+:[^T]/.test(line) || /^[0-9a-f]+:T[0-9a-f]+,/.test(line)) {
|
|
87
|
+
// End of current block
|
|
88
|
+
contentBlocks.push(currentBlock);
|
|
89
|
+
currentBlock = null;
|
|
90
|
+
|
|
91
|
+
// If it's a new T block, process it
|
|
92
|
+
const newBlock = line.match(/^[0-9a-f]+:T([0-9a-f]+),(.*)$/);
|
|
93
|
+
if (newBlock) {
|
|
94
|
+
expectedLength = parseInt(newBlock[1], 16);
|
|
95
|
+
currentBlock = newBlock[2];
|
|
96
|
+
collectedBytes = Buffer.byteLength(newBlock[2], 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
currentBlock += '\n' + line;
|
|
102
|
+
collectedBytes += Buffer.byteLength('\n' + line, 'utf-8');
|
|
103
|
+
|
|
104
|
+
// If we've collected enough bytes, end the block
|
|
105
|
+
if (collectedBytes >= expectedLength) {
|
|
106
|
+
contentBlocks.push(currentBlock);
|
|
107
|
+
currentBlock = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Don't forget the last block
|
|
113
|
+
if (currentBlock !== null) {
|
|
114
|
+
contentBlocks.push(currentBlock);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (contentBlocks.length === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Find the largest content block — that's the main page content
|
|
122
|
+
// (smaller blocks might be TOC section titles)
|
|
123
|
+
const mainContent = contentBlocks.reduce((a, b) =>
|
|
124
|
+
a.length > b.length ? a : b
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!mainContent || mainContent.length < 50) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean up the markdown
|
|
132
|
+
let content = mainContent;
|
|
133
|
+
|
|
134
|
+
// Extract title from first heading
|
|
135
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
136
|
+
const title = titleMatch
|
|
137
|
+
? `${titleMatch[1]} — ${org}/${repo} | DeepWiki`
|
|
138
|
+
: `${org}/${repo} | DeepWiki`;
|
|
139
|
+
|
|
140
|
+
// Fix relative links to point back to correct locations
|
|
141
|
+
content = content.replace(
|
|
142
|
+
/\[([^\]]+)\]\((?!https?:\/\/)([^)]+)\)/g,
|
|
143
|
+
(full, text, href) => {
|
|
144
|
+
// Source file links (e.g., README.md, src/foo.ts)
|
|
145
|
+
if (href.match(/\.(ts|js|md|json|tsx|jsx|py|rs|go|toml|yaml|yml|css|html)$/)) {
|
|
146
|
+
return `[${text}](https://github.com/${org}/${repo}/blob/main/${href})`;
|
|
147
|
+
}
|
|
148
|
+
// Section links (#2, #3.1, etc.)
|
|
149
|
+
if (href.startsWith('#')) {
|
|
150
|
+
return `[${text}](https://deepwiki.com/${org}/${repo}/${href.slice(1)})`;
|
|
151
|
+
}
|
|
152
|
+
// Other relative links
|
|
153
|
+
return `[${text}](https://deepwiki.com/${org}/${repo}/${href})`;
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const tokens = estimateTokens(content);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
url,
|
|
161
|
+
title,
|
|
162
|
+
author: null,
|
|
163
|
+
description: `DeepWiki documentation for ${org}/${repo}`,
|
|
164
|
+
content,
|
|
165
|
+
links: [],
|
|
166
|
+
images: [],
|
|
167
|
+
metadata: {
|
|
168
|
+
type: 'wiki',
|
|
169
|
+
publishedAt: null,
|
|
170
|
+
source: 'deepwiki-rsc',
|
|
171
|
+
canonical: `https://deepwiki.com/${org}/${repo}${pagePath}`,
|
|
172
|
+
siteName: 'DeepWiki',
|
|
173
|
+
},
|
|
174
|
+
cost: { tokens },
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|