@compilr-dev/cli 0.8.7 → 0.8.9

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 (40) hide show
  1. package/dist/agent.js +7 -3
  2. package/dist/commands-v2/handlers/settings.js +22 -3
  3. package/dist/commands-v2/types.d.ts +10 -0
  4. package/dist/compilr-diff-companion.vsix +0 -0
  5. package/dist/index.js +68 -1
  6. package/dist/models/model-registry.js +1 -0
  7. package/dist/models/providers.js +1 -0
  8. package/dist/repl-v2.d.ts +19 -1
  9. package/dist/repl-v2.js +37 -0
  10. package/dist/settings/index.d.ts +1 -1
  11. package/dist/ui/overlay/impl/model-overlay-v2.js +33 -20
  12. package/dist/ui/overlay-manager.js +10 -2
  13. package/dist/utils/credentials.d.ts +1 -1
  14. package/dist/utils/credentials.js +1 -0
  15. package/dist/utils/model-tiers.js +14 -9
  16. package/package.json +3 -3
  17. package/dist/episodes/recorder.d.ts +0 -51
  18. package/dist/episodes/recorder.js +0 -195
  19. package/dist/episodes/significant-work.d.ts +0 -21
  20. package/dist/episodes/significant-work.js +0 -56
  21. package/dist/episodes/store.d.ts +0 -38
  22. package/dist/episodes/store.js +0 -199
  23. package/dist/episodes/types.d.ts +0 -35
  24. package/dist/episodes/types.js +0 -6
  25. package/dist/episodes/work-at-risk.d.ts +0 -12
  26. package/dist/episodes/work-at-risk.js +0 -38
  27. package/dist/episodes/work-summary-anchor.d.ts +0 -23
  28. package/dist/episodes/work-summary-anchor.js +0 -73
  29. package/dist/tools/consult.d.ts +0 -14
  30. package/dist/tools/consult.js +0 -73
  31. package/dist/tools/delegate-background.d.ts +0 -27
  32. package/dist/tools/delegate-background.js +0 -115
  33. package/dist/tools/delegate.d.ts +0 -28
  34. package/dist/tools/delegate.js +0 -113
  35. package/dist/tools/handoff.d.ts +0 -17
  36. package/dist/tools/handoff.js +0 -79
  37. package/dist/tools/meta-tools.d.ts +0 -31
  38. package/dist/tools/meta-tools.js +0 -78
  39. package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +0 -56
  40. package/dist/ui/overlay/impl/skill-editor-overlay.js +0 -493
package/dist/agent.js CHANGED
@@ -157,15 +157,19 @@ function resolveProviderConfig(options) {
157
157
  `Run /keys to set it, or: export ${meta.envVar}=...` +
158
158
  (providerType === 'claude' ? '\nOr use --provider ollama for local models.' : ''));
159
159
  }
