codeep 2.18.1 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +53 -16
  2. package/dist/acp/commands.js +11 -55
  3. package/dist/acp/protocol.d.ts +34 -0
  4. package/dist/acp/server.d.ts +6 -1
  5. package/dist/acp/server.js +97 -2
  6. package/dist/api/index.js +9 -0
  7. package/dist/commands/core/index.d.ts +19 -0
  8. package/dist/commands/core/index.js +28 -0
  9. package/dist/commands/core/keysync.d.ts +2 -0
  10. package/dist/commands/core/keysync.js +34 -0
  11. package/dist/commands/core/telemetry.d.ts +2 -0
  12. package/dist/commands/core/telemetry.js +34 -0
  13. package/dist/config/index.js +2 -2
  14. package/dist/renderer/App.d.ts +9 -48
  15. package/dist/renderer/App.js +113 -338
  16. package/dist/renderer/Screen.d.ts +13 -0
  17. package/dist/renderer/Screen.js +22 -0
  18. package/dist/renderer/commands/registry.js +3 -3
  19. package/dist/renderer/commands.js +19 -51
  20. package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
  21. package/dist/renderer/components/CommandAutocomplete.js +103 -0
  22. package/dist/renderer/components/HunkPicker.d.ts +48 -0
  23. package/dist/renderer/components/HunkPicker.js +140 -0
  24. package/dist/renderer/components/MentionPicker.d.ts +60 -0
  25. package/dist/renderer/components/MentionPicker.js +111 -0
  26. package/dist/renderer/components/PasteDialog.d.ts +43 -0
  27. package/dist/renderer/components/PasteDialog.js +70 -0
  28. package/dist/renderer/layout.js +1 -0
  29. package/dist/renderer/main.js +15 -39
  30. package/dist/utils/agent.js +121 -26
  31. package/dist/utils/agentChat.d.ts +11 -4
  32. package/dist/utils/agentChat.js +53 -25
  33. package/dist/utils/codeepCloud.d.ts +3 -0
  34. package/dist/utils/codeepCloud.js +62 -7
  35. package/dist/utils/personalities.d.ts +63 -5
  36. package/dist/utils/personalities.js +583 -31
  37. package/dist/utils/shell.d.ts +11 -1
  38. package/dist/utils/shell.js +169 -82
  39. package/dist/utils/ssrfGuard.d.ts +18 -0
  40. package/dist/utils/ssrfGuard.js +83 -0
  41. package/dist/utils/taskPlanner.d.ts +7 -1
  42. package/dist/utils/taskPlanner.js +16 -7
  43. package/dist/utils/toolExecution.d.ts +1 -0
  44. package/dist/utils/toolExecution.js +48 -88
  45. package/dist/utils/tools.d.ts +3 -3
  46. package/dist/utils/tools.js +18 -13
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -677,9 +677,10 @@ Then call it as `/sec-review src/api/login.ts` (or `/sec` via the alias).
677
677
  **Discovery:** `/commands` lists all available templates. Project files shadow
678
678
  global files with the same name. Aliases also work for autocomplete.
679
679
 
680
- ### Personalities (`/personality`, new in 2.0.3)
680
+ ### Personalities and custom bots (`/personality`)
681
681
 
682
- Swap how the agent talks and what it prioritises mid-conversation:
682
+ Built-in personalities change tone and priorities. A structured custom bot can
683
+ also pin a model, restrict runtime tools, and limit where it is available:
683
684
 
684
685
  ```
685
686
  /personality # list available
@@ -694,23 +695,56 @@ Six built-in presets: `concise`, `verbose`, `security`, `senior-reviewer`,
694
695
  `junior-mentor`, `ship-it`. The active one persists across sessions
695
696
  (stored in `~/.codeep/config.json` as `activePersonality`).
696
697
 
697
- **Custom personalities** — drop a Markdown file in
698
+ **Custom bots** — build one in **Dashboard → Agent Studio**, or drop a Markdown
699
+ file in
698
700
  `.codeep/personalities/<name>.md` (project) or
699
701
  `~/.codeep/personalities/<name>.md` (global):
700
702
 
701
703
  ```markdown
702
- # Personality: PR Reviewer
704
+ ---
705
+ codeep: custom-bot/v1
706
+ description: Reviews changes without modifying the project.
707
+ model: automatic
708
+ tools: [files, git]
709
+ scope: selected
710
+ projects: [Codeep]
711
+ ---
712
+ # PR Reviewer
713
+
714
+ ## Responsibility
715
+ Review the requested change and cite concrete evidence.
716
+
717
+ ## Always
718
+ - Call out untested behavior.
703
719
 
