codeep 2.18.1 → 2.20.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 (51) 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/config/providers.js +7 -4
  15. package/dist/renderer/App.d.ts +9 -48
  16. package/dist/renderer/App.js +113 -338
  17. package/dist/renderer/Screen.d.ts +13 -0
  18. package/dist/renderer/Screen.js +22 -0
  19. package/dist/renderer/commands/registry.js +3 -3
  20. package/dist/renderer/commands.js +19 -51
  21. package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
  22. package/dist/renderer/components/CommandAutocomplete.js +103 -0
  23. package/dist/renderer/components/HunkPicker.d.ts +48 -0
  24. package/dist/renderer/components/HunkPicker.js +140 -0
  25. package/dist/renderer/components/MentionPicker.d.ts +60 -0
  26. package/dist/renderer/components/MentionPicker.js +111 -0
  27. package/dist/renderer/components/PasteDialog.d.ts +43 -0
  28. package/dist/renderer/components/PasteDialog.js +70 -0
  29. package/dist/renderer/layout.js +1 -0
  30. package/dist/renderer/main.js +15 -39
  31. package/dist/utils/agent.js +121 -26
  32. package/dist/utils/agentChat.d.ts +11 -4
  33. package/dist/utils/agentChat.js +53 -25
  34. package/dist/utils/codeepCloud.d.ts +3 -0
  35. package/dist/utils/codeepCloud.js +62 -7
  36. package/dist/utils/personalities.d.ts +63 -5
  37. package/dist/utils/personalities.js +583 -31
  38. package/dist/utils/shell.d.ts +11 -1
  39. package/dist/utils/shell.js +169 -82
  40. package/dist/utils/ssrfGuard.d.ts +18 -0
  41. package/dist/utils/ssrfGuard.js +83 -0
  42. package/dist/utils/taskPlanner.d.ts +7 -1
  43. package/dist/utils/taskPlanner.js +16 -7
  44. package/dist/utils/tokenTracker.js +5 -3
  45. package/dist/utils/toolExecution.d.ts +1 -0
  46. package/dist/utils/toolExecution.js +48 -88
  47. package/dist/utils/tools.d.ts +3 -3
  48. package/dist/utils/tools.js +18 -13
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +1 -1
@@ -23,6 +23,8 @@ import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './t
23
23
  import { parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
24
24
  import { formatToolDefinitions, getOpenAITools, getAnthropicTools } from './tools.js';
25
25
  import { readOpenRouterPreferences } from './openrouterPrefs.js';
26
+ import { checkApiRateLimit } from './ratelimit.js';
27
+ import { ApiError } from '../api/index.js';
26
28
  import { handleStream, handleOpenAIAgentStream, handleAnthropicAgentStream } from './agentStream.js';
27
29
  import { logger } from './logger.js';
28
30
  const debug = (...args) => {
@@ -282,21 +284,24 @@ export async function summarizeEarlierHistory(history, maxChars = 16000) {
282
284
  return ''; // graceful — recent verbatim history still gets injected
283
285
  }
284
286
  }
285
- export function getAgentSystemPrompt(projectContext) {
287
+ export function getAgentSystemPrompt(projectContext, runtime) {
286
288
  const root = projectContext.root || process.cwd();
287
289
  // State the real underlying model/provider so "which model are you"
288
290
  // gets a truthful answer instead of a hallucinated one.
289
- const model = String(config.get('model') || '');
290
- const providerId = String(config.get('provider') || '');
291
+ const model = String(runtime?.model || config.get('model') || '');
292
+ const providerId = String(runtime?.providerId || config.get('provider') || '');
291
293
  const identity = model
292
294
  ? `You are Codeep, an autonomous AI coding agent operating inside this project. The underlying model is \`${model}\` (via ${providerId}). If asked which model or provider you are, answer truthfully with these details. Never claim to be Claude or any other model unless that is genuinely the configured model.`
293
295
  : `You are Codeep, an autonomous AI coding agent operating inside this project. Never refer to yourself as Claude or any other AI unless that is genuinely the configured model.`;
296
+ const toolInstructions = runtime?.allowedToolNames !== undefined
297
+ ? `This run has an enforced tool allowlist. Only these tools may be used: ${runtime.allowedToolNames.length ? runtime.allowedToolNames.slice(0, 50).join(', ') : '(none)'}. Do not request or claim access to any other tool.`
298
+ : `- read_file / write_file / edit_file / delete_file — file ops (prefer edit_file for modifications to keep surrounding content intact)
299
+ - create_directory / list_files / search_code — project navigation
300
+ - execute_command — ONLY for package managers & version control: npm, yarn, pnpm, bun, git, composer, pip, cargo, go, make. Never for ls/cat/grep/mkdir/rm/cp/mv/touch — use the dedicated tools.`;
294
301
  return `${identity}
295
302
 
296
303
  ## Tools
297
- - read_file / write_file / edit_file / delete_file — file ops (prefer edit_file for modifications to keep surrounding content intact)
298
- - create_directory / list_files / search_code — project navigation
299
- - execute_command — ONLY for package managers & version control: npm, yarn, pnpm, bun, git, composer, pip, cargo, go, make. Never for ls/cat/grep/mkdir/rm/cp/mv/touch — use the dedicated tools.
304
+ ${toolInstructions}
300
305
 
301
306
  ## Behavior
302
307
  - Do what the user asked — in whatever language they wrote. Tool names stay English.
@@ -332,8 +337,9 @@ ${projectContext.structure ? `\n## Project Structure\n${projectContext.structure
332
337
  return intelligence ? `\n\n${generateContextFromIntelligence(intelligence)}` : '';
333
338
  })()}`;
334
339
  }
335
- export function getFallbackSystemPrompt(projectContext, additionalTools) {
336
- return getAgentSystemPrompt(projectContext) + '\n\n' + formatToolDefinitions(additionalTools);
340
+ export function getFallbackSystemPrompt(projectContext, additionalTools, runtime) {
341
+ const allowed = runtime?.allowedToolNames ? new Set(runtime.allowedToolNames) : undefined;
342
+ return getAgentSystemPrompt(projectContext, runtime) + '\n\n' + formatToolDefinitions(additionalTools, allowed);
337
343
  }
338
344
  /**
339
345
  * Make a chat API call for agent mode with native tool support.
@@ -346,17 +352,29 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
346
352
  * invoke). Optional — built-in tools work the same whether this is
347
353
  * omitted or an empty array.
348
354
  */
349
- additionalTools) {
350
- const protocol = config.get('protocol');
351
- const model = config.get('model');
352
- const providerId = config.get('provider');
353
- const apiKey = getApiKey() || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
355
+ additionalTools, runtime) {
356
+ const protocol = runtime?.protocol ?? config.get('protocol');
357
+ const model = runtime?.model ?? config.get('model');
358
+ const providerId = runtime?.providerId ?? config.get('provider');
359
+ const apiKey = getApiKey(providerId) || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
360
+ const allowedTools = runtime?.allowedToolNames ? new Set(runtime.allowedToolNames) : undefined;
354
361
  let baseUrl = resolveBaseUrl(providerId, protocol);
355
362
  const authHeader = getProviderAuthHeader(providerId, protocol);
356
363
  if (!baseUrl)
357
364
  throw new Error(`Provider ${providerId} does not support ${protocol} protocol`);
365
+ // Global API throttle — same choke point as api/chat(). Checked here so
366
+ // the agent loop (which can run up to agentMaxIterations iterations, each
367
+ // with its own API call) is rate-limited even when it never routes
368
+ // through api/chat(). Bypassed for no-key local providers (Ollama) —
369
+ // there is no quota to protect on localhost.
370
+ if (!isNoApiKeyProvider(providerId)) {
371
+ const rateCheck = checkApiRateLimit();
372
+ if (!rateCheck.allowed) {
373
+ throw new ApiError(rateCheck.message || 'API rate limit exceeded', 429);
374
+ }
375
+ }
358
376
  if (!supportsNativeTools(providerId, protocol)) {
359
- return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal);
377
+ return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout, additionalTools, runtime);
360
378
  }
361
379
  const controller = new AbortController();
362
380
  const timeoutMs = dynamicTimeout || config.get('apiTimeout');
@@ -427,7 +445,7 @@ additionalTools) {
427
445
  model,
428
446
  messages: [],
429
447
  rawMessages: [{ role: 'system', content: systemPrompt }, ...messages],
430
- tools: getOpenAITools(additionalTools),
448
+ tools: getOpenAITools(additionalTools, allowedTools),
431
449
  numCtx,
432
450
  keepAlive: config.get('ollamaKeepAlive') || undefined,
433
451
  temperature: requiresDefaultTemperature(providerId) ? undefined : Number(config.get('temperature')),
@@ -447,7 +465,7 @@ additionalTools) {
447
465
  }
448
466
  body = {
449
467
  model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
450
- tools: getOpenAITools(additionalTools), tool_choice: 'auto', stream: useStreaming,
468
+ tools: getOpenAITools(additionalTools, allowedTools), tool_choice: 'auto', stream: useStreaming,
451
469
  ...tempParam, ...tokParam, ...reasoningParam,
452
470
  // Ask ALL OpenAI-compatible providers to emit a usage block in the
453
471
  // stream — without this most (DeepSeek/Kimi/Grok/Qwen/GLM/…) send no
@@ -466,7 +484,7 @@ additionalTools) {
466
484
  // Cache hits cost 0.1× input. Misses ("cache creation") cost 1.25×.
467
485
  // Net win after the 2nd same-shape request. Below 1024 input tokens
468
486
  // Anthropic silently skips caching — no error path to handle.
469
- const anthropicTools = getAnthropicTools(additionalTools);
487
+ const anthropicTools = getAnthropicTools(additionalTools, allowedTools);
470
488
  const cachedTools = anthropicTools.length > 0
471
489
  ? [
472
490
  ...anthropicTools.slice(0, -1),
@@ -487,7 +505,7 @@ additionalTools) {
487
505
  if (!response.ok) {
488
506
  const errorText = await response.text();
489
507
  if (errorText.includes('tools') || errorText.includes('function') || response.status === 400) {
490
- return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal);
508
+ return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout, additionalTools, runtime);
491
509
  }
492
510
  throw new Error(`API error: ${response.status} - ${errorText}`);
493
511
  }
@@ -546,7 +564,7 @@ additionalTools) {
546
564
  throw error;
547
565
  }
548
566
  if (err.message.includes('tools') || err.message.includes('function')) {
549
- return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal);
567
+ return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout, additionalTools, runtime);
550
568
  }
551
569
  throw error;
552
570
  }
@@ -559,15 +577,25 @@ additionalTools) {
559
577
  /**
560
578
  * Fallback chat without native tools (text-based tool format)
561
579
  */
562
- export async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout) {
563
- const protocol = config.get('protocol');
564
- const model = config.get('model');
565
- const providerId = config.get('provider');
566
- const apiKey = getApiKey() || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
580
+ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout, additionalTools, runtime) {
581
+ const protocol = runtime?.protocol ?? config.get('protocol');
582
+ const model = runtime?.model ?? config.get('model');
583
+ const providerId = runtime?.providerId ?? config.get('provider');
584
+ const apiKey = getApiKey(providerId) || (isNoApiKeyProvider(providerId) ? 'ollama' : null);
585
+ const allowedTools = runtime?.allowedToolNames ? new Set(runtime.allowedToolNames) : undefined;
567
586
  let baseUrl = resolveBaseUrl(providerId, protocol);
568
587
  const authHeader = getProviderAuthHeader(providerId, protocol);
569
588
  if (!baseUrl)
570
589
  throw new Error(`Provider ${providerId} does not support ${protocol} protocol`);
590
+ // See rate-limit note in agentChat above — same choke point, same local-
591
+ // provider bypass. This path runs when the provider has no native tool
592
+ // support, so it's reached directly (not via agentChat's early return).
593
+ if (!isNoApiKeyProvider(providerId)) {
594
+ const rateCheck = checkApiRateLimit();
595
+ if (!rateCheck.allowed) {
596
+ throw new ApiError(rateCheck.message || 'API rate limit exceeded', 429);
597
+ }
598
+ }
571
599
  const controller = new AbortController();
572
600
  const timeoutMs = dynamicTimeout || config.get('apiTimeout');
573
601
  let isTimeout = false;
@@ -589,7 +617,7 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
589
617
  headers['anthropic-version'] = '2023-06-01';
590
618
  const fallbackPrompt = systemPrompt.includes('## Available Tools')
591
619
  ? systemPrompt
592
- : systemPrompt + '\n\n' + formatToolDefinitions();
620
+ : systemPrompt + '\n\n' + formatToolDefinitions(additionalTools, allowedTools);
593
621
  try {
594
622
  let endpoint;
595
623
  let body;
@@ -84,7 +84,9 @@ declare function readFileBundle(kind: 'personalities' | 'commands'): Record<stri
84
84
  * files. Only writes files that don't already exist (additive merge —
85
85
  * never clobber local edits). Returns the count of newly written files. */
86
86
  declare function writeFileBundle(kind: 'personalities' | 'commands', items: Record<string, string>): number;
87
+ declare function writePulledPersonalityBundle(items: Record<string, string>): number;
87
88
  export declare const pullPersonalities: () => Promise<number | null>;
89
+ export declare const getLastPersonalityPullBackupCount: () => number;
88
90
  export declare const pushPersonalities: () => Promise<number | null>;
89
91
  export declare const pullCommands: () => Promise<number | null>;
90
92
  export declare const pushCommands: () => Promise<number | null>;
@@ -181,4 +183,5 @@ export declare function syncMemoryNotes(projectName: string, notes: string[]): P
181
183
  export declare const _globalDirForTest: typeof globalDir;
182
184
  export declare const _readFileBundleForTest: typeof readFileBundle;
183
185
  export declare const _writeFileBundleForTest: typeof writeFileBundle;
186
+ export declare const _writePulledPersonalityBundleForTest: typeof writePulledPersonalityBundle;
184
187
  export {};
@@ -241,11 +241,11 @@ export async function purgeKeys() {
241
241
  //
242
242
  // Both are name → raw-.md-body bundles stored in a global dir
243
243
  // (~/.codeep/personalities, ~/.codeep/commands). The sync is bidirectional
244
- // and merge-based: pull writes any remote file not present locally; push
245
- // sends every local file. Last-write-wins on the server via upsert. We
246
- // never delete locally on pull — additive only, so a sync can't nuke
247
- // work you haven't pushed.
248
- import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from 'fs';
244
+ // and merge-based. Personality pulls are cloud-authoritative because the web
245
+ // builder edits the cloud copy: changed local files are backed up before the
246
+ // remote body replaces them. Commands retain the older additive-only merge.
247
+ // Pull never deletes a local file that disappeared from the server.
248
+ import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync, renameSync, unlinkSync } from 'fs';
249
249
  import { join } from 'path';
250
250
  import { homedir } from 'os';
251
251
  function globalDir(kind) {
@@ -284,7 +284,7 @@ function writeFileBundle(kind, items) {
284
284
  mkdirSync(dir, { recursive: true });
285
285
  let written = 0;
286
286
  for (const [name, body] of Object.entries(items)) {
287
- if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64 || typeof body !== 'string' || !body)
287
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64 || typeof body !== 'string' || !body || body.length > 64 * 1024)
288
288
  continue;
289
289
  const filePath = join(dir, `${name}.md`);
290
290
  if (existsSync(filePath))
@@ -297,6 +297,57 @@ function writeFileBundle(kind, items) {
297
297
  }
298
298
  return written;
299
299
  }
300
+ /** Apply a cloud personality bundle. Updated bodies replace the active local
301
+ * file so web edits actually take effect, but every divergent local body is
302
+ * first copied to ~/.codeep/backups/personalities/. */
303
+ let lastPersonalityPullBackupCount = 0;
304
+ function writePulledPersonalityBundle(items) {
305
+ lastPersonalityPullBackupCount = 0;
306
+ const dir = globalDir('personalities');
307
+ if (!existsSync(dir))
308
+ mkdirSync(dir, { recursive: true });
309
+ let written = 0;
310
+ for (const [name, body] of Object.entries(items)) {
311
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64 || typeof body !== 'string' || !body || body.length > 64 * 1024)
312
+ continue;
313
+ const filePath = join(dir, `${name}.md`);
314
+ let tempPath = '';
315
+ try {
316
+ if (existsSync(filePath)) {
317
+ const local = readFileSync(filePath, 'utf8');
318
+ if (local === body)
319
+ continue;
320
+ const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
321
+ if (!existsSync(backupDir))
322
+ mkdirSync(backupDir, { recursive: true });
323
+ let suffix = new Date().toISOString().replace(/[:.]/g, '-');
324
+ let backupPath = join(backupDir, `${name}-${suffix}.md`);
325
+ let collision = 1;
326
+ while (existsSync(backupPath)) {
327
+ backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
328
+ }
329
+ writeFileSync(backupPath, local);
330
+ lastPersonalityPullBackupCount++;
331
+ }
332
+ // Same-directory rename is atomic on supported local filesystems: a
333
+ // crash cannot leave a half-written active personality.
334
+ tempPath = join(dir, `.${name}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`);
335
+ writeFileSync(tempPath, body);
336
+ renameSync(tempPath, filePath);
337
+ written++;
338
+ }
339
+ catch {
340
+ // A backup/write failure leaves the existing active file untouched.
341
+ if (tempPath && existsSync(tempPath)) {
342
+ try {
343
+ unlinkSync(tempPath);
344
+ }
345
+ catch { /* best effort temp cleanup */ }
346
+ }
347
+ }
348
+ }
349
+ return written;
350
+ }
300
351
  async function pullBundle(kind) {
301
352
  const syncToken = getSyncToken();
302
353
  if (!syncToken)
@@ -308,7 +359,9 @@ async function pullBundle(kind) {
308
359
  const data = await res.json();
309
360
  if (!data.ok)
310
361
  return null;
311
- return writeFileBundle(kind, data.items ?? {});
362
+ return kind === 'personalities'
363
+ ? writePulledPersonalityBundle(data.items ?? {})
364
+ : writeFileBundle(kind, data.items ?? {});
312
365
  }
313
366
  catch {
314
367
  return null;
@@ -330,6 +383,7 @@ async function pushBundle(kind) {
330
383
  return res?.ok ? count : null;
331
384
  }
332
385
  export const pullPersonalities = () => pullBundle('personalities');
386
+ export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
333
387
  export const pushPersonalities = () => pushBundle('personalities');
334
388
  export const pullCommands = () => pullBundle('commands');
335
389
  export const pushCommands = () => pushBundle('commands');
@@ -644,3 +698,4 @@ async function fetchWithRetry(url, options, maxRetries = 2) {
644
698
  export const _globalDirForTest = globalDir;
645
699
  export const _readFileBundleForTest = readFileBundle;
646
700
  export const _writeFileBundleForTest = writeFileBundle;
701
+ export const _writePulledPersonalityBundleForTest = writePulledPersonalityBundle;
@@ -10,13 +10,21 @@
10
10
  *
11
11
  * Project shadows global shadows built-in, by name.
12
12
  *
13
- * File format (project / global):
13
+ * File format (project / global): legacy prompt-only Markdown remains valid;
14
+ * structured custom bots add a small versioned frontmatter block:
14
15
  * ```
