codeep 2.15.0 → 2.17.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/README.md +41 -7
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/acp/session.js +22 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +3 -2
- package/dist/config/providers.js +163 -69
- package/dist/renderer/App.d.ts +89 -0
- package/dist/renderer/App.js +637 -43
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +189 -0
- package/dist/renderer/commands/helpers.js +345 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +218 -267
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/components/Status.d.ts +2 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +110 -30
- package/dist/utils/agent.js +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.js +1 -1
- package/dist/utils/checkpoints.d.ts +1 -1
- package/dist/utils/checkpoints.js +1 -1
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.js +52 -37
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@web <url>` inline context — fetch a web page and attach its text
|
|
3
|
+
* content to the prompt, the same way `@file` attaches a file.
|
|
4
|
+
*
|
|
5
|
+
* Supported forms (anywhere in the message, like file mentions):
|
|
6
|
+
* @web https://example.com/docs → full URL
|
|
7
|
+
* @web example.com/docs → https:// is auto-prepended
|
|
8
|
+
* @web http://localhost:3000/api → dev servers work too
|
|
9
|
+
*
|
|
10
|
+
* The fetch is best-effort:
|
|
11
|
+
* - HTML is stripped to readable text (tags, scripts, styles removed).
|
|
12
|
+
* - Output is capped at `MAX_WEB_BYTES` (default 32 KB) so a single
|
|
13
|
+
* page can't blow the context window.
|
|
14
|
+
* - Markdown conversion is lightweight (headings, links, lists) — we
|
|
15
|
+
* don't run a full HTML→Markdown pipeline; the goal is "agent can
|
|
16
|
+
* read the page", not "pretty render".
|
|
17
|
+
* - Failures (network, non-2xx, non-text content type) surface as
|
|
18
|
+
* inline notifications, same as missing files.
|
|
19
|
+
*
|
|
20
|
+
* The fetcher is async, so `expandWebMentions` is async — unlike the
|
|
21
|
+
* sync `expandMentions` for files. Callers await it.
|
|
22
|
+
*/
|
|
23
|
+
/** Max bytes of text we'll inline from a fetched page (32 KB). */
|
|
24
|
+
export const MAX_WEB_BYTES = 32 * 1024;
|
|
25
|
+
/** Fetch timeout — don't hang the chat on a slow server. */
|
|
26
|
+
const FETCH_TIMEOUT_MS = 12_000;
|
|
27
|
+
/** User-Agent — some sites block the default `node` UA. */
|
|
28
|
+
const USER_AGENT = `Codeep/1 (+https://codeep.dev)`;
|
|
29
|
+
/**
|
|
30
|
+
* Regex matching a `@web <url>` mention.
|
|
31
|
+
*
|
|
32
|
+
* `@web` must be followed by whitespace, then a URL-like token (no
|
|
33
|
+
* spaces). We accept `http://`, `https://`, or bare host/path. The
|
|
34
|
+
* bare form (`example.com/docs`) gets `https://` auto-prepended.
|
|
35
|
+
*/
|
|
36
|
+
const WEB_MENTION_RE = /(?:^|[\s([{<,;])@web\s+(https?:\/\/[^\s<>"]+|[a-z0-9][a-z0-9.-]*\.[a-z]{2,}[^\s<>"]*)/gi;
|
|
37
|
+
/**
|
|
38
|
+
* Extract all `@web` mentions from `text`. Pure (no network).
|
|
39
|
+
* Returns them in document order.
|
|
40
|
+
*/
|
|
41
|
+
export function extractWebMentions(text) {
|
|
42
|
+
const tokens = [];
|
|
43
|
+
WEB_MENTION_RE.lastIndex = 0;
|
|
44
|
+
let m;
|
|
45
|
+
while ((m = WEB_MENTION_RE.exec(text)) !== null) {
|
|
46
|
+
let url = m[1];
|
|
47
|
+
if (!url)
|
|
48
|
+
continue;
|
|
49
|
+
// Auto-prepend https:// for bare hosts (e.g. "example.com/docs").
|
|
50
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
51
|
+
url = 'https://' + url;
|
|
52
|
+
}
|
|
53
|
+
// The match may include a leading boundary char (space, `(`, …);
|
|
54
|
+
// `tok.start` should point at the `@` so stripping leaves the
|
|
55
|
+
// boundary char in place.
|
|
56
|
+
const matchText = m[0];
|
|
57
|
+
const atIdx = matchText.indexOf('@');
|
|
58
|
+
const start = m.index + (atIdx >= 0 ? atIdx : 0);
|
|
59
|
+
tokens.push({ raw: matchText.slice(atIdx).trim(), url, start, end: m.index + m[0].length });
|
|
60
|
+
}
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Expand all `@web` mentions in `prompt`: fetch each URL, convert the
|
|
65
|
+
* HTML to readable text, and prepend it as a `[Web pages]` block.
|
|
66
|
+
* Failures are collected, not thrown.
|
|
67
|
+
*/
|
|
68
|
+
export async function expandWebMentions(prompt, opts = {}) {
|
|
69
|
+
const tokens = extractWebMentions(prompt);
|
|
70
|
+
if (tokens.length === 0) {
|
|
71
|
+
return { enrichedPrompt: prompt, loaded: [], failures: [] };
|
|
72
|
+
}
|
|
73
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
74
|
+
const loaded = [];
|
|
75
|
+
const failures = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
// Fetch sequentially — avoids hammering a server and keeps failure
|
|
78
|
+
// order stable (matches the order in the prompt).
|
|
79
|
+
for (const tok of tokens) {
|
|
80
|
+
if (seen.has(tok.url))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(tok.url);
|
|
83
|
+
// Session cache: skip the network if we fetched this URL recently.
|
|
84
|
+
const cached = cacheGet(tok.url);
|
|
85
|
+
if (cached) {
|
|
86
|
+
loaded.push({ url: tok.url, title: cached.title, content: cached.content });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const result = await safeFetch(tok.url, fetchImpl);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
failures.push({ mention: tok.raw, reason: result.reason });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Cache successful fetches for the rest of the session.
|
|
95
|
+
cacheSet(tok.url, result);
|
|
96
|
+
loaded.push({ url: tok.url, title: result.title, content: result.content });
|
|
97
|
+
}
|
|
98
|
+
// Strip the `@web ` and the URL from the visible prompt, leaving a
|
|
99
|
+
// bare URL (readable, and the agent still sees what was referenced).
|
|
100
|
+
let stripped = prompt;
|
|
101
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
102
|
+
const tok = tokens[i];
|
|
103
|
+
stripped = stripped.slice(0, tok.start) + tok.url + stripped.slice(tok.end);
|
|
104
|
+
}
|
|
105
|
+
const block = formatWebBlock(loaded);
|
|
106
|
+
return {
|
|
107
|
+
enrichedPrompt: block ? block + stripped.trimStart() : stripped,
|
|
108
|
+
loaded,
|
|
109
|
+
failures,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Session cache: normalized URL → entry. */
|
|
113
|
+
const webCache = new Map();
|
|
114
|
+
/** Cache TTL: 30 minutes. Docs rarely change faster than that. */
|
|
115
|
+
const WEB_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
116
|
+
/** Max entries — prevents unbounded growth in long sessions. */
|
|
117
|
+
const WEB_CACHE_MAX = 50;
|
|
118
|
+
/**
|
|
119
|
+
* Normalize a URL for cache keying: lowercase host, strip trailing
|
|
120
|
+
* slash, drop fragments. Query strings are kept (they can change
|
|
121
|
+
* content).
|
|
122
|
+
*/
|
|
123
|
+
function normalizeUrlForCache(url) {
|
|
124
|
+
try {
|
|
125
|
+
const u = new URL(url);
|
|
126
|
+
// The fragment is deliberately excluded: it never reaches the server, so
|
|
127
|
+
// `#a` and `#b` on the same URL are one cache entry.
|
|
128
|
+
return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/$/, '')}${u.search}`;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Not a parseable URL — fall back to the raw string.
|
|
132
|
+
return url;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Look up a cached successful fetch. Returns `null` if missing/expired. */
|
|
136
|
+
function cacheGet(url) {
|
|
137
|
+
const key = normalizeUrlForCache(url);
|
|
138
|
+
const entry = webCache.get(key);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return null;
|
|
141
|
+
if (Date.now() - entry.writtenAt > WEB_CACHE_TTL_MS) {
|
|
142
|
+
webCache.delete(key);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return entry.result;
|
|
146
|
+
}
|
|
147
|
+
/** Store a successful fetch in the cache (evicting oldest if full). */
|
|
148
|
+
function cacheSet(url, result) {
|
|
149
|
+
const key = normalizeUrlForCache(url);
|
|
150
|
+
// Evict oldest if at capacity. Map preserves insertion order, so the
|
|
151
|
+
// first key is the oldest.
|
|
152
|
+
if (webCache.size >= WEB_CACHE_MAX && !webCache.has(key)) {
|
|
153
|
+
const oldest = webCache.keys().next().value;
|
|
154
|
+
if (oldest !== undefined)
|
|
155
|
+
webCache.delete(oldest);
|
|
156
|
+
}
|
|
157
|
+
webCache.set(key, { result, writtenAt: Date.now() });
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Reset the session web cache. Public so callers (e.g. `/web-cache clear`
|
|
161
|
+
* command, or tests) can force a fresh fetch.
|
|
162
|
+
*/
|
|
163
|
+
export function clearWebCache() {
|
|
164
|
+
webCache.clear();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Stats about the session web cache — for `/web-cache status`.
|
|
168
|
+
*/
|
|
169
|
+
export function webCacheStats() {
|
|
170
|
+
return { entries: webCache.size, maxEntries: WEB_CACHE_MAX, ttlMinutes: WEB_CACHE_TTL_MS / 60000 };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Hosts that only make sense from *inside* the machine or network: loopback,
|
|
174
|
+
* RFC1918, link-local (which covers the 169.254.169.254 cloud-metadata
|
|
175
|
+
* endpoint), and `.internal`-style names.
|
|
176
|
+
*
|
|
177
|
+
* A user typing `@web http://localhost:3000` is a documented, intended use, so
|
|
178
|
+
* this is NOT a blanket block — it's only applied to where a fetch *ended up*
|
|
179
|
+
* after redirects, so a public URL can't bounce us into the private network.
|
|
180
|
+
*/
|
|
181
|
+
function isPrivateHost(hostname) {
|
|
182
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
183
|
+
if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.internal') || h.endsWith('.local'))
|
|
184
|
+
return true;
|
|
185
|
+
if (h === '::1' || h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:'))
|
|
186
|
+
return true;
|
|
187
|
+
const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
188
|
+
if (!m)
|
|
189
|
+
return false;
|
|
190
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
191
|
+
return a === 127 || a === 10 || a === 0
|
|
192
|
+
|| (a === 192 && b === 168)
|
|
193
|
+
|| (a === 172 && b >= 16 && b <= 31)
|
|
194
|
+
|| (a === 169 && b === 254);
|
|
195
|
+
}
|
|
196
|
+
/** Hard ceiling on bytes read from the network, before any text decoding. */
|
|
197
|
+
const MAX_WEB_FETCH_BYTES = MAX_WEB_BYTES * 4;
|
|
198
|
+
async function safeFetch(url, fetchImpl) {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
// Kept alive until the BODY is read, not just the headers — clearing it on
|
|
201
|
+
// header arrival let a slow-drip response hang the chat forever. Cleared in
|
|
202
|
+
// `finally` so a throw can't leak the timer either.
|
|
203
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
204
|
+
try {
|
|
205
|
+
const requestedPrivate = (() => {
|
|
206
|
+
try {
|
|
207
|
+
return isPrivateHost(new URL(url).hostname);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
const res = await fetchImpl(url, {
|
|
214
|
+
signal: controller.signal,
|
|
215
|
+
headers: { 'User-Agent': USER_AGENT, Accept: 'text/html, text/plain, */*' },
|
|
216
|
+
redirect: 'follow',
|
|
217
|
+
});
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
return { ok: false, reason: `HTTP ${res.status}` };
|
|
220
|
+
}
|
|
221
|
+
// SSRF guard: the user vouched for the host they typed, not for wherever
|
|
222
|
+
// it redirected us. Refuse a public → private hop (cloud metadata, LAN).
|
|
223
|
+
if (!requestedPrivate && res.url) {
|
|
224
|
+
try {
|
|
225
|
+
if (isPrivateHost(new URL(res.url).hostname)) {
|
|
226
|
+
return { ok: false, reason: 'redirected to a private/internal address — refused' };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch { /* unparseable res.url — fall through */ }
|
|
230
|
+
}
|
|
231
|
+
const declared = Number(res.headers.get('content-length') ?? '');
|
|
232
|
+
if (Number.isFinite(declared) && declared > MAX_WEB_FETCH_BYTES) {
|
|
233
|
+
return { ok: false, reason: `response too large (${Math.round(declared / 1024)}KB)` };
|
|
234
|
+
}
|
|
235
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
236
|
+
const text = await readCapped(res);
|
|
237
|
+
// Plain text or JSON — keep as-is (capped).
|
|
238
|
+
if (contentType.includes('text/plain') || contentType.includes('application/json')) {
|
|
239
|
+
const capped = text.length > MAX_WEB_BYTES ? text.slice(0, MAX_WEB_BYTES) + '\n…(truncated)' : text;
|
|
240
|
+
return { ok: true, title: url, content: capped };
|
|
241
|
+
}
|
|
242
|
+
// HTML — strip to readable text.
|
|
243
|
+
if (contentType.includes('text/html') || contentType.includes('application/xhtml')) {
|
|
244
|
+
const { title, content } = htmlToText(text);
|
|
245
|
+
const capped = content.length > MAX_WEB_BYTES ? content.slice(0, MAX_WEB_BYTES) + '\n…(truncated)' : content;
|
|
246
|
+
return { ok: true, title: title || url, content: capped };
|
|
247
|
+
}
|
|
248
|
+
// Anything else (images, PDFs, …) — we can't usefully inline it.
|
|
249
|
+
return { ok: false, reason: `unsupported content type (${contentType || 'unknown'})` };
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
+
if (msg.includes('abort'))
|
|
254
|
+
return { ok: false, reason: `timed out after ${FETCH_TIMEOUT_MS / 1000}s` };
|
|
255
|
+
return { ok: false, reason: msg };
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read a response body, stopping once `MAX_WEB_FETCH_BYTES` have arrived.
|
|
263
|
+
*
|
|
264
|
+
* `res.text()` buffers the whole body first and only then truncates, so a
|
|
265
|
+
* server streaming gigabytes would OOM the CLI before the cap was applied.
|
|
266
|
+
* Falls back to `res.text()` when the body isn't a stream (test doubles).
|
|
267
|
+
*/
|
|
268
|
+
async function readCapped(res) {
|
|
269
|
+
const body = res.body;
|
|
270
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
271
|
+
const whole = await res.text();
|
|
272
|
+
return whole.length > MAX_WEB_FETCH_BYTES ? whole.slice(0, MAX_WEB_FETCH_BYTES) : whole;
|
|
273
|
+
}
|
|
274
|
+
const reader = body.getReader();
|
|
275
|
+
const chunks = [];
|
|
276
|
+
let total = 0;
|
|
277
|
+
try {
|
|
278
|
+
for (;;) {
|
|
279
|
+
const { done, value } = await reader.read();
|
|
280
|
+
if (done)
|
|
281
|
+
break;
|
|
282
|
+
if (!value)
|
|
283
|
+
continue;
|
|
284
|
+
chunks.push(value);
|
|
285
|
+
total += value.byteLength;
|
|
286
|
+
if (total >= MAX_WEB_FETCH_BYTES)
|
|
287
|
+
break; // stop pulling; cap reached
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
try {
|
|
292
|
+
await reader.cancel();
|
|
293
|
+
}
|
|
294
|
+
catch { /* already closed */ }
|
|
295
|
+
}
|
|
296
|
+
const joined = new Uint8Array(total);
|
|
297
|
+
let offset = 0;
|
|
298
|
+
for (const c of chunks) {
|
|
299
|
+
joined.set(c, offset);
|
|
300
|
+
offset += c.byteLength;
|
|
301
|
+
}
|
|
302
|
+
return new TextDecoder('utf-8').decode(joined);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Convert HTML to readable plain text + a title.
|
|
306
|
+
*
|
|
307
|
+
* Lightweight — no full parser dependency. Strips `<script>`, `<style>`,
|
|
308
|
+
* and tags, collapses whitespace, extracts `<title>` and the first
|
|
309
|
+
* `<h1>` as the page title. Good enough for the agent to read docs;
|
|
310
|
+
* not a faithful rendering.
|
|
311
|
+
*/
|
|
312
|
+
export function htmlToText(html) {
|
|
313
|
+
// Title: prefer <title>, fall back to first <h1>.
|
|
314
|
+
let title = '';
|
|
315
|
+
const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
316
|
+
if (titleMatch)
|
|
317
|
+
title = decodeEntities(titleMatch[1].trim());
|
|
318
|
+
if (!title) {
|
|
319
|
+
const h1 = html.match(/<h1[^>]*>([^<]*)<\/h1>/i);
|
|
320
|
+
if (h1)
|
|
321
|
+
title = decodeEntities(h1[1].trim());
|
|
322
|
+
}
|
|
323
|
+
// Remove script/style/noscript blocks entirely.
|
|
324
|
+
let body = html
|
|
325
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
326
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
327
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
|
|
328
|
+
.replace(/<!--[\s\S]*?-->/g, '');
|
|
329
|
+
// Drop the <head> (we already have the title).
|
|
330
|
+
body = body.replace(/<head[\s\S]*?<\/head>/gi, '');
|
|
331
|
+
// Convert block-level tags to newlines so text doesn't run together.
|
|
332
|
+
body = body.replace(/<\/(p|div|section|article|li|h[1-6]|tr|blockquote|pre)>/gi, '\n');
|
|
333
|
+
body = body.replace(/<br\s*\/?>/gi, '\n');
|
|
334
|
+
// Convert links to "text (url)" so the agent keeps the reference.
|
|
335
|
+
body = body.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi, (_m, href, text) => {
|
|
336
|
+
const t = text.trim();
|
|
337
|
+
return t ? `${t} (${href})` : '';
|
|
338
|
+
});
|
|
339
|
+
// Strip all remaining tags.
|
|
340
|
+
body = body.replace(/<[^>]+>/g, '');
|
|
341
|
+
// Decode common HTML entities.
|
|
342
|
+
body = decodeEntities(body);
|
|
343
|
+
// Collapse runs of whitespace (but preserve newlines).
|
|
344
|
+
body = body
|
|
345
|
+
.split('\n')
|
|
346
|
+
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
|
|
347
|
+
.filter((line) => line.length > 0)
|
|
348
|
+
.join('\n');
|
|
349
|
+
return { title, content: body };
|
|
350
|
+
}
|
|
351
|
+
/** Decode the handful of HTML entities we're likely to see. */
|
|
352
|
+
function decodeEntities(s) {
|
|
353
|
+
return s
|
|
354
|
+
.replace(/&/g, '&')
|
|
355
|
+
.replace(/</g, '<')
|
|
356
|
+
.replace(/>/g, '>')
|
|
357
|
+
.replace(/"/g, '"')
|
|
358
|
+
.replace(/'/g, "'")
|
|
359
|
+
.replace(/'/g, "'")
|
|
360
|
+
.replace(/ /g, ' ')
|
|
361
|
+
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(Number(code)))
|
|
362
|
+
.replace(/&#x([0-9a-f]+);/gi, (_m, code) => String.fromCharCode(parseInt(code, 16)));
|
|
363
|
+
}
|
|
364
|
+
// ─── Formatting ───────────────────────────────────────────────────────────────
|
|
365
|
+
/** Format the `[Web pages]` block prepended to the enriched prompt. */
|
|
366
|
+
export function formatWebBlock(pages) {
|
|
367
|
+
if (pages.length === 0)
|
|
368
|
+
return '';
|
|
369
|
+
const parts = ['[Web pages]'];
|
|
370
|
+
for (const p of pages) {
|
|
371
|
+
const heading = p.title && p.title !== p.url ? `${p.title} — ${p.url}` : p.url;
|
|
372
|
+
parts.push(`\nURL: ${heading}\n${p.content}`);
|
|
373
|
+
}
|
|
374
|
+
return parts.join('\n') + '\n\n';
|
|
375
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "2.
|
|
1
|
+
export declare const VERSION = "2.17.0";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
|
|
2
2
|
// Baked from package.json at build time so the bun-compiled binary reports
|
|
3
3
|
// the right version (it has no package.json on disk to read at runtime).
|
|
4
|
-
export const VERSION = '2.
|
|
4
|
+
export const VERSION = '2.17.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"test:watch": "vitest",
|
|
17
17
|
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"version": "node scripts/gen-version.js && git add src/version.ts",
|
|
18
19
|
"release": "node scripts/release.js"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|