704
- You are reviewing a PR from a junior engineer:
705
- - Cite line numbers for every concern.
706
- - Suggest an alternative, don't just flag the problem.
707
- - Keep tone collaborative, not pedantic.
708
- - End with one thing the author did well.
720
+ ## Never
721
+ - Modify files or publish changes.
722
+
723
+ ## Advanced instructions
724
+ Keep the tone collaborative and end with the highest-risk finding.
709
725
  ```
710
726
 
711
- First `# Personality:` line is the display name; the rest is appended
712
- to the agent's system prompt verbatim when active. Project shadows
713
- global shadows built-in (by name).
727
+ Portable tool groups are `files`, `terminal`, `tests`, `git`, `web`, and
728
+ `mcp`; unselected groups are removed from the model's runtime and checked
729
+ again before execution. Missing, empty, or malformed `tools` metadata in a v1
730
+ file creates a conversation-only bot. Use
731
+ `scope: all`, `selected`, or `personal`; selected scope matches the project
732
+ directory basename against `projects` (with optional `*` and `?` wildcards).
733
+ An exact `provider/model` applies only to
734
+ that run, while `automatic` inherits the current selection.
735
+
736
+ Portable v1 fails closed for unclassified capabilities: Skills, delegation,
737
+ and vision are not exposed to structured bots yet. MCP is available only when
738
+ the active ACP session has registered those exact tools.
739
+
740
+ Legacy prompt-only Markdown remains supported and unrestricted. The older
741
+ section-only Agent Studio format is recognized when it contains
742
+ `Responsibility` plus another standard section; without a Tools section it
743
+ also stays unrestricted until explicitly converted. Invalid structured model
744
+ or scope metadata makes the bot unavailable rather than silently widening its
745
+ runtime. An explicit unsupported `codeep` schema marker is also unavailable;
746
+ it is never reinterpreted as legacy. Project files shadow global files, which
747
+ shadow built-ins with the same name.
714
748
 
715
749
  ### Activity Insights (`/insights`, new in 2.0.3)
716
750
 
@@ -885,10 +919,13 @@ codeep account push # Upload local personalities + commands to codeep.dev
885
919
  codeep account sync # Download them onto a new machine
886
920
  ```
887
921
 
888
- Merging is **additive** — `sync` only writes files that don't already exist
889
- locally, so it never clobbers a personality or command you've edited on this
890
- machine. View what's synced (and prune stale entries) under **Personalities**
891
- and **Custom commands** on the [dashboard](https://codeep.dev/dashboard).
922
+ A manual `sync` treats the dashboard personality as current. When a changed
923
+ cloud personality replaces a local file, Codeep first saves the local version
924
+ under `~/.codeep/backups/personalities/` and then swaps the new file in
925
+ atomically. Custom commands keep the older **additive** merge and are never
926
+ overwritten by a pull. View and edit custom bots in **Agent Studio**, and prune
927
+ commands under **Custom commands**, on the
928
+ [dashboard](https://codeep.dev/dashboard).
892
929
 
893
930
  Hooks and MCP server configs are deliberately **not** synced: hooks run
894
931
  arbitrary shell, and MCP configs often embed tokens, so both stay local to each
@@ -2,8 +2,10 @@
2
2
  // Slash command handler for ACP sessions.
3
3
  // Mirrors CLI commands from renderer/commands.ts but returns plain text
4
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, isTelemetryEnabled, telemetryForcedOffByEnv, isKeySyncEnabled, keySyncForcedOffByEnv, } from '../config/index.js';
5
+ import { config, getCurrentProvider, getModelsForCurrentProvider, setProvider, setApiKey, isConfigured, listSessionsWithInfo, startNewSession, loadSession, saveSession, initializeAsProject, isManuallyInitializedProject, setProjectPermission, hasWritePermission, hasReadPermission, } from '../config/index.js';
6
6
  import { getProviderList, getProvider } from '../config/providers.js';
7
+ import { telemetryCommand } from '../commands/core/telemetry.js';
8
+ import { keysyncCommand } from '../commands/core/keysync.js';
7
9
  import { getProjectContext } from '../utils/project.js';
8
10
  import { loadCustomCommands } from '../utils/customCommands.js';
9
11
  import { summarizeHooks } from '../utils/hooks.js';
@@ -210,60 +212,14 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
210
212
  return { handled: true, response: await setApiKeyCmd(args[0]) };
211
213
  }
212
214
  case 'telemetry': {
213
- const sub = args[0]?.toLowerCase();
214
- const envOff = telemetryForcedOffByEnv();
215
- if (sub === 'on' || sub === 'off') {
216
- if (envOff) {
217
- return { handled: true, response: 'Telemetry is forced **off** by the `CODEEP_NO_TELEMETRY` / `DO_NOT_TRACK` env var — unset it to change this. The config flag can\'t override an env var.' };
218
- }
219
- config.set('telemetry', sub === 'on');
220
- return {
221
- handled: true,
222
- response: sub === 'on'
223
- ? 'Telemetry **on** — usage stats, session transcripts, progress and memory notes sync to codeep.dev.'
224
- : 'Telemetry **off** — no automatic cloud uploads. Explicit `/account push` still works.',
225
- };
226
- }
227
- if (sub && sub !== 'status') {
228
- return { handled: true, response: 'Usage: `/telemetry` · `/telemetry on` · `/telemetry off`' };
229
- }
230
- const flag = config.get('telemetry') !== false;
231
- const lines = [
232
- `**Telemetry:** ${isTelemetryEnabled() ? 'on' : 'off'}`,
233
- `- Config flag \`telemetry\`: ${flag}`,
234
- ];
235
- if (envOff)
236
- lines.push('- Forced **off** by `CODEEP_NO_TELEMETRY` / `DO_NOT_TRACK` (env overrides the flag).');
237
- lines.push('', 'Toggle with `/telemetry on` | `/telemetry off`. Controls automatic uploads of usage stats, session transcripts, progress, and memory notes.');
238
- return { handled: true, response: lines.join('\n') };
215
+ // Core semantics in commands/core/telemetry.ts — this surface only renders.
216
+ const result = telemetryCommand(args);
217
+ return { handled: true, response: result.message };
239
218
  }