15
- * # Personality: Concise Reviewer
16
- * <free-form Markdown body — gets appended to system prompt verbatim>
16
+ * ---
17
+ * codeep: custom-bot/v1
18
+ * model: automatic
19
+ * tools: [files, tests, git]
20
+ * scope: all
21
+ * projects: []
22
+ * ---
23
+ * # Concise Reviewer
24
+ * <behavior sections — appended to the system prompt>
17
25
  * ```
18
- * (The first H1 line is parsed as the display name; everything else is
19
- * the prompt body.)
26
+ * The first H1 is the display name. Tools/model/scope are enforced only for
27
+ * structured files; old files stay unrestricted.
20
28
  *
21
29
  * Activation:
22
30
  * - `config.activePersonality` holds the active name (or null/undefined
@@ -26,7 +34,15 @@
26
34
  * personality is active.
27
35
  * - Persists across sessions until cleared with `/personality off`.
28
36
  */
37
+ import type { ToolCall } from './tools.js';
29
38
  export type PersonalityScope = 'builtin' | 'project' | 'global';
39
+ export type PersonalityCapability = 'files' | 'terminal' | 'tests' | 'git' | 'web' | 'mcp';
40
+ export type PersonalityProjectScope = 'all' | 'selected' | 'personal' | 'unspecified';
41
+ export interface PersonalityRuntimeModel {
42
+ providerId: string;
43
+ model: string;
44
+ protocol: 'openai' | 'anthropic';
45
+ }
30
46
  export interface Personality {
31
47
  /** Slug (filename without .md, or built-in id). Lowercase, hyphens. */
32
48
  name: string;
@@ -37,9 +53,50 @@ export interface Personality {
37
53
  /** Markdown body appended to the system prompt when active. */
38
54
  prompt: string;
39
55
  scope: PersonalityScope;
56
+ /** True for `custom-bot/v1` files and the previous web section format. */
57
+ structured?: boolean;
58
+ /** False when a frontmatter `codeep` schema marker is present but unsupported. */
59
+ schemaValid?: boolean;
60
+ /** `automatic` inherits the user's current provider/model for this run. */
61
+ modelPreference?: string;
62
+ /** Normalised high-level capabilities selected in the builder. */
63
+ tools?: PersonalityCapability[];
64
+ /** Original declared tool values, retained for diagnostics/UI. */
65
+ declaredTools?: string[];
66
+ /** True when a structured file explicitly declares Tools, including `[]`. */
67
+ restrictTools?: boolean;
68
+ projectScope?: PersonalityProjectScope;
69
+ /** False when versioned metadata explicitly declares an unknown scope. */
70
+ scopeValid?: boolean;
71
+ projects?: string[];
72
+ responsibility?: string;
73
+ responseStyle?: string;
74
+ always?: string[];
75
+ never?: string[];
76
+ advancedInstructions?: string;
40
77
  }
78
+ /** Whether a structured bot's model field satisfies the portable v1 contract. */
79
+ export declare function isPersonalityModelPreferenceValid(personality: Personality): boolean;
80
+ /** Parse both custom-bot/v1 and the original web builder's heading format. */
81
+ export declare function parsePersonalityMarkdown(raw: string, name: string, scope: PersonalityScope): Personality;
82
+ /**
83
+ * Scope is enforced against the workspace basename only. This intentionally
84
+ * avoids accepting arbitrary paths from cloud-authored metadata.
85
+ */
86
+ export declare function isPersonalityAvailable(personality: Personality, workspaceRoot?: string): boolean;
87
+ /** Concrete tool names that may be advertised for a structured custom bot. */
88
+ export declare function getPersonalityToolAllowlist(personality: Personality, registeredMcpToolNames?: ReadonlySet<string>): string[] | undefined;
89
+ /** Runtime gate. It is deliberately stricter than the prompt/tool catalog. */
90
+ export declare function isPersonalityToolCallAllowed(personality: Personality, toolCall: ToolCall, registeredMcpToolNames?: ReadonlySet<string>): boolean;
91
+ /** Resolve an exact provider/model preference without mutating global config. */
92
+ export declare function resolvePersonalityRuntimeModel(personality: Personality, current: {
93
+ providerId: string;
94
+ model: string;
95
+ protocol: 'openai' | 'anthropic';
96
+ }): PersonalityRuntimeModel | null;
41
97
  export declare function loadAllPersonalities(workspaceRoot?: string): Personality[];
42
98
  export declare function findPersonality(name: string, workspaceRoot?: string): Personality | null;
99
+ export declare function getActivePersonality(workspaceRoot?: string): Personality | null;
43
100
  /**
44
101
  * Returns the prompt addendum for the currently active personality, or
45
102
  * '' when none is set. Called from agent.ts after the base system prompt
@@ -47,4 +104,5 @@ export declare function findPersonality(name: string, workspaceRoot?: string): P
47
104
  * project rules conflict.
48
105
  */
49
106
  export declare function getActivePersonalityPrompt(workspaceRoot?: string): string;
107
+ export declare function formatPersonalityActivation(personality: Personality): string;
50
108
  export declare function formatPersonalityList(workspaceRoot?: string): string;