codeep 2.0.2 → 2.0.4
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 +58 -0
- package/dist/acp/commands.js +39 -0
- package/dist/acp/server.js +3 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/renderer/App.js +6 -1
- package/dist/renderer/commands.js +78 -0
- package/dist/renderer/components/Help.js +3 -0
- package/dist/utils/agent.js +13 -0
- package/dist/utils/insights.d.ts +30 -0
- package/dist/utils/insights.js +166 -0
- package/dist/utils/personalities.d.ts +50 -0
- package/dist/utils/personalities.js +226 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -526,6 +526,64 @@ Then call it as `/sec-review src/api/login.ts` (or `/sec` via the alias).
|
|
|
526
526
|
**Discovery:** `/commands` lists all available templates. Project files shadow
|
|
527
527
|
global files with the same name. Aliases also work for autocomplete.
|
|
528
528
|
|
|
529
|
+
### Personalities (`/personality`, new in 2.0.3)
|
|
530
|
+
|
|
531
|
+
Swap how the agent talks and what it prioritises mid-conversation:
|
|
532
|
+
|
|
533
|
+
```
|
|
534
|
+
/personality # list available
|
|
535
|
+
/personality concise # short answers, no preamble
|
|
536
|
+
/personality security # treat every input as hostile
|
|
537
|
+
/personality senior-reviewer # push back on shortcuts, name things well
|
|
538
|
+
/personality ship-it # pick first reasonable approach
|
|
539
|
+
/personality off # back to default tone
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Six built-in presets: `concise`, `verbose`, `security`, `senior-reviewer`,
|
|
543
|
+
`junior-mentor`, `ship-it`. The active one persists across sessions
|
|
544
|
+
(stored in `~/.codeep/config.json` as `activePersonality`).
|
|
545
|
+
|
|
546
|
+
**Custom personalities** — drop a Markdown file in
|
|
547
|
+
`.codeep/personalities/<name>.md` (project) or
|
|
548
|
+
`~/.codeep/personalities/<name>.md` (global):
|
|
549
|
+
|
|
550
|
+
```markdown
|
|
551
|
+
# Personality: PR Reviewer
|
|
552
|
+
|
|
553
|
+
You are reviewing a PR from a junior engineer:
|
|
554
|
+
- Cite line numbers for every concern.
|
|
555
|
+
- Suggest an alternative, don't just flag the problem.
|
|
556
|
+
- Keep tone collaborative, not pedantic.
|
|
557
|
+
- End with one thing the author did well.
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
First `# Personality:` line is the display name; the rest is appended
|
|
561
|
+
to the agent's system prompt verbatim when active. Project shadows
|
|
562
|
+
global shadows built-in (by name).
|
|
563
|
+
|
|
564
|
+
### Activity Insights (`/insights`, new in 2.0.3)
|
|
565
|
+
|
|
566
|
+
Summarise what the agent has actually done for you over a window — runs,
|
|
567
|
+
tool actions, projects touched, most-edited files — sourced from
|
|
568
|
+
`~/.codeep/history/<id>.json` (one file per agent run, automatic).
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
/insights # last 7 days (default)
|
|
572
|
+
/insights --days 30 # last month
|
|
573
|
+
/insights --days 1 # today only
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Surfaces (markdown rendered in chat):
|
|
577
|
+
|
|
578
|
+
- Headline tally: runs · actions · active time · active-days density · avg actions/run
|
|
579
|
+
- **By project** sorted by active time
|
|
580
|
+
- **Top tools** (read_file × 340, write_file × 80, …)
|
|
581
|
+
- **Most-touched files** (with `~` prefix for readability)
|
|
582
|
+
- **Recent runs** — 10 most recent with project, duration, and the user prompt that started them
|
|
583
|
+
|
|
584
|
+
Cost / token usage isn't in `/insights` (it lives in `/cost` per-session
|
|
585
|
+
since the token tracker is in-memory). Insights is history-only.
|
|
586
|
+
|
|
529
587
|
### Project Intelligence (`/init`, `/scan`)
|
|
530
588
|
|
|
531
589
|
Initialize a project and scan it once to cache deep analysis for faster AI responses:
|
package/dist/acp/commands.js
CHANGED
|
@@ -600,6 +600,45 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
// ─── Export ────────────────────────────────────────────────────────────────
|
|
603
|
+
// ─── Personalities + insights (2.0.3) ─────────────────────────────────────
|
|
604
|
+
case 'personality': {
|
|
605
|
+
const { formatPersonalityList, findPersonality } = await import('../utils/personalities.js');
|
|
606
|
+
const sub = args[0]?.toLowerCase();
|
|
607
|
+
if (!sub) {
|
|
608
|
+
return { handled: true, response: formatPersonalityList(session.workspaceRoot) };
|
|
609
|
+
}
|
|
610
|
+
if (sub === 'off' || sub === 'none' || sub === 'clear') {
|
|
611
|
+
config.set('activePersonality', null);
|
|
612
|
+
return { handled: true, response: 'Personality cleared — agent uses default tone.' };
|
|
613
|
+
}
|
|
614
|
+
const p = findPersonality(sub, session.workspaceRoot);
|
|
615
|
+
if (!p) {
|
|
616
|
+
return { handled: true, response: `No personality named \`${sub}\`. Run \`/personality\` to see available.` };
|
|
617
|
+
}
|
|
618
|
+
config.set('activePersonality', p.name);
|
|
619
|
+
return {
|
|
620
|
+
handled: true,
|
|
621
|
+
response: `Active personality: **${p.displayName}** (\`${p.name}\`, ${p.scope})\n\n_${p.description}_\n\nClear with \`/personality off\`.`,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
case 'insights': {
|
|
625
|
+
const { formatInsights } = await import('../utils/insights.js');
|
|
626
|
+
let days = 7;
|
|
627
|
+
for (let i = 0; i < args.length; i++) {
|
|
628
|
+
const a = args[i];
|
|
629
|
+
if (a === '--days' && args[i + 1]) {
|
|
630
|
+
const n = parseInt(args[i + 1], 10);
|
|
631
|
+
if (Number.isFinite(n))
|
|
632
|
+
days = n;
|
|
633
|
+
}
|
|
634
|
+
else if (a.startsWith('--days=')) {
|
|
635
|
+
const n = parseInt(a.slice('--days='.length), 10);
|
|
636
|
+
if (Number.isFinite(n))
|
|
637
|
+
days = n;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return { handled: true, response: formatInsights({ days }) };
|
|
641
|
+
}
|
|
603
642
|
// ─── Plan mode (2.0.2) ────────────────────────────────────────────────────
|
|
604
643
|
case 'plan': {
|
|
605
644
|
// Identical contract to TUI /plan: generate a pre-execution plan,
|
package/dist/acp/server.js
CHANGED
|
@@ -55,6 +55,9 @@ const AVAILABLE_COMMANDS = [
|
|
|
55
55
|
// Plan mode (2.0.2)
|
|
56
56
|
{ name: 'plan', description: 'Generate a numbered plan for a task — review before /go executes', input: { hint: '<task>' } },
|
|
57
57
|
{ name: 'go', description: 'Execute the pending plan from /plan' },
|
|
58
|
+
// Personalities + insights (2.0.3)
|
|
59
|
+
{ name: 'personality', description: 'List or switch agent tone preset', input: { hint: '[name | off]' } },
|
|
60
|
+
{ name: 'insights', description: 'Activity summary over the last N days (default 7)', input: { hint: '[--days N]' } },
|
|
58
61
|
// Project intelligence
|
|
59
62
|
{ name: 'scan', description: 'Scan project structure and generate summary' },
|
|
60
63
|
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },
|
package/dist/config/index.d.ts
CHANGED
|
@@ -57,6 +57,13 @@ interface ConfigSchema {
|
|
|
57
57
|
data_collection?: 'allow' | 'deny';
|
|
58
58
|
require_parameters?: boolean;
|
|
59
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Active personality preset (`concise`, `senior-reviewer`, custom user
|
|
62
|
+
* personalities from .codeep/personalities/*.md, …). When set, the
|
|
63
|
+
* loader text is appended to every agent system prompt. See
|
|
64
|
+
* utils/personalities.ts.
|
|
65
|
+
*/
|
|
66
|
+
activePersonality?: string | null;
|
|
60
67
|
}
|
|
61
68
|
export type { AgentMode };
|
|
62
69
|
export type { LanguageCode };
|
package/dist/renderer/App.js
CHANGED
|
@@ -93,6 +93,8 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
93
93
|
'openrouter': 'Tune OpenRouter routing (preferred / ignore providers, fallbacks, privacy)',
|
|
94
94
|
'plan': 'Generate a numbered plan for a task — review before /go executes it',
|
|
95
95
|
'go': 'Execute the pending plan from /plan',
|
|
96
|
+
'personality': 'Switch agent tone: concise / verbose / security / senior-reviewer / etc',
|
|
97
|
+
'insights': 'Activity summary over the last N days (default 7): runs, files, tools, projects',
|
|
96
98
|
};
|
|
97
99
|
import { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
98
100
|
import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
@@ -234,6 +236,8 @@ export class App {
|
|
|
234
236
|
'hooks', 'mcp', 'openrouter',
|
|
235
237
|
// 2.0.2 — plan mode.
|
|
236
238
|
'plan', 'go',
|
|
239
|
+
// 2.0.3 — personalities + insights.
|
|
240
|
+
'personality', 'insights',
|
|
237
241
|
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
|
|
238
242
|
];
|
|
239
243
|
constructor(options) {
|
|
@@ -1889,7 +1893,8 @@ export class App {
|
|
|
1889
1893
|
}
|
|
1890
1894
|
// Footer
|
|
1891
1895
|
const scrollInfo = allItems.length > maxVisible ? ` (${this.helpScrollIndex + 1}-${Math.min(this.helpScrollIndex + maxVisible, allItems.length)}/${allItems.length})` : '';
|
|
1892
|
-
this.screen.writeLine(y
|
|
1896
|
+
this.screen.writeLine(y++, `↑↓ scroll • PgUp/PgDn fast scroll • Esc close${scrollInfo}`, fg.gray);
|
|
1897
|
+
this.screen.writeLine(y, 'Full guides → codeep.dev/docs · /docs <command> (e.g. /docs personality)', fg.cyan);
|
|
1893
1898
|
}
|
|
1894
1899
|
/**
|
|
1895
1900
|
* Render inline autocomplete below status bar
|
|
@@ -216,6 +216,84 @@ export async function handleCommand(command, args, ctx) {
|
|
|
216
216
|
runAgentTask(args.join(' '), true, ctx, () => null, () => { });
|
|
217
217
|
break;
|
|
218
218
|
}
|
|
219
|
+
case 'docs': {
|
|
220
|
+
// Open per-command web docs in the system browser. Lets the inline
|
|
221
|
+
// /help stay terse (single-line entries) while users who want the
|
|
222
|
+
// long story get one keystroke away from a real page.
|
|
223
|
+
const cmd = (args[0] ?? '').toLowerCase().replace(/^\//, '');
|
|
224
|
+
const KNOWN = {
|
|
225
|
+
personality: 'https://codeep.dev/docs/agent#personalities',
|
|
226
|
+
personalities: 'https://codeep.dev/docs/agent#personalities',
|
|
227
|
+
insights: 'https://codeep.dev/docs/agent#insights',
|
|
228
|
+
plan: 'https://codeep.dev/docs/agent#plan-mode',
|
|
229
|
+
go: 'https://codeep.dev/docs/agent#plan-mode',
|
|
230
|
+
mcp: 'https://codeep.dev/docs/mcp',
|
|
231
|
+
skills: 'https://codeep.dev/docs/skills',
|
|
232
|
+
checkpoint: 'https://codeep.dev/docs/commands#checkpoints',
|
|
233
|
+
rewind: 'https://codeep.dev/docs/commands#checkpoints',
|
|
234
|
+
hooks: 'https://codeep.dev/docs/commands#hooks',
|
|
235
|
+
commands: 'https://codeep.dev/docs/commands#custom-commands',
|
|
236
|
+
openrouter: 'https://codeep.dev/docs/providers#openrouter',
|
|
237
|
+
memory: 'https://codeep.dev/docs/commands#intelligence',
|
|
238
|
+
profile: 'https://codeep.dev/docs/commands#settings',
|
|
239
|
+
compact: 'https://codeep.dev/docs/commands#session',
|
|
240
|
+
cost: 'https://codeep.dev/docs/dashboard',
|
|
241
|
+
};
|
|
242
|
+
const url = cmd ? (KNOWN[cmd] ?? `https://codeep.dev/docs/commands?q=${encodeURIComponent(cmd)}`) : 'https://codeep.dev/docs';
|
|
243
|
+
try {
|
|
244
|
+
const { default: open } = await import('open');
|
|
245
|
+
await open(url);
|
|
246
|
+
ctx.app.notify(`Opening ${url}`);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
ctx.app.notify(`Couldn't open browser. Visit: ${url}`);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'insights': {
|
|
254
|
+
const { formatInsights } = await import('../utils/insights.js');
|
|
255
|
+
// Parse `--days N` (default 7). Accept both `--days 30` and `--days=30`.
|
|
256
|
+
let days = 7;
|
|
257
|
+
for (let i = 0; i < args.length; i++) {
|
|
258
|
+
const a = args[i];
|
|
259
|
+
if (a === '--days' && args[i + 1]) {
|
|
260
|
+
const n = parseInt(args[i + 1], 10);
|
|
261
|
+
if (Number.isFinite(n))
|
|
262
|
+
days = n;
|
|
263
|
+
}
|
|
264
|
+
else if (a.startsWith('--days=')) {
|
|
265
|
+
const n = parseInt(a.slice('--days='.length), 10);
|
|
266
|
+
if (Number.isFinite(n))
|
|
267
|
+
days = n;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
ctx.app.addMessage({ role: 'system', content: formatInsights({ days }) });
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case 'personality': {
|
|
274
|
+
const { formatPersonalityList, findPersonality } = await import('../utils/personalities.js');
|
|
275
|
+
const sub = args[0]?.toLowerCase();
|
|
276
|
+
if (!sub) {
|
|
277
|
+
ctx.app.addMessage({ role: 'system', content: formatPersonalityList(ctx.projectPath) });
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (sub === 'off' || sub === 'none' || sub === 'clear') {
|
|
281
|
+
config.set('activePersonality', null);
|
|
282
|
+
ctx.app.notify('Personality cleared — agent uses default tone.');
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
const personality = findPersonality(sub, ctx.projectPath);
|
|
286
|
+
if (!personality) {
|
|
287
|
+
ctx.app.notify(`No personality named "${sub}". Run /personality to see available.`);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
config.set('activePersonality', personality.name);
|
|
291
|
+
ctx.app.addMessage({
|
|
292
|
+
role: 'system',
|
|
293
|
+
content: `Active personality: **${personality.displayName}** (\`${personality.name}\`, ${personality.scope})\n\n_${personality.description}_\n\nClear with \`/personality off\`.`,
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
219
297
|
case 'plan': {
|
|
220
298
|
// Plan mode: ask the model for a plan, surface it, hold as pending.
|
|
221
299
|
// The user runs /go to execute or /plan <revised> to revise. See
|
|
@@ -123,6 +123,9 @@ export const helpCategories = [
|
|
|
123
123
|
{ key: '/profile save <name>', description: 'Save current provider+model as profile' },
|
|
124
124
|
{ key: '/profile list', description: 'List saved profiles' },
|
|
125
125
|
{ key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
|
|
126
|
+
{ key: '/personality', description: 'List or switch agent tone (concise / verbose / security / senior-reviewer / …)' },
|
|
127
|
+
{ key: '/personality <name>', description: 'Activate a personality. /personality off to clear.' },
|
|
128
|
+
{ key: '/insights [--days N]', description: 'Activity summary — runs, files, tools, projects over the last N days (default 7)' },
|
|
126
129
|
],
|
|
127
130
|
},
|
|
128
131
|
{
|
package/dist/utils/agent.js
CHANGED
|
@@ -253,6 +253,19 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
253
253
|
if (skillCatalogBlock) {
|
|
254
254
|
systemPrompt += '\n\n' + skillCatalogBlock;
|
|
255
255
|
}
|
|
256
|
+
// Active personality goes LAST — appended after skills / project rules /
|
|
257
|
+
// smart context so its tone overrides earlier conventions. Set via
|
|
258
|
+
// `/personality <name>`; empty when no personality is active.
|
|
259
|
+
try {
|
|
260
|
+
const { getActivePersonalityPrompt } = await import('./personalities.js');
|
|
261
|
+
const personalityPrompt = getActivePersonalityPrompt(projectContext.root);
|
|
262
|
+
if (personalityPrompt) {
|
|
263
|
+
systemPrompt += personalityPrompt;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Personality loading must never block an agent run.
|
|
268
|
+
}
|
|
256
269
|
// Initial user message with optional task plan
|
|
257
270
|
let initialPrompt = prompt;
|
|
258
271
|
if (taskPlan) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/insights` — agent activity summary over a configurable window.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth: `~/.codeep/history/<id>.json`, one file per agent
|
|
5
|
+
* run, written by the agent loop. Schema (relevant fields):
|
|
6
|
+
* { id, startTime, endTime, prompt, projectRoot, actions: [
|
|
7
|
+
* { type: 'write' | 'read' | 'execute' | …, path?, command?, timestamp }
|
|
8
|
+
* ]
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* We deliberately don't read sessions/*.json here — sessions store
|
|
12
|
+
* chat history without tool-level detail, while history/ captures the
|
|
13
|
+
* exact actions which is what users want to see ("which file did I
|
|
14
|
+
* touch most this week?").
|
|
15
|
+
*
|
|
16
|
+
* Cost / token usage is per-process and lost across restarts (the
|
|
17
|
+
* token tracker is in-memory), so /insights reports actions and time
|
|
18
|
+
* but not historical dollar amounts. The current session's cost still
|
|
19
|
+
* shows in /cost.
|
|
20
|
+
*/
|
|
21
|
+
interface InsightsOptions {
|
|
22
|
+
/** Days to look back. Default 7. */
|
|
23
|
+
days?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Format `/insights` output as Markdown. Returns a friendly empty-state
|
|
27
|
+
* message when there's no history in the window — we don't error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatInsights(opts?: InsightsOptions): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/insights` — agent activity summary over a configurable window.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth: `~/.codeep/history/<id>.json`, one file per agent
|
|
5
|
+
* run, written by the agent loop. Schema (relevant fields):
|
|
6
|
+
* { id, startTime, endTime, prompt, projectRoot, actions: [
|
|
7
|
+
* { type: 'write' | 'read' | 'execute' | …, path?, command?, timestamp }
|
|
8
|
+
* ]
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* We deliberately don't read sessions/*.json here — sessions store
|
|
12
|
+
* chat history without tool-level detail, while history/ captures the
|
|
13
|
+
* exact actions which is what users want to see ("which file did I
|
|
14
|
+
* touch most this week?").
|
|
15
|
+
*
|
|
16
|
+
* Cost / token usage is per-process and lost across restarts (the
|
|
17
|
+
* token tracker is in-memory), so /insights reports actions and time
|
|
18
|
+
* but not historical dollar amounts. The current session's cost still
|
|
19
|
+
* shows in /cost.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
22
|
+
import { join, basename } from 'path';
|
|
23
|
+
import { homedir } from 'os';
|
|
24
|
+
function loadHistoryRuns(sinceMs) {
|
|
25
|
+
const dir = join(homedir(), '.codeep', 'history');
|
|
26
|
+
if (!existsSync(dir))
|
|
27
|
+
return [];
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const runs = [];
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(join(dir, file), 'utf8');
|
|
39
|
+
const run = JSON.parse(raw);
|
|
40
|
+
if (typeof run.startTime !== 'number')
|
|
41
|
+
continue;
|
|
42
|
+
if (run.startTime < sinceMs)
|
|
43
|
+
continue;
|
|
44
|
+
runs.push(run);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// skip malformed
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return runs.sort((a, b) => b.startTime - a.startTime);
|
|
51
|
+
}
|
|
52
|
+
function fmtDuration(ms) {
|
|
53
|
+
if (ms < 60_000)
|
|
54
|
+
return `${Math.round(ms / 1000)}s`;
|
|
55
|
+
if (ms < 3_600_000)
|
|
56
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
57
|
+
const h = Math.floor(ms / 3_600_000);
|
|
58
|
+
const m = Math.round((ms % 3_600_000) / 60_000);
|
|
59
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
60
|
+
}
|
|
61
|
+
function relativeDayBucket(ts, now) {
|
|
62
|
+
const dayMs = 86_400_000;
|
|
63
|
+
const days = Math.floor((now - ts) / dayMs);
|
|
64
|
+
if (days === 0)
|
|
65
|
+
return 'today';
|
|
66
|
+
if (days === 1)
|
|
67
|
+
return 'yesterday';
|
|
68
|
+
if (days < 7)
|
|
69
|
+
return `${days}d ago`;
|
|
70
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format `/insights` output as Markdown. Returns a friendly empty-state
|
|
74
|
+
* message when there's no history in the window — we don't error.
|
|
75
|
+
*/
|
|
76
|
+
export function formatInsights(opts = {}) {
|
|
77
|
+
const days = Math.max(1, Math.min(365, opts.days ?? 7));
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const sinceMs = now - days * 86_400_000;
|
|
80
|
+
const runs = loadHistoryRuns(sinceMs);
|
|
81
|
+
const lines = [`## Activity — last ${days} day${days === 1 ? '' : 's'}`, ''];
|
|
82
|
+
if (runs.length === 0) {
|
|
83
|
+
lines.push(`_No agent runs in the last ${days} day${days === 1 ? '' : 's'}._`);
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push('Run an agent task with `/agent <task>` (or just type a request when agent mode is on) — the activity here populates from `~/.codeep/history/`.');
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
// ── Headline metrics ──────────────────────────────────────────────────────
|
|
89
|
+
const totalRuns = runs.length;
|
|
90
|
+
const totalActions = runs.reduce((s, r) => s + (r.actions?.length ?? 0), 0);
|
|
91
|
+
const totalActiveMs = runs.reduce((s, r) => s + Math.max(0, (r.endTime ?? r.startTime) - r.startTime), 0);
|
|
92
|
+
const avgActions = (totalActions / totalRuns).toFixed(1);
|
|
93
|
+
const distinctDays = new Set(runs.map((r) => new Date(r.startTime).toISOString().slice(0, 10))).size;
|
|
94
|
+
lines.push(`**${totalRuns}** run${totalRuns === 1 ? '' : 's'}`
|
|
95
|
+
+ ` · **${totalActions}** tool action${totalActions === 1 ? '' : 's'}`
|
|
96
|
+
+ ` · **${fmtDuration(totalActiveMs)}** active`
|
|
97
|
+
+ ` · **${distinctDays}**/${days} active day${distinctDays === 1 ? '' : 's'}`
|
|
98
|
+
+ ` · avg **${avgActions}** action${avgActions === '1.0' ? '' : 's'}/run`);
|
|
99
|
+
// ── By project ────────────────────────────────────────────────────────────
|
|
100
|
+
const byProject = new Map();
|
|
101
|
+
for (const r of runs) {
|
|
102
|
+
const proj = r.projectRoot ? basename(r.projectRoot) : '(no project)';
|
|
103
|
+
const cur = byProject.get(proj) ?? { runs: 0, actions: 0, activeMs: 0 };
|
|
104
|
+
cur.runs++;
|
|
105
|
+
cur.actions += r.actions?.length ?? 0;
|
|
106
|
+
cur.activeMs += Math.max(0, (r.endTime ?? r.startTime) - r.startTime);
|
|
107
|
+
byProject.set(proj, cur);
|
|
108
|
+
}
|
|
109
|
+
const projects = [...byProject.entries()].sort((a, b) => b[1].activeMs - a[1].activeMs);
|
|
110
|
+
if (projects.length > 0) {
|
|
111
|
+
lines.push('', '### By project', '');
|
|
112
|
+
lines.push('| Project | Runs | Actions | Active time |');
|
|
113
|
+
lines.push('|---|---:|---:|---:|');
|
|
114
|
+
for (const [name, s] of projects.slice(0, 8)) {
|
|
115
|
+
lines.push(`| \`${name}\` | ${s.runs} | ${s.actions} | ${fmtDuration(s.activeMs)} |`);
|
|
116
|
+
}
|
|
117
|
+
if (projects.length > 8)
|
|
118
|
+
lines.push(`| _… and ${projects.length - 8} more_ | | | |`);
|
|
119
|
+
}
|
|
120
|
+
// ── Top tool types ────────────────────────────────────────────────────────
|
|
121
|
+
const byType = new Map();
|
|
122
|
+
for (const r of runs) {
|
|
123
|
+
for (const a of r.actions ?? []) {
|
|
124
|
+
byType.set(a.type, (byType.get(a.type) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const tools = [...byType.entries()].sort((a, b) => b[1] - a[1]);
|
|
128
|
+
if (tools.length > 0) {
|
|
129
|
+
lines.push('', '### Top tools', '');
|
|
130
|
+
for (const [name, count] of tools.slice(0, 8)) {
|
|
131
|
+
lines.push(`- \`${name}\` × **${count}**`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── Top files touched ─────────────────────────────────────────────────────
|
|
135
|
+
const byPath = new Map();
|
|
136
|
+
for (const r of runs) {
|
|
137
|
+
for (const a of r.actions ?? []) {
|
|
138
|
+
if (a.path)
|
|
139
|
+
byPath.set(a.path, (byPath.get(a.path) ?? 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const files = [...byPath.entries()].sort((a, b) => b[1] - a[1]);
|
|
143
|
+
if (files.length > 0) {
|
|
144
|
+
lines.push('', '### Most-touched files', '');
|
|
145
|
+
for (const [path, count] of files.slice(0, 8)) {
|
|
146
|
+
// Trim home prefix for readability
|
|
147
|
+
const display = path.replace(homedir(), '~');
|
|
148
|
+
lines.push(`- \`${display}\` × **${count}**`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ── Recent runs ───────────────────────────────────────────────────────────
|
|
152
|
+
lines.push('', '### Recent runs', '');
|
|
153
|
+
for (const r of runs.slice(0, 10)) {
|
|
154
|
+
const when = relativeDayBucket(r.startTime, now);
|
|
155
|
+
const dur = fmtDuration(Math.max(0, (r.endTime ?? r.startTime) - r.startTime));
|
|
156
|
+
const proj = r.projectRoot ? basename(r.projectRoot) : '—';
|
|
157
|
+
const prompt = (r.prompt || '').replace(/\s+/g, ' ').trim();
|
|
158
|
+
const promptShort = prompt.length > 80 ? prompt.slice(0, 77) + '…' : prompt;
|
|
159
|
+
lines.push(`- _${when}_ · **${proj}** · ${dur} · ${promptShort}`);
|
|
160
|
+
}
|
|
161
|
+
// ── Session cost callout ──────────────────────────────────────────────────
|
|
162
|
+
// We only track tokens in-memory per process, so historical cost isn't
|
|
163
|
+
// available. Tell the user where to look for the current session.
|
|
164
|
+
lines.push('', "_For this session's cost + cache savings, run `/cost`._");
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personalities — pluggable system prompt addenda that shape how the
|
|
3
|
+
* agent communicates and what it prioritises.
|
|
4
|
+
*
|
|
5
|
+
* Storage:
|
|
6
|
+
* - **Built-in**: hardcoded below (concise, verbose, security,
|
|
7
|
+
* senior-reviewer, junior-mentor, ship-it).
|
|
8
|
+
* - **Project**: `<workspace>/.codeep/personalities/<name>.md`
|
|
9
|
+
* - **Global**: `~/.codeep/personalities/<name>.md`
|
|
10
|
+
*
|
|
11
|
+
* Project shadows global shadows built-in, by name.
|
|
12
|
+
*
|
|
13
|
+
* File format (project / global):
|
|
14
|
+
* ```
|
|
15
|
+
* # Personality: Concise Reviewer
|
|
16
|
+
* <free-form Markdown body — gets appended to system prompt verbatim>
|
|
17
|
+
* ```
|
|
18
|
+
* (The first H1 line is parsed as the display name; everything else is
|
|
19
|
+
* the prompt body.)
|
|
20
|
+
*
|
|
21
|
+
* Activation:
|
|
22
|
+
* - `config.activePersonality` holds the active name (or null/undefined
|
|
23
|
+
* for default behaviour).
|
|
24
|
+
* - `getActivePersonalityPrompt(workspaceRoot)` returns the prompt
|
|
25
|
+
* addendum to inject into the agent's system prompt, or '' when no
|
|
26
|
+
* personality is active.
|
|
27
|
+
* - Persists across sessions until cleared with `/personality off`.
|
|
28
|
+
*/
|
|
29
|
+
export type PersonalityScope = 'builtin' | 'project' | 'global';
|
|
30
|
+
export interface Personality {
|
|
31
|
+
/** Slug (filename without .md, or built-in id). Lowercase, hyphens. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Human display label shown in `/personality` list. */
|
|
34
|
+
displayName: string;
|
|
35
|
+
/** One-line description for the list view. */
|
|
36
|
+
description: string;
|
|
37
|
+
/** Markdown body appended to the system prompt when active. */
|
|
38
|
+
prompt: string;
|
|
39
|
+
scope: PersonalityScope;
|
|
40
|
+
}
|
|
41
|
+
export declare function loadAllPersonalities(workspaceRoot?: string): Personality[];
|
|
42
|
+
export declare function findPersonality(name: string, workspaceRoot?: string): Personality | null;
|
|
43
|
+
/**
|
|
44
|
+
* Returns the prompt addendum for the currently active personality, or
|
|
45
|
+
* '' when none is set. Called from agent.ts after the base system prompt
|
|
46
|
+
* is composed — appended last so personality overrides apply even if
|
|
47
|
+
* project rules conflict.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getActivePersonalityPrompt(workspaceRoot?: string): string;
|
|
50
|
+
export declare function formatPersonalityList(workspaceRoot?: string): string;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personalities — pluggable system prompt addenda that shape how the
|
|
3
|
+
* agent communicates and what it prioritises.
|
|
4
|
+
*
|
|
5
|
+
* Storage:
|
|
6
|
+
* - **Built-in**: hardcoded below (concise, verbose, security,
|
|
7
|
+
* senior-reviewer, junior-mentor, ship-it).
|
|
8
|
+
* - **Project**: `<workspace>/.codeep/personalities/<name>.md`
|
|
9
|
+
* - **Global**: `~/.codeep/personalities/<name>.md`
|
|
10
|
+
*
|
|
11
|
+
* Project shadows global shadows built-in, by name.
|
|
12
|
+
*
|
|
13
|
+
* File format (project / global):
|
|
14
|
+
* ```
|
|
15
|
+
* # Personality: Concise Reviewer
|
|
16
|
+
* <free-form Markdown body — gets appended to system prompt verbatim>
|
|
17
|
+
* ```
|
|
18
|
+
* (The first H1 line is parsed as the display name; everything else is
|
|
19
|
+
* the prompt body.)
|
|
20
|
+
*
|
|
21
|
+
* Activation:
|
|
22
|
+
* - `config.activePersonality` holds the active name (or null/undefined
|
|
23
|
+
* for default behaviour).
|
|
24
|
+
* - `getActivePersonalityPrompt(workspaceRoot)` returns the prompt
|
|
25
|
+
* addendum to inject into the agent's system prompt, or '' when no
|
|
26
|
+
* personality is active.
|
|
27
|
+
* - Persists across sessions until cleared with `/personality off`.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
30
|
+
import { join } from 'path';
|
|
31
|
+
import { homedir } from 'os';
|
|
32
|
+
import { config } from '../config/index.js';
|
|
33
|
+
const BUILTIN = [
|
|
34
|
+
{
|
|
35
|
+
name: 'concise',
|
|
36
|
+
displayName: 'Concise',
|
|
37
|
+
description: 'Short answers. No preamble. No filler. Get in, get out.',
|
|
38
|
+
scope: 'builtin',
|
|
39
|
+
prompt: `
|
|
40
|
+
|
|
41
|
+
## Personality: Concise
|
|
42
|
+
|
|
43
|
+
Keep responses tight:
|
|
44
|
+
- Skip preamble ("Great question!", "Let me help…") — go straight to substance.
|
|
45
|
+
- Use bullet points over paragraphs for lists of 3+ items.
|
|
46
|
+
- One code block per answer when possible; no commentary around obvious code.
|
|
47
|
+
- Prefer "Done." over "I've successfully completed the task by…"
|
|
48
|
+
- No emojis unless the user explicitly uses them first.`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'verbose',
|
|
52
|
+
displayName: 'Verbose',
|
|
53
|
+
description: 'Detailed explanations with rationale, alternatives considered, and caveats.',
|
|
54
|
+
scope: 'builtin',
|
|
55
|
+
prompt: `
|
|
56
|
+
|
|
57
|
+
## Personality: Verbose
|
|
58
|
+
|
|
59
|
+
Take time to explain:
|
|
60
|
+
- For every non-trivial change, lay out: what / why / alternatives I considered / why I chose this one.
|
|
61
|
+
- Cite line numbers and file paths so the user can audit.
|
|
62
|
+
- When reading code, summarise what the surrounding context does before acting — this catches misunderstandings early.
|
|
63
|
+
- End complex tasks with a "what to verify" checklist for the user.`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'security',
|
|
67
|
+
displayName: 'Security-paranoid',
|
|
68
|
+
description: 'Flags every input as untrusted, second-guesses every API call, prefers defensive code.',
|
|
69
|
+
scope: 'builtin',
|
|
70
|
+
prompt: `
|
|
71
|
+
|
|
72
|
+
## Personality: Security-paranoid
|
|
73
|
+
|
|
74
|
+
Treat every input as hostile until proven otherwise:
|
|
75
|
+
- For any code that touches user input, env vars, file paths, or network: enumerate the attack surface in a short comment block above the code.
|
|
76
|
+
- Prefer allowlists over blocklists. Prefer parameterised queries / escape-on-output to ad-hoc sanitisation.
|
|
77
|
+
- Flag every secret/key reference and ensure it's read from env or secret manager — never inline.
|
|
78
|
+
- When suggesting dependencies, prefer audited ones (cite stars / last-publish date) and note known CVEs if any.
|
|
79
|
+
- After implementing, list 2-3 concrete attack scenarios you considered (e.g. "what if input contains '../'?") and how the code handles them.`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'senior-reviewer',
|
|
83
|
+
displayName: 'Senior reviewer',
|
|
84
|
+
description: 'Strong opinions on architecture, naming, abstraction boundaries. Pushes back on shortcuts.',
|
|
85
|
+
scope: 'builtin',
|
|
86
|
+
prompt: `
|
|
87
|
+
|
|
88
|
+
## Personality: Senior Reviewer
|
|
89
|
+
|
|
90
|
+
Critique like a staff engineer reviewing a PR from a colleague:
|
|
91
|
+
- If the proposed approach has a cleaner alternative, propose it first — even if the user's framing pushed toward the messier one.
|
|
92
|
+
- Name things with the team in mind. Reject lazy names (handler, util, manager) and propose specific ones.
|
|
93
|
+
- Watch for premature abstraction (one-call helpers) and missing abstractions (3rd copy of the same 5 lines).
|
|
94
|
+
- Push back on "just for now" hacks unless the user explicitly says it's a throwaway.
|
|
95
|
+
- Mention what's NOT tested when adding new code, and suggest the test cases that'd catch likely regressions.`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'junior-mentor',
|
|
99
|
+
displayName: 'Junior mentor',
|
|
100
|
+
description: 'Explains concepts as you go, links to docs, suggests what to learn next.',
|
|
101
|
+
scope: 'builtin',
|
|
102
|
+
prompt: `
|
|
103
|
+
|
|
104
|
+
## Personality: Junior Mentor
|
|
105
|
+
|
|
106
|
+
The user is learning — meet them where they are:
|
|
107
|
+
- Before introducing a new concept, give a 1-2 sentence "why this exists" context.
|
|
108
|
+
- Use analogies for abstract topics (closures = "a backpack the function carries"). Keep them grounded, not fancy.
|
|
109
|
+
- Link to canonical docs (MDN, language reference, official tutorial) rather than blog posts.
|
|
110
|
+
- After completing a task, suggest 1 thing to read or 1 small follow-up exercise that reinforces the concept just used.
|
|
111
|
+
- Resist showing off. Don't introduce ES2024 destructuring spread tricks when a plain for-loop teaches the lesson better.`,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'ship-it',
|
|
115
|
+
displayName: 'Ship it',
|
|
116
|
+
description: 'Optimise for speed-to-merge. No bikeshedding. "Done is better than perfect" mode.',
|
|
117
|
+
scope: 'builtin',
|
|
118
|
+
prompt: `
|
|
119
|
+
|
|
120
|
+
## Personality: Ship It
|
|
121
|
+
|
|
122
|
+
The user wants this merged today:
|
|
123
|
+
- Pick the first reasonable approach. Don't enumerate three alternatives — commit to one.
|
|
124
|
+
- Inline TODO comments are fine for cleanup-later items. Don't refactor adjacent code.
|
|
125
|
+
- Test the happy path. Edge cases can wait for follow-up unless they're security-relevant.
|
|
126
|
+
- Suggest minimum-viable solution, not robust-for-all-cases. The user can iterate.
|
|
127
|
+
- If the user asks "should we also…", default to "no, ship this first, that's a separate PR".`,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
/** Load custom personalities from a `.codeep/personalities/` directory. */
|
|
131
|
+
function loadFromDir(dir, scope) {
|
|
132
|
+
if (!existsSync(dir))
|
|
133
|
+
return [];
|
|
134
|
+
const out = [];
|
|
135
|
+
let entries;
|
|
136
|
+
try {
|
|
137
|
+
entries = readdirSync(dir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.endsWith('.md'))
|
|
144
|
+
continue;
|
|
145
|
+
const name = entry.slice(0, -3).toLowerCase();
|
|
146
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name))
|
|
147
|
+
continue; // skip weirdly-named files
|
|
148
|
+
try {
|
|
149
|
+
const raw = readFileSync(join(dir, entry), 'utf8');
|
|
150
|
+
if (raw.length > 64 * 1024)
|
|
151
|
+
continue; // cap at 64 KB
|
|
152
|
+
// First H1 → displayName; rest → prompt.
|
|
153
|
+
const h1 = raw.match(/^#\s+(?:Personality:\s+)?(.+)$/m);
|
|
154
|
+
const displayName = h1?.[1].trim() ?? name;
|
|
155
|
+
const body = h1 ? raw.slice(raw.indexOf('\n', raw.indexOf(h1[0])) + 1).trimStart() : raw;
|
|
156
|
+
// First paragraph (or line) → description (cap 200 chars).
|
|
157
|
+
const firstPara = body.split(/\n\s*\n/)[0]?.replace(/\s+/g, ' ').trim() ?? '';
|
|
158
|
+
const description = firstPara.length > 200 ? firstPara.slice(0, 197) + '…' : firstPara;
|
|
159
|
+
out.push({
|
|
160
|
+
name,
|
|
161
|
+
displayName,
|
|
162
|
+
description: description || `Custom personality from ${entry}`,
|
|
163
|
+
prompt: '\n\n## Personality: ' + displayName + '\n\n' + body,
|
|
164
|
+
scope,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Skip broken files — never crash personality loading.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
export function loadAllPersonalities(workspaceRoot) {
|
|
174
|
+
const project = workspaceRoot
|
|
175
|
+
? loadFromDir(join(workspaceRoot, '.codeep', 'personalities'), 'project')
|
|
176
|
+
: [];
|
|
177
|
+
const global = loadFromDir(join(homedir(), '.codeep', 'personalities'), 'global');
|
|
178
|
+
// Merge with scope priority: project > global > builtin.
|
|
179
|
+
const byName = new Map();
|
|
180
|
+
for (const p of BUILTIN)
|
|
181
|
+
byName.set(p.name, p);
|
|
182
|
+
for (const p of global)
|
|
183
|
+
byName.set(p.name, p);
|
|
184
|
+
for (const p of project)
|
|
185
|
+
byName.set(p.name, p);
|
|
186
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
187
|
+
}
|
|
188
|
+
export function findPersonality(name, workspaceRoot) {
|
|
189
|
+
const lower = name.toLowerCase();
|
|
190
|
+
return loadAllPersonalities(workspaceRoot).find((p) => p.name === lower) ?? null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns the prompt addendum for the currently active personality, or
|
|
194
|
+
* '' when none is set. Called from agent.ts after the base system prompt
|
|
195
|
+
* is composed — appended last so personality overrides apply even if
|
|
196
|
+
* project rules conflict.
|
|
197
|
+
*/
|
|
198
|
+
export function getActivePersonalityPrompt(workspaceRoot) {
|
|
199
|
+
const name = config.get('activePersonality');
|
|
200
|
+
if (!name)
|
|
201
|
+
return '';
|
|
202
|
+
const p = findPersonality(name, workspaceRoot);
|
|
203
|
+
return p?.prompt ?? '';
|
|
204
|
+
}
|
|
205
|
+
export function formatPersonalityList(workspaceRoot) {
|
|
206
|
+
const list = loadAllPersonalities(workspaceRoot);
|
|
207
|
+
const active = config.get('activePersonality');
|
|
208
|
+
const lines = ['## Personalities', ''];
|
|
209
|
+
if (active) {
|
|
210
|
+
lines.push(`**Active:** \`${active}\` — switch with \`/personality <name>\` or clear with \`/personality off\`.`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
lines.push('**Active:** _(none — agent uses default tone)_');
|
|
214
|
+
}
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push('| Name | Scope | Description |');
|
|
217
|
+
lines.push('|---|---|---|');
|
|
218
|
+
for (const p of list) {
|
|
219
|
+
const tag = p.scope === 'builtin' ? 'built-in' : p.scope;
|
|
220
|
+
const marker = active === p.name ? ' ✓' : '';
|
|
221
|
+
lines.push(`| \`${p.name}\`${marker} | ${tag} | ${p.description} |`);
|
|
222
|
+
}
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push('Drop a `<name>.md` file into `.codeep/personalities/` (project) or `~/.codeep/personalities/` (global) to add your own — first `#` line becomes the display name, body becomes the prompt addendum.');
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
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",
|