codeep 1.2.27 → 1.2.28

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.
@@ -1,10 +1,25 @@
1
+ import { Message } from '../config/index.js';
2
+ export interface AcpSession {
3
+ sessionId: string;
4
+ workspaceRoot: string;
5
+ /** In-memory message history for the ACP conversation */
6
+ history: Message[];
7
+ /** Codeep session name (maps to .codeep/sessions/<name>.json) */
8
+ codeepSessionId: string;
9
+ }
1
10
  export interface CommandResult {
2
11
  handled: boolean;
3
12
  response: string;
4
13
  }
5
14
  /**
6
- * Try to handle a slash command. Returns { handled: true, response } if the
7
- * input was a command, or { handled: false, response: '' } to let it pass
8
- * through to the agent loop.
15
+ * Ensure workspace has a .codeep folder, initialise it as a project if needed,
16
+ * and load the most recent session (or start a new one).
17
+ *
18
+ * Returns the welcome message to stream back to the client.
9
19
  */
10
- export declare function handleCommand(input: string): CommandResult;
20
+ export declare function initWorkspace(workspaceRoot: string): {
21
+ codeepSessionId: string;
22
+ history: Message[];
23
+ welcomeText: string;
24
+ };
25
+ export declare function handleCommand(input: string, session: AcpSession): CommandResult;
@@ -1,43 +1,157 @@
1
- // src/acp/commands.ts
1
+ // acp/commands.ts
2
2
  // Slash command handler for ACP sessions.
