codeep 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -257,7 +257,7 @@ Commands:
257
257
  - `/me learn project` — one-off learn scoped to this repo
258
258
  - `/me forget` — clear the auto-learned profile(s)
259
259
 
260
- Sync your global profile across machines (and edit it on the web) from the [dashboard](https://codeep.dev/dashboard) with `codeep account sync`. In VS Code: **Codeep: Edit Profile** and **Codeep: Toggle Profile Auto-Learn**.
260
+ Sync your global profile across machines (and edit it on the web) from the [dashboard](https://codeep.dev/dashboard): `/me sync` (or `codeep account sync`, which now carries the profile too). In VS Code: **Codeep: Edit Profile**, **Codeep: Toggle Profile Auto-Learn**, **Codeep: Sync Profile to Dashboard**.
261
261
 
262
262
  ### Sub-agents (delegation)
263
263
  The agent can delegate a self-contained sub-task to a specialist **sub-agent** that runs in its own fresh context window and returns only a summary — keeping the main context small and letting each sub-task run with a tuned persona and scoped tools.
@@ -1016,6 +1016,8 @@ In `dangerous` mode, configure which tools require confirmation via `/settings`:
1016
1016
  | `/memory remove <n>` | Remove note by index |
1017
1017
  | `/memory clear` | Clear all notes |
1018
1018
  | `/model pull <name>` | Pull an Ollama model (local Ollama only) |
1019
+ | `/model browse` | Browse a curated catalog of local coding models and pull one (Ollama) |
1020
+ | `/model rm <name>` | Remove a locally-installed Ollama model (local only) |
1019
1021
 
1020
1022
  ### Skills
1021
1023
 
@@ -697,6 +697,22 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
697
697
  ? 'Cleared the auto-learned profile(s).'
698
698
  : 'No learned profile to clear.' };
699
699
  }
700
+ if (sub === 'sync') {
701
+ const { getSyncToken } = await import('../config/index.js');
702
+ if (!getSyncToken())
703
+ return { handled: true, response: 'Not linked to codeep.dev. Run `codeep account` in a terminal first.' };
704
+ const { pushUserProfile, pullUserProfile } = await import('../utils/codeepCloud.js');
705
+ const pushed = await pushUserProfile();
706
+ const pulled = await pullUserProfile();
707
+ const lines = [];
708
+ if (pushed)
709
+ lines.push('✓ Profile pushed to the dashboard');
710
+ if (pulled === 1)
711
+ lines.push('✓ Profile pulled to this machine');
712
+ if (lines.length === 0)
713
+ lines.push('Nothing to sync yet — run `/me init` and fill in your profile first.');
714
+ return { handled: true, response: lines.join('\n') };
715
+ }
700
716
  return { handled: true, response: formatProfileView(session.workspaceRoot) };
701
717
  }
702
718
  case 'insights': {
@@ -58,7 +58,7 @@ const AVAILABLE_COMMANDS = [
58
58
  { name: 'go', description: 'Execute the pending plan from /plan' },
59
59
  // Personalities + insights (2.0.3)
60
60
  { name: 'personality', description: 'List or switch agent tone preset', input: { hint: '[name | off]' } },
61
- { name: 'me', description: 'Your user profile — adapts the agent to you (reply language, style, stack)', input: { hint: '[init [project] | on | off | learn [on|off|project] | forget]' } },
61
+ { name: 'me', description: 'Your user profile — adapts the agent to you (reply language, style, stack)', input: { hint: '[init [project] | on | off | learn [on|off|project] | forget | sync]' } },
62
62
  { name: 'agents', description: 'List sub-agents the agent can delegate self-contained tasks to' },
63
63
  { name: 'insights', description: 'Activity summary over the last N days (default 7)', input: { hint: '[--days N]' } },
64
64
  // Project intelligence
package/dist/api/index.js CHANGED
@@ -394,6 +394,30 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
394
394
  ...(openRouterProvider ? { provider: openRouterProvider } : {}),
395
395
  });
