@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.
- package/dist/canvas/color.d.ts +85 -0
- package/dist/canvas/color.js +216 -0
- package/dist/canvas/index.d.ts +6 -0
- package/dist/canvas/index.js +10 -0
- package/dist/canvas/quality-checks.d.ts +75 -0
- package/dist/canvas/quality-checks.js +217 -0
- package/dist/canvas/ramp.d.ts +45 -0
- package/dist/canvas/ramp.js +98 -0
- package/dist/capabilities/packs.js +10 -2
- package/dist/config.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +2 -0
- package/dist/models/model-registry.js +1 -0
- package/dist/models/ollama-discovery.d.ts +63 -0
- package/dist/models/ollama-discovery.js +172 -0
- package/dist/models/providers.js +10 -0
- package/dist/platform/tools/canvas-tools.d.ts +3 -1
- package/dist/platform/tools/canvas-tools.js +108 -4
- package/dist/provider.js +16 -0
- package/dist/skills/canvas-exemplars.d.ts +8 -0
- package/dist/skills/canvas-exemplars.js +36 -0
- package/dist/skills/canvas-icons.d.ts +6 -0
- package/dist/skills/canvas-icons.js +84 -0
- package/dist/skills/canvas-skills.js +160 -20
- package/dist/skills/platform-skills.d.ts +2 -0
- package/dist/skills/platform-skills.js +8 -0
- package/dist/team/tool-config.js +1 -0
- package/package.json +1 -1
|
@@ -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.
|
|
251
|
-
|
|
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
|
// =============================================================================
|
package/dist/models/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/models/index.js
CHANGED
|
@@ -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';
|
|
@@ -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
|
+
}
|
package/dist/models/providers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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':
|