@compilr-dev/sdk 0.18.9 → 0.18.11

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The surface ramp the host injects.
3
+ *
4
+ * ⚠️ WHY THE HOST AND NOT CSS. A single `color-mix` cannot serve both polarities: a light
5
+ * theme's ground has no headroom above it (Catppuccin Latte sits at ~95% lightness, so a
6
+ * card 6% "above" it would be past white), while a dark theme's has plenty. CSS cannot
7
+ * branch on a palette's polarity. Whatever computes this has to know the luminance, so it
8
+ * lives here — pure, testable, and shared with the validator so the two cannot disagree
9
+ * about what "5–7% apart" means.
10
+ *
11
+ * ⚠️ THE GROUND MOVES ONLY WHEN IT MUST. The handoff's worked examples darken the ground
12
+ * in every case; this does it only when there is no room for the raised surface, so a
13
+ * dark theme's canvas keeps exactly the background its theme specifies and nothing about
14
+ * an existing canvas shifts. Latte has no room, so it darkens — there is no alternative
15
+ * that also keeps the card raised, which is the point of the ramp.
16
+ */
17
+ import { parseColor, oklabLightness, SURFACE_DELTA_TARGET, HAIRLINE_MIN_PCT, HAIRLINE_MAX_PCT, } from './color.js';
18
+ const hex = (n) => Math.round(Math.min(1, Math.max(0, n)) * 255)
19
+ .toString(16)
20
+ .padStart(2, '0');
21
+ const toHex = ({ r, g, b }) => `#${hex(r)}${hex(g)}${hex(b)}`;
22
+ const mix = (a, b, t) => ({
23
+ r: a.r + (b.r - a.r) * t,
24
+ g: a.g + (b.g - a.g) * t,
25
+ b: a.b + (b.b - a.b) * t,
26
+ });
27
+ const WHITE = { r: 1, g: 1, b: 1 };
28
+ const BLACK = { r: 0, g: 0, b: 0 };
29
+ /**
30
+ * The same colour at a different perceived lightness.
31
+ *
32
+ * ⚠️ Binary search over a mix toward white or black rather than an OKLab inverse. It
33
+ * keeps the hue of the ground (a tinted terminal ground stays tinted, which is most of
34
+ * why these palettes look like themselves) and cannot produce an out-of-gamut value,
35
+ * which an inverse transform can.
36
+ */
37
+ export function withLightness(color, targetL) {
38
+ const current = oklabLightness(color);
39
+ if (Math.abs(current - targetL) < 0.001)
40
+ return color;
41
+ const toward = targetL > current ? WHITE : BLACK;
42
+ let lo = 0;
43
+ let hi = 1;
44
+ let out = color;
45
+ for (let i = 0; i < 24; i++) {
46
+ const mid = (lo + hi) / 2;
47
+ out = mix(color, toward, mid);
48
+ const l = oklabLightness(out);
49
+ if (Math.abs(l - targetL) < 0.0005)
50
+ break;
51
+ if (targetL > current ? l < targetL : l > targetL)
52
+ lo = mid;
53
+ else
54
+ hi = mid;
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Derive the three ramp tokens from a theme's ground and foreground.
60
+ *
61
+ * Raised is always lighter and inset always darker, in both polarities — a card that
62
+ * recedes from its page does not read as a card. When the ground cannot support both
63
+ * steps, the ground itself is moved just far enough to make room, and the returned `bg`
64
+ * says so.
65
+ */
66
+ export function deriveSurfaceRamp(bgInput, fgInput) {
67
+ const bg = parseColor(bgInput);
68
+ const fg = parseColor(fgInput);
69
+ if (!bg || !fg)
70
+ return null;
71
+ const step = SURFACE_DELTA_TARGET / 100;
72
+ let ground = oklabLightness(bg);
73
+ /*
74
+ Keep both steps inside the gamut by sliding the ground, never by shrinking the step —
75
+ a 3% ramp is the defect this exists to fix, so the separation is the invariant and the
76
+ ground's exact value is not.
77
+ */
78
+ const headroom = 1 - (ground + step);
79
+ const footroom = ground - step;
80
+ if (headroom < 0)
81
+ ground += headroom;
82
+ else if (footroom < 0)
83
+ ground -= footroom;
84
+ const groundRgb = withLightness(bg, ground);
85
+ /*
86
+ The hairline sits a fixed fraction of the way toward the foreground. Terminal palettes
87
+ derive their border from a comment or selection colour, which lands 35–100% of the way
88
+ there — that is the "hard borders" complaint, and it is why this ignores
89
+ --canvas-border entirely rather than trying to soften it.
90
+ */
91
+ const hairlineMix = (HAIRLINE_MIN_PCT + HAIRLINE_MAX_PCT) / 2 / 100;
92
+ return {
93
+ bg: toHex(groundRgb),
94
+ surface1: toHex(withLightness(groundRgb, ground + step)),
95
+ surface2: toHex(withLightness(groundRgb, ground - step)),
96
+ hairline: toHex(mix(groundRgb, fg, hairlineMix)),
97
+ };
98
+ }
@@ -244,11 +244,19 @@ export const CAPABILITY_PACKS = {
244
244
  'canvas_delete',
245
245
  'canvas_validate',
246
246
  'canvas_screenshot',
247
+ 'canvas_guide',
247
248
  ],
248
249
  readOnly: false,
249
250
  promptModules: ['platform-tool-hints'],
250
- promptSnippet: 'Visual canvases (infographic/carousel/board) rendered from sandboxed HTML/SVG. Create/replace with canvas_write (raw HTML only — host injects the CSP; inline <style>/<script> only, no network/remote assets; optional Tweaks controls manifest). EDIT via canvas_get (outline=true or startLine/maxLines) then canvas_edit (str_replace/append/prepend) — never re-send the whole document. After authoring, canvas_validate (static) then canvas_screenshot to SEE the render (vision models) — fix overflow/readability with canvas_edit.',
251
- estimatedPromptTokens: 150,
251
+ promptSnippet: 'Visual canvases (infographic/carousel/board) rendered from sandboxed HTML/SVG. ' +
252
+ // ⚠️ FIRST, and stated as a requirement rather than a suggestion. The craft guides
253
+ // used to be reachable only when a user typed /canvas, so most canvases were
254
+ // authored with no design guidance at all — see canvas_guide.
255
+ 'START by calling canvas_guide("canvas") — the design rules live there, not here, and a ' +
256
+ 'canvas authored without them renders correctly and still reads as generic. Then ' +
257
+ 'canvas_guide on the type you pick. ' +
258
+ 'Create/replace with canvas_write (raw HTML only — host injects the CSP; inline <style>/<script> only, no network/remote assets; optional Tweaks controls manifest). EDIT via canvas_get (outline=true or startLine/maxLines) then canvas_edit (str_replace/append/prepend) — never re-send the whole document. After authoring, canvas_validate (static) then canvas_screenshot to SEE the render (vision models) — fix overflow/readability with canvas_edit.',
259
+ estimatedPromptTokens: 190,
252
260
  estimatedToolTokens: 1500,
253
261
  },
254
262
  plans: {
package/dist/config.d.ts CHANGED
@@ -49,7 +49,7 @@ export interface CapabilitiesConfig {
49
49
  /**
50
50
  * Supported provider types for auto-detection
51
51
  */
52
- export type ProviderType = 'claude' | 'openai' | 'gemini' | 'ollama' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom';
52
+ export type ProviderType = 'claude' | 'openai' | 'gemini' | 'ollama' | 'ollama-anthropic' | 'together' | 'groq' | 'fireworks' | 'perplexity' | 'openrouter' | 'custom';
53
53
  /**
54
54
  * Tool configuration for controlling which tools are available
55
55
  */
package/dist/index.d.ts CHANGED
@@ -45,7 +45,7 @@ export type { Preset } from './presets/index.js';
45
45
  export type { AnyTool } from './presets/types.js';
46
46
  export { resolveProvider, detectProviderFromEnv, createProviderFromType } from './provider.js';
47
47
  export { DEFAULT_MODELS, getContextWindow, DEFAULT_CONTEXT_WINDOW } from './models.js';
48
- export { type ModelTier, type TierInfo, type ProviderModelMap, MODEL_TIERS, TIER_INFO, isValidTier, type ThinkingFormat, type ModelStatus, type ModelInfo, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, modelSupportsTools, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, type ProviderMetadata, type OthersProviderModel, PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, } from './models/index.js';
48
+ export { type ModelTier, type TierInfo, type ProviderModelMap, MODEL_TIERS, TIER_INFO, isValidTier, type ThinkingFormat, type ModelStatus, type ModelInfo, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, modelSupportsTools, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, type ProviderMetadata, type OthersProviderModel, PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, type OllamaModelInfo, type OllamaModelsResult, OLLAMA_ANTHROPIC_MIN_VERSION, listOllamaModels, getOllamaVersion, isOllamaAnthropicCompatible, clearOllamaModelsCache, formatOllamaModelSize, getOllamaModelDisplayName, getOllamaModelDescription, } from './models/index.js';
49
49
  export { assembleTools, deduplicateTools } from './tools.js';
50
50
  export { MetaToolsRegistry, createMetaTools, META_TOOLS_SYSTEM_PROMPT_PREFIX, } from './meta-tools/index.js';
51
51
  export type { MetaToolStats, MetaTools, FallbackOptions } from './meta-tools/index.js';
package/dist/index.js CHANGED
@@ -85,7 +85,7 @@ export { DEFAULT_MODELS, getContextWindow, DEFAULT_CONTEXT_WINDOW } from './mode
85
85
  // =============================================================================
86
86
  export { MODEL_TIERS, TIER_INFO, isValidTier, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, modelSupportsTools, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription,
87
87
  // Model tiers (pure, settings-free)
88
- getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, } from './models/index.js';
88
+ getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, OLLAMA_ANTHROPIC_MIN_VERSION, listOllamaModels, getOllamaVersion, isOllamaAnthropicCompatible, clearOllamaModelsCache, formatOllamaModelSize, getOllamaModelDisplayName, getOllamaModelDescription, } from './models/index.js';
89
89
  // =============================================================================
90
90
  // Tool Assembly
91
91
  // =============================================================================
@@ -5,3 +5,4 @@ export { type ModelTier, type TierInfo, type ProviderModelMap, MODEL_TIERS, TIER
5
5
  export { type ThinkingFormat, type ModelStatus, type ModelInfo, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, modelSupportsTools, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, } from './model-registry.js';
6
6
  export { getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, } from './model-tiers.js';
7
7
  export { type ProviderMetadata, type OthersProviderModel, PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, } from './providers.js';
8
+ export { type OllamaModelInfo, type OllamaModelsResult, OLLAMA_ANTHROPIC_MIN_VERSION, listOllamaModels, getOllamaVersion, isOllamaAnthropicCompatible, clearOllamaModelsCache, formatOllamaModelSize, getOllamaModelDisplayName, getOllamaModelDescription, } from './ollama-discovery.js';
@@ -9,3 +9,5 @@ export { MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getMod
9
9
  export { getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, } from './model-tiers.js';
10
10
  // Provider metadata
11
11
  export { PROVIDER_METADATA, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, getOthersProviders, getProviderMetadata, isOthersProvider, } from './providers.js';
12
+ // Ollama discovery (host-agnostic — list installed models + version gate)
13
+ export { OLLAMA_ANTHROPIC_MIN_VERSION, listOllamaModels, getOllamaVersion, isOllamaAnthropicCompatible, clearOllamaModelsCache, formatOllamaModelSize, getOllamaModelDisplayName, getOllamaModelDescription, } from './ollama-discovery.js';
@@ -684,6 +684,7 @@ function getProviderContextLimitFallback(provider) {
684
684
  case 'openai':
685
685
  return 128000;
686
686
  case 'ollama':
687
+ case 'ollama-anthropic':
687
688
  return 32000;
688
689
  case 'together':
689
690
  return 131072;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Ollama discovery — host-agnostic helpers to list locally-pulled models and
3
+ * detect the Ollama version. Shared by CLI + Desktop so both surface installed
4
+ * models (closing the Desktop parity gap) for BOTH ollama providers
5
+ * (`ollama` OpenAI-compat and `ollama-anthropic`). Uses Ollama's native
6
+ * `/api/tags` and `/api/version` (same on both providers — same port).
7
+ *
8
+ * The base URL is a parameter (hosts pass their configured value); this module
9
+ * has no settings dependency. Results are cached per base URL for 30s.
10
+ * See ollama-anthropic-provider-spec.md §6.
11
+ */
12
+ /** Minimum Ollama version that supports the native Anthropic `/v1/messages` endpoint. */
13
+ export declare const OLLAMA_ANTHROPIC_MIN_VERSION = "0.14.0";
14
+ /** Information about an Ollama model as returned by `/api/tags`. */
15
+ export interface OllamaModelInfo {
16
+ /** Model name/tag (e.g., "llama3.3:70b", "ornith:latest") */
17
+ name: string;
18
+ /** Model size in bytes */
19
+ size: number;
20
+ /** When the model was modified/pulled */
21
+ modifiedAt: string;
22
+ /** Model digest (hash) */
23
+ digest: string;
24
+ /** Model details (family, parameter size, quantization) */
25
+ details?: {
26
+ family?: string;
27
+ parameterSize?: string;
28
+ quantizationLevel?: string;
29
+ };
30
+ }
31
+ /** Result of listing Ollama models. */
32
+ export interface OllamaModelsResult {
33
+ success: boolean;
34
+ models: OllamaModelInfo[];
35
+ error?: string;
36
+ /** Whether the Ollama server responded at all */
37
+ ollamaRunning: boolean;
38
+ fetchedAt: number;
39
+ }
40
+ /** Clear the models cache (testing / forced refresh). */
41
+ export declare function clearOllamaModelsCache(): void;
42
+ /**
43
+ * List locally-available Ollama models via `/api/tags`. Cached per base URL for
44
+ * 30s. Returns gracefully (success:false) when Ollama isn't running.
45
+ */
46
+ export declare function listOllamaModels(baseUrl?: string, forceRefresh?: boolean): Promise<OllamaModelsResult>;
47
+ /**
48
+ * Get the running Ollama server version via `/api/version`, or null if
49
+ * unreachable. Used to gate the `ollama-anthropic` provider.
50
+ */
51
+ export declare function getOllamaVersion(baseUrl?: string): Promise<string | null>;
52
+ /**
53
+ * Whether the Ollama server supports the native Anthropic `/v1/messages`
54
+ * endpoint (version >= 0.14.0). Returns false if unreachable/unknown — callers
55
+ * should treat that as "offer classic ollama, gate/warn on ollama-anthropic".
56
+ */
57
+ export declare function isOllamaAnthropicCompatible(baseUrl?: string): Promise<boolean>;
58
+ /** Format a byte size for display (e.g. "4.1 GB"). */
59
+ export declare function formatOllamaModelSize(bytes: number): string;
60
+ /** Friendly display name for a model tag (e.g. "ornith:latest" → "Ornith (latest)"). */
61
+ export declare function getOllamaModelDisplayName(name: string): string;
62
+ /** Short description from model metadata (params · quant · size). */
63
+ export declare function getOllamaModelDescription(model: OllamaModelInfo): string;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Ollama discovery — host-agnostic helpers to list locally-pulled models and
3
+ * detect the Ollama version. Shared by CLI + Desktop so both surface installed
4
+ * models (closing the Desktop parity gap) for BOTH ollama providers
5
+ * (`ollama` OpenAI-compat and `ollama-anthropic`). Uses Ollama's native
6
+ * `/api/tags` and `/api/version` (same on both providers — same port).
7
+ *
8
+ * The base URL is a parameter (hosts pass their configured value); this module
9
+ * has no settings dependency. Results are cached per base URL for 30s.
10
+ * See ollama-anthropic-provider-spec.md §6.
11
+ */
12
+ const DEFAULT_BASE_URL = 'http://localhost:11434';
13
+ /** Minimum Ollama version that supports the native Anthropic `/v1/messages` endpoint. */
14
+ export const OLLAMA_ANTHROPIC_MIN_VERSION = '0.14.0';
15
+ const CACHE_DURATION_MS = 30_000;
16
+ const modelsCache = new Map();
17
+ function resolveBaseUrl(baseUrl) {
18
+ return baseUrl ?? process.env.OLLAMA_BASE_URL ?? process.env.OLLAMA_HOST ?? DEFAULT_BASE_URL;
19
+ }
20
+ /** Clear the models cache (testing / forced refresh). */
21
+ export function clearOllamaModelsCache() {
22
+ modelsCache.clear();
23
+ }
24
+ /**
25
+ * List locally-available Ollama models via `/api/tags`. Cached per base URL for
26
+ * 30s. Returns gracefully (success:false) when Ollama isn't running.
27
+ */
28
+ export async function listOllamaModels(baseUrl, forceRefresh = false) {
29
+ const host = resolveBaseUrl(baseUrl);
30
+ const cached = modelsCache.get(host);
31
+ if (!forceRefresh && cached && Date.now() - cached.fetchedAt < CACHE_DURATION_MS) {
32
+ return cached;
33
+ }
34
+ try {
35
+ const response = await fetch(`${host}/api/tags`, {
36
+ method: 'GET',
37
+ signal: AbortSignal.timeout(5000),
38
+ });
39
+ if (!response.ok) {
40
+ const result = {
41
+ success: false,
42
+ models: [],
43
+ error: `Ollama returned status ${String(response.status)}`,
44
+ ollamaRunning: true,
45
+ fetchedAt: Date.now(),
46
+ };
47
+ modelsCache.set(host, result);
48
+ return result;
49
+ }
50
+ const data = (await response.json());
51
+ const models = (data.models ?? [])
52
+ .map((m) => ({
53
+ name: m.name,
54
+ size: m.size,
55
+ modifiedAt: m.modified_at,
56
+ digest: m.digest,
57
+ details: m.details
58
+ ? {
59
+ family: m.details.family,
60
+ parameterSize: m.details.parameter_size,
61
+ quantizationLevel: m.details.quantization_level,
62
+ }
63
+ : undefined,
64
+ }))
65
+ .sort((a, b) => a.name.localeCompare(b.name));
66
+ const result = {
67
+ success: true,
68
+ models,
69
+ ollamaRunning: true,
70
+ fetchedAt: Date.now(),
71
+ };
72
+ modelsCache.set(host, result);
73
+ return result;
74
+ }
75
+ catch (error) {
76
+ let errorMsg = 'Unknown error';
77
+ const ollamaRunning = false;
78
+ if (error instanceof Error) {
79
+ if (error.name === 'AbortError' || error.message.includes('timeout')) {
80
+ errorMsg = 'Connection timed out';
81
+ }
82
+ else if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) {
83
+ errorMsg = 'Ollama is not running. Start it with: ollama serve';
84
+ }
85
+ else {
86
+ errorMsg = error.message;
87
+ }
88
+ }
89
+ const result = {
90
+ success: false,
91
+ models: [],
92
+ error: errorMsg,
93
+ ollamaRunning,
94
+ fetchedAt: Date.now(),
95
+ };
96
+ modelsCache.set(host, result);
97
+ return result;
98
+ }
99
+ }
100
+ /**
101
+ * Get the running Ollama server version via `/api/version`, or null if
102
+ * unreachable. Used to gate the `ollama-anthropic` provider.
103
+ */
104
+ export async function getOllamaVersion(baseUrl) {
105
+ const host = resolveBaseUrl(baseUrl);
106
+ try {
107
+ const response = await fetch(`${host}/api/version`, {
108
+ method: 'GET',
109
+ signal: AbortSignal.timeout(5000),
110
+ });
111
+ if (!response.ok)
112
+ return null;
113
+ const data = (await response.json());
114
+ return data.version ?? null;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /** Compare dotted numeric versions: -1 / 0 / 1. Non-numeric segments treated as 0. */
121
+ function compareVersions(a, b) {
122
+ const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
123
+ const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
124
+ const len = Math.max(pa.length, pb.length);
125
+ for (let i = 0; i < len; i++) {
126
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
127
+ if (d !== 0)
128
+ return d < 0 ? -1 : 1;
129
+ }
130
+ return 0;
131
+ }
132
+ /**
133
+ * Whether the Ollama server supports the native Anthropic `/v1/messages`
134
+ * endpoint (version >= 0.14.0). Returns false if unreachable/unknown — callers
135
+ * should treat that as "offer classic ollama, gate/warn on ollama-anthropic".
136
+ */
137
+ export async function isOllamaAnthropicCompatible(baseUrl) {
138
+ const version = await getOllamaVersion(baseUrl);
139
+ if (!version)
140
+ return false;
141
+ return compareVersions(version, OLLAMA_ANTHROPIC_MIN_VERSION) >= 0;
142
+ }
143
+ // ── Display helpers (shared so hosts format installed models consistently) ──
144
+ /** Format a byte size for display (e.g. "4.1 GB"). */
145
+ export function formatOllamaModelSize(bytes) {
146
+ if (bytes < 1024)
147
+ return `${String(bytes)} B`;
148
+ if (bytes < 1024 * 1024)
149
+ return `${(bytes / 1024).toFixed(1)} KB`;
150
+ if (bytes < 1024 * 1024 * 1024)
151
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
152
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
153
+ }
154
+ /** Friendly display name for a model tag (e.g. "ornith:latest" → "Ornith (latest)"). */
155
+ export function getOllamaModelDisplayName(name) {
156
+ const [baseName, tag] = name.split(':');
157
+ const formattedBase = baseName
158
+ .split(/[-_]/)
159
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
160
+ .join(' ');
161
+ return tag ? `${formattedBase} (${tag})` : formattedBase;
162
+ }
163
+ /** Short description from model metadata (params · quant · size). */
164
+ export function getOllamaModelDescription(model) {
165
+ const parts = [];
166
+ if (model.details?.parameterSize)
167
+ parts.push(model.details.parameterSize);
168
+ if (model.details?.quantizationLevel)
169
+ parts.push(model.details.quantizationLevel);
170
+ parts.push(formatOllamaModelSize(model.size));
171
+ return parts.join(' · ');
172
+ }
@@ -56,6 +56,16 @@ export const PROVIDER_METADATA = {
56
56
  requiresKey: false,
57
57
  category: 'local',
58
58
  },
59
+ 'ollama-anthropic': {
60
+ type: 'ollama-anthropic',
61
+ displayName: 'Ollama (Claude API)',
62
+ description: "Local models via Ollama's Anthropic endpoint (thinking; needs Ollama ≥ 0.14)",
63
+ endpoint: 'http://localhost:11434',
64
+ envVar: '',
65
+ keyUrl: 'https://ollama.ai',
66
+ requiresKey: false,
67
+ category: 'local',
68
+ },
59
69
  // "Others" providers (OpenAI-compatible APIs)
60
70
  together: {
61
71
  type: 'together',
@@ -29,7 +29,7 @@ export interface CanvasIssue {
29
29
  message: string;
30
30
  }
31
31
  /** Run deterministic static checks on canvas content. No rendering. */
32
- export declare function runCanvasChecks(html: string, type: CanvasType): CanvasIssue[];
32
+ export declare function runCanvasChecks(html: string, type: CanvasType, manifest?: ControlManifest): CanvasIssue[];
33
33
  export declare function createCanvasTools(config: PlatformToolsConfig): (import("@compilr-dev/agents").Tool<{
34
34
  type: string;
35
35
  title: string;
@@ -50,4 +50,6 @@ export declare function createCanvasTools(config: PlatformToolsConfig): (import(
50
50
  new_str?: string;
51
51
  content?: string;
52
52
  replace_all?: boolean;
53
+ }> | import("@compilr-dev/agents").Tool<{
54
+ topic: string;
53
55
  }>)[];
@@ -12,9 +12,13 @@
12
12
  */
13
13
  import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
14
14
  import { truncateContent } from './truncate.js';
15
+ import { runQualityChecks } from '../../canvas/quality-checks.js';
15
16
  const PAGE_SIZES = ['16:9', '4:3', '1:1', '4:5', '9:16'];
16
17
  import { seedValues } from '../../canvas/types.js';
17
18
  import { validateControlManifest } from '../../canvas/validate.js';
19
+ import { canvasSkill, infographicSkill, carouselSkill, boardSkill, canvasStylesSkill, } from '../../skills/canvas-skills.js';
20
+ import { canvasExemplarInfographicSkill, canvasExemplarAppScreenSkill, } from '../../skills/canvas-exemplars.js';
21
+ import { canvasIconsSkill } from '../../skills/canvas-icons.js';
18
22
  const CANVAS_TYPES = ['infographic', 'carousel', 'board'];
19
23
  /** Render content as numbered lines (1-based), optionally a slice. */
20
24
  function numberedLines(content, startLine, maxLines) {
@@ -133,7 +137,7 @@ function offCanvasNodes(src, bounds) {
133
137
  return out;
134
138
  }
135
139
  /** Run deterministic static checks on canvas content. No rendering. */
136
- export function runCanvasChecks(html, type) {
140
+ export function runCanvasChecks(html, type, manifest) {
137
141
  const issues = [];
138
142
  const src = html.trim();
139
143
  if (!src) {
@@ -200,6 +204,14 @@ export function runCanvasChecks(html, type) {
200
204
  });
201
205
  }
202
206
  }
207
+ /*
208
+ The quality checks — surfaces, contrast, hue count, dead Tweaks. A separate module
209
+ because they are pure arithmetic on declared values and want testing without a
210
+ platform context; see `canvas/quality-checks.ts` for what each one deliberately
211
+ cannot know.
212
+ */
213
+ for (const issue of runQualityChecks(src, manifest))
214
+ issues.push(issue);
203
215
  return issues;
204
216
  }
205
217
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -294,7 +306,19 @@ export function createCanvasTools(config) {
294
306
  });
295
307
  if (!updated)
296
308
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
297
- return createSuccessResult(`Updated canvas "${updated.title}" (${updated.type})\nID: ${String(updated.id)}`);
309
+ /*
310
+ ⚠️ The prose is UNCHANGED, deliberately. Desktop's auto-split parses the id out
311
+ of this exact wording (`renderer/src/lib/canvas-authored.ts`), and the model has
312
+ been reading it for every canvas it has ever written. The id now also travels as
313
+ a structured sibling so the host can stop regex-parsing prose — but both must
314
+ ship together for one version, or a Desktop that has not updated silently stops
315
+ splitting on author.
316
+ */
317
+ return {
318
+ success: true,
319
+ result: `Updated canvas "${updated.title}" (${updated.type})\nID: ${String(updated.id)}`,
320
+ canvas: { id: updated.id, title: updated.title, type: updated.type, action: 'updated' },
321
+ };
298
322
  }
299
323
  const projectId = input.project_id ?? ctx.currentProjectId;
300
324
  if (!projectId) {
@@ -310,7 +334,11 @@ export function createCanvasTools(config) {
310
334
  values: seedValues(controls),
311
335
  pageSize: input.page_size,
312
336
  });
313
- return createSuccessResult(`Created canvas "${created.title}" (${created.type})\nID: ${String(created.id)}`);
337
+ return {
338
+ success: true,
339
+ result: `Created canvas "${created.title}" (${created.type})\nID: ${String(created.id)}`,
340
+ canvas: { id: created.id, title: created.title, type: created.type, action: 'created' },
341
+ };
314
342
  }
315
343
  catch (error) {
316
344
  return createErrorResult(`Failed to write canvas: ${error instanceof Error ? error.message : String(error)}`);
@@ -537,12 +565,17 @@ export function createCanvasTools(config) {
537
565
  try {
538
566
  let html;
539
567
  let type;
568
+ // ⚠️ The manifest travels with a STORED canvas only. Validating raw html has no
569
+ // manifest to check against, so the dead-Tweak check is silent there rather than
570
+ // reporting every control as unbound — a canvas is written before it is stored.
571
+ let manifest;
540
572
  if (input.canvas_id !== undefined) {
541
573
  const c = await canvases.getById(input.canvas_id);
542
574
  if (!c)
543
575
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
544
576
  html = c.content;
545
577
  type = c.type;
578
+ manifest = c.controls;
546
579
  }
547
580
  else if (input.html !== undefined) {
548
581
  html = input.html;
@@ -551,7 +584,7 @@ export function createCanvasTools(config) {
551
584
  else {
552
585
  return createErrorResult('Provide canvas_id (a stored canvas) or html (+ type) to validate.');
553
586
  }
554
- const issues = runCanvasChecks(html, type);
587
+ const issues = runCanvasChecks(html, type, manifest);
555
588
  if (issues.length === 0) {
556
589
  return createSuccessResult('✓ Passed all static checks — no structural/technical issues. (Visual/aesthetic review is a separate pass.)');
557
590
  }
@@ -625,6 +658,76 @@ export function createCanvasTools(config) {
625
658
  }
626
659
  },
627
660
  });
661
+ /*
662
+ ⚠️ THIS TOOL EXISTS BECAUSE THE CRAFT GUIDANCE WAS UNREACHABLE.
663
+
664
+ Skills are delivered by the HOST when a user types a slash command — Desktop fetches
665
+ the prompt and sends it as a user message. Nothing else delivers them: there is no
666
+ path from the agent to a skill, and the capability pack's snippet covers mechanics
667
+ only (canvas_write, the CSP, editing). So "make me an infographic" produced a canvas
668
+ authored with no design guidance at all, and every rule about hierarchy, restraint and
669
+ the elevation ramp reached the model only if the user happened to know to type
670
+ `/canvas` first.
671
+
672
+ That is the wrong shape: the agent, not the user, is the one who knows it is about to
673
+ author a canvas. So the guides become readable on demand — lazy, like the pack itself,
674
+ costing nothing until a canvas is actually being made.
675
+ */
676
+ const canvasGuideTool = defineTool({
677
+ name: 'canvas_guide',
678
+ description: 'Read the design guidance for a canvas BEFORE authoring one. Call with topic "canvas" ' +
679
+ 'first — it covers the steps, the sandbox rules and the theme tokens, and routes you to ' +
680
+ 'the right craft guide. Then read the guide for the type you picked ("infographic", ' +
681
+ '"carousel" or "board"), "styles" when choosing a look, and an "exemplar-*" topic for a ' +
682
+ 'worked example to adapt. Skipping this produces a canvas that renders but reads as ' +
683
+ 'generic — the exemplars in particular are worth more than any amount of description.',
684
+ inputSchema: {
685
+ type: 'object',
686
+ properties: {
687
+ topic: {
688
+ type: 'string',
689
+ enum: [
690
+ 'canvas',
691
+ 'infographic',
692
+ 'carousel',
693
+ 'board',
694
+ 'styles',
695
+ 'exemplar-infographic',
696
+ 'exemplar-app-screen',
697
+ 'icons',
698
+ ],
699
+ description: 'canvas = mechanics + steps (read this first) · infographic/carousel/board = the ' +
700
+ 'craft guide for that type · styles = the aesthetic catalogue · ' +
701
+ 'exemplar-infographic / exemplar-app-screen = a worked example to adapt, which is ' +
702
+ 'worth more than any amount of description.',
703
+ },
704
+ },
705
+ required: ['topic'],
706
+ },
707
+ execute: (input) => {
708
+ /*
709
+ ⚠️ `| undefined` is load-bearing. Without it `Record<string, T>` indexing claims a
710
+ value for every key, so the linter calls the guard below dead code — while at
711
+ runtime an unknown topic is exactly what it catches (asserted in canvas-guide
712
+ tests). The type was lying, not the guard.
713
+ */
714
+ const guides = {
715
+ canvas: canvasSkill,
716
+ infographic: infographicSkill,
717
+ carousel: carouselSkill,
718
+ board: boardSkill,
719
+ styles: canvasStylesSkill,
720
+ 'exemplar-infographic': canvasExemplarInfographicSkill,
721
+ 'exemplar-app-screen': canvasExemplarAppScreenSkill,
722
+ icons: canvasIconsSkill,
723
+ };
724
+ const guide = guides[input.topic];
725
+ if (!guide) {
726
+ return Promise.resolve(createErrorResult(`Unknown topic "${input.topic}". One of: ${Object.keys(guides).join(', ')}.`));
727
+ }
728
+ return Promise.resolve(createSuccessResult(guide.prompt));
729
+ },
730
+ });
628
731
  return [
629
732
  canvasWriteTool,
630
733
  canvasEditTool,
@@ -633,5 +736,6 @@ export function createCanvasTools(config) {
633
736
  canvasDeleteTool,
634
737
  canvasValidateTool,
635
738
  canvasScreenshotTool,
739
+ canvasGuideTool,
636
740
  ];
637
741
  }
package/dist/provider.js CHANGED
@@ -48,6 +48,22 @@ export function createProviderFromType(type, options) {
48
48
  return createGeminiNativeProvider({ model, apiKey, estimateTokens, maxTokens });
49
49
  case 'ollama':
50
50
  return createOllamaProvider({ model, baseUrl, estimateTokens, maxTokens });
51
+ case 'ollama-anthropic':
52
+ // Ollama's native Anthropic Messages endpoint (Ollama >= 0.14). Reuses the
53
+ // Claude provider pointed at the Ollama base URL — the Anthropic SDK POSTs
54
+ // to {baseURL}/v1/messages. Anthropic-only features Ollama doesn't support
55
+ // are disabled; the dummy key is accepted-not-validated. Surfaces thinking
56
+ // that the OpenAI-compat `ollama` path drops. See ollama-anthropic spec.
57
+ return createClaudeProvider({
58
+ model,
59
+ apiKey: apiKey || 'ollama',
60
+ baseURL: baseUrl ?? 'http://localhost:11434',
61
+ estimateTokens,
62
+ maxTokens,
63
+ enablePromptCaching: false,
64
+ enableTokenEfficientTools: false,
65
+ enableExtendedContext: false,
66
+ });
51
67
  case 'together':
52
68
  return createTogetherProvider({ model, apiKey, estimateTokens, maxTokens });
53
69
  case 'groq':