codeep 2.15.0 → 2.16.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 +12 -3
- package/dist/acp/session.js +22 -1
- package/dist/config/providers.js +20 -14
- package/dist/renderer/App.d.ts +77 -0
- package/dist/renderer/App.js +283 -3
- package/dist/renderer/commands/helpers.d.ts +188 -0
- package/dist/renderer/commands/helpers.js +342 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +193 -264
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +34 -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/tokenTracker.js +9 -3
- 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 +1 -1
|
@@ -106,3 +106,345 @@ export function formatTaskList(tasks, scopeProjectName) {
|
|
|
106
106
|
lines.push('*Tasks loaded into agent context — agent will see them in the next message.*');
|
|
107
107
|
return lines.join('\n');
|
|
108
108
|
}
|
|
109
|
+
// ─── /profile formatting ──────────────────────────────────────────────────────
|
|
110
|
+
/** Render the `/profile list` Markdown message from saved profile names. */
|
|
111
|
+
export function formatProfileList(profiles) {
|
|
112
|
+
return `## Profiles\n\n${profiles.map((p) => `- ${p}`).join('\n')}\n\nUse /profile load <name> to apply.`;
|
|
113
|
+
}
|
|
114
|
+
// ─── /memory list formatting ──────────────────────────────────────────────────
|
|
115
|
+
/** Render the `/memory list` Markdown message from saved notes. */
|
|
116
|
+
export function formatMemoryList(notes) {
|
|
117
|
+
const lines = notes.map((n, i) => ` ${i + 1}. ${n}`).join('\n');
|
|
118
|
+
return `**Project memory notes:**\n${lines}`;
|
|
119
|
+
}
|
|
120
|
+
/** Format a single model-row's cost string, mirroring the inline logic. */
|
|
121
|
+
export function formatModelCost(provider, estimatedCost) {
|
|
122
|
+
if (provider === 'ollama')
|
|
123
|
+
return 'free';
|
|
124
|
+
return estimatedCost > 0 ? `~$${estimatedCost.toFixed(4)}` : '(no pricing data)';
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the full `/stats` Markdown report. `currentProvider` controls
|
|
128
|
+
* whether the total shows "free" (ollama) or a dollar figure.
|
|
129
|
+
*/
|
|
130
|
+
export function formatStatsReport(args) {
|
|
131
|
+
const { totals, breakdown, cache, pricing, currentProvider, fmt } = args;
|
|
132
|
+
const lines = ['## Session Cost', ''];
|
|
133
|
+
if (totals.requestCount === 0) {
|
|
134
|
+
lines.push('*No API calls made yet this session.*', '');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
lines.push(`Requests: ${totals.requestCount}`);
|
|
138
|
+
lines.push(`Tokens: ${fmt(totals.totalTokens)} total (${fmt(totals.totalPromptTokens)} in / ${fmt(totals.totalCompletionTokens)} out)`);
|
|
139
|
+
if (breakdown.length > 0) {
|
|
140
|
+
lines.push('', '### By model');
|
|
141
|
+
for (const b of breakdown) {
|
|
142
|
+
const costStr = formatModelCost(b.provider, b.estimatedCost);
|
|
143
|
+
lines.push(`- **${b.model}** (${b.provider}): ${fmt(b.promptTokens)} in / ${fmt(b.completionTokens)} out — ${costStr}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
if (currentProvider === 'ollama') {
|
|
147
|
+
lines.push(`**Total: free · ${fmt(totals.totalTokens)} tokens**`);
|
|
148
|
+
}
|
|
149
|
+
else if (totals.estimatedCost > 0) {
|
|
150
|
+
lines.push(`**Total: ~$${totals.estimatedCost.toFixed(4)}**`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
|
|
154
|
+
lines.push('', '### Prompt caching');
|
|
155
|
+
lines.push(`Cache reads: ${fmt(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
|
|
156
|
+
if (cache.cacheCreationTokens > 0) {
|
|
157
|
+
lines.push(`Cache writes: ${fmt(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
|
|
158
|
+
}
|
|
159
|
+
if (cache.estimatedSavingsUsd > 0) {
|
|
160
|
+
lines.push(`Estimated savings vs no caching: $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
lines.push('### Pricing (per 1M tokens)');
|
|
166
|
+
lines.push('| Model | Input | Output |');
|
|
167
|
+
lines.push('|---|---|---|');
|
|
168
|
+
for (const p of pricing) {
|
|
169
|
+
lines.push(`| ${p.model} | $${p.inputPer1M.toFixed(3)} | $${p.outputPer1M.toFixed(3)} |`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
// ─── /copy and /apply code-block extraction ───────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Extract every fenced code block body (the text inside ```…```) from a
|
|
176
|
+
* string, mirroring the `/copy` loop. Language fences (```ts) are ignored —
|
|
177
|
+
* only the body is captured.
|
|
178
|
+
*/
|
|
179
|
+
export function extractCodeBlocks(text) {
|
|
180
|
+
const blocks = [];
|
|
181
|
+
for (const match of text.matchAll(/```[\w]*\n([\s\S]*?)```/g)) {
|
|
182
|
+
blocks.push(match[1]);
|
|
183
|
+
}
|
|
184
|
+
return blocks;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Validate a 1-based block index against the available block list, as used
|
|
188
|
+
* by `/copy <n>`. Returns the 0-based index, or `null` when the index is
|
|
189
|
+
* out of range (the caller shows an error).
|
|
190
|
+
*/
|
|
191
|
+
export function resolveBlockIndex(blockNum, blockCount) {
|
|
192
|
+
if (blockCount === 0)
|
|
193
|
+
return null;
|
|
194
|
+
const index = blockNum === -1 ? blockCount - 1 : blockNum - 1;
|
|
195
|
+
if (Number.isNaN(index) || index < 0 || index >= blockCount)
|
|
196
|
+
return null;
|
|
197
|
+
return index;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extract file-change pairs from an assistant message, mirroring `/apply`.
|
|
201
|
+
* Two patterns are tried in order:
|
|
202
|
+
* 1. fence with a filename header: ```ts\nsrc/foo.ts\n<body>```
|
|
203
|
+
* 2. fence with a `// File:` / `# Path:` comment header.
|
|
204
|
+
* A path is only accepted when it contains a dot and no spaces.
|
|
205
|
+
*/
|
|
206
|
+
export function extractFileChanges(text) {
|
|
207
|
+
const changes = [];
|
|
208
|
+
const fenceFilePattern = /```\w*\s+([\w./\\-]+(?:\.\w+))\n([\s\S]*?)```/g;
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = fenceFilePattern.exec(text)) !== null) {
|
|
211
|
+
const p = match[1].trim();
|
|
212
|
+
if (p.includes('.') && !p.includes(' '))
|
|
213
|
+
changes.push({ path: p, content: match[2] });
|
|
214
|
+
}
|
|
215
|
+
if (changes.length > 0)
|
|
216
|
+
return changes;
|
|
217
|
+
const commentPattern = /```(\w+)?\s*\n(?:\/\/|#|--|\/\*)\s*(?:File|Path|file|path):\s*([^\n*]+)\n([\s\S]*?)```/g;
|
|
218
|
+
while ((match = commentPattern.exec(text)) !== null) {
|
|
219
|
+
changes.push({ path: match[2].trim(), content: match[3] });
|
|
220
|
+
}
|
|
221
|
+
return changes;
|
|
222
|
+
}
|
|
223
|
+
/** Truncate a path for display: keep the last 37 chars, prefixed with “…”. */
|
|
224
|
+
export function shortPathForDisplay(path, max = 40) {
|
|
225
|
+
return path.length > max ? '...' + path.slice(-(max - 3)) : path;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Build a single diff-line summary for a file change, mirroring `/apply`.
|
|
229
|
+
* Returns `null` when `existingContent` is empty (a CREATE), otherwise a
|
|
230
|
+
* MODIFY line with the line-count delta.
|
|
231
|
+
*/
|
|
232
|
+
export function formatApplyDiffLine(change, existingContent) {
|
|
233
|
+
const shortPath = shortPathForDisplay(change.path);
|
|
234
|
+
if (!existingContent) {
|
|
235
|
+
return [`+ CREATE: ${shortPath}`, ` (${change.content.split('\n').length} lines)`];
|
|
236
|
+
}
|
|
237
|
+
const oldLines = existingContent.split('\n').length;
|
|
238
|
+
const newLines = change.content.split('\n').length;
|
|
239
|
+
const lineDiff = newLines - oldLines;
|
|
240
|
+
return [`~ MODIFY: ${shortPath}`, ` ${oldLines} → ${newLines} lines (${lineDiff >= 0 ? '+' : ''}${lineDiff})`];
|
|
241
|
+
}
|
|
242
|
+
// ─── /mcp helpers ─────────────────────────────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* Parse `key=value` tokens (as used by `/mcp prompt <server> <name> [k=v...]`)
|
|
245
|
+
* into a record. Tokens without an `=` (or with `=` at position 0) are
|
|
246
|
+
* skipped. Mirrors the inline loop.
|
|
247
|
+
*/
|
|
248
|
+
export function parsePromptArgs(tokens) {
|
|
249
|
+
const out = {};
|
|
250
|
+
for (const tok of tokens) {
|
|
251
|
+
const eq = tok.indexOf('=');
|
|
252
|
+
if (eq > 0)
|
|
253
|
+
out[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/** Pluralise "tool"/"tools" based on the count. */
|
|
258
|
+
export function pluralTools(n) {
|
|
259
|
+
return `tool${n === 1 ? '' : 's'}`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Group a flat list of tools by `serverName`, preserving first-seen order.
|
|
263
|
+
* Used by `/mcp` (default), `/mcp reload`, and the install report.
|
|
264
|
+
*/
|
|
265
|
+
export function groupToolsByServer(tools) {
|
|
266
|
+
const byServer = new Map();
|
|
267
|
+
for (const t of tools) {
|
|
268
|
+
if (!byServer.has(t.serverName))
|
|
269
|
+
byServer.set(t.serverName, []);
|
|
270
|
+
byServer.get(t.serverName).push(t);
|
|
271
|
+
}
|
|
272
|
+
return Array.from(byServer, ([serverName, serverTools]) => ({ serverName, serverTools }));
|
|
273
|
+
}
|
|
274
|
+
/** Format the `/mcp` default server/tool listing. */
|
|
275
|
+
export function formatMcpServerList(tools, errors) {
|
|
276
|
+
if (tools.length === 0 && errors.length === 0) {
|
|
277
|
+
return [
|
|
278
|
+
'_No MCP servers connected to this session._',
|
|
279
|
+
'',
|
|
280
|
+
'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
|
|
281
|
+
'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
|
|
282
|
+
].join('\n');
|
|
283
|
+
}
|
|
284
|
+
const lines = ['## MCP servers', ''];
|
|
285
|
+
if (tools.length > 0) {
|
|
286
|
+
for (const { serverName, serverTools } of groupToolsByServer(tools)) {
|
|
287
|
+
lines.push(`**${serverName}** — ${serverTools.length} ${pluralTools(serverTools.length)}`);
|
|
288
|
+
for (const t of serverTools) {
|
|
289
|
+
const desc = t.description ? ` — ${t.description}` : '';
|
|
290
|
+
lines.push(`- \`${t.agentName}\`${desc}`);
|
|
291
|
+
}
|
|
292
|
+
lines.push('');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (errors.length > 0) {
|
|
296
|
+
lines.push('### Failed servers');
|
|
297
|
+
for (const e of errors)
|
|
298
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
299
|
+
}
|
|
300
|
+
return lines.join('\n').trim();
|
|
301
|
+
}
|
|
302
|
+
/** Format the `/mcp reload` report. */
|
|
303
|
+
export function formatMcpReloadReport(toolCount, serverCount, errors) {
|
|
304
|
+
const lines = ['## MCP reloaded', '', `**${toolCount}** ${pluralTools(toolCount)} from **${serverCount}** server${serverCount === 1 ? '' : 's'}.`];
|
|
305
|
+
if (errors.length > 0) {
|
|
306
|
+
lines.push('', '### Failed servers');
|
|
307
|
+
for (const e of errors)
|
|
308
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
309
|
+
}
|
|
310
|
+
return lines.join('\n');
|
|
311
|
+
}
|
|
312
|
+
/** Format the `/mcp resources` listing. */
|
|
313
|
+
export function formatMcpResourcesList(groups) {
|
|
314
|
+
if (groups.length === 0)
|
|
315
|
+
return '_No MCP server in this session exposes resources._';
|
|
316
|
+
const lines = ['## MCP resources', ''];
|
|
317
|
+
for (const g of groups) {
|
|
318
|
+
lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
|
|
319
|
+
for (const r of g.resources) {
|
|
320
|
+
const label = r.name ? `${r.name} — ` : '';
|
|
321
|
+
const mime = r.mimeType ? ` (${r.mimeType})` : '';
|
|
322
|
+
lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
|
|
323
|
+
}
|
|
324
|
+
lines.push('');
|
|
325
|
+
}
|
|
326
|
+
lines.push('Read one with `/mcp read <uri>`.');
|
|
327
|
+
return lines.join('\n').trim();
|
|
328
|
+
}
|
|
329
|
+
/** Format the `/mcp read` output for a list of resource contents. */
|
|
330
|
+
export function formatMcpResourceRead(uri, contents) {
|
|
331
|
+
if (contents.length === 0)
|
|
332
|
+
return `_No content returned for \`${uri}\`._`;
|
|
333
|
+
const lines = [`## Resource: \`${uri}\``, ''];
|
|
334
|
+
for (const c of contents) {
|
|
335
|
+
if (c.text !== undefined) {
|
|
336
|
+
const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
|
|
337
|
+
lines.push('```' + fence);
|
|
338
|
+
lines.push(c.text);
|
|
339
|
+
lines.push('```');
|
|
340
|
+
}
|
|
341
|
+
else if (c.blob) {
|
|
342
|
+
lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return lines.join('\n');
|
|
346
|
+
}
|
|
347
|
+
/** Format the `/mcp prompts` listing. */
|
|
348
|
+
export function formatMcpPromptsList(groups) {
|
|
349
|
+
if (groups.length === 0)
|
|
350
|
+
return '_No MCP server in this session exposes prompt templates._';
|
|
351
|
+
const lines = ['## MCP prompt templates', ''];
|
|
352
|
+
for (const g of groups) {
|
|
353
|
+
lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
|
|
354
|
+
for (const p of g.prompts) {
|
|
355
|
+
const argList = p.arguments?.length
|
|
356
|
+
? ` (${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(', ')})`
|
|
357
|
+
: '';
|
|
358
|
+
lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
|
|
363
|
+
return lines.join('\n').trim();
|
|
364
|
+
}
|
|
365
|
+
/** Format the `/mcp prompt` materialised output. */
|
|
366
|
+
export function formatMcpPromptResult(serverName, name, description, messages) {
|
|
367
|
+
const lines = [`## Prompt \`${serverName}/${name}\``];
|
|
368
|
+
if (description)
|
|
369
|
+
lines.push(`_${description}_`);
|
|
370
|
+
lines.push('');
|
|
371
|
+
for (const m of messages) {
|
|
372
|
+
const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
|
|
373
|
+
lines.push(`**${m.role}:** ${text}`, '');
|
|
374
|
+
}
|
|
375
|
+
return lines.join('\n').trim();
|
|
376
|
+
}
|
|
377
|
+
// ─── /insights --days parser ──────────────────────────────────────────────────
|
|
378
|
+
/**
|
|
379
|
+
* Parse the `--days N` / `--days=N` flag from `/insights` args. Returns the
|
|
380
|
+
* default (7) when absent or unparseable; clamps negatives to 0.
|
|
381
|
+
*/
|
|
382
|
+
export function parseInsightsDays(args, fallback = 7) {
|
|
383
|
+
let days = fallback;
|
|
384
|
+
for (let i = 0; i < args.length; i++) {
|
|
385
|
+
const a = args[i];
|
|
386
|
+
if (a === '--days' && args[i + 1]) {
|
|
387
|
+
const n = parseInt(args[i + 1], 10);
|
|
388
|
+
if (Number.isFinite(n))
|
|
389
|
+
days = n;
|
|
390
|
+
}
|
|
391
|
+
else if (a.startsWith('--days=')) {
|
|
392
|
+
const n = parseInt(a.slice('--days='.length), 10);
|
|
393
|
+
if (Number.isFinite(n))
|
|
394
|
+
days = n;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return days;
|
|
398
|
+
}
|
|
399
|
+
// ─── /cloud session-row formatter ─────────────────────────────────────────────
|
|
400
|
+
/**
|
|
401
|
+
* Format a single cloud-session row for the `/cloud` picker, mirroring the
|
|
402
|
+
* inline template: `title · date · N msg · [project?]`.
|
|
403
|
+
*/
|
|
404
|
+
export function formatCloudSessionLabel(s) {
|
|
405
|
+
const date = new Date(s.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
406
|
+
const title = s.sessionName || s.sessionId.slice(0, 8);
|
|
407
|
+
const projectTag = s.projectName ? ` · ${s.projectName}` : '';
|
|
408
|
+
return `${title} · ${date} · ${s.messageCount} msg${projectTag}`;
|
|
409
|
+
}
|
|
410
|
+
// ─── /me sync + learn + init formatters ───────────────────────────────────────
|
|
411
|
+
/** Format the `/me sync` result list. `pulled` is `1` on success, `0` or `null` otherwise. */
|
|
412
|
+
export function formatMeSyncReport(pushed, pulled) {
|
|
413
|
+
const lines = [];
|
|
414
|
+
if (pushed)
|
|
415
|
+
lines.push('✓ Profile pushed to the dashboard');
|
|
416
|
+
if (pulled === 1)
|
|
417
|
+
lines.push('✓ Profile pulled to this machine');
|
|
418
|
+
if (lines.length === 0)
|
|
419
|
+
lines.push('Nothing to sync yet — run `/me init` and fill in your profile first.');
|
|
420
|
+
return `## Profile sync\n\n${lines.join('\n')}`;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Format the `/me learn` result. `updated` distinguishes "new facts written"
|
|
424
|
+
* from "already covered"; `file` is the human-readable path.
|
|
425
|
+
*/
|
|
426
|
+
export function formatMeLearnResult(scope, file, res) {
|
|
427
|
+
return res.updated
|
|
428
|
+
? `Updated your ${scope} learned profile (\`${file}\`):\n\n${res.facts}\n\nClear it anytime with \`/me forget\`.`
|
|
429
|
+
: `No changes — your ${scope} learned profile already covers this:\n\n${res.facts}`;
|
|
430
|
+
}
|
|
431
|
+
/** Format the `/me init` result. */
|
|
432
|
+
export function formatMeInitResult(scope, res) {
|
|
433
|
+
return res.created
|
|
434
|
+
? `Created ${scope} profile: \`${res.path}\`\n\nEdit it in your editor — Codeep uses it automatically. View anytime with \`/me\`.`
|
|
435
|
+
: `${scope === 'global' ? 'Global' : 'Project'} profile already exists: \`${res.path}\`\n\nEdit it directly, or view it with \`/me\`.`;
|
|
436
|
+
}
|
|
437
|
+
// ─── /skills show formatter ───────────────────────────────────────────────────
|
|
438
|
+
/** Format the `/skills show` detail view from a skill bundle. */
|
|
439
|
+
export function formatSkillsShow(bundle) {
|
|
440
|
+
return `# ${bundle.name}\n_${bundle.description}_\n\n**Source:** ${bundle.source}\n\n---\n\n${bundle.body}`;
|
|
441
|
+
}
|
|
442
|
+
// ─── /skills browse + publish formatters ──────────────────────────────────────
|
|
443
|
+
/** Format the `/skills browse` empty-state message. */
|
|
444
|
+
export function formatSkillsBrowseEmpty(query) {
|
|
445
|
+
return query ? `_No public skills matching "${query}"._` : '_No public skills published yet._';
|
|
446
|
+
}
|
|
447
|
+
/** Format the `/skills publish` success message. */
|
|
448
|
+
export function formatSkillsPublishResult(slug, isPublic, owner) {
|
|
449
|
+
return `Published \`${slug}\` (${isPublic ? 'public' : 'private'}) to codeep.dev. Install elsewhere with \`/skills install ${owner ?? '<you>'}/${slug}\`.`;
|
|
450
|
+
}
|
|
@@ -144,7 +144,7 @@ export const COMMANDS = [
|
|
|
144
144
|
{ name: 'copy', description: 'Copy code block to clipboard', category: 'code', usage: ['[n]'] },
|
|
145
145
|
{ name: 'paste', description: 'Paste from clipboard', category: 'code' },
|
|
146
146
|
{ name: 'apply', description: 'Apply file changes from AI', category: 'code' },
|
|
147
|
-
{ name: 'add', description: 'Add file to context', category: 'code', usage: ['<path>'] },
|
|
147
|
+
{ name: 'add', description: 'Add file to context (or type @path inline)', category: 'code', usage: ['<path>'] },
|
|
148
148
|
{ name: 'drop', description: 'Remove file (or all) from context', category: 'code', usage: ['[path]'] },
|
|
149
149
|
{ name: 'multiline', description: 'Toggle multi-line input mode', category: 'code' },
|
|
150
150
|
// ── skills (shortcuts) ─────────────────────────────────────────────────────
|
|
@@ -216,6 +216,7 @@ export const COMMANDS = [
|
|
|
216
216
|
},
|
|
217
217
|
{ name: 'hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)', category: 'extensions' },
|
|
218
218
|
{ name: 'commands', description: 'List custom slash commands in .codeep/commands/*.md', category: 'extensions' },
|
|
219
|
+
{ name: 'web-cache', aliases: ['webcache'], description: 'Show @web fetch cache stats (alias: /web-cache clear)', category: 'extensions' },
|
|
219
220
|
// ── cloud & account ────────────────────────────────────────────────────────
|
|
220
221
|
{ name: 'account', description: 'Link this machine to your codeep.dev account', category: 'cloud' },
|
|
221
222
|
{ name: 'tasks', description: 'List/add/done/delete codeep.dev tasks', category: 'cloud', usage: ['add <title> [--bug|--feature]'] },
|