koneck 1.0.2 → 2.0.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.
Files changed (52) hide show
  1. package/dist/auto-docs.d.ts +4 -0
  2. package/dist/auto-docs.d.ts.map +1 -0
  3. package/dist/auto-docs.js +57 -0
  4. package/dist/auto-docs.js.map +1 -0
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +676 -102
  7. package/dist/chat.js.map +1 -1
  8. package/dist/code-graph.d.ts +14 -0
  9. package/dist/code-graph.d.ts.map +1 -0
  10. package/dist/code-graph.js +167 -0
  11. package/dist/code-graph.js.map +1 -0
  12. package/dist/critique.d.ts +4 -0
  13. package/dist/critique.d.ts.map +1 -0
  14. package/dist/critique.js +36 -0
  15. package/dist/critique.js.map +1 -0
  16. package/dist/daemon.d.ts +3 -0
  17. package/dist/daemon.d.ts.map +1 -0
  18. package/dist/daemon.js +111 -0
  19. package/dist/daemon.js.map +1 -0
  20. package/dist/engine.d.ts +5 -0
  21. package/dist/engine.d.ts.map +1 -1
  22. package/dist/engine.js +17 -3
  23. package/dist/engine.js.map +1 -1
  24. package/dist/health.d.ts +2 -0
  25. package/dist/health.d.ts.map +1 -0
  26. package/dist/health.js +201 -0
  27. package/dist/health.js.map +1 -0
  28. package/dist/index.js +163 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/issues.d.ts +3 -0
  31. package/dist/issues.d.ts.map +1 -0
  32. package/dist/issues.js +77 -0
  33. package/dist/issues.js.map +1 -0
  34. package/dist/knowledge-graph.d.ts +3 -0
  35. package/dist/knowledge-graph.d.ts.map +1 -0
  36. package/dist/knowledge-graph.js +85 -0
  37. package/dist/knowledge-graph.js.map +1 -0
  38. package/dist/pipeline.d.ts +3 -0
  39. package/dist/pipeline.d.ts.map +1 -0
  40. package/dist/pipeline.js +79 -0
  41. package/dist/pipeline.js.map +1 -0
  42. package/dist/reasoning-cache.d.ts +9 -0
  43. package/dist/reasoning-cache.d.ts.map +1 -0
  44. package/dist/reasoning-cache.js +67 -0
  45. package/dist/reasoning-cache.js.map +1 -0
  46. package/dist/recon.d.ts +2 -0
  47. package/dist/recon.d.ts.map +1 -0
  48. package/dist/recon.js +121 -0
  49. package/dist/recon.js.map +1 -0
  50. package/dist/types.d.ts +12 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +1 -1
package/dist/chat.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import readline from 'readline';
2
2
  import chalk from 'chalk';
3
- import { createAgentSession } from './engine.js';
3
+ import { execa } from 'execa';
4
+ import { createAgentSession, runAgent } from './engine.js';
4
5
  import { tui } from './tui.js';
5
6
  import { saveSession, listSessions, generateSessionId } from './session.js';
6
7
  import { estimateCost, formatCost } from './pricing.js';
7
8
  import { PROVIDERS } from './providers.js';
8
9
  import { loadMemory } from './memory.js';
10
+ import { detectProject } from './project.js';
11
+ import { runDoctor } from './doctor.js';
12
+ import { runGenerateKoneckMd } from './generate-koneck-md.js';
13
+ import { runHealth } from './health.js';
14
+ import { runPipeline } from './pipeline.js';
15
+ import { queryKnowledgeGraph } from './knowledge-graph.js';
16
+ import { buildCodeGraph, queryGraph, getCallGraph } from './code-graph.js';
17
+ import { runAutoDocs } from './auto-docs.js';
9
18
  const MODES = ['auto', 'plan', 'code'];