160
- const model = options.model ?? getDefaultModelForTier(providerType, 'balanced')?.id;
161
- const baseUrl = providerType === 'ollama'
160
+ // Both Ollama variants hit the same local server; only the endpoint differs.
161
+ const isOllama = providerType === 'ollama' || providerType === 'ollama-anthropic';
162
+ // ollama-anthropic has no registry rows — default to classic ollama's balanced local model.
163
+ const tierSource = providerType === 'ollama-anthropic' ? 'ollama' : providerType;
164
+ const model = options.model ?? getDefaultModelForTier(tierSource, 'balanced')?.id;
165
+ const baseUrl = isOllama
162
166
  ? ((typeof getSetting('ollamaBaseUrl') === 'string'
163
167
  ? getSetting('ollamaBaseUrl')
164
168
  : undefined) ?? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434')
165
169
  : undefined;
166
170
  if (!options.quiet) {
167
171
  log.info({ component: 'agent', provider: meta.displayName, model: model ?? 'default' }, 'Provider configured');
168
- if (providerType === 'ollama' && baseUrl !== 'http://localhost:11434') {
172
+ if (isOllama && baseUrl !== 'http://localhost:11434') {
169
173
  log.info({ component: 'agent', baseUrl }, 'Ollama server');
170
174
  }
171
175
  }
@@ -146,11 +146,30 @@ export const modelCommand = {
146
146
  currentProvider: currentProvider === 'auto' ? undefined : currentProvider,
147
147
  });
148
148
  await ctx.ui.showOverlay(modelOverlay);
149
- // After overlay closes, check if the model changed (overlay calls setSetting)
150
- // Apply it to the active agent immediately via setModel()
149
+ // After overlay closes, apply the selection (the overlay writes both
150
+ // defaultModel and defaultProvider to settings).
151
151
  if (agent) {
152
152
  const newModel = getSetting('defaultModel');
153
- if (newModel && newModel !== agentModel) {
153
+ const newProvider = getSetting('defaultProvider');
154
+ const providerChanged = !!newProvider && !!ctx.provider && newProvider !== ctx.provider;
155
+ if (providerChanged) {
156
+ // setModel() only swaps the model string on the existing provider
157
+ // client — it can't cross providers. Rebuild the agent on the new
158
+ // provider (history preserved) instead.
159
+ if (ctx.rebuildActiveAgent) {
160
+ const res = await ctx.rebuildActiveAgent(newProvider, newModel ?? '');
161
+ if (res.ok) {
162
+ ctx.ui.print({ type: 'success', message: `Switched to ${newProvider}${newModel ? ` · ${newModel}` : ''}` });
163
+ }
164
+ else {
165
+ ctx.ui.print({ type: 'error', message: res.error ?? 'Failed to switch provider' });
166
+ }
167
+ }
168
+ else {
169
+ ctx.ui.print({ type: 'error', message: 'Provider switch takes effect on the next restart in this mode.' });
170
+ }
171
+ }
172
+ else if (newModel && newModel !== agentModel) {
154
173
  try {
155
174
  agent.setModel(newModel);
156
175
  const agentId = ctx.team ? ctx.team.getActive().id : 'default';
@@ -52,6 +52,16 @@ export interface CommandContextV2 {
52
52
  * Used by /resume command to restore from a saved session.
53
53
  */
54
54
  restoreAgent?: (agent: Agent) => void;
55
+ /**
56
+ * Rebuild the active agent on a new provider (+ model), preserving history.
57
+ * Used by /model when the provider changes: setModel() only swaps the model
58
+ * string on the existing provider client, so it can't cross providers.
59
+ * Resolves { ok: false, error } if the target provider has no API key.
60
+ */
61
+ rebuildActiveAgent?: (provider: string, model: string) => Promise<{
62
+ ok: boolean;
63
+ error?: string;
64
+ }>;
55
65
  /** Current LLM model ID */
56
66
  model?: string;
57
67
  /** Current LLM provider (claude, openai, gemini, ollama) */
Binary file
package/dist/index.js CHANGED
@@ -595,6 +595,63 @@ async function main() {
595
595
  sharedState.grantSession = (toolName) => {
596
596
  defaultAgentRef?.grantSessionPermission(toolName);
597
597
  };
598
+ // Build a full-fidelity default agent for a given provider+model. Mirrors the
599
+ // initial createAgent() above (incl. plan-mode callbacks) so a hot-switched
600
+ // agent keeps every capability. Used by the REPL's rebuildActiveAgent() when
601
+ // /model changes the provider — setModel() can't cross providers. Throws if
602
+ // the target provider has no API key (surfaced to the user by the REPL).
603
+ const buildDefaultAgent = (agentProvider, agentModel) => {
604
+ const built = createAgent({
605
+ provider: agentProvider,
606
+ model: agentModel,
607
+ minimal: options.minimal,
608
+ quiet: true,
609
+ enableMetaTools: !options.minimal && getProviderMetadata(agentProvider).supportsMetaTools,
610
+ onPermissionRequest,
611
+ onIterationLimitReached,
612
+ onSuggest: (event) => {
613
+ sharedState.pendingSuggestion = event.action;
614
+ },
615
+ projectName: liveProjectState.dbProject?.displayName,
616
+ projectSlug: liveProjectState.dbProject?.name,
617
+ projectId: liveProjectState.dbProject?.id,
618
+ projectContext: liveProjectState.projectContext,
619
+ guidedModeContext: liveProjectState.guidedModeContext,
620
+ enableAnchors: true,
621
+ persistedAnchors: persistedAnchors.length > 0 ? persistedAnchors : undefined,
622
+ enableGuardrails: true,
623
+ onGuardrailTriggered,
624
+ onSubagentStart: (toolUseId, agentType, description) => {
625
+ sharedState.onSubagentStart?.(toolUseId, agentType, description);
626
+ },
627
+ onSubagentEnd: (toolUseId, success, tokenCount, error) => {
628
+ sharedState.onSubagentEnd?.(toolUseId, success, tokenCount, error);
629
+ },
630
+ onSubagentToolUse: (toolUseId, toolName, summary) => {
631
+ sharedState.onSubagentToolUse?.(toolUseId, toolName, summary);
632
+ },
633
+ onSubagentTrackingReady: (clearTracking) => {
634
+ sharedState.clearSubagentTracking = clearTracking;
635
+ },
636
+ episodeRecorder: defaultEpisodeRecorder,
637
+ planModeCallbacks: {
638
+ onPlanSubmit: async (info) => {
639
+ if (sharedState.onPlanSubmit)
640
+ return sharedState.onPlanSubmit(info);
641
+ return { action: 'reject' };
642
+ },
643
+ onPlanModeExit: async (info) => {
644
+ if (sharedState.onPlanModeExit)
645
+ await sharedState.onPlanModeExit(info);
646
+ },
647
+ },
648
+ });
649
+ defaultAgentRef = built;
650
+ sharedState.grantSession = (toolName) => {
651
+ defaultAgentRef?.grantSessionPermission(toolName);
652
+ };
653
+ return built;
654
+ };
598
655
  // ==========================================================================
599
656
  // Create AgentTeam with the default agent (or restore from persistence)
600
657
  // ==========================================================================
@@ -828,6 +885,16 @@ async function main() {
828
885
  onDefaultAgentSwapped: (newAgent) => {
829
886
  defaultAgentRef = newAgent;
830
887
  },
888
+ // Rebuild the default agent on a new provider (+ model) for /model's
889
+ // provider hot-switch. Throws if the provider has no API key. Also
890
+ // updates the outer provider/model so later team-agent creation and
891
+ // status reflect the switch.
892
+ onRebuildAgent: (rebuildProvider, rebuildModel) => {
893
+ const built = buildDefaultAgent(rebuildProvider, rebuildModel);
894
+ provider = rebuildProvider;
895
+ model = rebuildModel;
896
+ return Promise.resolve(built);
897
+ },
831
898
  // Set session prefix for VS Code diff IPC after session registration
832
899
  onSessionRegistered: (sessionId) => {
833
900
  sharedState.sessionPrefix = sessionId.slice(0, 8);
@@ -968,7 +1035,7 @@ function getDefaultModel(provider) {
968
1035
  return model.id;
969
1036
  // Fallback to any supported model for this provider
970
1037
  const anyModel = getDefaultModelForTier(provider, 'fast') ?? getDefaultModelForTier(provider, 'powerful');
971
- return anyModel?.id ?? 'claude-sonnet-4-6';
1038
+ return anyModel?.id ?? 'claude-sonnet-5';
972
1039
  }
973
1040
  // =============================================================================
974
1041
  // Run
@@ -23,6 +23,7 @@ export function getDefaultModelsForUI() {
23
23
  openai: 1,
24
24
  gemini: 2,
25
25
  ollama: 3,
26
+ 'ollama-anthropic': 3,
26
27
  together: 4,
27
28
  groq: 5,
28
29
  fireworks: 6,
@@ -15,6 +15,7 @@ const CREDENTIAL_KEYS = {
15
15
  openai: 'openai',
16
16
  gemini: 'google',
17
17
  ollama: 'ollama',
18
+ 'ollama-anthropic': 'ollama', // keyless; reuses ollama's credential slot
18
19
  together: 'together',
19
20
  groq: 'groq',
20
21
  fireworks: 'fireworks',
package/dist/repl-v2.d.ts CHANGED
@@ -34,6 +34,13 @@ export interface ReplV2Options {
34
34
  team?: AgentTeam;
35
35
  /** Agent factory function for creating new agents (for team switching) */
36
36
  agentFactory?: (config: Record<string, unknown>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean) => Promise<Agent>;
37
+ /**
38
+ * Rebuild the default agent on a new provider (+ model), full fidelity.
39
+ * Implemented by index.ts (which holds the full createAgent context).
40
+ * Throws if the provider has no API key. Used by rebuildActiveAgent() to
41
+ * hot-switch providers without a restart.
42
+ */
43
+ onRebuildAgent?: (provider: string, model: string) => Promise<Agent>;
37
44
  /** Current model ID */
38
45
  model?: string;
39
46
  /** Current provider */
@@ -235,8 +242,9 @@ export declare class ReplV2 {
235
242
  private agent?;
236
243
  private team?;
237
244
  private readonly agentFactory?;
245
+ private readonly onRebuildAgent?;
238
246
  private readonly model;
239
- private readonly provider;
247
+ private provider;
240
248
  private readonly version;
241
249
  private readonly updateAvailable;
242
250
  private readonly projectName;
@@ -461,6 +469,16 @@ export declare class ReplV2 {
461
469
  * Called by /resume command to swap in a restored agent.
462
470
  */
463
471
  restoreAgent(newAgent: Agent): void;
472
+ /**
473
+ * Hot-switch the active agent to a new provider (+ model), preserving the
474
+ * conversation history. setModel() only swaps the model string on the
475
+ * existing provider client, so a provider change needs a full rebuild.
476
+ * Returns { ok: false, error } if rebuild fails (e.g. no API key).
477
+ */
478
+ rebuildActiveAgent(provider: string, model: string): Promise<{
479
+ ok: boolean;
480
+ error?: string;
481
+ }>;
464
482
  /**
465
483
  * Get the current agent instance.
466
484
  * Used by commands that need to serialize/checkpoint the agent state.
package/dist/repl-v2.js CHANGED
@@ -171,6 +171,7 @@ export class ReplV2 {
171
171
  agent;
172
172
  team;
173
173
  agentFactory;
174
+ onRebuildAgent;
174
175
  model;
175
176
  provider;
176
177
  version;
@@ -235,6 +236,7 @@ export class ReplV2 {
235
236
  this.agent = options.agent;
236
237
  this.team = options.team;
237
238
  this.agentFactory = options.agentFactory;
239
+ this.onRebuildAgent = options.onRebuildAgent;
238
240
  this.model = options.model ?? 'simulation';
239
241
  this.provider = options.provider ?? 'simulation';
240
242
  this.version = options.version ?? STANDALONE_VERSION;
@@ -933,6 +935,38 @@ export class ReplV2 {
933
935
  this.agent = newAgent;
934
936
  this.onDefaultAgentSwapped?.(newAgent);
935
937
  }
938
+ /**
939
+ * Hot-switch the active agent to a new provider (+ model), preserving the
940
+ * conversation history. setModel() only swaps the model string on the
941
+ * existing provider client, so a provider change needs a full rebuild.
942
+ * Returns { ok: false, error } if rebuild fails (e.g. no API key).
943
+ */
944
+ async rebuildActiveAgent(provider, model) {
945
+ if (!this.onRebuildAgent) {
946
+ return { ok: false, error: 'Provider switching is not available in this mode.' };
947
+ }
948
+ let fresh;
949
+ try {
950
+ fresh = await this.onRebuildAgent(provider, model);
951
+ }
952
+ catch (err) {
953
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
954
+ }
955
+ // Carry the conversation history across the provider swap.
956
+ if (this.agent) {
957
+ try {
958
+ const history = this.agent.getHistory();
959
+ if (history.length > 0)
960
+ await fresh.setHistory(history);
961
+ }
962
+ catch {
963
+ // Best-effort — a fresh agent on the right provider still beats a broken one.
964
+ }
965
+ }
966
+ this.restoreAgent(fresh);
967
+ this.provider = provider;
968
+ return { ok: true };
969
+ }
936
970
  /**
937
971
  * Get the current agent instance.
938
972
  * Used by commands that need to serialize/checkpoint the agent state.
@@ -2000,6 +2034,9 @@ export class ReplV2 {
2000
2034
  restoreAgent: (newAgent) => {
2001
2035
  this.restoreAgent(newAgent);
2002
2036
  },
2037
+ rebuildActiveAgent: (provider, model) => {
2038
+ return this.rebuildActiveAgent(provider, model);
2039
+ },
2003
2040
  model: this.model,
2004
2041
  provider: this.provider,
2005
2042
  getContextManager: () => {
@@ -7,7 +7,7 @@
7
7
  export type { PermissionRule, PermissionMode, PermissionLevel } from '@compilr-dev/sdk';
8
8
  import type { PermissionRule, PermissionMode, PermissionLevel } from '@compilr-dev/sdk';
9
9
  export type NotificationMode = 'auto' | 'bell' | 'disabled';
10
- export type ProviderType = 'auto' | 'claude' | 'openai' | 'gemini' | 'ollama' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom';
10
+ export type ProviderType = 'auto' | 'claude' | 'openai' | 'gemini' | 'ollama' | 'ollama-anthropic' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom';
11
11
  export type StartupMode = 'menu' | 'repl';
12
12
  export type ProjectStartupMode = 'last' | 'off';
13
13
  export type MascotSetting = 'none' | 'random' | 'neutral' | 'thinking' | 'looking_left' | 'looking_right' | 'sleeping' | 'alert' | 'error' | 'success' | 'success_minimal' | 'searching' | 'skeptical';
@@ -30,14 +30,25 @@ const TIER_WIDTH = 14;
30
30
  const LABEL_WIDTH = 14;
31
31
  const MODEL_NAME_WIDTH = 28;
32
32
  /** Primary providers that show tier-based selection */
33
- const PRIMARY_PROVIDERS = ['claude', 'openai', 'gemini', 'ollama'];
33
+ const PRIMARY_PROVIDERS = ['claude', 'openai', 'gemini', 'ollama', 'ollama-anthropic'];
34
34
  const PROVIDER_TABS = [
35
35
  { id: 'claude', label: 'Claude' },
36
36
  { id: 'openai', label: 'OpenAI' },
37
37
  { id: 'gemini', label: 'Gemini' },
38
38
  { id: 'ollama', label: 'Ollama (local)' },
39
+ { id: 'ollama-anthropic', label: 'Ollama (Claude API)' },
39
40
  { id: 'others', label: 'Others' },
40
41
  ];
42
+ /** Both Ollama providers share local-model behavior + installed-model discovery
43
+ * (same server/port); only the API endpoint differs. */
44
+ function isOllamaProvider(p) {
45
+ return p === 'ollama' || p === 'ollama-anthropic';
46
+ }
47
+ /** `ollama-anthropic` has no registry rows of its own — it borrows the classic
48
+ * `ollama` local-model tiers (same local models, different endpoint). */
49
+ function registryProviderFor(p) {
50
+ return p === 'ollama-anthropic' ? 'ollama' : p;
51
+ }
41
52
  /** All model tiers in display order */
42
53
  const ALL_TIERS = [...MODEL_TIERS];
43
54
  /**
@@ -47,11 +58,13 @@ function getListItems() {
47
58
  const items = [];
48
59
  // For each primary provider, add 3 tier items
49
60
  for (const provider of PRIMARY_PROVIDERS) {
61
+ // ollama-anthropic has no registry rows — resolve tiers from classic ollama.
62
+ const tierSource = registryProviderFor(provider);
50
63
  for (const tier of ALL_TIERS) {
51
64
  // Get default model from registry
52
- const defaultModel = getDefaultModelForTier(provider, tier);
65
+ const defaultModel = getDefaultModelForTier(tierSource, tier);
53
66
  // Get current model (may have user override)
54
- const currentModelId = getModelForTier(provider, tier);
67
+ const currentModelId = getModelForTier(tierSource, tier);
55
68
  const currentModelInfo = getModelInfo(currentModelId);
56
69
  items.push({
57
70
  type: 'tier',
@@ -446,7 +459,7 @@ class TierDetailScreen extends BaseScreen {
446
459
  this.state = {
447
460
  editMode: false,
448
461
  // For Ollama, start with URL field; otherwise just model
449
- selectedField: tierItem.provider === 'ollama' ? 'url' : 'model',
462
+ selectedField: isOllamaProvider(tierItem.provider) ? 'url' : 'model',
450
463
  editValue: '',
451
464
  cursorPos: 0,
452
465
  testing: false,
@@ -462,7 +475,7 @@ class TierDetailScreen extends BaseScreen {
462
475
  */
463
476
  buildAvailableModels() {
464
477
  const models = [];
465
- const registryModels = getModelsForProvider(this.tierItem.provider);
478
+ const registryModels = getModelsForProvider(registryProviderFor(this.tierItem.provider));
466
479
  // Add registry models (supported ones)
467
480
  for (const model of registryModels) {
468
481
  if (model.status !== 'supported')
@@ -470,7 +483,7 @@ class TierDetailScreen extends BaseScreen {
470
483
  const isDefault = model.id === this.tierItem.defaultModelId;
471
484
  // For Ollama: check if locally available
472
485
  let isLocal;
473
- if (this.tierItem.provider === 'ollama') {
486
+ if (isOllamaProvider(this.tierItem.provider)) {
474
487
  const modelName = model.id.toLowerCase();
475
488
  const baseName = modelName.split(':')[0];
476
489
  isLocal = this.ollamaState.availableModels.has(modelName) ||
@@ -484,7 +497,7 @@ class TierDetailScreen extends BaseScreen {
484
497
  });
485
498
  }
486
499
  // For Ollama: add any local models not in registry
487
- if (this.tierItem.provider === 'ollama') {
500
+ if (isOllamaProvider(this.tierItem.provider)) {
488
501
  const registryIds = new Set(models.map(m => m.id.toLowerCase()));
489
502
  for (const localModel of this.ollamaState.availableModels) {
490
503
  // Skip base names (we only want full model:tag names)
@@ -574,7 +587,7 @@ class TierDetailScreen extends BaseScreen {
574
587
  lines.push(` ${s.muted('Tier:'.padEnd(LABEL_WIDTH))}${tierInfo.label} ${s.muted(`- ${tierInfo.shortDescription}`)}`);
575
588
  lines.push('');
576
589
  // API Key Status (not shown for Ollama - no auth needed)
577
- if (this.tierItem.provider !== 'ollama') {
590
+ if (!isOllamaProvider(this.tierItem.provider)) {
578
591
  const keyStatus = checkApiKeyStatus(this.tierItem.provider);
579
592
  const keyIndicator = keyStatus.hasKey ? s.success('● Set') : s.error('○ Not set');
580
593
  const keyHint = !keyStatus.hasKey && keyStatus.hint ? s.warning(` (${keyStatus.hint})`) : '';
@@ -582,7 +595,7 @@ class TierDetailScreen extends BaseScreen {
582
595
  lines.push('');
583
596
  }
584
597
  // Ollama Server URL (only for Ollama provider)
585
- if (this.tierItem.provider === 'ollama') {
598
+ if (isOllamaProvider(this.tierItem.provider)) {
586
599
  const isUrlSelected = this.state.editMode && this.state.selectedField === 'url';
587
600
  const sectionHeader = isUrlSelected ? s.primary('─── Server URL ───') : s.muted('─── Server URL ───');
588
601
  lines.push(` ${sectionHeader}`);
@@ -648,7 +661,7 @@ class TierDetailScreen extends BaseScreen {
648
661
  const displayName = currentModelInfo?.displayName ?? this.currentModelId;
649
662
  // For Ollama: show local availability
650
663
  let localBadge = '';
651
- if (this.tierItem.provider === 'ollama' && currentModelInfo) {
664
+ if (isOllamaProvider(this.tierItem.provider) && currentModelInfo) {
652
665
  localBadge = currentModelInfo.isLocal ? s.success(' ●') : s.warning(' ○');
653
666
  }
654
667
  lines.push(` ${s.muted('Current:'.padEnd(LABEL_WIDTH))}${currentStyle(displayName)}${localBadge}${cycleIndicator}`);
@@ -676,7 +689,7 @@ class TierDetailScreen extends BaseScreen {
676
689
  lines.push(` ${s.warning('Testing model...')}`);
677
690
  }
678
691
  else if (this.state.editMode) {
679
- const arrowHint = this.tierItem.provider === 'ollama' ? '↑↓ Switch field · ' : '';
692
+ const arrowHint = isOllamaProvider(this.tierItem.provider) ? '↑↓ Switch field · ' : '';
680
693
  lines.push(` ${s.muted(`${arrowHint}Enter Save · Esc Cancel · Clear = Reset to default`)}`);
681
694
  }
682
695
  else {
@@ -737,7 +750,7 @@ class TierDetailScreen extends BaseScreen {
737
750
  if (char === 'e') {
738
751
  this.state.editMode = true;
739
752
  // For Ollama, start with URL field; otherwise just model
740
- if (this.tierItem.provider === 'ollama') {
753
+ if (isOllamaProvider(this.tierItem.provider)) {
741
754
  this.state.selectedField = 'url';
742
755
  this.state.editValue = this.currentOllamaUrl;
743
756
  this.state.cursorPos = this.currentOllamaUrl.length;
@@ -805,7 +818,7 @@ class TierDetailScreen extends BaseScreen {
805
818
  return stay(true);
806
819
  }
807
820
  // Up/Down arrows - switch between fields (only for Ollama)
808
- if (this.tierItem.provider === 'ollama') {
821
+ if (isOllamaProvider(this.tierItem.provider)) {
809
822
  // Up arrow
810
823
  if (data[2] === 0x41) {
811
824
  if (this.state.selectedField === 'model') {
@@ -915,7 +928,7 @@ class TierDetailScreen extends BaseScreen {
915
928
  }
916
929
  getMinHeight() {
917
930
  // Ollama needs more height for the URL section
918
- return this.tierItem.provider === 'ollama' ? 26 : 22;
931
+ return isOllamaProvider(this.tierItem.provider) ? 26 : 22;
919
932
  }
920
933
  }
921
934
  // =============================================================================
@@ -986,7 +999,7 @@ export class ModelOverlayV2 extends TabbedListOverlayV2 {
986
999
  const isActive = item.currentModelId === activeModel && item.provider === activeProvider;
987
1000
  // For Ollama, show availability status; for others, show API key status
988
1001
  let statusIndicator;
989
- if (item.provider === 'ollama') {
1002
+ if (isOllamaProvider(item.provider)) {
990
1003
  const isLocallyAvailable = this.ollamaState.availableModels.has(item.currentModelId.toLowerCase()) ||
991
1004
  this.ollamaState.availableModels.has(item.currentModelId.split(':')[0].toLowerCase());
992
1005
  statusIndicator = isLocallyAvailable ? styles.success('●') : styles.warning('○');
@@ -1014,8 +1027,8 @@ export class ModelOverlayV2 extends TabbedListOverlayV2 {
1014
1027
  if (currentTabId === 'others') {
1015
1028
  return 'Tab · ↑↓ · / Search · Enter Details · q Close';
1016
1029
  }
1017
- // Ollama doesn't need API key
1018
- if (currentTabId === 'ollama') {
1030
+ // Ollama (both variants) doesn't need an API key
1031
+ if (currentTabId === 'ollama' || currentTabId === 'ollama-anthropic') {
1019
1032
  return 'Tab · ↑↓ · / Search · Enter Details · q Close';
1020
1033
  }
1021
1034
  // Standard providers: show s Set Key option
@@ -1026,8 +1039,8 @@ export class ModelOverlayV2 extends TabbedListOverlayV2 {
1026
1039
  // Skip for "Others" tab (has its own detail screen)
1027
1040
  if (tabId === 'others')
1028
1041
  return [];
1029
- // Ollama: show connection status
1030
- if (tabId === 'ollama') {
1042
+ // Ollama (both variants): show connection status
1043
+ if (tabId === 'ollama' || tabId === 'ollama-anthropic') {
1031
1044
  const status = this.ollamaState.status;
1032
1045
  let indicator;
1033
1046
  switch (status) {
@@ -1057,7 +1070,7 @@ export class ModelOverlayV2 extends TabbedListOverlayV2 {
1057
1070
  // Handle 's' key to set API key for current provider
1058
1071
  handleCustomKey: (key, tabId) => {
1059
1072
  const char = key.toString();
1060
- if (char === 's' && tabId !== 'others' && tabId !== 'ollama') {
1073
+ if (char === 's' && tabId !== 'others' && tabId !== 'ollama' && tabId !== 'ollama-anthropic') {
1061
1074
  const providerName = PROVIDER_TABS.find(t => t.id === tabId)?.label ?? tabId;
1062
1075
  return {
1063
1076
  handled: true,
@@ -38,8 +38,14 @@ export class OverlayManager {
38
38
  this.clearOverlayRender();
39
39
  }
40
40
  this.stack.push(overlay);
41
- overlay.setRenderCallback?.(() => { this.requestOverlayRender(); });
42
- await overlay.onMount?.();
41
+ // Enter overlay mode (resets renderState + clears the footer) BEFORE wiring
42
+ // the render callback and mounting. Otherwise an async render triggered
43
+ // during the `await onMount` microtask gap — e.g. ModelOverlay's
44
+ // fetchOllamaModels() resolving and calling requestRender() — draws a frame
45
+ // and sets renderState.maxLineCount, then enterInlineOverlayMode() wipes
46
+ // that count to 0, so the final renderOverlay() clears nothing and draws a
47
+ // second frame below the first (the "ghost" overlay). Resetting first means
48
+ // any gap render is tracked and the final render clears it correctly.
43
49
  if (!overlay.usesAlternateScreen) {
44
50
  if (overlay.type === 'fullscreen') {
45
51
  this.enterFullscreenOverlayMode();
@@ -48,6 +54,8 @@ export class OverlayManager {
48
54
  this.enterInlineOverlayMode();
49
55
  }
50
56
  }
57
+ overlay.setRenderCallback?.(() => { this.requestOverlayRender(); });
58
+ await overlay.onMount?.();
51
59
  this.renderOverlay();
52
60
  return new Promise((resolve) => {
53
61
  this.resolvers.set(overlay.id, resolve);
@@ -71,4 +71,4 @@ export declare function getProvidersByCategory(category: ProviderCategory): Prov
71
71
  /**
72
72
  * Map provider type from settings to credential provider key
73
73
  */
74
- export declare function settingsProviderToCredentialKey(provider: 'auto' | 'claude' | 'openai' | 'gemini' | 'ollama' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom'): ProviderKey;
74
+ export declare function settingsProviderToCredentialKey(provider: 'auto' | 'claude' | 'openai' | 'gemini' | 'ollama' | 'ollama-anthropic' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom'): ProviderKey;
@@ -364,6 +364,7 @@ export function settingsProviderToCredentialKey(provider) {
364
364
  case 'openai':
365
365
  return 'openai';
366
366
  case 'ollama':
367
+ case 'ollama-anthropic':
367
368
  return 'ollama';
368
369
  case 'together':
369
370
  return 'together';
@@ -18,19 +18,23 @@
18
18
  // Model Tier Mappings
19
19
  // =============================================================================
20
20
  const MODEL_TIERS = {
21
- // Claude (Anthropic) - 4.6 series (current)
21
+ // Claude (Anthropic) - current
22
+ 'claude-fable-5': { tier: 'large', provider: 'anthropic' },
23
+ 'claude-opus-4-8': { tier: 'large', provider: 'anthropic' },
24
+ 'claude-sonnet-5': { tier: 'large', provider: 'anthropic' },
25
+ 'claude-haiku-4-5': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
26
+ // Claude legacy (for backward compatibility)
27
+ 'claude-opus-4-7': { tier: 'large', provider: 'anthropic' },
22
28
  'claude-opus-4-6': { tier: 'large', provider: 'anthropic' },
23
29
  'claude-sonnet-4-6': { tier: 'large', provider: 'anthropic' },
24
- 'claude-haiku-4-5': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-4-6' },
25
- // Claude legacy (for backward compatibility)
26
30
  'claude-opus-4-5': { tier: 'large', provider: 'anthropic' },
27
31
  'claude-sonnet-4-5': { tier: 'large', provider: 'anthropic' },
28
32
  'claude-opus-4-1': { tier: 'large', provider: 'anthropic' },
29
33
  'claude-opus-4': { tier: 'large', provider: 'anthropic' },
30
34
  'claude-sonnet-4': { tier: 'large', provider: 'anthropic' },
31
35
  'claude-3-5-sonnet': { tier: 'large', provider: 'anthropic' },
32
- 'claude-3-5-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-4-6' },
33
- 'claude-3-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-4-6' },
36
+ 'claude-3-5-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
37
+ 'claude-3-haiku': { tier: 'medium', provider: 'anthropic', suggestedUpgrade: 'claude-sonnet-5' },
34
38
  // OpenAI - GPT-5 series (current)
35
39
  'gpt-5.2': { tier: 'large', provider: 'openai' },
36
40
  'gpt-5-mini': { tier: 'large', provider: 'openai' },
@@ -56,10 +60,11 @@ const MODEL_TIERS = {
56
60
  'gemini-1.5-pro': { tier: 'large', provider: 'google' },
57
61
  'gemini-1.5-flash': { tier: 'medium', provider: 'google', suggestedUpgrade: 'gemini-3-flash-preview' },
58
62
  // Ollama / Local models (common ones)
59
- 'llama3.2:8b': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.2:70b' },
60
- 'llama3.2:70b': { tier: 'large', provider: 'ollama' },
61
- 'llama3.2': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.2:70b' },
62
- 'llama3.1': { tier: 'medium', provider: 'ollama', suggestedUpgrade: 'llama3.1:70b' },
63
+ 'llama3.3:70b': { tier: 'large', provider: 'ollama' },
64
+ 'llama3.3': { tier: 'large', provider: 'ollama' },
65
+ 'llama3.2:3b': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
66
+ 'llama3.2': { tier: 'small', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
67
+ 'llama3.1': { tier: 'medium', provider: 'ollama', suggestedUpgrade: 'llama3.3:70b' },
63
68
  'llama3.1:70b': { tier: 'large', provider: 'ollama' },
64
69
  'mistral': { tier: 'small', provider: 'ollama' },
65
70
  'mixtral': { tier: 'medium', provider: 'ollama' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/cli",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "AI-powered coding assistant CLI using @compilr-dev/agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,12 +55,12 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@anthropic-ai/sdk": "^0.74.0",
58
- "@compilr-dev/agents": "^0.6.3",
58
+ "@compilr-dev/agents": "^0.6.8",
59
59
  "@compilr-dev/agents-coding": "^1.0.4",
60
60
  "@compilr-dev/editor-core": "^0.0.3",
61
61
  "@compilr-dev/factory": "^0.1.35",
62
62
  "@compilr-dev/logger": "^0.1.0",
63
- "@compilr-dev/sdk": "^0.18.2",
63
+ "@compilr-dev/sdk": "^0.18.10",
64
64
  "@compilr-dev/ui-core": "^0.0.1",
65
65
  "@modelcontextprotocol/sdk": "^1.23.0",
66
66
  "ansi-escapes": "^7.3.0",
@@ -1,51 +0,0 @@
1
- /**
2
- * Episode Recorder
3
- *
4
- * Collects tool call signals via an AfterToolHook, batches them within
5
- * a time window, and persists as WorkEpisode records.
6
- */
7
- import type { AfterToolHook, Effort, EpisodeStore } from '@compilr-dev/agents';
8
- export interface EpisodeRecorderConfig {
9
- /** Episode store for persistence */
10
- store: EpisodeStore;
11
- /** Agent ID (e.g., 'default', 'backend') */
12
- agentId: string;
13
- /** Terminal session prefix (first 8 chars) */
14
- terminalPrefix: string;
15
- /** Session ID for grouping */
16
- sessionId: string;
17
- /** Enable automatic recording via hook (default: true) */
18
- autoRecord?: boolean;
19
- /** Minimum effort level to record (default: 'low') */
20
- minEffortToRecord?: Effort;
21
- /** Batch window in milliseconds (default: 5000) */
22
- batchWindowMs?: number;
23
- /** Called after each batch is flushed as a WorkEpisode */
24
- onBatchFlushed?: () => void;
25
- }
26
- export declare class EpisodeRecorder {
27
- private readonly store;
28
- private readonly agentId;
29
- private readonly terminalPrefix;
30
- private readonly sessionId;
31
- private readonly autoRecord;
32
- private readonly minEffortToRecord;
33
- private readonly batchWindowMs;
34
- private readonly onBatchFlushed?;
35
- private pendingSignals;
36
- private batchTimer;
37
- private batchStartTime;
38
- constructor(config: EpisodeRecorderConfig);
39
- /**
40
- * Create an AfterToolHook for observing tool calls.
41
- * Always returns undefined (observation only — never modifies results).
42
- */
43
- createHook(): AfterToolHook;
44
- /**
45
- * Force-flush all pending signals as a WorkEpisode.
46
- * Called on shutdown to avoid losing data.
47
- */
48
- flush(): void;
49
- private scheduleBatch;
50
- private buildAndSaveEpisode;
51
- }