396
396
  try {
397
+ // Opt-in native Ollama transport (plain chat). When `ollamaNativeApi` is on,
398
+ // route through /api/chat so num_ctx + keep_alive apply and the real context
399
+ // window is used. OFF by default → falls through to the /v1 shim below
400
+ // unchanged. Plain chat sends no tools, so the native call is simple here.
401
+ if (providerId === 'ollama' && config.get('ollamaNativeApi') === true) {
402
+ const { streamOllamaNativeChat, getOllamaContextLength } = await import('./ollamaNative.js');
403
+ const ollamaUrl = config.get('ollamaUrl') || 'http://localhost:11434';
404
+ const cfgCtx = Number(config.get('ollamaNumCtx')) || 0;
405
+ const numCtx = cfgCtx > 0 ? cfgCtx : ((await getOllamaContextLength(model, ollamaUrl)) ?? undefined);
406
+ const result = await streamOllamaNativeChat({
407
+ baseUrl: ollamaUrl,
408
+ model,
409
+ messages,
410
+ numCtx,
411
+ keepAlive: config.get('ollamaKeepAlive') || undefined,
412
+ temperature: omitTemperature ? undefined : temperature,
413
+ timeoutMs: timeout,
414
+ onChunk: stream ? onChunk : undefined,
415
+ });
416
+ if (result.promptTokens != null && result.completionTokens != null) {
417
+ recordTokenUsage({ promptTokens: result.promptTokens, completionTokens: result.completionTokens, totalTokens: result.promptTokens + result.completionTokens }, model, providerId);
418
+ }
419
+ return stripThinkTags(result.text);
420
+ }
397
421
  // Use node:http for Ollama — bypasses undici connection pooling (AggregateError in Node v24)
398
422
  if (providerId === 'ollama') {
399
423
  const nodeStream = await httpRequest(`${baseUrl}/chat/completions`, {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Native Ollama `/api/chat` transport.
3
+ *
4
+ * Why this exists: Codeep normally talks to Ollama through its OpenAI-compatible
5
+ * `/v1/chat/completions` shim, which silently IGNORES Ollama-native options like
6
+ * `num_ctx` (context window) and `keep_alive` (model residency). The shim also
7
+ * defaults to a small context (~2-4k) regardless of the model's real window, so
8
+ * long sessions get silently truncated server-side.
9
+ *
10
+ * The native `/api/chat` endpoint accepts those options and streams
11
+ * newline-delimited JSON (NOT SSE). Each line is a full JSON object:
12
+ * {"message":{"role":"assistant","content":"…"},"done":false}
13
+ * …
14
+ * {"message":{"content":""},"done":true,"prompt_eval_count":N,"eval_count":M}
15
+ *
16
+ * This module keeps the line-parsing as PURE functions so they can be unit
17
+ * tested without a live server. The networking wrapper lives at the bottom.
18
+ */
19
+ /** A tool call as Ollama's native /api/chat returns it: `function.arguments`
20
+ * is a JSON OBJECT (unlike OpenAI's JSON-string form). */
21
+ export interface OllamaToolCall {
22
+ name: string;
23
+ arguments: Record<string, unknown>;
24
+ }
25
+ export interface OllamaStreamDelta {
26
+ /** Text chunk from this line (may be empty). */
27
+ content: string;
28
+ /** True on the terminating line. */
29
+ done: boolean;
30
+ /** Tool calls present on this line (Ollama emits them on one message). */
31
+ toolCalls?: OllamaToolCall[];
32
+ /** Prompt (input) tokens — only present on the final line. */
33
+ promptTokens?: number;
34
+ /** Completion (output) tokens — only present on the final line. */
35
+ completionTokens?: number;
36
+ }
37
+ /** Extract + normalize `message.tool_calls` from a parsed message. Returns
38
+ * undefined when none. Tolerates missing/malformed entries (never throws). */
39
+ export declare function extractOllamaToolCalls(msg: unknown): OllamaToolCall[] | undefined;
40
+ /**
41
+ * Parse a single newline-delimited JSON line from `/api/chat`. Returns null for
42
+ * blank lines or unparseable keepalives (never throws). Tolerates both the
43
+ * streaming shape (`message.content`) and the non-stream shape.
44
+ */
45
+ export declare function parseOllamaChatLine(line: string): OllamaStreamDelta | null;
46
+ export interface OllamaAccumulator {
47
+ text: string;
48
+ toolCalls: OllamaToolCall[];
49
+ promptTokens?: number;
50
+ completionTokens?: number;
51
+ done: boolean;
52
+ }
53
+ /**
54
+ * Fold a parsed delta into a running accumulator. Pure — no I/O. The caller
55
+ * feeds each line's parse result here and reads `text` / token counts at the end.
56
+ */
57
+ export declare function foldOllamaDelta(acc: OllamaAccumulator, delta: OllamaStreamDelta | null): OllamaAccumulator;
58
+ /**
59
+ * Split a buffer into complete lines + a trailing remainder. Pure helper so the
60
+ * stream reader can carry partial lines across chunk boundaries correctly.
61
+ * Returns { lines, rest } where `rest` is the unfinished tail (no newline yet).
62
+ */
63
+ export declare function splitOllamaLines(buffer: string): {
64
+ lines: string[];
65
+ rest: string;
66
+ };
67
+ export declare const initialOllamaAccumulator: () => OllamaAccumulator;
68
+ /**
69
+ * Pull the real context_length out of an `/api/show` response's `model_info`.
70
+ * The key is architecture-prefixed (e.g. `llama.context_length`,
71
+ * `qwen2.context_length`), so we scan for any key ending in `.context_length`
72
+ * (or the bare `context_length`). Pure + tolerant — returns null when absent.
73
+ */
74
+ export declare function extractContextLength(showResponse: unknown): number | null;
75
+ /**
76
+ * Fetch a model's real maximum context window via `/api/show`. Cached per model;
77
+ * returns null on any error (caller falls back to its default). Never throws.
78
+ */
79
+ export declare function getOllamaContextLength(model: string, ollamaBaseUrl: string): Promise<number | null>;
80
+ /** Test seam — reset the per-model context cache. */
81
+ export declare function _clearOllamaContextCache(): void;
82
+ export interface OllamaChatOptions {
83
+ /** Ollama base URL (without /v1), e.g. http://localhost:11434 */
84
+ baseUrl: string;
85
+ model: string;
86
+ /** Messages in OpenAI shape {role, content} — Ollama /api/chat accepts these. */
87
+ messages: {
88
+ role: string;
89
+ content: string;
90
+ }[];
91
+ /** num_ctx — the context window to allocate. 0/undefined = let Ollama decide. */
92
+ numCtx?: number;
93
+ /** keep_alive — how long to keep the model resident, e.g. "30m". */
94
+ keepAlive?: string;
95
+ temperature?: number;
96
+ timeoutMs?: number;
97
+ onChunk?: (text: string) => void;
98
+ /** Tool definitions in OpenAI function format. Ollama's /api/chat accepts the
99
+ * same `{type:'function',function:{...}}` shape and returns `tool_calls`. */
100
+ tools?: unknown[];
101
+ /** Pass-through for non-string message parts (tool results carry tool_call_id
102
+ * etc.). When set, used verbatim instead of `messages`. */
103
+ rawMessages?: unknown[];
104
+ }
105
+ export interface OllamaChatResult {
106
+ text: string;
107
+ toolCalls: OllamaToolCall[];
108
+ promptTokens?: number;
109
+ completionTokens?: number;
110
+ }
111
+ /**
112
+ * Stream a chat completion from Ollama's native `/api/chat`. Uses node:http to
113
+ * sidestep undici's connection pooling (which throws AggregateError against
114
+ * localhost Ollama on Node 24). Parses the newline-JSON stream via the pure
115
+ * helpers above. Resolves with the full text + token usage.
116
+ */
117
+ export declare function streamOllamaNativeChat(opts: OllamaChatOptions): Promise<OllamaChatResult>;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Native Ollama `/api/chat` transport.
3
+ *
4
+ * Why this exists: Codeep normally talks to Ollama through its OpenAI-compatible
5
+ * `/v1/chat/completions` shim, which silently IGNORES Ollama-native options like
6
+ * `num_ctx` (context window) and `keep_alive` (model residency). The shim also
7
+ * defaults to a small context (~2-4k) regardless of the model's real window, so
8
+ * long sessions get silently truncated server-side.
9
+ *
10
+ * The native `/api/chat` endpoint accepts those options and streams
11
+ * newline-delimited JSON (NOT SSE). Each line is a full JSON object:
12
+ * {"message":{"role":"assistant","content":"…"},"done":false}
13
+ * …
14
+ * {"message":{"content":""},"done":true,"prompt_eval_count":N,"eval_count":M}
15
+ *
16
+ * This module keeps the line-parsing as PURE functions so they can be unit
17
+ * tested without a live server. The networking wrapper lives at the bottom.
18
+ */
19
+ import http from 'http';
20
+ import https from 'https';
21
+ /** Extract + normalize `message.tool_calls` from a parsed message. Returns
22
+ * undefined when none. Tolerates missing/malformed entries (never throws). */
23
+ export function extractOllamaToolCalls(msg) {
24
+ const raw = msg?.tool_calls;
25
+ if (!Array.isArray(raw) || raw.length === 0)
26
+ return undefined;
27
+ const out = [];
28
+ for (const tc of raw) {
29
+ const fn = tc?.function;
30
+ const name = typeof fn?.name === 'string' ? fn.name : '';
31
+ if (!name)
32
+ continue;
33
+ let args = {};
34
+ const a = fn?.arguments;
35
+ if (a && typeof a === 'object')
36
+ args = a;
37
+ else if (typeof a === 'string') {
38
+ try {
39
+ args = JSON.parse(a);
40
+ }
41
+ catch {
42
+ args = {};
43
+ }
44
+ }
45
+ out.push({ name, arguments: args });
46
+ }
47
+ return out.length > 0 ? out : undefined;
48
+ }
49
+ /**
50
+ * Parse a single newline-delimited JSON line from `/api/chat`. Returns null for
51
+ * blank lines or unparseable keepalives (never throws). Tolerates both the
52
+ * streaming shape (`message.content`) and the non-stream shape.
53
+ */
54
+ export function parseOllamaChatLine(line) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed)
57
+ return null;
58
+ let json;
59
+ try {
60
+ json = JSON.parse(trimmed);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ const msg = json.message;
66
+ const content = typeof msg?.content === 'string' ? msg.content : '';
67
+ const done = json.done === true;
68
+ const out = { content, done };
69
+ const toolCalls = extractOllamaToolCalls(msg);
70
+ if (toolCalls)
71
+ out.toolCalls = toolCalls;
72
+ if (typeof json.prompt_eval_count === 'number')
73
+ out.promptTokens = json.prompt_eval_count;
74
+ if (typeof json.eval_count === 'number')
75
+ out.completionTokens = json.eval_count;
76
+ return out;
77
+ }
78
+ /**
79
+ * Fold a parsed delta into a running accumulator. Pure — no I/O. The caller
80
+ * feeds each line's parse result here and reads `text` / token counts at the end.
81
+ */
82
+ export function foldOllamaDelta(acc, delta) {
83
+ if (!delta)
84
+ return acc;
85
+ return {
86
+ text: acc.text + delta.content,
87
+ toolCalls: delta.toolCalls ? [...acc.toolCalls, ...delta.toolCalls] : acc.toolCalls,
88
+ promptTokens: delta.promptTokens ?? acc.promptTokens,
89
+ completionTokens: delta.completionTokens ?? acc.completionTokens,
90
+ done: acc.done || delta.done,
91
+ };
92
+ }
93
+ /**
94
+ * Split a buffer into complete lines + a trailing remainder. Pure helper so the
95
+ * stream reader can carry partial lines across chunk boundaries correctly.
96
+ * Returns { lines, rest } where `rest` is the unfinished tail (no newline yet).
97
+ */
98
+ export function splitOllamaLines(buffer) {
99
+ const parts = buffer.split('\n');
100
+ const rest = parts.pop() ?? '';
101
+ return { lines: parts, rest };
102
+ }
103
+ export const initialOllamaAccumulator = () => ({ text: '', toolCalls: [], done: false });
104
+ /**
105
+ * Pull the real context_length out of an `/api/show` response's `model_info`.
106
+ * The key is architecture-prefixed (e.g. `llama.context_length`,
107
+ * `qwen2.context_length`), so we scan for any key ending in `.context_length`
108
+ * (or the bare `context_length`). Pure + tolerant — returns null when absent.
109
+ */
110
+ export function extractContextLength(showResponse) {
111
+ const info = showResponse?.model_info;
112
+ if (!info || typeof info !== 'object')
113
+ return null;
114
+ for (const [key, value] of Object.entries(info)) {
115
+ if ((key === 'context_length' || key.endsWith('.context_length')) && typeof value === 'number' && value > 0) {
116
+ return value;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ // Per-model context-length cache so we hit /api/show at most once per model.
122
+ const contextLengthCache = new Map();
123
+ /**
124
+ * Fetch a model's real maximum context window via `/api/show`. Cached per model;
125
+ * returns null on any error (caller falls back to its default). Never throws.
126
+ */
127
+ export async function getOllamaContextLength(model, ollamaBaseUrl) {
128
+ if (contextLengthCache.has(model))
129
+ return contextLengthCache.get(model) ?? null;
130
+ const base = ollamaBaseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
131
+ try {
132
+ const res = await fetch(`${base}/api/show`, {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({ name: model }),
136
+ signal: AbortSignal.timeout(5000),
137
+ });
138
+ if (!res.ok) {
139
+ contextLengthCache.set(model, null);
140
+ return null;
141
+ }
142
+ const data = await res.json();
143
+ const ctx = extractContextLength(data);
144
+ contextLengthCache.set(model, ctx);
145
+ return ctx;
146
+ }
147
+ catch {
148
+ contextLengthCache.set(model, null);
149
+ return null;
150
+ }
151
+ }
152
+ /** Test seam — reset the per-model context cache. */
153
+ export function _clearOllamaContextCache() {
154
+ contextLengthCache.clear();
155
+ }
156
+ /**
157
+ * Stream a chat completion from Ollama's native `/api/chat`. Uses node:http to
158
+ * sidestep undici's connection pooling (which throws AggregateError against
159
+ * localhost Ollama on Node 24). Parses the newline-JSON stream via the pure
160
+ * helpers above. Resolves with the full text + token usage.
161
+ */
162
+ export function streamOllamaNativeChat(opts) {
163
+ const base = opts.baseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
164
+ const url = `${base}/api/chat`;
165
+ const options = {};
166
+ if (opts.numCtx && opts.numCtx > 0)
167
+ options.num_ctx = opts.numCtx;
168
+ if (typeof opts.temperature === 'number')
169
+ options.temperature = opts.temperature;
170
+ // When tools are present, Ollama only emits tool_calls in non-streaming mode
171
+ // reliably; streaming tool_calls support varies by version. We stream when
172
+ // there are no tools (plain chat) and fall to non-stream when tools are sent.
173
+ const useStream = !opts.tools || opts.tools.length === 0;
174
+ const body = JSON.stringify({
175
+ model: opts.model,
176
+ messages: opts.rawMessages ?? opts.messages,
177
+ stream: useStream,
178
+ ...(opts.tools && opts.tools.length ? { tools: opts.tools } : {}),
179
+ ...(Object.keys(options).length ? { options } : {}),
180
+ ...(opts.keepAlive ? { keep_alive: opts.keepAlive } : {}),
181
+ });
182
+ return new Promise((resolve, reject) => {
183
+ const u = new URL(url);
184
+ const lib = u.protocol === 'https:' ? https : http;
185
+ const req = lib.request({
186
+ hostname: u.hostname,
187
+ port: u.port,
188
+ path: u.pathname + u.search,
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
191
+ timeout: opts.timeoutMs ?? 120_000,
192
+ }, (res) => {
193
+ if (res.statusCode && res.statusCode >= 400) {
194
+ let errBody = '';
195
+ res.on('data', (c) => { errBody += c.toString(); });
196
+ res.on('end', () => reject(new Error(`Ollama /api/chat ${res.statusCode}: ${errBody.slice(0, 300)}`)));
197
+ return;
198
+ }
199
+ let buffer = '';
200
+ let acc = initialOllamaAccumulator();
201
+ const decoder = new TextDecoder();
202
+ res.on('data', (chunk) => {
203
+ buffer += decoder.decode(chunk, { stream: true });
204
+ const { lines, rest } = splitOllamaLines(buffer);
205
+ buffer = rest;
206
+ for (const line of lines) {
207
+ const delta = parseOllamaChatLine(line);
208
+ if (delta?.content)
209
+ opts.onChunk?.(delta.content);
210
+ acc = foldOllamaDelta(acc, delta);
211
+ }
212
+ });
213
+ res.on('end', () => {
214
+ // Flush any trailing line (final object often arrives without a newline).
215
+ const delta = parseOllamaChatLine(buffer);
216
+ if (delta?.content)
217
+ opts.onChunk?.(delta.content);
218
+ acc = foldOllamaDelta(acc, delta);
219
+ resolve({ text: acc.text, toolCalls: acc.toolCalls, promptTokens: acc.promptTokens, completionTokens: acc.completionTokens });
220
+ });
221
+ res.on('error', reject);
222
+ });
223
+ req.on('error', reject);
224
+ req.on('timeout', () => { req.destroy(new Error('Ollama request timed out')); });
225
+ req.write(body);
226
+ req.end();
227
+ });
228
+ }
@@ -54,6 +54,19 @@ interface ConfigSchema {
54
54
  rateLimitCommands: number;
55
55
  agentMode: AgentMode;
56
56
  ollamaUrl: string;
57
+ /** Route Ollama through its NATIVE /api/chat endpoint instead of the
58
+ * OpenAI-compatible /v1 shim. The native endpoint honors num_ctx + keep_alive
59
+ * and exposes the model's real context window. OFF by default — opt-in until
60
+ * verified against a live Ollama; when off, the existing /v1 path is used
61
+ * unchanged so current behavior is preserved. */
62
+ ollamaNativeApi: boolean;
63
+ /** How long Ollama keeps the model loaded in memory between requests (e.g.
64
+ * "30m", "1h", "-1" for forever). Avoids reload latency every turn. Sent on
65
+ * native /api/chat requests (only when ollamaNativeApi is on). Default "30m". */
66
+ ollamaKeepAlive: string;
67
+ /** Override num_ctx (context window) for Ollama. 0 = auto-detect the model's
68
+ * real max via /api/show. Set a specific number to cap VRAM use. Default 0. */
69
+ ollamaNumCtx: number;
57
70
  customBaseUrl: string;
58
71
  agentConfirmation: 'always' | 'dangerous' | 'never';
59
72
  agentConfirmDeleteFile: boolean;
@@ -144,6 +144,9 @@ function createConfig() {
144
144
  model: 'glm-5.1',
145
145
  agentMode: 'on',
146
146
  ollamaUrl: 'http://localhost:11434',
147
+ ollamaNativeApi: false,
148
+ ollamaKeepAlive: '30m',
149
+ ollamaNumCtx: 0,
147
150
  customBaseUrl: '',
148
151
  agentConfirmation: 'dangerous',
149
152
  agentConfirmDeleteFile: true,
@@ -243,12 +243,13 @@ export const PROVIDERS = {
243
243
  },
244
244
  },
245
245
  models: [
246
- { id: 'claude-opus-4-7', name: 'Claude Opus 4.7', description: 'Most capable Claude model' },
247
- { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', description: 'Previous generation Opus' },
246
+ { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', description: 'Most capable Claude model' },
247
+ { id: 'claude-opus-4-7', name: 'Claude Opus 4.7', description: 'Previous generation Opus' },
248
+ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', description: 'Older generation Opus' },
248
249
  { id: 'claude-sonnet-4-6', name: 'Claude Sonnet', description: 'Best balance of speed and intelligence' },
249
250
  { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku', description: 'Fastest and most affordable' },
250
251
  ],
251
- defaultModel: 'claude-opus-4-7',
252
+ defaultModel: 'claude-opus-4-8',
252
253
  defaultProtocol: 'anthropic',
253
254
  envKey: 'ANTHROPIC_API_KEY',
254
255
  groupLabel: 'Anthropic',
@@ -266,7 +267,7 @@ export const PROVIDERS = {
266
267
  },
267
268
  models: [
268
269
  { id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', description: 'Most capable Gemini model' },
269
- { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', description: 'Pro-level intelligence at Flash speed' },
270
+ { id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', description: 'Pro-level intelligence at Flash speed' },
270
271
  ],
271
272
  defaultModel: 'gemini-3.1-pro-preview',
272
273
  defaultProtocol: 'openai',
@@ -94,7 +94,7 @@ const COMMAND_DESCRIPTIONS = {
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
96
  'personality': 'Switch agent tone: concise / verbose / security / senior-reviewer / etc',
97
- 'me': 'Your user profile (reply language, style, stack) — adapts the agent to you. /me init, /me learn',
97
+ 'me': 'Your user profile (reply language, style, stack) — adapts the agent to you. /me init, /me learn, /me sync',
98
98
  'agents': 'List sub-agents the agent can delegate self-contained tasks to (researcher / reviewer / tester / custom)',
99
99
  'insights': 'Activity summary over the last N days (default 7): runs, files, tools, projects',
100
100
  'recall': 'Search across ALL saved sessions (cross-session; /search is current-session only)',
@@ -93,6 +93,56 @@ export async function handleCommand(command, args, ctx) {
93
93
  });
94
94
  break;
95
95
  }
96
+ // /model browse — curated catalog of recommended local coding models.
97
+ if (args[0] === 'browse') {
98
+ const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').toLowerCase();
99
+ const isLocal = ollamaUrl.includes('localhost') || ollamaUrl.includes('127.0.0.1') || ollamaUrl.includes('[::1]');
100
+ const { OLLAMA_CODING_MODELS, catalogAgentHint } = await import('../utils/ollamaCatalog.js');
101
+ const items = OLLAMA_CODING_MODELS.map(m => ({
102
+ key: m.pull,
103
+ label: m.name,
104
+ description: `${m.pull} · ${m.vram} · ${catalogAgentHint(m.params)} — ${m.description}`,
105
+ }));
106
+ ctx.app.showSelect('Recommended coding models (pull)', items, '', (item) => {
107
+ if (!isLocal) {
108
+ ctx.app.notify(`Ollama is on a remote server. SSH in and run: ollama pull ${item.key}`);
109
+ return;
110
+ }
111
+ ctx.app.notify(`Pulling ${item.key}... (this may take a while)`);
112
+ import('child_process').then(({ execFile }) => {
113
+ execFile('ollama', ['pull', item.key], { timeout: 1_200_000 }, (err) => {
114
+ if (err)
115
+ ctx.app.notify(`Pull failed: ${err.message}`);
116
+ else
117
+ ctx.app.notify(`✓ ${item.key} ready — use /model to select it`);
118
+ });
119
+ });
120
+ });
121
+ break;
122
+ }
123
+ // /model rm <name> — delete a locally-installed Ollama model.
124
+ if (args[0] === 'rm' || args[0] === 'remove' || args[0] === 'delete') {
125
+ const modelName = args[1];
126
+ if (!modelName) {
127
+ ctx.app.notify('Usage: /model rm <model-name>');
128
+ break;
129
+ }
130
+ const ollamaUrl = (config.get('ollamaUrl') || 'http://localhost:11434').toLowerCase();
131
+ const isLocal = ollamaUrl.includes('localhost') || ollamaUrl.includes('127.0.0.1') || ollamaUrl.includes('[::1]');
132
+ if (!isLocal) {
133
+ ctx.app.notify(`Ollama is on a remote server. SSH in and run: ollama rm ${modelName}`);
134
+ break;
135
+ }
136
+ ctx.app.notify(`Removing ${modelName}…`);
137
+ const { execFile } = await import('child_process');
138
+ execFile('ollama', ['rm', modelName], { timeout: 60_000 }, (err) => {
139
+ if (err)
140
+ ctx.app.notify(`Remove failed: ${err.message}`);
141
+ else
142
+ ctx.app.notify(`✓ Removed ${modelName}`);
143
+ });
144
+ break;
145
+ }
96
146
  const providerId = config.get('provider');
97
147
  const { isDynamicModelsProvider, isNoApiKeyProvider: _noKey } = await import('../config/providers.js');
98
148
  if (isDynamicModelsProvider(providerId)) {
@@ -375,6 +425,26 @@ export async function handleCommand(command, args, ctx) {
375
425
  : 'No learned profile to clear.');
376
426
  break;
377
427
  }
428
+ if (sub === 'sync') {
429
+ const { getSyncToken } = await import('../config/index.js');
430
+ if (!getSyncToken()) {
431
+ ctx.app.notify('Not linked to codeep.dev. Run: codeep account');
432
+ break;
433
+ }
434
+ const { pushUserProfile, pullUserProfile } = await import('../utils/codeepCloud.js');
435
+ ctx.app.notify('Syncing your profile with codeep.dev…');
436
+ const pushed = await pushUserProfile();
437
+ const pulled = await pullUserProfile();
438
+ const lines = [];
439
+ if (pushed)
440
+ lines.push('✓ Profile pushed to the dashboard');
441
+ if (pulled === 1)
442
+ lines.push('✓ Profile pulled to this machine');
443
+ if (lines.length === 0)
444
+ lines.push('Nothing to sync yet — run `/me init` and fill in your profile first.');
445
+ ctx.app.addMessage({ role: 'system', content: `## Profile sync\n\n${lines.join('\n')}` });
446
+ break;
447
+ }
378
448
  if (sub === 'init') {
379
449
  const scope = args[1]?.toLowerCase() === 'project' ? 'project' : 'global';
380
450
  if (scope === 'project' && !ctx.projectPath) {
@@ -131,6 +131,7 @@ export const helpCategories = [
131
131
  { key: '/me', description: 'Your user profile (reply language, style, stack) — adapts the agent to you' },
132
132
  { key: '/me init [project]', description: 'Scaffold a profile template (global, or for this project). /me off to disable' },
133
133
  { key: '/me learn [on|off]', description: 'Learn durable prefs from this session now; on/off toggles auto-learn. /me forget clears it' },
134
+ { key: '/me sync', description: 'Push your profile to the codeep.dev dashboard (and pull on a fresh machine)' },
134
135
  { key: '/agents', description: 'List sub-agents the agent can delegate to (researcher / reviewer / tester / your own)' },
135
136
  { key: '/insights [--days N]', description: 'Activity summary — runs, files, tools, projects over the last N days (default 7)' },
136
137
  ],
@@ -170,6 +171,8 @@ export const helpCategories = [
170
171
  { key: '/settings > Ollama URL', description: 'Set URL (default: http://localhost:11434)' },
171
172
  { key: '/model', description: 'Pick installed Ollama model dynamically' },
172
173
  { key: '/model pull <model>', description: 'Pull an Ollama model (local Ollama only)' },
174
+ { key: '/model browse', description: 'Browse recommended local coding models and pull one' },
175
+ { key: '/model rm <model>', description: 'Remove a locally-installed Ollama model (local only)' },
173
176
  { key: 'OLLAMA_HOST=0.0.0.0', description: 'Required env var for remote Ollama access' },
174
177
  ],
175
178
  },
@@ -131,6 +131,28 @@ export const SETTINGS = [
131
131
  getValue: () => config.get('ollamaUrl') || 'http://localhost:11434',
132
132
  type: 'text',
133
133
  },
134
+ {
135
+ key: 'ollamaNativeApi',
136
+ label: 'Ollama Native API (beta)',
137
+ getValue: () => config.get('ollamaNativeApi'),
138
+ type: 'select',
139
+ options: [
140
+ { value: false, label: 'Off (OpenAI-compatible /v1)' },
141
+ { value: true, label: 'On — beta: native /api/chat (num_ctx + keep_alive)' },
142
+ ],
143
+ },
144
+ {
145
+ key: 'ollamaKeepAlive',
146
+ label: 'Ollama Keep-Alive',
147
+ getValue: () => String(config.get('ollamaKeepAlive') ?? '30m'),
148
+ type: 'text',
149
+ },
150
+ {
151
+ key: 'ollamaNumCtx',
152
+ label: 'Ollama Context (num_ctx, 0=auto)',
153
+ getValue: () => String(config.get('ollamaNumCtx') ?? 0),
154
+ type: 'text',
155
+ },
134
156
  {
135
157
  key: 'customBaseUrl',
136
158
  label: 'Custom Base URL',
@@ -386,8 +386,8 @@ Codeep - AI-powered coding assistant TUI
386
386
  Usage:
387
387
  codeep Start interactive chat
388
388
  codeep account Link CLI to your codeep.dev dashboard
389
- codeep account sync Pull keys + personalities + commands from codeep.dev
390
- codeep account push Push local keys + personalities + commands to codeep.dev
389
+ codeep account sync Pull keys + personalities + commands + profile from codeep.dev
390
+ codeep account push Push local keys + personalities + commands + profile to codeep.dev
391
391
  codeep acp Start ACP server (for Zed editor integration)
392
392
  codeep --version Show version
393
393
  codeep --help Show this help
@@ -427,9 +427,9 @@ Commands (in chat):
427
427
  }
428
428
  console.log(` synced ${count} key${count !== 1 ? 's' : ''}.`);
429
429
  }
430
- // Also pull portable personal config — personalities + custom
431
- // commands. Additive merge (never clobbers local files).
432
- const { pullPersonalities, pullCommands } = await import('../utils/codeepCloud.js');
430
+ // Also pull portable personal config — personalities + custom commands +
431
+ // the user profile. Additive merge (never clobbers local files).
432
+ const { pullPersonalities, pullCommands, pullUserProfile } = await import('../utils/codeepCloud.js');
433
433
  const pCount = await pullPersonalities();
434
434
  if (typeof pCount === 'number' && pCount > 0) {
435
435
  console.log(` Pulled ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
@@ -438,6 +438,10 @@ Commands (in chat):
438
438
  if (typeof cCount === 'number' && cCount > 0) {
439
439
  console.log(` Pulled ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
440
440
  }
441
+ const profPulled = await pullUserProfile();
442
+ if (profPulled === 1) {
443
+ console.log(' Pulled your profile (about you).');
444
+ }
441
445
  console.log('');
442
446
  process.exit(0);
443
447
  }
@@ -465,8 +469,8 @@ Commands (in chat):
465
469
  process.stdout.write(` Pushing ${count} key${count !== 1 ? 's' : ''} to codeep.dev...`);
466
470
  const ok = await pushKeys(keys);
467
471
  console.log(ok ? ' done.' : ' failed.');
468
- // Also push portable personal config.
469
- const { pushPersonalities, pushCommands } = await import('../utils/codeepCloud.js');
472
+ // Also push portable personal config — personalities + commands + profile.
473
+ const { pushPersonalities, pushCommands, pushUserProfile } = await import('../utils/codeepCloud.js');
470
474
  const pCount = await pushPersonalities();
471
475
  if (typeof pCount === 'number' && pCount > 0) {
472
476
  console.log(` Pushed ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
@@ -475,6 +479,9 @@ Commands (in chat):
475
479
  if (typeof cCount === 'number' && cCount > 0) {
476
480
  console.log(` Pushed ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
477
481
  }
482
+ if (await pushUserProfile()) {
483
+ console.log(' Pushed your profile (about you).');
484
+ }
478
485
  console.log('');
479
486
  process.exit(ok ? 0 : 1);
480
487
  }
@@ -373,6 +373,38 @@ additionalTools) {
373
373
  if (prefs)
374
374
  openRouterExtras.provider = prefs;
375
375
  }
376
+ // Opt-in native Ollama transport for AGENT mode. When `ollamaNativeApi`
377
+ // is on, route through /api/chat (honors num_ctx + keep_alive + the real
378
+ // context window) and parse native tool_calls. OFF by default → the /v1
379
+ // path below runs unchanged. Returns the agent response directly.
380
+ if (providerId === 'ollama' && config.get('ollamaNativeApi') === true) {
381
+ const { streamOllamaNativeChat, getOllamaContextLength } = await import('../api/ollamaNative.js');
382
+ const ollamaUrl = config.get('ollamaUrl') || 'http://localhost:11434';
383
+ const cfgCtx = Number(config.get('ollamaNumCtx')) || 0;
384
+ const numCtx = cfgCtx > 0 ? cfgCtx : ((await getOllamaContextLength(model, ollamaUrl)) ?? undefined);
385
+ const res = await streamOllamaNativeChat({
386
+ baseUrl: ollamaUrl,
387
+ model,
388
+ messages: [],
389
+ rawMessages: [{ role: 'system', content: systemPrompt }, ...messages],
390
+ tools: getOpenAITools(additionalTools),
391
+ numCtx,
392
+ keepAlive: config.get('ollamaKeepAlive') || undefined,
393
+ temperature: requiresDefaultTemperature(providerId) ? undefined : Number(config.get('temperature')),
394
+ timeoutMs,
395
+ onChunk: useStreaming ? onChunk : undefined,
396
+ });
397
+ if (res.promptTokens != null && res.completionTokens != null) {
398
+ recordTokenUsage({ promptTokens: res.promptTokens, completionTokens: res.completionTokens, totalTokens: res.promptTokens + res.completionTokens }, model, providerId);
399
+ }
400
+ const toolCalls = res.toolCalls.map((tc) => ({ tool: tc.name, parameters: tc.arguments }));
401
+ if (toolCalls.length === 0 && res.text) {
402
+ const textCalls = parseToolCalls(res.text);
403
+ if (textCalls.length > 0)
404
+ return { content: res.text, toolCalls: textCalls, usedNativeTools: false };
405
+ }
406
+ return { content: res.text, toolCalls, usedNativeTools: true };
407
+ }
376
408
  body = {
377
409
  model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
378
410
  tools: getOpenAITools(additionalTools), tool_choice: 'auto', stream: useStreaming,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Curated catalog of recommended local coding models for Ollama, surfaced via
3
+ * `/model browse`. Mirrors the MCP catalog pattern: a hand-picked shortlist so
4
+ * users don't have to hunt ollama.com for good coding models.
5
+ *
6
+ * Each entry's `pull` is the exact `ollama pull` tag. `params` is the parameter
7
+ * count (drives the agent-mode hint — <7B models struggle with tool-calling
8
+ * loops). `vram` is a rough "comfortable" requirement for the default quant.
9
+ *
10
+ * Curated, not exhaustive — users can still `/model pull <anything>`.
11
+ */
12
+ export interface OllamaCatalogEntry {
13
+ /** Exact `ollama pull` tag, e.g. "qwen2.5-coder:7b". */
14
+ pull: string;
15
+ /** Display name. */
16
+ name: string;
17
+ /** Parameter count in billions (for the agent-mode hint). */
18
+ params: number;
19
+ /** Rough comfortable RAM/VRAM for the default quantization. */
20
+ vram: string;
21
+ /** One-line description. */
22
+ description: string;
23
+ }
24
+ /**
25
+ * Recommended coding models. Ordered roughly best-for-agent first within size
26
+ * tiers. All are real Ollama library tags as of this release.
27
+ */
28
+ export declare const OLLAMA_CODING_MODELS: OllamaCatalogEntry[];
29
+ /** Agent-mode suitability hint for a catalog entry (≥7B → agent-capable). */
30
+ export declare function catalogAgentHint(params: number): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Curated catalog of recommended local coding models for Ollama, surfaced via
3
+ * `/model browse`. Mirrors the MCP catalog pattern: a hand-picked shortlist so
4
+ * users don't have to hunt ollama.com for good coding models.
5
+ *
6
+ * Each entry's `pull` is the exact `ollama pull` tag. `params` is the parameter
7
+ * count (drives the agent-mode hint — <7B models struggle with tool-calling
8
+ * loops). `vram` is a rough "comfortable" requirement for the default quant.
9
+ *
10
+ * Curated, not exhaustive — users can still `/model pull <anything>`.
11
+ */
12
+ /**
13
+ * Recommended coding models. Ordered roughly best-for-agent first within size
14
+ * tiers. All are real Ollama library tags as of this release.
15
+ */
16
+ export const OLLAMA_CODING_MODELS = [
17
+ // ── Coding-tuned, agent-capable ──────────────────────────────────────────
18
+ { pull: 'qwen2.5-coder:32b', name: 'Qwen2.5 Coder 32B', params: 32, vram: '~20 GB', description: 'Top open coding model — strong at tool use and multi-file edits' },
19
+ { pull: 'qwen2.5-coder:14b', name: 'Qwen2.5 Coder 14B', params: 14, vram: '~9 GB', description: 'Great balance of quality and speed for agent work' },
20
+ { pull: 'qwen2.5-coder:7b', name: 'Qwen2.5 Coder 7B', params: 7, vram: '~5 GB', description: 'Smallest Qwen Coder that still handles agent mode' },
21
+ { pull: 'deepseek-coder-v2:16b', name: 'DeepSeek Coder V2 16B', params: 16, vram: '~10 GB', description: 'MoE coding model — fast for its quality' },
22
+ // ── General-purpose, agent-capable ───────────────────────────────────────
23
+ { pull: 'llama3.1:8b', name: 'Llama 3.1 8B', params: 8, vram: '~5 GB', description: 'Solid all-rounder; reliable tool-calling for its size' },
24
+ { pull: 'llama3.1:70b', name: 'Llama 3.1 70B', params: 70, vram: '~40 GB', description: 'Large general model — strongest reasoning if you have the VRAM' },
25
+ { pull: 'mistral-nemo:12b', name: 'Mistral Nemo 12B', params: 12, vram: '~8 GB', description: '128K context general model, good instruction following' },
26
+ { pull: 'gemma2:9b', name: 'Gemma 2 9B', params: 9, vram: '~6 GB', description: 'Google open model — capable general assistant' },
27
+ // ── Reasoning ────────────────────────────────────────────────────────────
28
+ { pull: 'deepseek-r1:14b', name: 'DeepSeek R1 14B', params: 14, vram: '~9 GB', description: 'Reasoning model — thinks before answering; good for hard bugs' },
29
+ { pull: 'deepseek-r1:8b', name: 'DeepSeek R1 8B', params: 8, vram: '~5 GB', description: 'Smaller reasoning model for modest hardware' },
30
+ // ── Small / fast (chat-first; weaker at agent loops) ─────────────────────
31
+ { pull: 'qwen2.5-coder:3b', name: 'Qwen2.5 Coder 3B', params: 3, vram: '~3 GB', description: 'Fast inline completions / chat; light for full agent tasks' },
32
+ { pull: 'llama3.2:3b', name: 'Llama 3.2 3B', params: 3, vram: '~3 GB', description: 'Tiny + fast; great for quick chat on low-end machines' },
33
+ ];
34
+ /** Agent-mode suitability hint for a catalog entry (≥7B → agent-capable). */
35
+ export function catalogAgentHint(params) {
36
+ return params >= 7 ? '✓ agent mode' : '⚠ chat / completions (small)';
37
+ }
@@ -16,6 +16,7 @@ const MODEL_CONTEXT_WINDOWS = {
16
16
  'gpt-5.4-mini': 400_000,
17
17
  'gpt-5.4-nano': 400_000,
18
18
  // Anthropic
19
+ 'claude-opus-4-8': 1_000_000,
19
20
  'claude-opus-4-7': 1_000_000,
20
21
  'claude-opus-4-6': 1_000_000,
21
22
  'claude-sonnet-4-6': 1_000_000,
@@ -25,6 +26,7 @@ const MODEL_CONTEXT_WINDOWS = {
25
26
  'deepseek-v4-flash': 1_000_000,
26
27
  // Google
27
28
  'gemini-3.1-pro-preview': 1_048_576,
29
+ 'gemini-3.5-flash': 1_000_000,
28
30
  'gemini-3-flash-preview': 1_000_000,
29
31
  // MiniMax
30
32
  'MiniMax-M2.7': 204_800,
@@ -50,6 +52,7 @@ const MODEL_PRICING = {
50
52
  'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
51
53
  'gpt-5.4-nano': { inputPer1M: 0.20, outputPer1M: 1.25 },
52
54
  // Anthropic
55
+ 'claude-opus-4-8': { inputPer1M: 5.00, outputPer1M: 25.00 },
53
56
  'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00 },
54
57
  'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00 },
55
58
  'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
@@ -59,6 +62,7 @@ const MODEL_PRICING = {
59
62
  'deepseek-v4-flash': { inputPer1M: 0.14, outputPer1M: 0.28 },
60
63
  // Google
61
64
  'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
65
+ 'gemini-3.5-flash': { inputPer1M: 1.50, outputPer1M: 9.00 },
62
66
  'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
63
67
  // MiniMax
64
68
  'MiniMax-M2.7': { inputPer1M: 0.30, outputPer1M: 1.20 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
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",