240
219
  case 'keysync': {
241
- const sub = args[0]?.toLowerCase();
242
- const envOff = keySyncForcedOffByEnv();
243
- if (sub === 'on' || sub === 'off') {
244
- if (envOff) {
245
- return { handled: true, response: 'Cloud key sync is forced **off** by the `CODEEP_NO_KEY_SYNC` env var — unset it to change this. The config flag can\'t override an env var.' };
246
- }
247
- config.set('syncKeysToCloud', sub === 'on');
248
- return {
249
- handled: true,
250
- response: sub === 'on'
251
- ? 'Cloud key sync **on** — `codeep account push`/`sync` will now upload/download API keys. Note: synced keys are stored server-readable on codeep.dev.'
252
- : 'Cloud key sync **off** — API keys stay in your OS keychain only. (`codeep account purge-keys` wipes any keys already on the server.)',
253
- };
254
- }
255
- if (sub && sub !== 'status') {
256
- return { handled: true, response: 'Usage: `/keysync` · `/keysync on` · `/keysync off`' };
257
- }
258
- const flag = config.get('syncKeysToCloud') === true;
259
- const lines = [
260
- `**Cloud key sync:** ${isKeySyncEnabled() ? 'on' : 'off'}`,
261
- `- Config flag \`syncKeysToCloud\`: ${flag}`,
262
- ];
263
- if (envOff)
264
- lines.push('- Forced **off** by `CODEEP_NO_KEY_SYNC` (env overrides the flag).');
265
- lines.push('', 'OFF by default — API keys live only in your OS keychain unless enabled. When on, `codeep account push`/`sync` move keys, stored **server-readable** on codeep.dev.');
266
- return { handled: true, response: lines.join('\n') };
220
+ // Core semantics in commands/core/keysync.ts — this surface only renders.
221
+ const result = keysyncCommand(args);
222
+ return { handled: true, response: result.message };
267
223
  }
268
224
  case 'login': {
269
225
  const [providerId, apiKey] = args;
@@ -681,7 +637,7 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
681
637
  }
682
638
  // ─── Personalities + insights (2.0.3) ─────────────────────────────────────
