codeep 3.3.0 → 3.3.2

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.
@@ -183,6 +183,19 @@ export declare function initializeAsProject(path: string): boolean;
183
183
  * Check if folder was manually initialized as project
184
184
  */
185
185
  export declare function isManuallyInitializedProject(path: string): boolean;
186
+ /**
187
+ * Create config with fallback logic
188
+ * 1. Try standard Conf location (~/.config/codeep-nodejs on Linux, etc.)
189
+ * 2. If not writable, use .codeep in current working directory
190
+ */
191
+ /**
192
+ * What a fresh install starts on. Exported so a test can hold the pair to the
193
+ * catalogue: the model had stayed `glm-5.2` after Z.AI's default moved to
194
+ * `glm-5.3`, so new users started a flagship behind what the website promised,
195
+ * and nothing failed because 5.2 still works.
196
+ */
197
+ export declare const DEFAULT_PROVIDER = "z.ai";
198
+ export declare const DEFAULT_MODEL = "glm-5.3";
186
199
  export declare const config: Conf<ConfigSchema>;
187
200
  export declare const LANGUAGES: Record<string, string>;
188
201
  export declare const PROTOCOLS: Record<string, string>;
@@ -138,12 +138,20 @@ function isWritable(dir) {
138
138
  * 1. Try standard Conf location (~/.config/codeep-nodejs on Linux, etc.)
139
139
  * 2. If not writable, use .codeep in current working directory
140
140
  */
141
+ /**
142
+ * What a fresh install starts on. Exported so a test can hold the pair to the
143
+ * catalogue: the model had stayed `glm-5.2` after Z.AI's default moved to
144
+ * `glm-5.3`, so new users started a flagship behind what the website promised,
145
+ * and nothing failed because 5.2 still works.
146
+ */
147
+ export const DEFAULT_PROVIDER = 'z.ai';
148
+ export const DEFAULT_MODEL = 'glm-5.3';
141
149
  function createConfig() {
142
150
  const defaults = {
143
151
  apiKey: '',
144
152
  migrationVersion: 0,
145
- provider: 'z.ai',
146
- model: 'glm-5.2',
153
+ provider: DEFAULT_PROVIDER,
154
+ model: DEFAULT_MODEL,
147
155
  agentMode: 'on',
148
156
  ollamaUrl: 'http://localhost:11434',
149
157
  ollamaNativeApi: false,
@@ -85,6 +85,10 @@ export interface StatsCache {
85
85
  cacheReadTokens: number;
86
86
  cacheCreationTokens: number;
87
87
  estimatedSavingsUsd: number;
88
+ /** Rates that applied, from getCacheStats. Absent → no rate is quoted rather
89
+ * than assuming Anthropic's 0.1×, which was wrong for DeepSeek, Kimi, Qwen
90
+ * and Fable 5.1. */
91
+ cacheReadRates?: number[];
88
92
  }
89
93
  export interface PricingRow {
90
94
  model: string;
@@ -8,6 +8,7 @@
8
8
  * coverage.
9
9
  */
10
10
  import { isFlatFeeProvider } from '../../config/providers.js';
11
+ import { formatCacheReadRates } from '../../utils/tokenTracker.js';
11
12
  /** Snippet window: chars of context before / after the match. */
12
13
  export const SEARCH_SNIPPET_BEFORE = 30;
13
14
  export const SEARCH_SNIPPET_AFTER = 50;
@@ -166,7 +167,7 @@ export function formatStatsReport(args) {
166
167
  }
167
168
  if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
168
169
  lines.push('', '### Prompt caching');
169
- lines.push(`Cache reads: ${fmt(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
170
+ lines.push(`Cache reads: ${fmt(cache.cacheReadTokens)} tokens${formatCacheReadRates(cache.cacheReadRates ?? [])}`);
170
171
  if (cache.cacheCreationTokens > 0) {
171
172
  lines.push(`Cache writes: ${fmt(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
172
173
  }
@@ -1,11 +1,24 @@
1
1
  /**
2
- * Single source of truth for Codeep slash commands.
2
+ * Single source of truth for Codeep's TUI slash commands.
3
3
  *
4
- * Every other place that needs command metadata — the `/` autocomplete in
5
- * `App.ts`, the `/help` screen in `components/Help.ts`, the dispatcher in
6
- * `commands.ts`, and the ACP command handler in `acp/commands.ts` — derives
7
- * from this registry. Adding a command means adding one entry here; the
8
- * autocomplete list, help screen, and command index all pick it up.
4
+ * The `/` autocomplete in `App.ts` and alias resolution in the dispatcher
5
+ * (`commands.ts`) derive from this registry. Adding a command means:
6
+ *
7
+ * 1. an entry in `COMMANDS` below;
8
+ * 2. a handler — a `case` in `renderer/commands.ts`, a `case` in `App.ts`, or
9
+ * a built-in skill of the same name;
10
+ * 3. a row in `HELP_LAYOUT` at the bottom of this file;
11
+ * 4. `npm run export:commands`, which regenerates the website's command
12
+ * reference (`Codeep-web/src/data/commands.json`) from this file.
13
+ *
14
+ * `registry.test.ts` fails if 2 or 3 is missed: a command offered in the
15
+ * autocomplete with no handler answers "Unknown command" (this is how
16
+ * `/account` shipped broken), and one missing from `HELP_LAYOUT` is invisible
17
+ * in `/help` and on the website.
18
+ *
19
+ * The ACP server (VS Code, Zed) is NOT derived from here: it keeps its own
20
+ * `AVAILABLE_COMMANDS` list in `acp/server.ts` and resolves its own aliases,
21
+ * so a command added here is not automatically offered to editors.
9
22
  *
10
23
  * ## What lives here vs. elsewhere
11
24
  *
@@ -76,7 +89,11 @@ export declare const ALL_ALIASES: ReadonlySet<string>;
76
89
  export interface HelpItemSpec {
77
90
  /** Visible key in `/help`, including the leading `/`. */
78
91
  key: string;
92
+ /** One line, sized for the terminal. */
79
93
  description: string;
94
+ /** Longer explanation used on the website's command reference instead of
95
+ * `description`, where a terminal row is too short to say what matters. */
96
+ web?: string;
80
97
  }
81
98
  export interface HelpCategorySpec {
82
99
  title: string;
@@ -1,11 +1,24 @@
1
1
  /**
2
- * Single source of truth for Codeep slash commands.
2
+ * Single source of truth for Codeep's TUI slash commands.
3
3
  *
4
- * Every other place that needs command metadata — the `/` autocomplete in
5
- * `App.ts`, the `/help` screen in `components/Help.ts`, the dispatcher in
6
- * `commands.ts`, and the ACP command handler in `acp/commands.ts` — derives
7
- * from this registry. Adding a command means adding one entry here; the
8
- * autocomplete list, help screen, and command index all pick it up.
4
+ * The `/` autocomplete in `App.ts` and alias resolution in the dispatcher
5
+ * (`commands.ts`) derive from this registry. Adding a command means:
6
+ *
7
+ * 1. an entry in `COMMANDS` below;
8
+ * 2. a handler — a `case` in `renderer/commands.ts`, a `case` in `App.ts`, or
9
+ * a built-in skill of the same name;
10
+ * 3. a row in `HELP_LAYOUT` at the bottom of this file;
11
+ * 4. `npm run export:commands`, which regenerates the website's command
12
+ * reference (`Codeep-web/src/data/commands.json`) from this file.
13
+ *
14
+ * `registry.test.ts` fails if 2 or 3 is missed: a command offered in the
15
+ * autocomplete with no handler answers "Unknown command" (this is how
16
+ * `/account` shipped broken), and one missing from `HELP_LAYOUT` is invisible
17
+ * in `/help` and on the website.
18
+ *
19
+ * The ACP server (VS Code, Zed) is NOT derived from here: it keeps its own
20
+ * `AVAILABLE_COMMANDS` list in `acp/server.ts` and resolves its own aliases,
21
+ * so a command added here is not automatically offered to editors.
9
22
  *
10
23
  * ## What lives here vs. elsewhere
11
24
  *
@@ -67,13 +80,12 @@ export const COMMANDS = [
67
80
  { name: 'settings', description: 'Open settings', category: 'general' },
68
81
  { name: 'version', description: 'Show version', category: 'general' },
69
82
  { name: 'update', description: 'Check for updates', category: 'general' },
70
- {
71
- name: 'cost',
72
- aliases: ['stats'],
73
- description: 'Token usage & cost this session',
74
- category: 'general',
75
- aliasListed: true,
76
- },
83
+ { name: 'cost', description: 'Token usage & cost this session', category: 'general' },
84
+ // Its own command, not an alias of /cost. From 2026-06-24 it was
85
+ // `aliases: ['stats']` on /cost, and the dispatcher resolves aliases before
86
+ // the switch — so /stats printed the /cost report and its own handler (per-
87
+ // model breakdown, cache summary, pricing table) never ran.
88
+ { name: 'stats', description: 'Detailed session view — per-model breakdown, cache, and pricing table', category: 'general' },
77
89
  { name: 'clear', description: 'Clear chat', category: 'general', hidden: true },
78
90
  { name: 'exit', description: 'Quit application', category: 'general', hidden: true },
79
91
  // ── sessions ───────────────────────────────────────────────────────────────
@@ -220,7 +232,7 @@ export const COMMANDS = [
220
232
  { name: 'commands', description: 'List custom slash commands in .codeep/commands/*.md', category: 'extensions' },
221
233
  { name: 'web-cache', aliases: ['webcache'], description: 'Show @web fetch cache stats (alias: /web-cache clear)', category: 'extensions' },
222
234
  // ── cloud & account ────────────────────────────────────────────────────────
223
- { name: 'account', description: 'Link this machine to your codeep.dev account', category: 'cloud' },
235
+ { name: 'account', description: 'Show whether this machine is linked to codeep.dev, and how to link it', category: 'cloud' },
224
236
  { name: 'tasks', description: 'List/add/done/delete codeep.dev tasks', category: 'cloud', usage: ['add <title> [--bug|--feature]'] },
225
237
  { name: 'sync', description: 'Sync learning preferences and profiles to codeep.dev', category: 'cloud' },
226
238
  { name: 'telemetry', description: 'Show or toggle automatic cloud telemetry (on/off)', category: 'cloud' },
@@ -229,7 +241,11 @@ export const COMMANDS = [
229
241
  { name: 'context-save', description: 'Save conversation', category: 'cloud', hidden: true },
230
242
  { name: 'context-load', description: 'Load conversation', category: 'cloud', hidden: true },
231
243
  { name: 'context-clear', description: 'Clear saved context', category: 'cloud', hidden: true },
232
- { name: 'save', description: 'Save current session', category: 'cloud', hidden: true },
244
+ // ACP-only. acp/commands.ts handles /save (an editor has no autosave); the TUI
245
+ // saves sessions on its own and has no handler, so this stays hidden — offering
246
+ // it in the terminal would only answer "Unknown command". Registered because
247
+ // registry.test.ts requires every ACP case label to be.
248
+ { name: 'save', description: 'Save current session (VS Code / Zed only)', category: 'cloud', hidden: true },
233
249
  // ── code generation ────────────────────────────────────────────────────────
234
250
  { name: 'build', description: 'Build the project', category: 'codegen' },
235
251
  { name: 'deploy', description: 'Build and deploy', category: 'codegen' },
@@ -333,7 +349,10 @@ export const HELP_LAYOUT = [
333
349
  { key: '/settings', description: 'Open settings' },
334
350
  { key: '/version', description: 'Show version' },
335
351
  { key: '/update', description: 'Check for updates' },
336
- { key: '/stats (/cost)', description: 'Token usage & cost this session' },
352
+ { key: '/cost', description: 'Token usage & cost this session',
353
+ web: 'Token usage and estimated cost this session, per provider and model, with prompt-cache savings' },
354
+ { key: '/stats', description: 'Detailed view — per-model breakdown, cache, pricing table',
355
+ web: 'Detailed session view — per-model breakdown, plan-aware totals, prompt-cache reads and writes, and the per-1M pricing table' },
337
356
  { key: '/clear', description: 'Clear chat' },
338
357
  { key: '/exit', description: 'Quit application' },
339
358
  ],
@@ -345,11 +364,15 @@ export const HELP_LAYOUT = [
345
364
  { key: '/new', description: 'Start new session' },
346
365
  { key: '/rename <name>', description: 'Rename current session' },
347
366
  { key: '/search <term>', description: 'Search the current session' },
348
- { key: '/recall <query>', description: 'Search across ALL saved sessions (cross-session)' },
349
- { key: '/recall … --resume', description: 'Load the top-matching session directly' },
367
+ { key: '/recall <query>', description: 'Search across ALL saved sessions (cross-session)',
368
+ web: 'Search across all saved sessions, ranked by relevance and recency' },
369
+ { key: '/recall … --resume', description: 'Load the top-matching session directly',
370
+ web: 'Load the top-matching session directly, skipping the picker' },
350
371
  { key: '/recall … --summarize', description: 'LLM recap of what you did across matches' },
372
+ { key: '/cloud', description: 'List and resume sessions synced from other devices' },
351
373
  { key: '/export [md|json|txt]', description: 'Export chat' },
352
- { key: '/compact [keepN]', description: 'AI-summarize older messages to free up context (keeps last N)' },
374
+ { key: '/compact [keepN]', description: 'AI-summarize older messages to free up context (keeps last N)',
375
+ web: 'AI-summarize older messages to free up context, keeping the last N (default 4)' },
353
376
  ],
354
377
  },
355
378
  {
@@ -357,7 +380,8 @@ export const HELP_LAYOUT = [
357
380
  items: [
358
381
  { key: '/checkpoint [name]', description: 'Snapshot conversation + provider/model + git HEAD' },
359
382
  { key: '/checkpoints', description: 'List saved checkpoints in this workspace' },
360
- { key: '/rewind <id>', description: 'Restore conversation from a checkpoint' },
383
+ { key: '/rewind <id>', description: 'Restore conversation from a checkpoint',
384
+ web: 'Restore a checkpoint — conversation and provider/model; files roll back via the git command printed afterwards' },
361
385
  { key: '/checkpoint delete <id>', description: 'Delete a saved checkpoint' },
362
386
  ],
363
387
  },
@@ -387,6 +411,9 @@ export const HELP_LAYOUT = [
387
411
  { key: '/amend', description: 'Amend last commit' },
388
412
  { key: '/branch', description: 'Create/manage branches' },
389
413
  { key: '/stash', description: 'Stash changes' },
414
+ { key: '/unstash', description: 'Apply and drop the most recent stash' },
415
+ { key: '/pr', description: 'Create a pull request description' },
416
+ { key: '/changelog', description: 'Generate changelog from recent commits' },
390
417
  { key: '/init', description: 'Initialize project (.codeep/ folder)' },
391
418
  { key: '/scan', description: 'Scan project structure' },
392
419
  { key: '/memory <note>', description: 'Add note to project intelligence' },
@@ -414,19 +441,58 @@ export const HELP_LAYOUT = [
414
441
  title: 'Skills (Shortcuts)',
415
442
  items: [
416
443
  { key: '/test (/t)', description: 'Generate/run tests' },
444
+ { key: '/test-fix', description: 'Fix failing tests' },
445
+ { key: '/coverage', description: 'Run/analyze test coverage' },
446
+ { key: '/e2e', description: 'Generate end-to-end tests' },
447
+ { key: '/mock', description: 'Generate mock data for testing' },
417
448
  { key: '/docs (/d)', description: 'Open web docs for a command' },
449
+ { key: '/readme', description: 'Generate or update README' },
450
+ { key: '/api-docs', description: 'Generate API documentation' },
418
451
  { key: '/refactor (/r)', description: 'Improve code quality' },
419
452
  { key: '/fix (/f)', description: 'Debug and fix issues' },
420
453
  { key: '/explain (/e)', description: 'Explain code' },
421
454
  { key: '/optimize (/o)', description: 'Optimize performance' },
422
455
  { key: '/debug (/b)', description: 'Debug problems' },
423
456
  { key: '/security', description: 'Security audit (SQLi, XSS, secrets, auth)' },
424
- { key: '/coverage', description: 'Run/analyze test coverage' },
457
+ { key: '/types', description: 'Add or improve TypeScript types' },
458
+ { key: '/cleanup', description: 'Clean up code (remove unused, format)' },
459
+ { key: '/modernize', description: 'Update code to use modern syntax' },
460
+ { key: '/migrate', description: 'Migrate code to newer version' },
461
+ { key: '/split', description: 'Split a large file into smaller modules' },
462
+ { key: '/log', description: 'Add logging to code' },
463
+ { key: '/translate', description: 'Translate code comments to English' },
464
+ { key: '/skills', description: 'List all skills' },
465
+ { key: '/skills <query>', description: 'Search skills by keyword' },
466
+ { key: '/skills bundles', description: 'List skill bundles (project + global)' },
467
+ { key: '/skills create-bundle <name>', description: 'Scaffold a bundle in .codeep/skills/' },
468
+ { key: '/skills show <name>', description: "Print a bundle's SKILL.md" },
469
+ { key: '/skills browse [query]', description: 'Search the marketplace at codeep.dev/skills' },
470
+ { key: '/skills install <owner>/<slug>', description: 'Install a marketplace bundle into the project' },
471
+ { key: '/skills publish <name> [--public]', description: 'Share a bundle to codeep.dev' },
472
+ { key: '/skills unpublish <owner>/<slug>', description: 'Remove a bundle you published' },
473
+ ],
474
+ },
475
+ {
476
+ title: 'Code Generation',
477
+ items: [
425
478
  { key: '/component <name>', description: 'Generate UI component' },
426
479
  { key: '/api <name>', description: 'Generate API endpoint' },
480
+ { key: '/hook', description: 'Generate a React hook' },
481
+ { key: '/service', description: 'Generate a service/utility module' },
482
+ { key: '/page', description: 'Generate a new page/route' },
483
+ { key: '/form', description: 'Generate a form with validation' },
484
+ { key: '/crud', description: 'Generate full CRUD for an entity' },
427
485
  { key: '/docker', description: 'Generate Dockerfile + compose' },
428
- { key: '/skills', description: 'List all 50+ skills' },
429
- { key: '/skills <query>', description: 'Search skills by keyword' },
486
+ { key: '/ci', description: 'Generate CI/CD configuration' },
487
+ { key: '/env', description: 'Setup environment configuration' },
488
+ { key: '/k8s', description: 'Generate Kubernetes manifests' },
489
+ { key: '/terraform', description: 'Generate Terraform configuration' },
490
+ { key: '/nginx', description: 'Generate Nginx configuration' },
491
+ { key: '/monitor', description: 'Add monitoring and observability' },
492
+ { key: '/build', description: 'Build the project' },
493
+ { key: '/deploy', description: 'Build and deploy' },
494
+ { key: '/release', description: 'Create a new release' },
495
+ { key: '/publish', description: 'Publish package to npm' },
430
496
  ],
431
497
  },
432
498
  {
@@ -441,16 +507,33 @@ export const HELP_LAYOUT = [
441
507
  { key: '/login', description: 'Login with API key' },
442
508
  { key: '/logout', description: 'Logout from provider' },
443
509
  { key: '/profile save <name>', description: 'Save current provider+model as profile' },
510
+ { key: '/profile load <name>', description: 'Load a saved profile' },
444
511
  { key: '/profile list', description: 'List saved profiles' },
445
- { key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
512
+ { key: '/profile delete <name>', description: 'Delete a saved profile' },
513
+ { key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)',
514
+ web: 'OpenRouter routing preferences: show, prefer or ignore upstream providers, fallbacks on/off, privacy strict/allow, clear' },
446
515
  { key: '/personality', description: 'List personalities and custom bots with model, tools, and scope' },
447
516
  { key: '/personality <name>', description: 'Activate a personality or custom bot. /personality off to clear.' },
448
- { key: '/me', description: 'Your user profile (reply language, style, stack) — adapts the agent to you' },
517
+ { key: '/me', description: 'Your user profile (reply language, style, stack) — adapts the agent to you',
518
+ web: 'Your user profile — reply language, style, stack — injected into every run on every surface' },
449
519
  { key: '/me init [project]', description: 'Scaffold a profile template (global, or for this project). /me off to disable' },
450
520
  { key: '/me learn [on|off]', description: 'Learn durable prefs from this session now; on/off toggles auto-learn. /me forget clears it' },
451
521
  { key: '/me sync', description: 'Push your profile to the codeep.dev dashboard (and pull on a fresh machine)' },
452
- { key: '/agents', description: 'List sub-agents the agent can delegate to (researcher / reviewer / tester / your own)' },
522
+ { key: '/agents', description: 'List sub-agents the agent can delegate to (researcher / reviewer / tester / your own)',
523
+ web: 'List the sub-agents the agent can delegate to — planner, researcher, reviewer, tester, or your own in .codeep/agents/' },
453
524
  { key: '/insights [--days N]', description: 'Activity summary — runs, files, tools, projects over the last N days (default 7)' },
525
+ { key: '/telegram', description: 'Set up answering confirmations on your phone' },
526
+ { key: '/telegram on|off', description: 'Turn Telegram approvals on or off' },
527
+ { key: '/audit', description: 'What agents did here — runs, tools, and what the boundary refused' },
528
+ { key: '/audit on|off', description: 'Turn the project audit record on or off' },
529
+ ],
530
+ },
531
+ {
532
+ title: 'Thinking',
533
+ items: [
534
+ { key: '/thinking (/effort)', description: 'Show the thinking tier and what this model supports' },
535
+ { key: '/thinking <auto|low|medium|high|max>', description: 'Set how hard the model reasons',
536
+ web: "Set the reasoning-effort tier. Auto uses each model's own default; other tiers are clamped to the levels the current provider and model actually distinguish, so an unsupported value is never sent. Models with no graded control ignore it." },
454
537
  ],
455
538
  },
456
539
  {
@@ -468,6 +551,25 @@ export const HELP_LAYOUT = [
468
551
  { key: '/mcp prompt <server> <name>', description: 'Materialize a prompt with arguments (key=value)' },
469
552
  { key: '/hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)' },
470
553
  { key: '/commands', description: 'List custom slash commands (.codeep/commands/*.md)' },
554
+ { key: '/web-cache', description: 'Show @web fetch cache stats' },
555
+ { key: '/web-cache clear', description: 'Clear the @web fetch cache' },
556
+ ],
557
+ },
558
+ {
559
+ title: 'Cloud & Account',
560
+ items: [
561
+ { key: '/account', description: 'Is this machine linked to codeep.dev? How to link it',
562
+ web: 'Show whether this machine is linked to codeep.dev. Linking runs in your shell as `codeep account`, and the approval link it prints can be opened on any device where you are already signed in' },
563
+ { key: '/tasks', description: 'List codeep.dev tasks' },
564
+ { key: '/tasks add <title> [--bug|--feature] [--desc <text>]', description: 'Create a task on the dashboard' },
565
+ { key: '/tasks done <n>', description: 'Mark task #n as done' },
566
+ { key: '/tasks delete <n>', description: 'Delete task #n' },
567
+ { key: '/sync', description: 'Sync learning preferences and profiles to codeep.dev' },
568
+ { key: '/sync learning', description: 'Sync only learning preferences' },
569
+ { key: '/sync profiles', description: 'Sync only saved profiles' },
570
+ { key: '/telemetry [on|off]', description: 'Show or toggle automatic cloud telemetry' },
571
+ { key: '/keysync [on|off]', description: 'Show or toggle syncing API keys to codeep.dev',
572
+ web: 'Show or toggle syncing API keys to codeep.dev. Off by default — keys stay in your OS keychain unless you opt in' },
471
573
  ],
472
574
  },
473
575
  {
@@ -288,7 +288,6 @@ export async function handleCommand(command, args, ctx) {
288
288
  }
289
289
  break;
290
290
  }
291
- case 'effort':
292
291
  case 'thinking': {
293
292
  const providerId = config.get('provider');
294
293
  const model = config.get('model');
@@ -302,7 +301,7 @@ export async function handleCommand(command, args, ctx) {
302
301
  ctx.app.notify('Thinking effort: auto — each model uses its own default.');
303
302
  }
304
303
  else if (!supported) {
305
- ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4, Kimi K3).`);
304
+ ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x or GPT-6, Gemini 3, DeepSeek V4.1 Flash, GLM-5.x, Kimi K3).`);
306
305
  }
307
306
  else {
308
307
  // Tell the user what THIS model will actually run (the tier may
@@ -377,7 +376,6 @@ export async function handleCommand(command, args, ctx) {
377
376
  runAgentTask(args.join(' '), true, ctx, () => null, () => { });
378
377
  break;
379
378
  }
380
- case 'd':
381
379
  case 'docs': {
382
380
  // Open per-command web docs in the system browser. Lets the inline
383
381
  // /help stay terse (single-line entries) while users who want the
@@ -1771,21 +1769,13 @@ Format: use headers per category, only include categories where you found issues
1771
1769
  break;
1772
1770
  }
1773
1771
  // Built-in skill shortcuts
1774
- case 'c':
1775
1772
  case 'commit':
1776
- case 't':
1777
1773
  case 'test':
1778
- case 'r':
1779
1774
  case 'refactor':
1780
- case 'f':
1781
1775
  case 'fix':
1782
- case 'e':
1783
1776
  case 'explain':
1784
- case 'o':
1785
1777
  case 'optimize':
1786
- case 'b':
1787
1778
  case 'debug':
1788
- case 'p':
1789
1779
  case 'push':
1790
1780
  case 'pull':
1791
1781
  case 'amend':
@@ -2025,6 +2015,23 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2025
2015
  });
2026
2016
  break;
2027
2017
  }
2018
+ case 'account': {
2019
+ // Linking cannot run here: `codeep account` prints to stdout and exits the
2020
+ // process when it finishes, which would tear down the TUI. Before this
2021
+ // case existed the autocomplete offered /account and it answered
2022
+ // "Unknown command" — so this says where the flow runs, and what state
2023
+ // the machine is in now.
2024
+ const { getSyncToken, getGithubId } = await import('../config/index.js');
2025
+ const linked = Boolean(getSyncToken());
2026
+ const githubId = getGithubId();
2027
+ ctx.app.addMessage({
2028
+ role: 'system',
2029
+ content: linked
2030
+ ? `## codeep.dev account\n\nThis machine is **linked**${githubId ? ` (GitHub id ${githubId})` : ''}. Usage and sessions sync to your dashboard.\n\nTo cut it off, use **Settings → Connected devices** on codeep.dev.`
2031
+ : `## codeep.dev account\n\nThis machine is **not linked**.\n\nQuit Codeep and run this in your shell:\n\n\`\`\`bash\ncodeep account\n\`\`\`\n\nIt prints a one-time approval link. You can open it on **any** device where you are already signed in to codeep.dev — it does not have to be this machine, so there is no need to sign in to GitHub here.`,
2032
+ });
2033
+ break;
2034
+ }
2028
2035
  case 'tasks': {
2029
2036
  const { fetchTasks, markTaskDone, generateProjectId } = await import('../utils/codeepCloud.js');
2030
2037
  const { setTaskContext, clearTaskContext } = await import('../utils/taskContext.js');
@@ -93,6 +93,21 @@ export interface ProviderCostBreakdown {
93
93
  cacheReadTokens: number;
94
94
  estimatedCost: number;
95
95
  }
96
+ /**
97
+ * What a cached prompt token costs, as a fraction of the model's input rate.
98
+ * Model first (a property of the model), then provider, then the default.
99
+ *
100
+ * One lookup for everything that needs it. Cost used this chain while savings
101
+ * hardcoded 0.1, so every provider priced differently from Anthropic had a
102
+ * cost and a "saved" figure that disagreed with each other.
103
+ */
104
+ export declare function cacheReadRateFor(model: string, provider: string | undefined): number;
105
+ /**
106
+ * The rate note for a report. One rate reads as "0.02×"; a session mixing
107
+ * providers reads as a range, because any single number there would be wrong
108
+ * for part of it.
109
+ */
110
+ export declare function formatCacheReadRates(rates: readonly number[]): string;
96
111
  /**
97
112
  * Get cost breakdown grouped by provider/model.
98
113
  *
@@ -120,6 +135,9 @@ export interface CacheStats {
120
135
  /** True when EVERY cached token came from a flat-fee plan — there is no
121
136
  * metered spend to have saved against. */
122
137
  isEntirelyFlatFeeCache: boolean;
138
+ /** The read rate of each metered record that read from cache, so a report
139
+ * can state the rate that actually applied instead of assuming 0.1×. */
140
+ cacheReadRates: number[];
123
141
  }
124
142
  export declare function getCacheStats(): CacheStats;
125
143
  /**
@@ -127,9 +127,10 @@ const MODEL_PRICING = {
127
127
  // rate, so it carries PEAK — an over-estimate by design. The previous rows
128
128
  // (0.435/0.87 and 0.14/0.28) were two to four and a half times below today's
129
129
  // peak and so under-reported, which is the one direction this table must not
130
- // err in. Cache hits cost 2% of a miss, but DeepSeek reports them as
131
- // `prompt_cache_hit_tokens`, which this tracker does not read — so every input
132
- // token bills at the miss rate: an over-estimate again, not a gap.
130
+ // err in. Cache hits are read (DeepSeek reports them both nested as
131
+ // `prompt_tokens_details.cached_tokens` and top-level as
132
+ // `prompt_cache_hit_tokens`) and billed at DeepSeek's own hit rate — see
133
+ // CACHE_READ_RATE below.
133
134
  'deepseek-flash': { inputPer1M: 0.30, outputPer1M: 1.20 },
134
135
  // Retired V4 Flash is served by V4.1 Flash and billed at its price.
135
136
  'deepseek-v4-flash': { inputPer1M: 0.30, outputPer1M: 1.20 },
@@ -250,8 +251,12 @@ export function extractOpenAIUsage(data) {
250
251
  // later. Reading only the nested form zeroed every Kimi cache hit, so the
251
252
  // cached portion of a run billed at the full cache-miss rate — five times
252
253
  // what it costs — with nothing anywhere to say so.
254
+ // DeepSeek sends the same number twice — nested `cached_tokens` and
255
+ // top-level `prompt_cache_hit_tokens` — so the nested read already covers
256
+ // it; the top-level field is a last resort, never added to the other.
253
257
  const nested = data.usage.prompt_tokens_details?.cached_tokens;
254
- const cached = (typeof nested === 'number' ? nested : data.usage.cached_tokens) || 0;
258
+ const topLevel = data.usage.cached_tokens ?? data.usage.prompt_cache_hit_tokens;
259
+ const cached = (typeof nested === 'number' ? nested : topLevel) || 0;
255
260
  return {
256
261
  promptTokens: data.usage.prompt_tokens || 0,
257
262
  completionTokens: data.usage.completion_tokens || 0,
@@ -301,8 +306,16 @@ export function extractAnthropicUsage(data) {
301
306
  */
302
307
  const MODEL_CACHE_READ_RATE = {
303
308
  'claude-fable-5-1': 0.025,
309
+ // V4 Pro's own ratio: $0.044 hit against $1.32 miss (peak; off-peak halves
310
+ // both, so the ratio holds). Historical — V4 Pro routes to V4.1 Flash from
311
+ // 2026-09-14 and configs holding it are migrated.
312
+ 'deepseek-v4-pro': 0.044 / 1.32,
304
313
  };
305
314
  const CACHE_READ_RATE = {
315
+ // V4.1 Flash: $0.006 hit against $0.30 miss at peak, $0.003 against $0.15
316
+ // off-peak — 0.02 either way. Without an entry DeepSeek fell to the 0.1
317
+ // default and every cached token billed at five times its price.
318
+ 'deepseek': 0.02,
306
319
  'kimi': 0.2,
307
320
  'kimi-api': 0.2,
308
321
  'qwen': 0.2,
@@ -313,6 +326,33 @@ const CACHE_READ_RATE = {
313
326
  };
314
327
  /** Anthropic's ratio, and the safest guess for a provider we have not priced. */
315
328
  const DEFAULT_CACHE_READ_RATE = 0.1;
329
+ /**
330
+ * What a cached prompt token costs, as a fraction of the model's input rate.
331
+ * Model first (a property of the model), then provider, then the default.
332
+ *
333
+ * One lookup for everything that needs it. Cost used this chain while savings
334
+ * hardcoded 0.1, so every provider priced differently from Anthropic had a
335
+ * cost and a "saved" figure that disagreed with each other.
336
+ */
337
+ export function cacheReadRateFor(model, provider) {
338
+ return MODEL_CACHE_READ_RATE[model]
339
+ ?? CACHE_READ_RATE[provider?.trim().toLowerCase() ?? '']
340
+ ?? DEFAULT_CACHE_READ_RATE;
341
+ }
342
+ /**
343
+ * The rate note for a report. One rate reads as "0.02×"; a session mixing
344
+ * providers reads as a range, because any single number there would be wrong
345
+ * for part of it.
346
+ */
347
+ export function formatCacheReadRates(rates) {
348
+ const fmt = (r) => `${Number(r.toFixed(3))}×`;
349
+ const distinct = [...new Set(rates.map(r => Number(r.toFixed(4))))].sort((a, b) => a - b);
350
+ if (distinct.length === 0)
351
+ return '';
352
+ if (distinct.length === 1)
353
+ return ` (billed at ${fmt(distinct[0])} input rate)`;
354
+ return ` (billed at ${fmt(distinct[0])}–${fmt(distinct[distinct.length - 1])} input rate, by model)`;
355
+ }
316
356
  /**
317
357
  * Get cost breakdown grouped by provider/model.
318
358
  *
@@ -346,9 +386,7 @@ export function getCostBreakdown(startIndex = 0) {
346
386
  // prompt tokens bill at the standard 1.0× rate.
347
387
  const cacheCreate = record.cacheCreationTokens ?? 0;
348
388
  const cacheRead = record.cacheReadTokens ?? 0;
349
- const cacheReadRate = MODEL_CACHE_READ_RATE[record.model]
350
- ?? CACHE_READ_RATE[record.provider?.trim().toLowerCase()]
351
- ?? DEFAULT_CACHE_READ_RATE;
389
+ const cacheReadRate = cacheReadRateFor(record.model, record.provider);
352
390
  const uncachedPrompt = Math.max(0, record.promptTokens - cacheCreate - cacheRead);
353
391
  existing.estimatedCost +=
354
392
  (uncachedPrompt / 1_000_000) * pricing.inputPer1M
@@ -367,6 +405,7 @@ export function getCacheStats() {
367
405
  let savings = 0;
368
406
  let flatFeeCached = 0;
369
407
  let meteredCached = 0;
408
+ const readRates = [];
370
409
  for (const record of currentRecords()) {
371
410
  const cached = (record.cacheCreationTokens ?? 0) + (record.cacheReadTokens ?? 0);
372
411
  cacheCreate += record.cacheCreationTokens ?? 0;
@@ -380,11 +419,16 @@ export function getCacheStats() {
380
419
  }
381
420
  meteredCached += cached;
382
421
  // Savings = what cache-read tokens would have cost at full input rate,
383
- // minus what they actually cost at 0.1×. (Cache creation is a slight
384
- // *penalty* of 0.25× — netted in for honest reporting.)
422
+ // minus what they cost at the model's own read rate. This hardcoded 0.9
423
+ // (a 0.1 read) for every provider, so Kimi and Qwen (0.2) over-reported
424
+ // savings while DeepSeek (0.02) and Fable 5.1 (0.025) under-reported them.
425
+ // (Cache creation is a slight *penalty* of 0.25× — netted in.)
385
426
  const pricing = MODEL_PRICING[record.model];
386
427
  if (pricing) {
387
- const cReadSaved = ((record.cacheReadTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.9;
428
+ const readRate = cacheReadRateFor(record.model, record.provider);
429
+ if ((record.cacheReadTokens ?? 0) > 0)
430
+ readRates.push(readRate);
431
+ const cReadSaved = ((record.cacheReadTokens ?? 0) / 1_000_000) * pricing.inputPer1M * (1 - readRate);
388
432
  const cCreateCost = ((record.cacheCreationTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.25;
389
433
  savings += cReadSaved - cCreateCost;
390
434
  }
@@ -395,6 +439,7 @@ export function getCacheStats() {
395
439
  estimatedSavingsUsd: Math.max(0, savings),
396
440
  hasFlatFeeCacheUsage: flatFeeCached > 0,
397
441
  isEntirelyFlatFeeCache: flatFeeCached > 0 && meteredCached === 0,
442
+ cacheReadRates: readRates,
398
443
  };
399
444
  }
400
445
  /**
@@ -506,7 +551,7 @@ export function formatCostReport() {
506
551
  // The billing multipliers only describe a metered account. On a plan
507
552
  // nothing is billed per token, so quoting a rate there would be as invented
508
553
  // as the per-model prices this report already refuses to show.
509
- const readNote = cache.isEntirelyFlatFeeCache ? '' : ' (billed at 0.1× input rate)';
554
+ const readNote = cache.isEntirelyFlatFeeCache ? '' : formatCacheReadRates(cache.cacheReadRates);
510
555
  const writeNote = cache.isEntirelyFlatFeeCache ? '' : ' (billed at 1.25× input rate)';
511
556
  lines.push(`**Cache reads:** ${formatTokenCount(cache.cacheReadTokens)} tokens${readNote}`);
512
557
  if (cache.cacheCreationTokens > 0) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "3.3.0";
1
+ export declare const VERSION = "3.3.2";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '3.3.0';
4
+ export const VERSION = '3.3.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
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",
@@ -17,6 +17,7 @@
17
17
  "test:coverage": "vitest run --coverage",
18
18
  "version": "node scripts/gen-version.js && git add src/version.ts",
19
19
  "export:catalogue": "node --import tsx scripts/export-catalogue.ts",
20
+ "export:commands": "node --import tsx scripts/export-commands.ts",
20
21
  "release": "node scripts/release.js"
21
22
  },
22
23
  "repository": {