codeep 2.14.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 +47 -27
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/acp/session.js +22 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -24
- package/dist/renderer/App.d.ts +77 -30
- package/dist/renderer/App.js +429 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +251 -0
- package/dist/renderer/commands/helpers.js +450 -0
- package/dist/renderer/commands/registry.js +7 -1
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +363 -318
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +58 -0
- package/dist/renderer/components/Autocomplete.js +75 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +219 -0
- package/dist/renderer/layout.js +338 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +79 -11
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.js +21 -5
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- 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 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers extracted from `renderer/commands.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The dispatcher is one giant switch/case; many cases contain small but
|
|
5
|
+
* tricky bits of pure logic (arg parsing, snippet extraction, message
|
|
6
|
+
* formatting) that were previously untestable because they were inlined
|
|
7
|
+
* alongside `ctx.app.*` calls. Pulling them here gives them direct unit
|
|
8
|
+
* coverage.
|
|
9
|
+
*/
|
|
10
|
+
/** Snippet window: chars of context before / after the match. */
|
|
11
|
+
export const SEARCH_SNIPPET_BEFORE = 30;
|
|
12
|
+
export const SEARCH_SNIPPET_AFTER = 50;
|
|
13
|
+
/**
|
|
14
|
+
* Build search-result snippets for messages matching `term`. Mirrors the
|
|
15
|
+
* inline loop that used to live in the `/search` case. Case-insensitive.
|
|
16
|
+
*/
|
|
17
|
+
export function buildSearchSnippets(messages, term) {
|
|
18
|
+
const lowerTerm = term.toLowerCase();
|
|
19
|
+
const results = [];
|
|
20
|
+
messages.forEach((m, index) => {
|
|
21
|
+
const lowerContent = m.content.toLowerCase();
|
|
22
|
+
if (!lowerContent.includes(lowerTerm))
|
|
23
|
+
return;
|
|
24
|
+
const matchIdx = lowerContent.indexOf(lowerTerm);
|
|
25
|
+
const matchStart = Math.max(0, matchIdx - SEARCH_SNIPPET_BEFORE);
|
|
26
|
+
const matchEnd = Math.min(m.content.length, matchIdx + lowerTerm.length + SEARCH_SNIPPET_AFTER);
|
|
27
|
+
const matchedText = (matchStart > 0 ? '...' : '') +
|
|
28
|
+
m.content.slice(matchStart, matchEnd).replace(/\n/g, ' ') +
|
|
29
|
+
(matchEnd < m.content.length ? '...' : '');
|
|
30
|
+
results.push({ role: m.role, messageIndex: index, matchedText });
|
|
31
|
+
});
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
// ─── Argument parsing ─────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Parse the `/compact <n>` argument. Returns a value of at least 2
|
|
37
|
+
* (never compacts below 2 messages); defaults to `fallback` when the arg
|
|
38
|
+
* is missing or unparseable.
|
|
39
|
+
*
|
|
40
|
+
* Note: we use `Number.isNaN` rather than `parsed || fallback` because
|
|
41
|
+
* `0` is a valid (if useless) numeric input that should clamp to 2, not
|
|
42
|
+
* silently fall through to the default.
|
|
43
|
+
*/
|
|
44
|
+
export function parseKeepRecent(arg, fallback = 4) {
|
|
45
|
+
if (!arg)
|
|
46
|
+
return fallback;
|
|
47
|
+
const parsed = parseInt(arg, 10);
|
|
48
|
+
return Math.max(2, Number.isNaN(parsed) ? fallback : parsed);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Join slash-command args into a single hyphen-separated name, as used by
|
|
52
|
+
* `/rename`. Empty args are dropped so `/rename my session ` still
|
|
53
|
+
* yields `my-session`.
|
|
54
|
+
*/
|
|
55
|
+
export function joinSessionName(args) {
|
|
56
|
+
return args.filter((a) => a.length > 0).join('-');
|
|
57
|
+
}
|
|
58
|
+
// ─── /tasks helpers ───────────────────────────────────────────────────────────
|
|
59
|
+
export const TASK_TYPES = ['task', 'bug', 'feature'];
|
|
60
|
+
/**
|
|
61
|
+
* Parse the args following `/tasks add` into a title, description, and
|
|
62
|
+
* type. Flags (`--bug`, `--feature`, `--task`) set the type; `--desc` /
|
|
63
|
+
* `--description` captures the following words until the next flag.
|
|
64
|
+
* Non-flag words before any `--desc` form the title.
|
|
65
|
+
*/
|
|
66
|
+
export function parseTaskAddArgs(args) {
|
|
67
|
+
let type = 'task';
|
|
68
|
+
const titleWords = [];
|
|
69
|
+
const descWords = [];
|
|
70
|
+
let capturingDesc = false;
|
|
71
|
+
for (const w of args) {
|
|
72
|
+
const flag = /^--([\w-]+)$/.exec(w);
|
|
73
|
+
if (flag) {
|
|
74
|
+
const name = flag[1].toLowerCase();
|
|
75
|
+
if (name === 'desc' || name === 'description') {
|
|
76
|
+
capturingDesc = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (TASK_TYPES.includes(name))
|
|
80
|
+
type = name;
|
|
81
|
+
capturingDesc = false; // any non-desc flag ends description capture
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (capturingDesc)
|
|
85
|
+
descWords.push(w);
|
|
86
|
+
else
|
|
87
|
+
titleWords.push(w);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
title: titleWords.join(' ').trim(),
|
|
91
|
+
description: descWords.join(' ').trim(),
|
|
92
|
+
type,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Icon/badge for a task type, used in list rendering. */
|
|
96
|
+
const TYPE_ICON = { bug: '[bug]', feature: '[feature]', task: '[task]' };
|
|
97
|
+
/** Render a list of tasks as a Markdown list, mirroring `/tasks`. */
|
|
98
|
+
export function formatTaskList(tasks, scopeProjectName) {
|
|
99
|
+
const lines = [`## Tasks${scopeProjectName ? ` — ${scopeProjectName}` : ''}`, ''];
|
|
100
|
+
tasks.forEach((t, i) => {
|
|
101
|
+
const icon = TYPE_ICON[t.type ?? 'task'] ?? '[task]';
|
|
102
|
+
const proj = !scopeProjectName && t.project_name ? ` _(${t.project_name})_` : '';
|
|
103
|
+
lines.push(`${i + 1}. ${icon} ${t.title}${proj}${t.description ? `\n ${t.description}` : ''}`);
|
|
104
|
+
});
|
|
105
|
+
lines.push('', `*${tasks.length} pending task${tasks.length > 1 ? 's' : ''}. Use /tasks done <n> to mark complete.*`);
|
|
106
|
+
lines.push('*Tasks loaded into agent context — agent will see them in the next message.*');
|
|
107
|
+
return lines.join('\n');
|
|
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
|
+
}
|
|
@@ -87,6 +87,11 @@ export const COMMANDS = [
|
|
|
87
87
|
category: 'sessions',
|
|
88
88
|
usage: ['<query>', '<query> --resume', '<query> --summarize'],
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'cloud',
|
|
92
|
+
description: 'List and resume sessions synced from other devices',
|
|
93
|
+
category: 'sessions',
|
|
94
|
+
},
|
|
90
95
|
{ name: 'export', description: 'Export chat', category: 'sessions', usage: ['[md|json|txt]'] },
|
|
91
96
|
{ name: 'compact', description: 'AI-summarize older messages to free up context (keeps last N)', category: 'sessions', usage: ['[keepN]'] },
|
|
92
97
|
// ── checkpoints ────────────────────────────────────────────────────────────
|
|
@@ -139,7 +144,7 @@ export const COMMANDS = [
|
|
|
139
144
|
{ name: 'copy', description: 'Copy code block to clipboard', category: 'code', usage: ['[n]'] },
|
|
140
145
|
{ name: 'paste', description: 'Paste from clipboard', category: 'code' },
|
|
141
146
|
{ name: 'apply', description: 'Apply file changes from AI', category: 'code' },
|
|
142
|
-
{ 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>'] },
|
|
143
148
|
{ name: 'drop', description: 'Remove file (or all) from context', category: 'code', usage: ['[path]'] },
|
|
144
149
|
{ name: 'multiline', description: 'Toggle multi-line input mode', category: 'code' },
|
|
145
150
|
// ── skills (shortcuts) ─────────────────────────────────────────────────────
|
|
@@ -211,6 +216,7 @@ export const COMMANDS = [
|
|
|
211
216
|
},
|
|
212
217
|
{ name: 'hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)', category: 'extensions' },
|
|
213
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' },
|
|
214
220
|
// ── cloud & account ────────────────────────────────────────────────────────
|
|
215
221
|
{ name: 'account', description: 'Link this machine to your codeep.dev account', category: 'cloud' },
|
|
216
222
|
{ name: 'tasks', description: 'List/add/done/delete codeep.dev tasks', category: 'cloud', usage: ['add <title> [--bug|--feature]'] },
|
|
@@ -13,4 +13,8 @@ export interface AppCommandContext extends AppExecutionContext {
|
|
|
13
13
|
setProjectContext: (ctx: ReturnType<typeof getProjectContext>) => void;
|
|
14
14
|
setHasWriteAccess: (v: boolean) => void;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns a hint for an Ollama model name based on parameter count.
|
|
18
|
+
* Models ≥7B are suitable for agent mode; smaller ones are chat-only.
|
|
19
|
+
*/
|
|
16
20
|
export declare function handleCommand(command: string, args: string[], ctx: AppCommandContext): Promise<void>;
|