683
639
  case 'personality': {
684
- const { formatPersonalityList, findPersonality } = await import('../utils/personalities.js');
640
+ const { formatPersonalityList, findPersonality, formatPersonalityActivation } = await import('../utils/personalities.js');
685
641
  const sub = args[0]?.toLowerCase();
686
642
  if (!sub) {
687
643
  return { handled: true, response: formatPersonalityList(session.workspaceRoot) };
@@ -697,7 +653,7 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
697
653
  config.set('activePersonality', p.name);
698
654
  return {
699
655
  handled: true,
700
- response: `Active personality: **${p.displayName}** (\`${p.name}\`, ${p.scope})\n\n_${p.description}_\n\nClear with \`/personality off\`.`,
656
+ response: formatPersonalityActivation(p),
701
657
  };
702
658
  }
703
659
  case 'agents': {
@@ -268,6 +268,40 @@ export interface ListSessionsResult {
268
268
  sessions: AcpSessionInfo[];
269
269
  nextCursor?: string | null;
270
270
  }
271
+ export interface ListPersonalitiesParams {
272
+ sessionId: string;
273
+ }
274
+ export interface AcpPersonalityInfo {
275
+ name: string;
276
+ displayName: string;
277
+ description: string;
278
+ structured: boolean;
279
+ /** True only when Tools was explicitly declared; [] then means conversation-only. */
280
+ restrictTools: boolean;
281
+ scope: 'builtin' | 'project' | 'global';
282
+ model: string;
283
+ tools: string[];
284
+ projectScope: 'all' | 'selected' | 'personal' | 'unspecified';
285
+ projects: string[];
286
+ available: boolean;
287
+ }
288
+ export interface ListPersonalitiesResult {
289
+ personalities: AcpPersonalityInfo[];
290
+ activePersonality: string | null;
291
+ }
292
+ export interface SetPersonalityParams {
293
+ sessionId: string;
294
+ personalityId: string | null;
295
+ }
296
+ export interface SetPersonalityResult {
297
+ activePersonality: string | null;
298
+ }
299
+ export interface SyncPersonalitiesParams {
300
+ sessionId: string;
301
+ }
302
+ export interface SyncPersonalitiesResult extends ListPersonalitiesResult {
303
+ updated: number;
304
+ }
271
305
  export interface DeleteSessionParams {
272
306
  sessionId: string;
273
307
  cwd?: string;
@@ -1,4 +1,5 @@
1
- import { SessionModeState, SessionConfigOption } from './protocol.js';
1
+ import { SessionModeState, SessionConfigOption, ListPersonalitiesResult } from './protocol.js';
2
+ import { type Personality } from '../utils/personalities.js';
2
3
  export declare const AGENT_MODES: SessionModeState;
3
4
  /**
4
5
  * Format a tool call's parameters into a human-readable object for the
@@ -39,4 +40,8 @@ export declare function providerHasKey(providerId: string): boolean;
39
40
  * Exported for unit testing (see server.test.ts).
40
41
  */
41
42
  export declare function buildConfigOptions(): SessionConfigOption[];
43
+ /** Build the stable Codeep ACP personality extension payload for a workspace. */
44
+ export declare function buildPersonalityListResult(workspaceRoot: string): ListPersonalitiesResult;
45
+ /** Resolve an ACP selection using the same scope/model availability gate as list. */
46
+ export declare function resolvePersonalitySelection(personalityId: unknown, workspaceRoot: string): Personality | null;
42
47
  export declare function startAcpServer(): Promise<void>;
@@ -17,10 +17,11 @@ import { autoSaveSession, config, getApiKey, getConfiguredProviders } from '../c
17
17
  import { ApiError } from '../api/index.js';
18
18
  import { PROVIDERS } from '../config/providers.js';
19
19
  import { getCurrentVersion } from '../utils/update.js';
20
- import { reportStats, syncSession, generateProjectId } from '../utils/codeepCloud.js';
20
+ import { reportStats, syncSession, generateProjectId, pullPersonalities } from '../utils/codeepCloud.js';
21
21
  import { getCostBreakdown, getRecordCount, createTokenScope, runWithTokenScope } from '../utils/tokenTracker.js';
22
22
  import { isGitRepository } from '../utils/git.js';
23
23
  import { getProjectContext } from '../utils/project.js';
24
+ import { findPersonality, isPersonalityAvailable, loadAllPersonalities } from '../utils/personalities.js';
24
25
  // ─── Slash commands advertised to Zed ────────────────────────────────────────
25
26
  const AVAILABLE_COMMANDS = [
26
27
  // Configuration
@@ -58,7 +59,7 @@ const AVAILABLE_COMMANDS = [
58
59
  { name: 'plan', description: 'Generate a numbered plan for a task — review before /go executes', input: { hint: '<task>' } },
59
60
  { name: 'go', description: 'Execute the pending plan from /plan' },
60
61
  // Personalities + insights (2.0.3)
61
- { name: 'personality', description: 'List or switch agent tone preset', input: { hint: '[name | off]' } },
62
+ { name: 'personality', description: 'List or switch a personality or structured custom bot', input: { hint: '[name | off]' } },
62
63
  { 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]' } },
63
64
  { name: 'agents', description: 'List sub-agents the agent can delegate self-contained tasks to' },
64
65
  { name: 'insights', description: 'Activity summary over the last N days (default 7)', input: { hint: '[--days N]' } },
@@ -366,6 +367,34 @@ export function buildConfigOptions() {
366
367
  },
367
368
  ];
368
369
  }
370
+ /** Build the stable Codeep ACP personality extension payload for a workspace. */
371
+ export function buildPersonalityListResult(workspaceRoot) {
372
+ const personalities = loadAllPersonalities(workspaceRoot).map(personality => ({
373
+ name: personality.name,
374
+ displayName: personality.displayName,
375
+ description: personality.description,
376
+ structured: personality.structured === true,
377
+ restrictTools: personality.restrictTools === true,
378
+ scope: personality.scope,
379
+ model: personality.structured ? (personality.modelPreference ?? 'automatic') : 'automatic',
380
+ tools: personality.structured ? (personality.tools ?? []) : [],
381
+ projectScope: personality.projectScope ?? 'unspecified',
382
+ projects: personality.projects ?? [],
383
+ available: isPersonalityAvailable(personality, workspaceRoot),
384
+ }));
385
+ const configuredActive = config.get('activePersonality') ?? null;
386
+ const activePersonality = configuredActive && personalities.some(personality => personality.name === configuredActive && personality.available)
387
+ ? configuredActive
388
+ : null;
389
+ return { personalities, activePersonality };
390
+ }
391
+ /** Resolve an ACP selection using the same scope/model availability gate as list. */
392
+ export function resolvePersonalitySelection(personalityId, workspaceRoot) {
393
+ if (typeof personalityId !== 'string')
394
+ return null;
395
+ const personality = findPersonality(personalityId, workspaceRoot);
396
+ return personality && isPersonalityAvailable(personality, workspaceRoot) ? personality : null;
397
+ }
369
398
  // ─── Server ───────────────────────────────────────────────────────────────────
370
399
  export function startAcpServer() {
371
400
  const transport = new StdioTransport();
@@ -440,6 +469,15 @@ export function startAcpServer() {
440
469
  case 'session/list_providers':
441
470
  handleListProviders(req);
442
471
  break;
472
+ case 'session/list_personalities':
473
+ handleListPersonalities(req);
474
+ break;
475
+ case 'session/set_personality':
476
+ handleSetPersonality(req);
477
+ break;
478
+ case 'session/sync_personalities':
479
+ handleSyncPersonalities(req);
480
+ break;
443
481
  default:
444
482
  process.stderr.write(`[codeep-acp] Unknown method: ${req.method}\n`);
445
483
  transport.error(req.id, -32601, `Method not found: ${req.method}`);
@@ -783,6 +821,63 @@ export function startAcpServer() {
783
821
  function handleListProviders(msg) {
784
822
  handleListProvidersExternal(msg, handlerDeps);
785
823
  }
824
+ // ── Codeep personality extensions ─────────────────────────────────────────
825
+ // These methods are intentionally additive to ACP v1. VS Code can render a
826
+ // native picker without scraping markdown from `/personality`; clients that
827
+ // do not know the extension continue using the slash command unchanged.
828
+ function personalityListResult(sessionId) {
829
+ const session = sessions.get(sessionId);
830
+ if (!session)
831
+ return null;
832
+ return buildPersonalityListResult(session.workspaceRoot);
833
+ }
834
+ function handleListPersonalities(msg) {
835
+ const params = (msg.params ?? {});
836
+ const result = personalityListResult(params.sessionId);
837
+ if (!result) {
838
+ transport.error(msg.id, -32602, `Unknown sessionId: ${params.sessionId}`);
839
+ return;
840
+ }
841
+ transport.respond(msg.id, result);
842
+ }
843
+ function handleSetPersonality(msg) {
844
+ const params = (msg.params ?? {});
845
+ const session = sessions.get(params.sessionId);
846
+ if (!session) {
847
+ transport.error(msg.id, -32602, `Unknown sessionId: ${params.sessionId}`);
848
+ return;
849
+ }
850
+ if (params.personalityId === null) {
851
+ config.set('activePersonality', null);
852
+ const result = { activePersonality: null };
853
+ transport.respond(msg.id, result);
854
+ return;
855
+ }
856
+ const selected = resolvePersonalitySelection(params.personalityId, session.workspaceRoot);
857
+ if (!selected) {
858
+ transport.error(msg.id, -32602, `Personality is unknown or unavailable here: ${String(params.personalityId)}`);
859
+ return;
860
+ }
861
+ const personalityId = params.personalityId.toLowerCase();
862
+ config.set('activePersonality', personalityId);
863
+ const result = { activePersonality: personalityId };
864
+ transport.respond(msg.id, result);
865
+ }
866
+ async function handleSyncPersonalities(msg) {
867
+ const params = (msg.params ?? {});
868
+ if (!sessions.has(params.sessionId)) {
869
+ transport.error(msg.id, -32602, `Unknown sessionId: ${params.sessionId}`);
870
+ return;
871
+ }
872
+ const updated = await pullPersonalities();
873
+ if (updated === null) {
874
+ transport.error(msg.id, -32001, 'Personality sync failed or this device is not linked to codeep.dev.');
875
+ return;
876
+ }
877
+ const list = personalityListResult(params.sessionId);
878
+ const result = { updated, ...list };
879
+ transport.respond(msg.id, result);
880
+ }
786
881
  // ── session/prompt ──────────────────────────────────────────────────────────
787
882
  async function handleSessionPrompt(msg) {
788
883
  const params = msg.params;
package/dist/api/index.js CHANGED
@@ -2,6 +2,7 @@ import * as http from 'node:http';
2
2
  import * as https from 'node:https';
3
3
  import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
4
4
  import { withRetry, isNetworkError } from '../utils/retry.js';
5
+ import { checkApiRateLimit } from '../utils/ratelimit.js';
5
6
  import { getProvider, getProviderBaseUrl, getProviderAuthHeader, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, reasoningParamsFor } from '../config/providers.js';
6
7
  import { logApiRequest, logApiResponse } from '../utils/logger.js';
7
8
  import { loadProjectIntelligence, generateContextFromIntelligence } from '../utils/projectIntelligence.js';
@@ -154,6 +155,14 @@ export async function chat(message, history = [], onChunk, onRetry, projectConte
154
155
  const protocol = config.get('protocol');
155
156
  const model = config.get('model');
156
157
  const providerId = config.get('provider');
158
+ // Global API throttle — the single choke point every surface funnels
159
+ // through (TUI chat, agent loop, ACP sessions, sub-agents, session
160
+ // titles). Without this, a runaway agent loop burns provider quota and
161
+ // hits provider-side 429s with nothing on our side slowing it down.
162
+ const rateCheck = checkApiRateLimit();
163
+ if (!rateCheck.allowed) {
164
+ throw new ApiError(rateCheck.message || 'API rate limit exceeded', 429);
165
+ }
157
166
  const { isNoApiKeyProvider } = await import('../config/providers.js');
158
167
  const apiKey = getApiKey() || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
159
168
  if (!apiKey) {
@@ -0,0 +1,19 @@
1
+ /** Result of executing a core command. Plain data — no UI. */
2
+ export interface CommandResult {
3
+ /** Outcome for the user, in a presentation-neutral voice. */
4
+ message: string;
5
+ /** Severity hints the surface's rendering (banner vs error style). */
6
+ kind?: 'ok' | 'info' | 'warn' | 'error';
7
+ /** Optional follow-up action the surface may offer (unused for now). */
8
+ hint?: string;
9
+ }
10
+ /** Context a core command may need. All fields optional — commands declare
11
+ * what they use via their signature, surfaces pass what they have. */
12
+ export interface CoreCommandContext {
13
+ /** Raw args after the command name (e.g. ['on'] for `/telemetry on`). */
14
+ args: string[];
15
+ }
16
+ export declare function ok(message: string): CommandResult;
17
+ export declare function info(message: string): CommandResult;
18
+ export declare function warn(message: string): CommandResult;
19
+ export declare function error(message: string): CommandResult;
@@ -0,0 +1,28 @@
1
+ // commands/core/index.ts
2
+ // Shared command core — single implementation of command semantics used by
3
+ // BOTH dispatch surfaces (TUI renderer/commands.ts and ACP acp/commands.ts).
4
+ //
5
+ // Problem this solves: ~40 commands are implemented twice with different
6
+ // presentation (TUI: app.notify / interactive pickers; ACP: response
7
+ // strings). Every behavior change had to be made twice, and they drifted.
8
+ //
9
+ // Contract: a core command does validation + state mutation and returns a
10
+ // CommandResult — plain data describing the outcome. The surface adapter
11
+ // (TUI or ACP) renders that result in its own style. Side-effectful UI
12
+ // (pickers, dialogs) stays in the surface layer; pure decisions live here.
13
+ //
14
+ // Migration is incremental: each command moves over one at a time, in its
15
+ // own change, with the old inline branches deleted only once both surfaces
16
+ // call the core.
17
+ export function ok(message) {
18
+ return { message, kind: 'ok' };
19
+ }
20
+ export function info(message) {
21
+ return { message, kind: 'info' };
22
+ }
23
+ export function warn(message) {
24
+ return { message, kind: 'warn' };
25
+ }
26
+ export function error(message) {
27
+ return { message, kind: 'error' };
28
+ }
@@ -0,0 +1,2 @@
1
+ import { type CommandResult } from './index';
2
+ export declare function keysyncCommand(args: string[]): CommandResult;
@@ -0,0 +1,34 @@
1
+ // commands/core/keysync.ts
2
+ // Core semantics of `/keysync` — shared by TUI and ACP dispatch.
3
+ //
4
+ // Single source of truth for: env-forced-off check, config toggle, and the
5
+ // status report facts (including the server-readable-keys disclosure that
6
+ // must always accompany enabling sync).
7
+ import { config, keySyncForcedOffByEnv, isKeySyncEnabled } from '../../config/index.js';
8
+ import { ok, warn, info } from './index.js';
9
+ export function keysyncCommand(args) {
10
+ const sub = args[0]?.toLowerCase();
11
+ const envOff = keySyncForcedOffByEnv();
12
+ if (sub === 'on' || sub === 'off') {
13
+ if (envOff) {
14
+ return warn('Cloud key sync is forced off by CODEEP_NO_KEY_SYNC — unset that env var to change it. The config flag can\'t override an env var.');
15
+ }
16
+ config.set('syncKeysToCloud', sub === 'on');
17
+ return ok(sub === 'on'
18
+ ? 'Cloud key sync on — `codeep account push`/`sync` will now upload/download API keys. Note: synced keys are stored server-readable on codeep.dev.'
19
+ : 'Cloud key sync off — API keys stay in your OS keychain only. (`codeep account purge-keys` wipes any keys already on the server.)');
20
+ }
21
+ if (sub && sub !== 'status') {
22
+ return info('Usage: /keysync · /keysync on · /keysync off');
23
+ }
24
+ const flag = config.get('syncKeysToCloud') === true;
25
+ const lines = [
26
+ `Cloud key sync: ${isKeySyncEnabled() ? 'on' : 'off'}`,
27
+ `- Config flag \`syncKeysToCloud\`: ${flag}`,
28
+ ];
29
+ if (envOff) {
30
+ lines.push('- Forced off by `CODEEP_NO_KEY_SYNC` (env overrides the flag).');
31
+ }
32
+ lines.push('', 'OFF by default — API keys live only in your OS keychain unless enabled. When on, `codeep account push`/`sync` move keys, stored server-readable on codeep.dev.');
33
+ return info(lines.join('\n'));
34
+ }
@@ -0,0 +1,2 @@
1
+ import { type CommandResult } from './index';
2
+ export declare function telemetryCommand(args: string[]): CommandResult;
@@ -0,0 +1,34 @@
1
+ // commands/core/telemetry.ts
2
+ // Core semantics of `/telemetry` — shared by TUI and ACP dispatch.
3
+ //
4
+ // Single source of truth for: env-forced-off check, config toggle,
5
+ // and the status report facts. Presentation (markdown vs banner) is the
6
+ // surface's job; this returns plain-data CommandResults.
7
+ import { config, isTelemetryEnabled, telemetryForcedOffByEnv } from '../../config/index.js';
8
+ import { ok, warn, info } from './index.js';
9
+ export function telemetryCommand(args) {
10
+ const sub = args[0]?.toLowerCase();
11
+ const envOff = telemetryForcedOffByEnv();
12
+ if (sub === 'on' || sub === 'off') {
13
+ if (envOff) {
14
+ return warn('Telemetry is forced off by CODEEP_NO_TELEMETRY / DO_NOT_TRACK — unset that env var to change it. The config flag can\'t override an env var.');
15
+ }
16
+ config.set('telemetry', sub === 'on');
17
+ return ok(sub === 'on'
18
+ ? 'Telemetry on — usage stats, session transcripts, progress and memory notes sync to codeep.dev.'
19
+ : 'Telemetry off — no automatic cloud uploads. Explicit /account push still works.');
20
+ }
21
+ if (sub && sub !== 'status') {
22
+ return info('Usage: /telemetry · /telemetry on · /telemetry off');
23
+ }
24
+ const flag = config.get('telemetry') !== false;
25
+ const lines = [
26
+ `Telemetry: ${isTelemetryEnabled() ? 'on' : 'off'}`,
27
+ `- Config flag \`telemetry\`: ${flag}`,
28
+ ];
29
+ if (envOff) {
30
+ lines.push('- Forced off by `CODEEP_NO_TELEMETRY` / `DO_NOT_TRACK` (env overrides the flag).');
31
+ }
32
+ lines.push('', 'Toggle with `/telemetry on` | `/telemetry off`. Controls automatic uploads of usage stats, session transcripts, progress, and memory notes.');
33
+ return info(lines.join('\n'));
34
+ }
@@ -182,8 +182,8 @@ function createConfig() {
182
182
  maxTokens: 32768,
183
183
  reasoningEffort: 'auto',
184
184
  apiTimeout: 60000,
185
- rateLimitApi: 10000,
186
- rateLimitCommands: 10000,
185
+ rateLimitApi: 240, // API requests per minute — generous for a 50-iteration agent, still stops runaway loops
186
+ rateLimitCommands: 120, // Commands per minute
187
187
  projectPermissions: [],
188
188
  providerApiKeys: [],
189
189
  configuredProviderIds: [],
@@ -20,37 +20,13 @@ export interface ConfirmOptions {
20
20
  onConfirm: () => void;
21
21
  onCancel?: () => void;
22
22
  }
23
+ export { HunkPickerItem, HunkPickerOptions } from './components/HunkPicker';
24
+ import { type HunkPickerOptions } from './components/HunkPicker';
23
25
  /**
24
- * One hunk in the interactive `/apply --interactive` picker.
25
- * `lines` are already-formatted diff lines (e.g. `+ added`, `- removed`).
26
+ * Options for the interactive hunk picker — see components/HunkPicker.ts.
27
+ * (`onComplete` fires once with the accepted [path, hunkIndex] pairs so the
28
+ * caller can apply them via `applyHunksToFiles`.)
26
29
  */
27
- export interface HunkPickerItem {
28
- /** File path this hunk belongs to. */
29
- path: string;
30
- /** 0-based hunk index within the file diff. */
31
- hunkIndex: number;
32
- /** Human-readable hunk header, e.g. `@@ -12,3 +12,5 @@`. */
33
- header: string;
34
- /** Pre-formatted diff lines to display. */
35
- lines: string[];
36
- }
37
- /**
38
- * Options for the interactive hunk picker. The picker walks the user
39
- * through `items` one at a time; for each they accept (`y`/Enter) or
40
- * skip (`n`). `a` accepts all remaining, `q`/Esc quits.
41
- *
42
- * `onComplete` fires once with the set of accepted `[path, hunkIndex]`
43
- * pairs (possibly empty) so the caller can apply them via
44
- * `applyHunksToFiles`.
45
- */
46
- export interface HunkPickerOptions {
47
- title: string;
48
- items: HunkPickerItem[];
49
- onComplete: (accepted: Array<{
50
- path: string;
51
- hunkIndex: number;
52
- }>) => void;
53
- }
54
30
  export interface AppOptions {
55
31
  onSubmit: (message: string) => Promise<void>;
56
32
  onCommand: (command: string, args: string[]) => void;
@@ -93,30 +69,19 @@ export declare class App {
93
69
  private appStartedAt;
94
70
  /** Start of the current agent run; unlike app uptime, resets per task. */
95
71
  private agentStartedAt;
96
- private pasteInfo;
97
- private pasteInfoOpen;
72
+ private pasteDialog;
98
73
  private codeBlockCounter;
99
74
  private messageCache;
100
75
  private helpOpen;
101
76
  private helpScrollIndex;
102
77
  private statusOpen;
103
78
  private settingsState;
104
- private showAutocomplete;
105
- private autocompleteIndex;
106
- private autocompleteItems;
107
- private showMentionAutocomplete;
108
- private mentionIndex;
109
- private mentionItems;
110
- private mentionAtStart;
111
- /** Project root for resolving `suggestMentions`. Cached per update. */
112
- private mentionRoot;
79
+ private autocomplete;
80
+ private mention;
113
81
  private confirmOpen;
114
82
  private confirmOptions;
115
83
  private confirmSelection;
116
- private hunkPickerOpen;
117
- private hunkPickerOptions;
118
- private hunkPickerIndex;
119
- private hunkPickerAccepted;
84
+ private hunkPicker;
120
85
  private menuOpen;
121
86
  private menuTitle;
122
87
  /** Filtered view shown to the user; derived from `menuItemsAll` + `menuFilter`. */
@@ -449,10 +414,6 @@ export declare class App {
449
414
  * Render inline confirmation dialog below status bar
450
415
  */
451
416
  private renderInlineConfirm;
452
- /**
453
- * Render inline hunk picker (`/apply --interactive`).
454
- * Shows the current hunk's diff + the y/n/a/q key legend.
455
- */
456
417
  private renderInlineHunkPicker;
457
418
  /**
458
419
  * Render input line