3
- // Commands are intercepted before the agent loop and handled directly.
4
- import { config, setProvider, getModelsForCurrentProvider, setApiKey, getMaskedApiKey } from '../config/index.js';
5
- import { PROVIDERS } from '../config/providers.js';
3
+ // Mirrors CLI commands from renderer/commands.ts but returns plain text
4
+ // responses (no TUI) suitable for streaming back via session/update.
5
+ import { config, getCurrentProvider, getModelsForCurrentProvider, setProvider, setApiKey, isConfigured, listSessionsWithInfo, startNewSession, loadSession, saveSession, initializeAsProject, isManuallyInitializedProject, setProjectPermission, hasWritePermission, hasReadPermission, } from '../config/index.js';
6
+ import { getProviderList, getProvider } from '../config/providers.js';
7
+ import { getProjectContext } from '../utils/project.js';
8
+ import { existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ // ─── Workspace / session init (called on session/new) ─────────────────────────
6
11
  /**
7
- * Try to handle a slash command. Returns { handled: true, response } if the
8
- * input was a command, or { handled: false, response: '' } to let it pass
9
- * through to the agent loop.
12
+ * Ensure workspace has a .codeep folder, initialise it as a project if needed,
13
+ * and load the most recent session (or start a new one).
14
+ *
15
+ * Returns the welcome message to stream back to the client.
10
16
  */
11
- export function handleCommand(input) {
17
+ export function initWorkspace(workspaceRoot) {
18
+ // 1. Ensure .codeep directory exists
19
+ const codeepDir = join(workspaceRoot, '.codeep');
20
+ if (!existsSync(codeepDir)) {
21
+ mkdirSync(codeepDir, { recursive: true });
22
+ }
23
+ // 2. Auto-initialize as project if not already (workspace root is always a project in ACP context)
24
+ if (!isManuallyInitializedProject(workspaceRoot)) {
25
+ initializeAsProject(workspaceRoot);
26
+ }
27
+ // 3. Grant read+write permission for this workspace (ACP always has access)
28
+ if (!hasReadPermission(workspaceRoot)) {
29
+ setProjectPermission(workspaceRoot, true, true);
30
+ }
31
+ // 4. Load most recent session, or start fresh
32
+ const sessions = listSessionsWithInfo(workspaceRoot);
33
+ let codeepSessionId;
34
+ let history = [];
35
+ if (sessions.length > 0) {
36
+ const latest = sessions[0]; // already sorted newest-first
37
+ const loaded = loadSession(latest.name, workspaceRoot);
38
+ if (loaded) {
39
+ codeepSessionId = latest.name;
40
+ history = loaded;
41
+ }
42
+ else {
43
+ codeepSessionId = startNewSession();
44
+ }
45
+ }
46
+ else {
47
+ codeepSessionId = startNewSession();
48
+ }
49
+ // 5. Build welcome text
50
+ const provider = getCurrentProvider();
51
+ const model = config.get('model');
52
+ const projectCtx = getProjectContext(workspaceRoot);
53
+ const hasWrite = hasWritePermission(workspaceRoot);
54
+ const lines = [
55
+ `**Codeep** • ${provider.name} • \`${model}\``,
56
+ '',
57
+ `**Workspace:** ${workspaceRoot}`,
58
+ projectCtx
59
+ ? `**Project:** ${projectCtx.name} (${projectCtx.type})`
60
+ : '**Project:** detected',
61
+ hasWrite ? '**Access:** Read & Write' : '**Access:** Read only',
62
+ '',
63
+ sessions.length > 0
64
+ ? `**Session:** ${codeepSessionId} (${history.length} messages restored)`
65
+ : '**Session:** new',
66
+ '',
67
+ 'Type `/help` to see available commands.',
68
+ ];
69
+ return { codeepSessionId, history, welcomeText: lines.join('\n') };
70
+ }
71
+ // ─── Command dispatch ─────────────────────────────────────────────────────────
72
+ export function handleCommand(input, session) {
12
73
  const trimmed = input.trim();
13
74
  if (!trimmed.startsWith('/'))
14
75
  return { handled: false, response: '' };
15
- const [cmd, ...args] = trimmed.slice(1).split(/\s+/);
16
- switch (cmd.toLowerCase()) {
76
+ const [rawCmd, ...args] = trimmed.slice(1).split(/\s+/);
77
+ const cmd = rawCmd.toLowerCase();
78
+ switch (cmd) {
17
79
  case 'help':
18
80
  return { handled: true, response: buildHelp() };
19
- case 'settings':
20
- return { handled: true, response: buildSettings() };
81
+ case 'status':
82
+ return { handled: true, response: buildStatus(session) };
83
+ case 'version': {
84
+ const provider = getCurrentProvider();
85
+ const model = config.get('model');
86
+ return { handled: true, response: `Codeep • ${provider.name} • \`${model}\`` };
87
+ }
21
88
  case 'provider': {
22
- const id = args[0];
23
- if (!id)
89
+ if (!args.length)
24
90
  return { handled: true, response: buildProviderList() };
25
- return { handled: true, response: setProviderCmd(id) };
91
+ return { handled: true, response: setProviderCmd(args[0]) };
26
92
  }
27
93
  case 'model': {
28
- const id = args[0];
29
- if (!id)
94
+ if (!args.length)
30
95
  return { handled: true, response: buildModelList() };
31
- return { handled: true, response: setModelCmd(id) };
96
+ return { handled: true, response: setModelCmd(args[0]) };
32
97
  }
33
98
  case 'apikey': {
34
- const key = args[0];
35
- if (!key)
99
+ if (!args.length)
36
100
  return { handled: true, response: showApiKey() };
37
- return { handled: true, response: setApiKeyCmd(key) };
101
+ return { handled: true, response: setApiKeyCmd(args[0]) };
102
+ }
103
+ case 'login': {
104
+ // In ACP context login = set API key for a provider
105
+ // Usage: /login <providerId> <apiKey>
106
+ const [providerId, apiKey] = args;
107
+ if (!providerId || !apiKey) {
108
+ return { handled: true, response: 'Usage: `/login <providerId> <apiKey>`\n\n' + buildProviderList() };
109
+ }
110
+ return { handled: true, response: loginCmd(providerId, apiKey) };
111
+ }
112
+ case 'sessions': {
113
+ return { handled: true, response: buildSessionList(session.workspaceRoot) };
114
+ }
115
+ case 'session': {
116
+ const sub = args[0];
117
+ if (sub === 'new') {
118
+ const id = startNewSession();
119
+ session.codeepSessionId = id;
120
+ session.history = [];
121
+ return { handled: true, response: `New session started: \`${id}\`` };
122
+ }
123
+ if (sub === 'load' && args[1]) {
124
+ const loaded = loadSession(args[1], session.workspaceRoot);
125
+ if (loaded) {
126
+ session.codeepSessionId = args[1];
127
+ session.history = loaded;
128
+ return { handled: true, response: `Session loaded: \`${args[1]}\` (${session.history.length} messages)` };
129
+ }
130
+ return { handled: true, response: `Session not found: \`${args[1]}\`` };
131
+ }
132
+ return { handled: true, response: 'Usage: `/session new` or `/session load <name>`' };
133
+ }
134
+ case 'save': {
135
+ const name = args.length ? args.join('-') : session.codeepSessionId;
136
+ if (saveSession(name, session.history, session.workspaceRoot)) {
137
+ session.codeepSessionId = name;
138
+ return { handled: true, response: `Session saved as: \`${name}\`` };
139
+ }
140
+ return { handled: true, response: 'Failed to save session.' };
141
+ }
142
+ case 'grant': {
143
+ setProjectPermission(session.workspaceRoot, true, true);
144
+ const ctx = getProjectContext(session.workspaceRoot);
145
+ return { handled: true, response: `Write access granted for \`${ctx?.name || session.workspaceRoot}\`` };
146
+ }
147
+ case 'lang': {
148
+ if (!args.length) {
149
+ const current = config.get('language') || 'auto';
150
+ return { handled: true, response: `Current language: \`${current}\`. Usage: \`/lang <code>\` (e.g. \`en\`, \`hr\`, \`auto\`)` };
151
+ }
152
+ config.set('language', args[0]);
153
+ return { handled: true, response: `Language set to \`${args[0]}\`` };
38
154
  }
39
- case 'status':
40
- return { handled: true, response: buildSettings() };
41
155
  default:
42
156
  return {
43
157
  handled: true,
@@ -45,7 +159,7 @@ export function handleCommand(input) {
45
159
  };
46
160
  }
47
161
  }
48
- // ─── renderers ────────────────────────────────────────────────────────────────
162
+ // ─── Renderers ────────────────────────────────────────────────────────────────
49
163
  function buildHelp() {
50
164
  return [
51
165
  '## Codeep Commands',
@@ -53,50 +167,58 @@ function buildHelp() {
53
167
  '| Command | Description |',
54
168
  '|---------|-------------|',
55
169
  '| `/help` | Show this help |',
56
- '| `/settings` | Show current configuration |',
170
+ '| `/status` | Show current configuration and session info |',
171
+ '| `/version` | Show version and current model |',
57
172
  '| `/provider` | List available providers |',
58
- '| `/provider <id>` | Switch to a provider (e.g. `/provider anthropic`) |',
173
+ '| `/provider <id>` | Switch provider (e.g. `/provider anthropic`) |',
59
174
  '| `/model` | List models for current provider |',
60
175
  '| `/model <id>` | Switch model (e.g. `/model claude-opus-4-5`) |',
176
+ '| `/login <providerId> <apiKey>` | Set API key for a provider |',
177
+ '| `/apikey` | Show masked API key for current provider |',
61
178
  '| `/apikey <key>` | Set API key for current provider |',
62
- '| `/apikey` | Show masked API key |',
63
- '| `/status` | Same as /settings |',
179
+ '| `/sessions` | List saved sessions |',
180
+ '| `/session new` | Start a new session |',
181
+ '| `/session load <name>` | Load a saved session |',
182
+ '| `/save [name]` | Save current session |',
183
+ '| `/grant` | Grant write access for current workspace |',
184
+ '| `/lang <code>` | Set response language (e.g. `en`, `hr`, `auto`) |',
64
185
  ].join('\n');
65
186
  }
66
- function buildSettings() {
67
- const provider = config.get('provider');
187
+ function buildStatus(session) {
188
+ const provider = getCurrentProvider();
68
189
  const model = config.get('model');
69
- const protocol = config.get('protocol');
70
- const maskedKey = getMaskedApiKey(provider);
71
- const providerConfig = PROVIDERS[provider];
190
+ const lang = config.get('language') || 'auto';
191
+ const hasWrite = hasWritePermission(session.workspaceRoot);
192
+ const configured = isConfigured();
72
193
  return [
73
- '## Current Configuration',
194
+ '## Current Status',
74
195
  '',
75
- `- **Provider:** ${providerConfig?.name ?? provider} (\`${provider}\`)`,
196
+ `- **Provider:** ${provider.name} (\`${provider.id}\`)`,
76
197
  `- **Model:** \`${model}\``,
77
- `- **Protocol:** \`${protocol}\``,
78
- `- **API Key:** ${maskedKey ? `\`${maskedKey}\`` : '_not set_'}`,
79
- '',
80
- 'Use `/provider`, `/model`, or `/apikey` to change settings.',
198
+ `- **API Key:** ${configured ? 'configured' : '_not set_ — use `/login` or `/apikey`_'}`,
199
+ `- **Language:** \`${lang}\``,
200
+ `- **Workspace:** \`${session.workspaceRoot}\``,
201
+ `- **Access:** ${hasWrite ? 'Read & Write' : 'Read only'}`,
202
+ `- **Session:** \`${session.codeepSessionId}\` (${session.history.length} messages)`,
81
203
  ].join('\n');
82
204
  }
83
205
  function buildProviderList() {
84
- const current = config.get('provider');
206
+ const current = getCurrentProvider();
207
+ const providers = getProviderList();
85
208
  const lines = ['## Available Providers', ''];
86
- for (const [id, p] of Object.entries(PROVIDERS)) {
87
- const marker = id === current ? ' ✓' : '';
88
- lines.push(`- \`${id}\`${marker} — **${p.name}**: ${p.description}`);
209
+ for (const p of providers) {
210
+ const marker = p.id === current.id ? ' ✓' : '';
211
+ lines.push(`- \`${p.id}\`${marker} — **${p.name}**: ${p.description || ''}`);
89
212
  }
90
213
  lines.push('', 'Use `/provider <id>` to switch.');
91
214
  return lines.join('\n');
92
215
  }
93
216
  function setProviderCmd(id) {
94
- if (!PROVIDERS[id]) {
217
+ const provider = getProvider(id);
218
+ if (!provider)
95
219
  return `Provider \`${id}\` not found.\n\n${buildProviderList()}`;
96
- }
97
220
  setProvider(id);
98
- const p = PROVIDERS[id];
99
- return `Switched to **${p.name}** (\`${id}\`). Default model: \`${p.defaultModel}\`.`;
221
+ return `Switched to **${provider.name}** (\`${id}\`). Default model: \`${provider.defaultModel}\`.`;
100
222
  }
101
223
  function buildModelList() {
102
224
  const current = config.get('model');
@@ -111,21 +233,40 @@ function buildModelList() {
111
233
  }
112
234
  function setModelCmd(id) {
113
235
  const models = getModelsForCurrentProvider();
114
- if (!models[id]) {
115
- return `Model \`${id}\` not available for current provider.\n\n${buildModelList()}`;
116
- }
236
+ if (!models[id])
237
+ return `Model \`${id}\` not available.\n\n${buildModelList()}`;
117
238
  config.set('model', id);
118
239
  return `Model set to \`${id}\`.`;
119
240
  }
120
241
  function showApiKey() {
121
- const provider = config.get('provider');
122
- const masked = getMaskedApiKey(provider);
123
- return masked
124
- ? `API key for \`${provider}\`: \`${masked}\``
125
- : `No API key set for \`${provider}\`. Use \`/apikey <key>\` to set one.`;
242
+ const providerId = getCurrentProvider().id;
243
+ const configured = isConfigured(providerId);
244
+ return configured
245
+ ? `API key for \`${providerId}\`: configured (use \`/apikey <key>\` to update)`
246
+ : `No API key set for \`${providerId}\`. Use \`/apikey <key>\` to set one.`;
126
247
  }
127
248
  function setApiKeyCmd(key) {
128
- const provider = config.get('provider');
129
- setApiKey(key, provider);
130
- return `API key for \`${provider}\` saved.`;
249
+ const providerId = getCurrentProvider().id;
250
+ // setApiKey is async (keychain) — fire-and-forget, config cache updated synchronously
251
+ setApiKey(key, providerId);
252
+ return `API key for \`${providerId}\` saved.`;
253
+ }
254
+ function loginCmd(providerId, apiKey) {
255
+ const provider = getProvider(providerId);
256
+ if (!provider)
257
+ return `Provider \`${providerId}\` not found.\n\n${buildProviderList()}`;
258
+ setProvider(providerId);
259
+ setApiKey(apiKey, providerId);
260
+ return `Logged in as **${provider.name}** (\`${providerId}\`). Model: \`${provider.defaultModel}\`.`;
261
+ }
262
+ function buildSessionList(workspaceRoot) {
263
+ const sessions = listSessionsWithInfo(workspaceRoot);
264
+ if (sessions.length === 0)
265
+ return 'No saved sessions. Start chatting to create one.';
266
+ const lines = ['## Saved Sessions', ''];
267
+ for (const s of sessions) {
268
+ lines.push(`- \`${s.name}\` — ${s.messageCount} messages — ${new Date(s.createdAt).toLocaleString()}`);
269
+ }
270
+ lines.push('', 'Use `/session load <name>` to restore.');
271
+ return lines.join('\n');
131
272
  }
@@ -3,9 +3,11 @@
3
3
  import { randomUUID } from 'crypto';
4
4
  import { StdioTransport } from './transport.js';
5
5
  import { runAgentSession } from './session.js';
6
+ import { initWorkspace, handleCommand } from './commands.js';
7
+ import { autoSaveSession } from '../config/index.js';
6
8
  export function startAcpServer() {
7
9
  const transport = new StdioTransport();
8
- // sessionId → { workspaceRoot, abortController }
10
+ // ACP sessionId → full AcpSession (includes history + codeep session tracking)
9
11
  const sessions = new Map();
10
12
  transport.start((msg) => {
11
13
  switch (msg.method) {
@@ -46,9 +48,25 @@ export function startAcpServer() {
46
48
  }
47
49
  function handleSessionNew(msg) {
48
50
  const params = msg.params;
49
- const sessionId = randomUUID();
50
- sessions.set(sessionId, { workspaceRoot: params.cwd, abortController: null });
51
- transport.respond(msg.id, { sessionId });
51
+ const acpSessionId = randomUUID();
52
+ // Initialise workspace: create .codeep folder, load/create codeep session
53
+ const { codeepSessionId, history, welcomeText } = initWorkspace(params.cwd);
54
+ sessions.set(acpSessionId, {
55
+ sessionId: acpSessionId,
56
+ workspaceRoot: params.cwd,
57
+ history,
58
+ codeepSessionId,
59
+ abortController: null,
60
+ });
61
+ transport.respond(msg.id, { sessionId: acpSessionId });
62
+ // Stream welcome message so the client sees it immediately after session/new
63
+ transport.notify('session/update', {
64
+ sessionId: acpSessionId,
65
+ update: {
66
+ sessionUpdate: 'agent_message_chunk',
67
+ content: { type: 'text', text: welcomeText },
68
+ },
69
+ });
52
70
  }
53
71
  function handleSessionPrompt(msg) {
54
72
  const params = msg.params;
@@ -62,6 +80,19 @@ export function startAcpServer() {
62
80
  .filter((b) => b.type === 'text')
63
81
  .map((b) => b.text)
64
82
  .join('\n');
83
+ // Handle slash commands — no agent loop needed
84
+ const cmd = handleCommand(prompt, session);
85
+ if (cmd.handled) {
86
+ transport.notify('session/update', {
87
+ sessionId: params.sessionId,
88
+ update: {
89
+ sessionUpdate: 'agent_message_chunk',
90
+ content: { type: 'text', text: cmd.response },
91
+ },
92
+ });
93
+ transport.respond(msg.id, { stopReason: 'end_turn' });
94
+ return;
95
+ }
65
96
  const abortController = new AbortController();
66
97
  session.abortController = abortController;
67
98
  runAgentSession({
@@ -82,6 +113,9 @@ export function startAcpServer() {
82
113
  // file edits are streamed via onChunk for now
83
114
  },
84
115
  }).then(() => {
116
+ // Persist conversation history after each agent turn
117
+ session.history.push({ role: 'user', content: prompt });
118
+ autoSaveSession(session.history, session.workspaceRoot);
85
119
  transport.respond(msg.id, { stopReason: 'end_turn' });
86
120
  }).catch((err) => {
87
121
  if (err.name === 'AbortError') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
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",