10
19
  const MODE_LABELS = {
11
20
  auto: chalk.cyan('auto'),
@@ -14,55 +23,142 @@ const MODE_LABELS = {
14
23
  };
15
24
  const MODE_PREFIXES = {
16
25
  auto: '',
17
- plan: 'Think step-by-step, create an explicit plan before making any changes. List every file you will touch and why.\n\nTask: ',
18
- code: 'Be concise and direct. Focus only on code changes. No explanations unless something is non-obvious.\n\nTask: ',
26
+ plan: 'Think step-by-step. Create an explicit plan before touching any file. List every file you will change and why.\n\nTask: ',
27
+ code: 'Be concise and direct. Only code changes. Skip explanations unless non-obvious.\n\nTask: ',
19
28
  };
20
- // ─── Slash command registry ───────────────────────────────────────────────────
29
+ const EFFORT_HINTS = {
30
+ low: '[Low effort: be brief, skip explanations, fastest answer]\n',
31
+ high: '[High effort: be thorough, cover edge cases, deep analysis]\n',
32
+ max: '[Maximum effort: most rigorous analysis possible, check everything]\n',
33
+ };
34
+ const PERSONALITY_HINTS = {
35
+ default: '',
36
+ friendly: '[Be conversational, encouraging, and explain your reasoning warmly]\n',
37
+ minimal: '[Be extremely terse. Numbers, filenames, one-liners only. No prose.]\n',
38
+ };
39
+ // ─── Command registry ─────────────────────────────────────────────────────────
21
40
  const COMMANDS = [
22
- { cmd: '/help', desc: 'Show this message' },
23
- { cmd: '/status', desc: 'Show provider, model, mode, tokens, cost' },
24
- { cmd: '/model', args: '[name]', desc: 'Show or switch model (resets session)' },
25
- { cmd: '/provider', args: '[name]', desc: 'Show or switch provider (resets session)' },
41
+ { cmd: '/help', desc: 'Show this help' },
42
+ { cmd: '/status', desc: 'Provider, model, mode, tokens, cost, CWD' },
43
+ { cmd: '/model', args: '[name]', desc: 'Show or switch model' },
44
+ { cmd: '/provider', args: '[name]', desc: 'Show or switch provider' },
26
45
  { cmd: '/mode', args: '[auto|plan|code]', desc: 'Show or set mode' },
27
- { cmd: '/cost', desc: 'Show cost of current session' },
28
- { cmd: '/tokens', desc: 'Show token usage of current session' },
29
- { cmd: '/compact', desc: 'Summarise conversation to save context' },
30
- { cmd: '/memory', desc: 'Show project memory (.koneck/MEMORY.md)' },
46
+ { cmd: '/effort', args: '[low|medium|high|max]', desc: 'Set reasoning effort level' },
47
+ { cmd: '/personality', args: '[default|friendly|minimal]', desc: 'Toggle response tone' },
48
+ { cmd: '/permissions', args: '[on|off]', desc: 'Show or toggle require-approval' },
49
+ { cmd: '/btw', args: '<question>', desc: 'Side question not added to history' },
50
+ { cmd: '/goal', args: '[text]', desc: 'Set a persistent goal prepended to every message' },
51
+ { cmd: '/add-dir', args: '<path>', desc: 'Add a directory to agent context' },
52
+ { cmd: '/diff', desc: 'Show uncommitted git changes' },
53
+ { cmd: '/verify', desc: 'Run project test command' },
54
+ { cmd: '/code-review', args: '[--fix]', desc: 'Review current git diff for bugs' },
55
+ { cmd: '/security-review', desc: 'Security audit of current git diff' },
56
+ { cmd: '/mcp', desc: 'Show MCP server connections' },
57
+ { cmd: '/init', desc: 'Generate KONECK.md for this project' },
58
+ { cmd: '/doctor', desc: 'Run setup and config health check' },
59
+ { cmd: '/debug', desc: 'Show runtime diagnostics' },
60
+ { cmd: '/rewind', desc: 'Undo last exchange (remove last turn from history)' },
61
+ { cmd: '/fork', desc: 'Fork conversation into a fresh session with same history' },
62
+ { cmd: '/branch', desc: 'Alias for /fork' },
63
+ { cmd: '/subtask', args: '<task>', desc: 'Delegate task to an isolated subagent' },
64
+ { cmd: '/ps', desc: 'List active background tasks' },
65
+ { cmd: '/copy', desc: 'Copy last response to clipboard' },
66
+ { cmd: '/rename', args: '<name>', desc: 'Rename current session' },
67
+ { cmd: '/compact', args: '[focus]', desc: 'Summarise conversation to save context' },
68
+ { cmd: '/memory', desc: 'Show .koneck/MEMORY.md' },
31
69
  { cmd: '/cwd', args: '[path]', desc: 'Show or change working directory' },
32
- { cmd: '/clear', desc: 'Clear conversation, start fresh' },
70
+ { cmd: '/cost', desc: 'Show session cost' },
71
+ { cmd: '/tokens', desc: 'Show token usage breakdown' },
72
+ { cmd: '/clear', desc: 'Clear conversation history' },
33
73
  { cmd: '/reset', desc: 'Alias for /clear' },
34
- { cmd: '/save', desc: 'Save session to disk now' },
74
+ { cmd: '/new', desc: 'Alias for /clear' },
75
+ { cmd: '/save', desc: 'Save session to disk' },
35
76
  { cmd: '/sessions', desc: 'List recent saved sessions' },
36
- { cmd: '/exit', desc: 'Exit and auto-save' },
77
+ { cmd: '/feedback', desc: 'Submit feedback / report a bug' },
78
+ { cmd: '/architect', desc: 'Analyze current architecture, show problems and suggestions' },
79
+ { cmd: '/performance', desc: 'Find N+1 queries, memory leaks, blocking I/O, slow paths' },
80
+ { cmd: '/security', desc: 'Full OWASP security audit of the codebase' },
81
+ { cmd: '/pipeline', args: '<task>', desc: 'Run multi-agent pipeline (planner→coder→reviewer→tester)' },
82
+ { cmd: '/knowledge', args: '[concept]', desc: 'Query knowledge graph — where is X implemented?' },
83
+ { cmd: '/health', desc: 'Repository health scan (dead code, complexity, unused deps)' },
84
+ { cmd: '/graph', args: '[symbol]', desc: 'Build or query semantic code graph' },
85
+ { cmd: '/docs', desc: 'Regenerate docs for recent changes' },
86
+ { cmd: '/exit', desc: 'Save and exit' },
37
87
  { cmd: '/quit', desc: 'Alias for /exit' },
38
88
  ];
39
89
  const COMMAND_NAMES = COMMANDS.map(c => c.cmd);
40
- function buildHelpText() {
90
+ function buildHelp() {
41
91
  const lines = ['\n ' + chalk.bold('Slash commands') + '\n'];
42
- const w = Math.max(...COMMANDS.map(c => c.cmd.length + (c.args ? c.args.length + 1 : 0)));
92
+ const w = Math.max(...COMMANDS.map(c => c.cmd.length + (c.args?.length ?? -1) + 1));
43
93
  for (const { cmd, args, desc } of COMMANDS) {
44
94
  const left = (cmd + (args ? ' ' + args : '')).padEnd(w + 2);
45
95
  lines.push(' ' + chalk.cyan(left) + chalk.dim(desc));
46
96
  }
47
97
  lines.push('');
48
- lines.push(chalk.dim(' Shift+Tab Cycle mode (auto → plan → code → auto)'));
49
- lines.push(chalk.dim(' Ctrl+C Cancel running task / exit if idle'));
98
+ lines.push(chalk.dim(' Shift+Tab Cycle mode: auto → plan → code → auto'));
99
+ lines.push(chalk.dim(' Esc Esc Edit previous message'));
100
+ lines.push(chalk.dim(' Ctrl+C Cancel running task / exit if idle'));
101
+ lines.push(chalk.dim(' Ctrl+L Clear terminal screen'));
50
102
  lines.push('');
51
103
  return lines.join('\n');
52
104
  }
53
105
  // ─── Prompt renderer ─────────────────────────────────────────────────────────
54
- function renderPrompt(mode) {
55
- return chalk.cyan('koneck') + chalk.dim('[') + MODE_LABELS[mode] + chalk.dim(']') + chalk.cyan('> ');
106
+ function renderPrompt(mode, effort, personality, goal) {
107
+ const extras = [];
108
+ if (effort !== 'medium')
109
+ extras.push(chalk.dim(effort));
110
+ if (personality !== 'default')
111
+ extras.push(chalk.dim(personality));
112
+ if (goal)
113
+ extras.push(chalk.dim('goal'));
114
+ const suffix = extras.length ? chalk.dim(' ' + extras.join('·')) : '';
115
+ return chalk.cyan('koneck') + chalk.dim('[') + MODE_LABELS[mode] + chalk.dim(']') + suffix + chalk.cyan('> ');
116
+ }
117
+ // ─── Clipboard helper ─────────────────────────────────────────────────────────
118
+ async function copyToClipboard(text) {
119
+ const cmds = [
120
+ ['wl-copy'],
121
+ ['xclip', '-selection', 'clipboard'],
122
+ ['xsel', '--clipboard', '--input'],
123
+ ['pbcopy'],
124
+ ];
125
+ for (const [bin, ...args] of cmds) {
126
+ try {
127
+ await execa(bin, args, { input: text });
128
+ return true;
129
+ }
130
+ catch { /* try next */ }
131
+ }
132
+ return false;
133
+ }
134
+ // ─── MCP servers reader ───────────────────────────────────────────────────────
135
+ async function readMcpConfig(cwd) {
136
+ const { default: fs } = await import('fs/promises');
137
+ const { default: path } = await import('path');
138
+ try {
139
+ const raw = await fs.readFile(path.join(cwd, '.koneck', 'mcp.json'), 'utf-8');
140
+ return JSON.parse(raw);
141
+ }
142
+ catch {
143
+ return null;
144
+ }
56
145
  }
57
146
  // ─── Main chat function ───────────────────────────────────────────────────────
58
147
  export async function runChatMode(config) {
59
148
  tui.printBanner();
60
149
  const providerStr = chalk.dim(`${config.provider} / ${config.model}`);
61
150
  console.log(chalk.cyan(' KONECK chat ') + providerStr);
62
- console.log(chalk.dim(' Shift+Tab: cycle mode │ /help: commands │ Ctrl+C: cancel/exit\n'));
151
+ console.log(chalk.dim(' Shift+Tab: cycle mode │ /help: all commands │ Ctrl+C: cancel/exit\n'));
63
152
  const sessionId = generateSessionId();
64
153
  let session = await createAgentSession(config);
65
154
  let mode = 'auto';
155
+ let effort = 'medium';
156
+ let personality = 'default';
157
+ let goal = null;
158
+ let sessionName = null;
159
+ let addedDirs = [];
160
+ let lastAssistantMsg = '';
161
+ let prevInput = '';
66
162
  let busy = false;
67
163
  let currentAbort = null;
68
164
  let cfg = { ...config };
@@ -79,30 +175,49 @@ export async function runChatMode(config) {
79
175
  return [[], line];
80
176
  },
81
177
  });
82
- // ── Shift+Tab via raw keypress ─────────────────────────────────────────────
178
+ // ── Shift+Tab + Esc Esc + Ctrl+L ──────────────────────────────────────────
83
179
  if (process.stdin.isTTY) {
84
180
  readline.emitKeypressEvents(process.stdin, rl);
181
+ let lastEsc = 0;
85
182
  process.stdin.on('keypress', (_char, key) => {
86
183
  if (!key)
87
184
  return;
185
+ // Shift+Tab
88
186
  if (key.sequence === '\x1b[Z' || (key.shift && key.name === 'tab')) {
89
187
  const idx = MODES.indexOf(mode);
90
188
  mode = MODES[(idx + 1) % MODES.length];
91
- process.stdout.write('\r\x1b[K' + renderPrompt(mode));
189
+ process.stdout.write('\r\x1b[K' + renderPrompt(mode, effort, personality, !!goal));
190
+ return;
191
+ }
192
+ // Ctrl+L — clear screen
193
+ if (key.name === 'l' && key.ctrl) {
194
+ process.stdout.write('\x1b[2J\x1b[H');
195
+ process.stdout.write(renderPrompt(mode, effort, personality, !!goal));
196
+ return;
197
+ }
198
+ // Esc Esc — recall previous input
199
+ if (key.name === 'escape') {
200
+ const now = Date.now();
201
+ if (now - lastEsc < 400 && prevInput) {
202
+ // Clear line and inject previous input
203
+ process.stdout.write('\r\x1b[K' + renderPrompt(mode, effort, personality, !!goal));
204
+ process.stdout.write(prevInput);
205
+ rl.write(prevInput);
206
+ }
207
+ lastEsc = now;
92
208
  }
93
209
  });
94
210
  }
95
211
  function writePrompt() {
96
- process.stdout.write(renderPrompt(mode));
212
+ process.stdout.write(renderPrompt(mode, effort, personality, !!goal));
97
213
  }
98
214
  async function doSave() {
99
215
  const firstUser = session.messages.find(m => m.role === 'user');
100
- const task = firstUser && typeof firstUser.content === 'string'
101
- ? firstUser.content.slice(0, 120)
102
- : '(chat session)';
216
+ const defaultTask = firstUser && typeof firstUser.content === 'string'
217
+ ? firstUser.content.slice(0, 120) : '(chat session)';
103
218
  return saveSession(cfg.cwd, sessionId, {
104
219
  id: sessionId,
105
- task,
220
+ task: sessionName ?? defaultTask,
106
221
  provider: cfg.provider,
107
222
  model: cfg.model,
108
223
  cwd: cfg.cwd,
@@ -115,45 +230,52 @@ export async function runChatMode(config) {
115
230
  const input = line.trim();
116
231
  if (!input)
117
232
  return;
118
- // ── Slash commands ─────────────────────────────────────────────────────────
233
+ if (!input.startsWith('/'))
234
+ prevInput = input;
119
235
  if (input.startsWith('/')) {
120
- const [cmd, ...rest] = input.split(/\s+/);
121
- const arg = rest.join(' ').trim();
236
+ const spaceIdx = input.indexOf(' ');
237
+ const cmd = spaceIdx === -1 ? input : input.slice(0, spaceIdx);
238
+ const arg = spaceIdx === -1 ? '' : input.slice(spaceIdx + 1).trim();
122
239
  switch (cmd) {
123
- // ── Exit ──────────────────────────────────────────────────────────────
240
+ // ── Help ────────────────────────────────────────────────────────────
241
+ case '/help':
242
+ console.log(buildHelp());
243
+ return;
244
+ // ── Exit ────────────────────────────────────────────────────────────
124
245
  case '/exit':
125
246
  case '/quit': {
126
247
  const file = await doSave();
127
- console.log(chalk.dim(`\n Session saved: ${file}`));
128
- console.log(chalk.dim(' Goodbye.\n'));
248
+ console.log(chalk.dim(`\n Session saved: ${file}\n Goodbye.\n`));
129
249
  rl.close();
130
250
  process.exit(0);
131
251
  }
132
- // ── Help ──────────────────────────────────────────────────────────────
133
- case '/help':
134
- console.log(buildHelpText());
135
- return;
136
- // ── Status ────────────────────────────────────────────────────────────
252
+ // ── Status ──────────────────────────────────────────────────────────
137
253
  case '/status': {
138
254
  const cost = estimateCost(cfg.model, session.stats.promptTokens, session.stats.completionTokens);
139
- const costStr = cost.known ? formatCost(cost.total) : 'unknown';
140
255
  console.log('');
141
- console.log(` ${chalk.dim('Provider :')} ${chalk.white(cfg.provider)}`);
142
- console.log(` ${chalk.dim('Model :')} ${chalk.white(cfg.model)}`);
256
+ console.log(` ${chalk.dim('Provider :')} ${chalk.white(cfg.provider)}`);
257
+ console.log(` ${chalk.dim('Model :')} ${chalk.white(cfg.model)}`);
143
258
  if (cfg.baseURL)
144
- console.log(` ${chalk.dim('Base URL :')} ${chalk.dim(cfg.baseURL)}`);
145
- console.log(` ${chalk.dim('Mode :')} ${MODE_LABELS[mode]}`);
146
- console.log(` ${chalk.dim('CWD :')} ${chalk.dim(cfg.cwd)}`);
147
- console.log(` ${chalk.dim('Turns :')} ${chalk.white(session.stats.turns)}`);
148
- console.log(` ${chalk.dim('Tokens :')} ${chalk.white(session.stats.totalTokens.toLocaleString())}`);
149
- console.log(` ${chalk.dim('Cost :')} ${chalk.white(costStr)}`);
259
+ console.log(` ${chalk.dim('Base URL :')} ${chalk.dim(cfg.baseURL)}`);
260
+ console.log(` ${chalk.dim('Mode :')} ${MODE_LABELS[mode]}`);
261
+ console.log(` ${chalk.dim('Effort :')} ${chalk.white(effort)}`);
262
+ console.log(` ${chalk.dim('Personality :')} ${chalk.white(personality)}`);
263
+ console.log(` ${chalk.dim('Approval :')} ${chalk.white(cfg.requireApproval ? 'on' : 'off')}`);
264
+ console.log(` ${chalk.dim('CWD :')} ${chalk.dim(cfg.cwd)}`);
265
+ if (addedDirs.length)
266
+ console.log(` ${chalk.dim('Extra dirs :')} ${chalk.dim(addedDirs.join(', '))}`);
267
+ if (goal)
268
+ console.log(` ${chalk.dim('Goal :')} ${chalk.white(goal)}`);
269
+ console.log(` ${chalk.dim('Turns :')} ${chalk.white(session.stats.turns)}`);
270
+ console.log(` ${chalk.dim('Tokens :')} ${chalk.white(session.stats.totalTokens.toLocaleString())}`);
271
+ console.log(` ${chalk.dim('Cost :')} ${chalk.white(cost.known ? formatCost(cost.total) : 'unknown')}`);
150
272
  console.log('');
151
273
  return;
152
274
  }
153
- // ── Model ─────────────────────────────────────────────────────────────
275
+ // ── Model ───────────────────────────────────────────────────────────
154
276
  case '/model': {
155
277
  if (!arg) {
156
- console.log(`\n Current model: ${chalk.cyan(cfg.model)}\n`);
278
+ console.log(`\n Current model: ${chalk.cyan(cfg.model)}`);
157
279
  console.log(chalk.dim(' Usage: /model <name> e.g. /model claude-opus-4-5\n'));
158
280
  return;
159
281
  }
@@ -162,12 +284,12 @@ export async function runChatMode(config) {
162
284
  console.log(chalk.green(`\n ✔ Model → ${chalk.white(arg)} (session reset)\n`));
163
285
  return;
164
286
  }
165
- // ── Provider ──────────────────────────────────────────────────────────
287
+ // ── Provider ────────────────────────────────────────────────────────
166
288
  case '/provider': {
167
289
  if (!arg) {
168
290
  const known = Object.keys(PROVIDERS).join(', ');
169
291
  console.log(`\n Current provider: ${chalk.cyan(cfg.provider)}`);
170
- console.log(chalk.dim(` Available: ${known}\n`));
292
+ console.log(chalk.dim(` Available: ${known}`));
171
293
  console.log(chalk.dim(' Usage: /provider <name> e.g. /provider anthropic\n'));
172
294
  return;
173
295
  }
@@ -178,7 +300,7 @@ export async function runChatMode(config) {
178
300
  console.log(chalk.green(`\n ✔ Provider → ${chalk.white(arg)} model → ${chalk.white(newModel)} (session reset)\n`));
179
301
  return;
180
302
  }
181
- // ── Mode ──────────────────────────────────────────────────────────────
303
+ // ── Mode ────────────────────────────────────────────────────────────
182
304
  case '/mode':
183
305
  case '/auto':
184
306
  case '/plan':
@@ -197,71 +319,358 @@ export async function runChatMode(config) {
197
319
  console.log(`\n Mode → ${MODE_LABELS[mode]}\n`);
198
320
  return;
199
321
  }
200
- // ── Cost ──────────────────────────────────────────────────────────────
201
- case '/cost': {
202
- const cost = estimateCost(cfg.model, session.stats.promptTokens, session.stats.completionTokens);
203
- if (cost.known) {
204
- console.log(`\n Session cost: ${chalk.white(formatCost(cost.total))} (input ${formatCost(cost.inputCost)} + output ${formatCost(cost.outputCost)})\n`);
322
+ // ── Effort ──────────────────────────────────────────────────────────
323
+ case '/effort': {
324
+ const levels = ['low', 'medium', 'high', 'max'];
325
+ if (!arg) {
326
+ console.log(`\n Current effort: ${chalk.cyan(effort)}`);
327
+ console.log(chalk.dim(' Usage: /effort low|medium|high|max\n'));
328
+ return;
205
329
  }
206
- else {
207
- console.log(chalk.dim(`\n Cost unknown no pricing data for "${cfg.model}"\n`));
330
+ if (!levels.includes(arg)) {
331
+ console.log(chalk.dim(`\n Unknown level "${arg}". Use: low, medium, high, max\n`));
332
+ return;
208
333
  }
334
+ effort = arg;
335
+ console.log(chalk.green(`\n ✔ Effort → ${effort}\n`));
209
336
  return;
210
337
  }
211
- // ── Tokens ────────────────────────────────────────────────────────────
212
- case '/tokens': {
213
- const { promptTokens, completionTokens, totalTokens, turns } = session.stats;
338
+ // ── Personality ─────────────────────────────────────────────────────
339
+ case '/personality': {
340
+ const opts = ['default', 'friendly', 'minimal'];
341
+ if (!arg) {
342
+ console.log(`\n Current personality: ${chalk.cyan(personality)}`);
343
+ console.log(chalk.dim(' Usage: /personality default|friendly|minimal\n'));
344
+ return;
345
+ }
346
+ if (!opts.includes(arg)) {
347
+ console.log(chalk.dim(`\n Unknown: "${arg}". Use: default, friendly, minimal\n`));
348
+ return;
349
+ }
350
+ personality = arg;
351
+ console.log(chalk.green(`\n ✔ Personality → ${personality}\n`));
352
+ return;
353
+ }
354
+ // ── Permissions ─────────────────────────────────────────────────────
355
+ case '/permissions': {
356
+ if (!arg) {
357
+ const state = cfg.requireApproval ? chalk.yellow('on (manual approval)') : chalk.green('off (auto-approve)');
358
+ console.log(`\n Approval: ${state}`);
359
+ console.log(chalk.dim(' Usage: /permissions on|off\n'));
360
+ return;
361
+ }
362
+ cfg = { ...cfg, requireApproval: arg === 'on' };
363
+ console.log(chalk.green(`\n ✔ Approval → ${arg}\n`));
364
+ return;
365
+ }
366
+ // ── BTW (ephemeral side question) ────────────────────────────────────
367
+ case '/btw': {
368
+ if (!arg) {
369
+ console.log(chalk.dim('\n Usage: /btw <question> (not added to history)\n'));
370
+ return;
371
+ }
372
+ const checkpoint = session.messages.length;
373
+ busy = true;
374
+ rl.pause();
375
+ const sideAbort = new AbortController();
376
+ try {
377
+ await session.send('[Side question — do not reference this in future replies] ' + arg, sideAbort.signal);
378
+ const last = session.messages.filter(m => m.role === 'assistant').at(-1);
379
+ if (last && typeof last.content === 'string')
380
+ lastAssistantMsg = last.content;
381
+ }
382
+ catch { /* ignore */ }
383
+ finally {
384
+ session.messages.splice(checkpoint);
385
+ busy = false;
386
+ rl.resume();
387
+ }
388
+ return;
389
+ }
390
+ // ── Goal ────────────────────────────────────────────────────────────
391
+ case '/goal': {
392
+ if (!arg) {
393
+ if (goal)
394
+ console.log(`\n Current goal: ${chalk.cyan(goal)}\n Use /goal clear to remove.\n`);
395
+ else
396
+ console.log(chalk.dim('\n No goal set. Usage: /goal <description>\n'));
397
+ return;
398
+ }
399
+ if (arg === 'clear') {
400
+ goal = null;
401
+ console.log(chalk.green('\n ✔ Goal cleared.\n'));
402
+ return;
403
+ }
404
+ goal = arg;
405
+ console.log(chalk.green(`\n ✔ Goal set: ${goal}\n`));
406
+ return;
407
+ }
408
+ // ── Add-dir ─────────────────────────────────────────────────────────
409
+ case '/add-dir': {
410
+ if (!arg) {
411
+ console.log(chalk.dim('\n Usage: /add-dir <path>\n'));
412
+ return;
413
+ }
414
+ const { default: path } = await import('path');
415
+ const resolved = path.resolve(cfg.cwd, arg);
416
+ if (!addedDirs.includes(resolved))
417
+ addedDirs.push(resolved);
418
+ console.log(chalk.green(`\n ✔ Added to context: ${resolved}\n`));
419
+ return;
420
+ }
421
+ // ── Diff ────────────────────────────────────────────────────────────
422
+ case '/diff': {
423
+ try {
424
+ const result = await execa('git', ['diff'], { cwd: cfg.cwd, all: true, reject: false });
425
+ const out = result.all?.trim() ?? '';
426
+ if (!out) {
427
+ console.log(chalk.dim('\n No uncommitted changes.\n'));
428
+ return;
429
+ }
430
+ console.log('');
431
+ const lines = out.split('\n');
432
+ for (const line of lines) {
433
+ if (line.startsWith('+'))
434
+ process.stdout.write(chalk.green(' ' + line) + '\n');
435
+ else if (line.startsWith('-'))
436
+ process.stdout.write(chalk.red(' ' + line) + '\n');
437
+ else
438
+ process.stdout.write(chalk.dim(' ' + line) + '\n');
439
+ }
440
+ console.log('');
441
+ }
442
+ catch {
443
+ console.log(chalk.dim('\n Not a git repository.\n'));
444
+ }
445
+ return;
446
+ }
447
+ // ── Verify ──────────────────────────────────────────────────────────
448
+ case '/verify': {
449
+ const project = await detectProject(cfg.cwd);
450
+ const cmd2 = project.testCommand;
451
+ if (!cmd2) {
452
+ console.log(chalk.dim('\n No test command detected for this project.\n'));
453
+ return;
454
+ }
455
+ console.log(chalk.dim(`\n Running: ${cmd2}\n`));
456
+ const r = await execa(cmd2, { shell: true, cwd: cfg.cwd, all: true, reject: false });
457
+ const output = r.all ?? '';
458
+ output.split('\n').forEach(l => console.log(chalk.dim(' ') + l));
459
+ if (r.exitCode === 0)
460
+ console.log(chalk.green('\n ✔ Tests passed.\n'));
461
+ else
462
+ console.log(chalk.red('\n ✗ Tests failed.\n'));
463
+ return;
464
+ }
465
+ // ── Code review ─────────────────────────────────────────────────────
466
+ case '/code-review': {
467
+ const doFix = arg === '--fix';
468
+ const diff = (await execa('git', ['diff'], { cwd: cfg.cwd, all: true, reject: false })).all?.trim() ?? '';
469
+ if (!diff) {
470
+ console.log(chalk.dim('\n No changes to review.\n'));
471
+ return;
472
+ }
473
+ busy = true;
474
+ rl.pause();
475
+ currentAbort = new AbortController();
476
+ try {
477
+ const prompt = doFix
478
+ ? `Review and fix these code changes:\n\`\`\`diff\n${diff}\n\`\`\``
479
+ : `Review these code changes for bugs, logic errors, and quality issues. Be specific about line numbers:\n\`\`\`diff\n${diff}\n\`\`\``;
480
+ await session.send(prompt, currentAbort.signal);
481
+ }
482
+ finally {
483
+ currentAbort = null;
484
+ busy = false;
485
+ rl.resume();
486
+ }
487
+ return;
488
+ }
489
+ // ── Security review ─────────────────────────────────────────────────
490
+ case '/security-review': {
491
+ const diff = (await execa('git', ['diff'], { cwd: cfg.cwd, all: true, reject: false })).all?.trim() ?? '';
492
+ if (!diff) {
493
+ console.log(chalk.dim('\n No changes to review.\n'));
494
+ return;
495
+ }
496
+ busy = true;
497
+ rl.pause();
498
+ currentAbort = new AbortController();
499
+ try {
500
+ await session.send(`Perform a security audit of these changes. Check for: injection, auth bypass, secrets, XSS, CSRF, insecure deserialization, path traversal, and OWASP Top 10 issues.\n\`\`\`diff\n${diff}\n\`\`\``, currentAbort.signal);
501
+ }
502
+ finally {
503
+ currentAbort = null;
504
+ busy = false;
505
+ rl.resume();
506
+ }
507
+ return;
508
+ }
509
+ // ── MCP ─────────────────────────────────────────────────────────────
510
+ case '/mcp': {
511
+ const mcpCfg = await readMcpConfig(cfg.cwd);
512
+ if (!mcpCfg) {
513
+ console.log(chalk.dim('\n No .koneck/mcp.json found.\n'));
514
+ console.log(chalk.dim(' Example: { "servers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] } } }\n'));
515
+ return;
516
+ }
517
+ const servers = (mcpCfg['servers'] ?? {});
518
+ const names = Object.keys(servers);
519
+ if (!names.length) {
520
+ console.log(chalk.dim('\n No MCP servers configured.\n'));
521
+ return;
522
+ }
523
+ console.log('\n ' + chalk.bold('MCP servers') + '\n');
524
+ for (const name of names) {
525
+ const s = servers[name];
526
+ const cmd2 = [s['command'], ...(s['args'] ?? [])].join(' ');
527
+ console.log(` ${chalk.cyan(name.padEnd(20))} ${chalk.dim(cmd2)}`);
528
+ }
529
+ console.log('');
530
+ return;
531
+ }
532
+ // ── Init ────────────────────────────────────────────────────────────
533
+ case '/init':
534
+ await runGenerateKoneckMd(cfg);
535
+ return;
536
+ // ── Doctor ──────────────────────────────────────────────────────────
537
+ case '/doctor':
538
+ await runDoctor(cfg.cwd);
539
+ return;
540
+ // ── Debug ───────────────────────────────────────────────────────────
541
+ case '/debug': {
542
+ const mem = process.memoryUsage();
214
543
  console.log('');
215
- console.log(` ${chalk.dim('Prompt tokens :')} ${chalk.white(promptTokens.toLocaleString())}`);
216
- console.log(` ${chalk.dim('Completion tokens :')} ${chalk.white(completionTokens.toLocaleString())}`);
217
- console.log(` ${chalk.dim('Total tokens :')} ${chalk.white(totalTokens.toLocaleString())}`);
218
- console.log(` ${chalk.dim('Turns :')} ${chalk.white(turns)}`);
544
+ console.log(` ${chalk.dim('Node.js :')} ${process.version}`);
545
+ console.log(` ${chalk.dim('Platform :')} ${process.platform} ${process.arch}`);
546
+ console.log(` ${chalk.dim('PID :')} ${process.pid}`);
547
+ console.log(` ${chalk.dim('Heap used :')} ${Math.round(mem.heapUsed / 1024 / 1024)} MB`);
548
+ console.log(` ${chalk.dim('Heap total :')} ${Math.round(mem.heapTotal / 1024 / 1024)} MB`);
549
+ console.log(` ${chalk.dim('Messages :')} ${session.messages.length}`);
550
+ console.log(` ${chalk.dim('Turns :')} ${session.stats.turns}`);
551
+ console.log(` ${chalk.dim('Tokens :')} ${session.stats.totalTokens.toLocaleString()}`);
219
552
  console.log('');
220
553
  return;
221
554
  }
222
- // ── Compact ───────────────────────────────────────────────────────────
555
+ // ── Rewind ──────────────────────────────────────────────────────────
556
+ case '/rewind': {
557
+ const msgs = session.messages;
558
+ const lastUser = [...msgs].reverse().findIndex(m => m.role === 'user');
559
+ if (lastUser === -1) {
560
+ console.log(chalk.dim('\n Nothing to rewind.\n'));
561
+ return;
562
+ }
563
+ const idx = msgs.length - 1 - lastUser;
564
+ msgs.splice(idx);
565
+ console.log(chalk.green(`\n ✔ Rewound — removed last ${msgs.length - idx + session.messages.length - msgs.length} message(s).\n`));
566
+ console.log(chalk.green(` ✔ History now has ${session.messages.length} message(s).\n`));
567
+ return;
568
+ }
569
+ // ── Fork / Branch ───────────────────────────────────────────────────
570
+ case '/fork':
571
+ case '/branch': {
572
+ const forkedSession = await createAgentSession(cfg);
573
+ forkedSession.messages.push(...session.messages.map(m => ({ ...m })));
574
+ session = forkedSession;
575
+ console.log(chalk.green(`\n ✔ Forked. Continuing on a fresh copy of the conversation (${session.messages.length} messages carried over).\n`));
576
+ return;
577
+ }
578
+ // ── Subtask ─────────────────────────────────────────────────────────
579
+ case '/subtask': {
580
+ if (!arg) {
581
+ console.log(chalk.dim('\n Usage: /subtask <task description>\n'));
582
+ return;
583
+ }
584
+ console.log(chalk.dim(`\n Delegating to subagent: "${arg}"\n`));
585
+ busy = true;
586
+ rl.pause();
587
+ try {
588
+ await runAgent(arg, { ...cfg, ci: true });
589
+ }
590
+ catch (e) {
591
+ console.log(chalk.red(` ✗ Subtask error: ${e instanceof Error ? e.message : String(e)}\n`));
592
+ }
593
+ finally {
594
+ busy = false;
595
+ rl.resume();
596
+ }
597
+ return;
598
+ }
599
+ // ── PS ──────────────────────────────────────────────────────────────
600
+ case '/ps':
601
+ console.log(busy
602
+ ? chalk.yellow('\n 1 task running (current)\n')
603
+ : chalk.dim('\n No active background tasks.\n'));
604
+ return;
605
+ // ── Copy ────────────────────────────────────────────────────────────
606
+ case '/copy': {
607
+ if (!lastAssistantMsg) {
608
+ console.log(chalk.dim('\n No response to copy yet.\n'));
609
+ return;
610
+ }
611
+ const ok = await copyToClipboard(lastAssistantMsg);
612
+ if (ok)
613
+ console.log(chalk.green('\n ✔ Copied to clipboard.\n'));
614
+ else {
615
+ console.log(chalk.dim('\n Clipboard tool not found (wl-copy/xclip/xsel/pbcopy). Last response:\n'));
616
+ console.log(lastAssistantMsg.split('\n').map(l => ' ' + l).join('\n') + '\n');
617
+ }
618
+ return;
619
+ }
620
+ // ── Rename ──────────────────────────────────────────────────────────
621
+ case '/rename':
622
+ if (!arg) {
623
+ console.log(chalk.dim('\n Usage: /rename <session name>\n'));
624
+ return;
625
+ }
626
+ sessionName = arg;
627
+ console.log(chalk.green(`\n ✔ Session renamed: "${arg}"\n`));
628
+ return;
629
+ // ── Compact ─────────────────────────────────────────────────────────
223
630
  case '/compact': {
224
631
  if (session.messages.length < 4) {
225
632
  console.log(chalk.dim('\n Nothing to compact yet.\n'));
226
633
  return;
227
634
  }
228
635
  console.log(chalk.dim('\n Compacting conversation…'));
229
- const summary = await session.send('Summarise our entire conversation so far into a concise briefing. ' +
230
- 'Cover: what was asked, what was done, key decisions, current state of any files modified. ' +
231
- 'This will replace the full history, so be complete.');
232
- void summary;
233
- // Replace message history with just the summary
234
- const lastAssistant = session.messages.filter(m => m.role === 'assistant').at(-1);
235
- const summaryText = lastAssistant && typeof lastAssistant.content === 'string'
236
- ? lastAssistant.content
237
- : 'Conversation summarised.';
238
- session.messages.splice(0, session.messages.length, { role: 'user', content: '[Compacted conversation]\n\n' + summaryText }, { role: 'assistant', content: 'Understood. I have the context from our previous exchange.' });
239
- console.log(chalk.green(' ✔ Compacted. Conversation history replaced with summary.\n'));
240
- return;
241
- }
242
- // ── Memory ────────────────────────────────────────────────────────────
636
+ busy = true;
637
+ rl.pause();
638
+ const compactAbort = new AbortController();
639
+ try {
640
+ const focus = arg ? `Focus on: ${arg}.` : '';
641
+ await session.send(`Summarise our entire conversation so far into a concise briefing. ${focus} Cover: what was asked, what was done, key decisions, current file states. This will replace full history so be thorough.`, compactAbort.signal);
642
+ const last = session.messages.filter(m => m.role === 'assistant').at(-1);
643
+ const summary = last && typeof last.content === 'string' ? last.content : 'Conversation summarised.';
644
+ session.messages.splice(0, session.messages.length, { role: 'user', content: '[Compacted history]\n\n' + summary }, { role: 'assistant', content: 'Understood. I have context from our previous exchange.' });
645
+ console.log(chalk.green(' Compacted. History replaced with summary.\n'));
646
+ }
647
+ finally {
648
+ busy = false;
649
+ rl.resume();
650
+ }
651
+ return;
652
+ }
653
+ // ── Memory ──────────────────────────────────────────────────────────
243
654
  case '/memory': {
244
655
  const mem = await loadMemory(cfg.cwd);
245
- if (!mem || !mem.trim()) {
246
- console.log(chalk.dim('\n No memory found in .koneck/MEMORY.md\n'));
656
+ if (!mem?.trim()) {
657
+ console.log(chalk.dim('\n No memory in .koneck/MEMORY.md\n'));
247
658
  return;
248
659
  }
249
660
  console.log('\n' + chalk.bold(' Project memory') + '\n');
250
- for (const line of mem.split('\n')) {
251
- console.log(chalk.dim(' ') + line);
252
- }
661
+ mem.split('\n').forEach(l => console.log(chalk.dim(' ') + l));
253
662
  console.log('');
254
663
  return;
255
664
  }
256
- // ── CWD ───────────────────────────────────────────────────────────────
665
+ // ── CWD ─────────────────────────────────────────────────────────────
257
666
  case '/cwd': {
258
667
  if (!arg) {
259
668
  console.log(`\n CWD: ${chalk.cyan(cfg.cwd)}\n`);
260
669
  return;
261
670
  }
262
671
  const { default: path } = await import('path');
263
- const newCwd = path.resolve(cfg.cwd, arg);
264
672
  const { default: fs } = await import('fs/promises');
673
+ const newCwd = path.resolve(cfg.cwd, arg);
265
674
  try {
266
675
  await fs.access(newCwd);
267
676
  cfg = { ...cfg, cwd: newCwd };
@@ -273,23 +682,45 @@ export async function runChatMode(config) {
273
682
  }
274
683
  return;
275
684
  }
276
- // ── Clear / Reset ─────────────────────────────────────────────────────
685
+ // ── Cost ────────────────────────────────────────────────────────────
686
+ case '/cost': {
687
+ const cost = estimateCost(cfg.model, session.stats.promptTokens, session.stats.completionTokens);
688
+ if (cost.known)
689
+ console.log(`\n Session cost: ${chalk.white(formatCost(cost.total))} (input ${formatCost(cost.inputCost)} + output ${formatCost(cost.outputCost)})\n`);
690
+ else
691
+ console.log(chalk.dim(`\n Cost unknown — no pricing data for "${cfg.model}"\n`));
692
+ return;
693
+ }
694
+ // ── Tokens ──────────────────────────────────────────────────────────
695
+ case '/tokens': {
696
+ const { promptTokens, completionTokens, totalTokens, turns } = session.stats;
697
+ console.log('');
698
+ console.log(` ${chalk.dim('Prompt :')} ${chalk.white(promptTokens.toLocaleString())}`);
699
+ console.log(` ${chalk.dim('Completion :')} ${chalk.white(completionTokens.toLocaleString())}`);
700
+ console.log(` ${chalk.dim('Total :')} ${chalk.white(totalTokens.toLocaleString())}`);
701
+ console.log(` ${chalk.dim('Turns :')} ${chalk.white(turns)}`);
702
+ console.log('');
703
+ return;
704
+ }
705
+ // ── Clear / Reset / New ─────────────────────────────────────────────
277
706
  case '/clear':
278
- case '/reset': {
707
+ case '/reset':
708
+ case '/new':
279
709
  session = await createAgentSession(cfg);
710
+ goal = null;
711
+ addedDirs = [];
280
712
  console.log(chalk.green(' ✔ Conversation cleared.\n'));
281
713
  return;
282
- }
283
- // ── Save ──────────────────────────────────────────────────────────────
714
+ // ── Save ────────────────────────────────────────────────────────────
284
715
  case '/save': {
285
716
  const file = await doSave();
286
717
  console.log(chalk.green(` ✔ Saved: ${file}\n`));
287
718
  return;
288
719
  }
289
- // ── Sessions ──────────────────────────────────────────────────────────
720
+ // ── Sessions ────────────────────────────────────────────────────────
290
721
  case '/sessions': {
291
722
  const sessions = await listSessions(cfg.cwd);
292
- if (sessions.length === 0) {
723
+ if (!sessions.length) {
293
724
  console.log(chalk.dim('\n No sessions found.\n'));
294
725
  return;
295
726
  }
@@ -304,22 +735,165 @@ export async function runChatMode(config) {
304
735
  console.log('');
305
736
  return;
306
737
  }
738
+ // ── Feedback ────────────────────────────────────────────────────────
739
+ case '/feedback':
740
+ console.log('\n Report issues at: ' + chalk.cyan('https://github.com/Gubevu/konech/issues'));
741
+ console.log(chalk.dim(' Include your koneck version (koneck --version) and the error message.\n'));
742
+ return;
743
+ // ── Architect ───────────────────────────────────────────────────────
744
+ case '/architect': {
745
+ busy = true;
746
+ rl.pause();
747
+ currentAbort = new AbortController();
748
+ try {
749
+ await session.send('Analyze the current project architecture. Produce:\n1. Overview of the architecture\n2. Current problems (coupling, violations, god objects)\n3. Concrete improvement suggestions\n4. A dependency diagram in ASCII\nBe specific with file paths and module names.', currentAbort.signal);
750
+ }
751
+ finally {
752
+ currentAbort = null;
753
+ busy = false;
754
+ rl.resume();
755
+ }
756
+ return;
757
+ }
758
+ // ── Performance ─────────────────────────────────────────────────────
759
+ case '/performance': {
760
+ busy = true;
761
+ rl.pause();
762
+ currentAbort = new AbortController();
763
+ try {
764
+ await session.send('Perform a performance audit of this codebase. Find and report:\n1. N+1 query patterns\n2. Memory leaks or unbounded growth\n3. Blocking I/O in async contexts\n4. Slow rendering or re-render triggers (if React)\n5. Missing database indexes\n6. Unnecessary re-computation\nInclude file:line references for each finding.', currentAbort.signal);
765
+ }
766
+ finally {
767
+ currentAbort = null;
768
+ busy = false;
769
+ rl.resume();
770
+ }
771
+ return;
772
+ }
773
+ // ── Security ────────────────────────────────────────────────────────
774
+ case '/security': {
775
+ busy = true;
776
+ rl.pause();
777
+ currentAbort = new AbortController();
778
+ try {
779
+ await session.send('Perform a full security audit of this codebase. Check for:\n- OWASP Top 10 vulnerabilities\n- JWT/session security\n- XSS, CSRF, SSRF vectors\n- SQL/command injection\n- Exposed secrets or API keys\n- Unsafe regex (ReDoS)\n- Insecure dependencies\n- Path traversal\nReport each finding with file:line and severity (critical/high/medium/low).', currentAbort.signal);
780
+ }
781
+ finally {
782
+ currentAbort = null;
783
+ busy = false;
784
+ rl.resume();
785
+ }
786
+ return;
787
+ }
788
+ // ── Pipeline ────────────────────────────────────────────────────────
789
+ case '/pipeline': {
790
+ if (!arg) {
791
+ console.log(chalk.dim('\n Usage: /pipeline <task description>\n'));
792
+ return;
793
+ }
794
+ busy = true;
795
+ rl.pause();
796
+ try {
797
+ await runPipeline(arg, cfg);
798
+ }
799
+ catch (e) {
800
+ console.log(chalk.red(` ✗ Pipeline error: ${e instanceof Error ? e.message : String(e)}\n`));
801
+ }
802
+ finally {
803
+ busy = false;
804
+ rl.resume();
805
+ }
806
+ return;
807
+ }
808
+ // ── Knowledge graph ──────────────────────────────────────────────────
809
+ case '/knowledge': {
810
+ if (!arg) {
811
+ console.log(chalk.dim('\n Usage: /knowledge <concept> e.g. /knowledge authentication\n'));
812
+ return;
813
+ }
814
+ console.log(chalk.dim('\n Querying knowledge graph…\n'));
815
+ try {
816
+ const result = await queryKnowledgeGraph(arg, cfg.cwd);
817
+ console.log(result || chalk.dim(' No results found. Run /knowledge build to index the project first.\n'));
818
+ }
819
+ catch (e) {
820
+ console.log(chalk.red(` ✗ ${e instanceof Error ? e.message : String(e)}\n`));
821
+ }
822
+ return;
823
+ }
824
+ // ── Health ──────────────────────────────────────────────────────────
825
+ case '/health':
826
+ await runHealth(cfg.cwd);
827
+ return;
828
+ // ── Code graph ──────────────────────────────────────────────────────
829
+ case '/graph': {
830
+ if (arg === 'build') {
831
+ console.log(chalk.dim('\n Building code graph…\n'));
832
+ await buildCodeGraph(cfg.cwd);
833
+ return;
834
+ }
835
+ if (arg === 'calls') {
836
+ const callGraph = await getCallGraph(cfg.cwd);
837
+ console.log('\n' + callGraph.split('\n').map(l => ' ' + l).join('\n') + '\n');
838
+ return;
839
+ }
840
+ if (arg) {
841
+ const results = await queryGraph(arg, cfg.cwd);
842
+ if (!results.length) {
843
+ console.log(chalk.dim(`\n No symbols matching "${arg}"\n`));
844
+ return;
845
+ }
846
+ console.log('');
847
+ for (const s of results) {
848
+ console.log(` ${chalk.cyan(s.name.padEnd(30))} ${chalk.dim(s.kind.padEnd(12))} ${s.file}:${s.line}`);
849
+ }
850
+ console.log('');
851
+ return;
852
+ }
853
+ console.log(chalk.dim('\n Usage: /graph build | /graph calls | /graph <symbol-name>\n'));
854
+ return;
855
+ }
856
+ // ── Docs ────────────────────────────────────────────────────────────
857
+ case '/docs': {
858
+ console.log(chalk.dim('\n Regenerating documentation…\n'));
859
+ try {
860
+ await runAutoDocs(cfg);
861
+ console.log(chalk.green(' ✔ Documentation updated.\n'));
862
+ }
863
+ catch (e) {
864
+ console.log(chalk.red(` ✗ ${e instanceof Error ? e.message : String(e)}\n`));
865
+ }
866
+ return;
867
+ }
307
868
  default:
308
- console.log(chalk.dim(`\n Unknown command: ${cmd}. Type /help for the list.\n`));
869
+ console.log(chalk.dim(`\n Unknown command: ${cmd}. Type /help for the full list.\n`));
309
870
  return;
310
871
  }
311
872
  }
312
873
  // ── Task execution ────────────────────────────────────────────────────────
313
874
  busy = true;
314
875
  rl.pause();
315
- const taskText = MODE_PREFIXES[mode] + input;
876
+ // Build prefix from mode + effort + personality + goal + extra dirs
877
+ let prefix = MODE_PREFIXES[mode];
878
+ if (effort !== 'medium' && EFFORT_HINTS[effort])
879
+ prefix = EFFORT_HINTS[effort] + prefix;
880
+ if (personality !== 'default')
881
+ prefix = PERSONALITY_HINTS[personality] + prefix;
882
+ if (goal)
883
+ prefix = `[Persistent goal: ${goal}]\n` + prefix;
884
+ if (addedDirs.length)
885
+ prefix = `[Extra context directories: ${addedDirs.join(', ')}]\n` + prefix;
886
+ const taskText = prefix + input;
316
887
  currentAbort = new AbortController();
317
888
  const { signal } = currentAbort;
318
889
  const taskStart = Date.now();
319
890
  try {
320
891
  await session.send(taskText, signal);
321
- const elapsed = Date.now() - taskStart;
322
- tui.printStats(session.stats, cfg.provider, cfg.model, elapsed);
892
+ // Capture last assistant message for /copy
893
+ const last = session.messages.filter(m => m.role === 'assistant').at(-1);
894
+ if (last && typeof last.content === 'string')
895
+ lastAssistantMsg = last.content;
896
+ tui.printStats(session.stats, cfg.provider, cfg.model, Date.now() - taskStart);
323
897
  }
324
898
  catch (err) {
325
899
  if (err?.name !== 'AbortError') {
@@ -332,11 +906,11 @@ export async function runChatMode(config) {
332
906
  rl.resume();
333
907
  }
334
908
  }
335
- // ── Ctrl+C: cancel task if busy, exit if not ─────────────────────────────
909
+ // ── Ctrl+C ────────────────────────────────────────────────────────────────
336
910
  process.on('SIGINT', async () => {
337
911
  if (busy && currentAbort) {
338
912
  currentAbort.abort();
339
- process.stdout.write(chalk.yellow('\n ⚠ Interrupted — task cancelled.\n\n'));
913
+ process.stdout.write(chalk.yellow('\n ⚠ Interrupted.\n\n'));
340
914
  return;
341
915
  }
342
916
  process.stdout.write('\n');