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 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:
@@ -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,
@@ -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…]' } },
@@ -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 };
@@ -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, `↑↓ scroll • PgUp/PgDn fast scroll • Esc close${scrollInfo}`, fg.gray);
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
  {
@@ -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.2",
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",