@compilr-dev/sdk 0.18.2 → 0.18.3

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.
@@ -236,10 +236,17 @@ export const CAPABILITY_PACKS = {
236
236
  canvas: {
237
237
  id: 'canvas',
238
238
  label: 'Visual Canvas',
239
- tools: ['canvas_write', 'canvas_edit', 'canvas_list', 'canvas_get', 'canvas_delete'],
239
+ tools: [
240
+ 'canvas_write',
241
+ 'canvas_edit',
242
+ 'canvas_list',
243
+ 'canvas_get',
244
+ 'canvas_delete',
245
+ 'canvas_validate',
246
+ ],
240
247
  readOnly: false,
241
248
  promptModules: ['platform-tool-hints'],
242
- 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.',
249
+ 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 to catch won't-render content, off-canvas board nodes, and un-themed colors.",
243
250
  estimatedPromptTokens: 150,
244
251
  estimatedToolTokens: 1500,
245
252
  },
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, 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, 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';
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
@@ -83,7 +83,7 @@ export { DEFAULT_MODELS, getContextWindow, DEFAULT_CONTEXT_WINDOW } from './mode
83
83
  // =============================================================================
84
84
  // Model Registry, Tiers, Providers (full model system)
85
85
  // =============================================================================
86
- export { MODEL_TIERS, TIER_INFO, isValidTier, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription,
86
+ export { MODEL_TIERS, TIER_INFO, isValidTier, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription,
87
87
  // Model tiers (pure, settings-free)
88
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';
89
89
  // =============================================================================
@@ -2,6 +2,6 @@
2
2
  * Models Module — Barrel Export
3
3
  */
4
4
  export { type ModelTier, type TierInfo, type ProviderModelMap, MODEL_TIERS, TIER_INFO, isValidTier, } from './types.js';
5
- export { type ThinkingFormat, type ModelStatus, type ModelInfo, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, } from './model-registry.js';
5
+ export { type ThinkingFormat, type ModelStatus, type ModelInfo, MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, 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';
@@ -4,7 +4,7 @@
4
4
  // Types & constants
5
5
  export { MODEL_TIERS, TIER_INFO, isValidTier, } from './types.js';
6
6
  // Model registry
7
- export { MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, } from './model-registry.js';
7
+ export { MODEL_REGISTRY, getModelsForProvider, getModelsSortedForDisplay, getModelInfo, isKnownModel, isModelSupported, modelSupportsImages, getThinkingFormat, getStatusIndicator, getStatusLabel, getDefaultModelForTier, areThinkingFormatsCompatible, shouldClearHistoryOnModelChange, getModelContextWindow, getModelDisplayName, getModelDescription, } from './model-registry.js';
8
8
  // Model tiers (pure, settings-free)
9
9
  export { getModelForTier, getTierMappings, getTierDisplayName, getShortModelName, getDefaultTierMappings, } from './model-tiers.js';
10
10
  // Provider metadata
@@ -31,6 +31,9 @@ export interface ModelInfo {
31
31
  description: string;
32
32
  /** Provider this model belongs to */
33
33
  provider: ProviderType;
34
+ /** Whether the model accepts image inputs (vision / multimodal). When unset,
35
+ * callers fall back to a provider/id heuristic — see modelSupportsImages(). */
36
+ supportsImages?: boolean;
34
37
  /** Default tier mapping (fast/balanced/powerful) - undefined if not a default */
35
38
  defaultTier?: ModelTier;
36
39
  /** Thinking block format this model uses */
@@ -72,6 +75,17 @@ export declare function isKnownModel(modelId: string): boolean;
72
75
  * Returns false for unknown models (conservative default).
73
76
  */
74
77
  export declare function isModelSupported(modelId: string): boolean;
78
+ /**
79
+ * Whether a model accepts image inputs (vision / multimodal).
80
+ *
81
+ * Uses the registry's `supportsImages` when set; otherwise infers from the
82
+ * provider/id — all modern Claude and Gemini are multimodal, as are OpenAI
83
+ * GPT-4o / GPT-4-turbo / GPT-4.1 / GPT-5; local/text llama·mistral·qwen·deepseek
84
+ * (and search models) are not. Defaults to FALSE for anything unrecognized —
85
+ * safe, because the caller then surfaces a warning rather than silently
86
+ * dropping an attached image (canvas-robustness: image on a text-only model).
87
+ */
88
+ export declare function modelSupportsImages(modelId: string): boolean;
75
89
  /**
76
90
  * Get the thinking format for a model.
77
91
  * Returns 'none' for unknown models (safe default).
@@ -24,6 +24,7 @@ export const MODEL_REGISTRY = [
24
24
  displayName: 'Haiku 4.5',
25
25
  description: 'Fast, low cost',
26
26
  provider: 'claude',
27
+ supportsImages: true,
27
28
  defaultTier: 'fast',
28
29
  thinkingFormat: 'claude',
29
30
  status: 'supported',
@@ -34,6 +35,7 @@ export const MODEL_REGISTRY = [
34
35
  displayName: 'Sonnet 4.6',
35
36
  description: 'Balanced (recommended)',
36
37
  provider: 'claude',
38
+ supportsImages: true,
37
39
  defaultTier: 'balanced',
38
40
  thinkingFormat: 'claude',
39
41
  status: 'supported',
@@ -45,6 +47,7 @@ export const MODEL_REGISTRY = [
45
47
  displayName: 'Opus 4.6',
46
48
  description: 'Most capable',
47
49
  provider: 'claude',
50
+ supportsImages: true,
48
51
  defaultTier: 'powerful',
49
52
  thinkingFormat: 'claude',
50
53
  status: 'supported',
@@ -57,6 +60,7 @@ export const MODEL_REGISTRY = [
57
60
  displayName: 'Sonnet 4.5',
58
61
  description: 'Previous generation',
59
62
  provider: 'claude',
63
+ supportsImages: true,
60
64
  thinkingFormat: 'claude',
61
65
  status: 'supported',
62
66
  contextWindow: 200000,
@@ -67,6 +71,7 @@ export const MODEL_REGISTRY = [
67
71
  displayName: 'Opus 4.5',
68
72
  description: 'Previous generation',
69
73
  provider: 'claude',
74
+ supportsImages: true,
70
75
  thinkingFormat: 'claude',
71
76
  status: 'supported',
72
77
  contextWindow: 200000,
@@ -77,6 +82,7 @@ export const MODEL_REGISTRY = [
77
82
  displayName: 'Sonnet 4',
78
83
  description: 'Previous generation',
79
84
  provider: 'claude',
85
+ supportsImages: true,
80
86
  thinkingFormat: 'claude',
81
87
  status: 'supported',
82
88
  contextWindow: 200000,
@@ -92,6 +98,7 @@ export const MODEL_REGISTRY = [
92
98
  displayName: 'Gemini 2.0 Flash',
93
99
  description: 'Fast, no thinking',
94
100
  provider: 'gemini',
101
+ supportsImages: true,
95
102
  thinkingFormat: 'none',
96
103
  status: 'supported',
97
104
  contextWindow: 1000000,
@@ -102,6 +109,7 @@ export const MODEL_REGISTRY = [
102
109
  displayName: 'Gemini 2.5 Flash Lite',
103
110
  description: 'Fast, low cost',
104
111
  provider: 'gemini',
112
+ supportsImages: true,
105
113
  defaultTier: 'fast',
106
114
  thinkingFormat: 'gemini-v2',
107
115
  status: 'supported',
@@ -112,6 +120,7 @@ export const MODEL_REGISTRY = [
112
120
  displayName: 'Gemini 2.5 Flash',
113
121
  description: 'Balanced (recommended)',
114
122
  provider: 'gemini',
123
+ supportsImages: true,
115
124
  defaultTier: 'balanced',
116
125
  thinkingFormat: 'gemini-v2',
117
126
  status: 'supported',
@@ -122,6 +131,7 @@ export const MODEL_REGISTRY = [
122
131
  displayName: 'Gemini 2.5 Pro',
123
132
  description: 'Most capable',
124
133
  provider: 'gemini',
134
+ supportsImages: true,
125
135
  defaultTier: 'powerful',
126
136
  thinkingFormat: 'gemini-v2',
127
137
  status: 'supported',
@@ -133,6 +143,7 @@ export const MODEL_REGISTRY = [
133
143
  displayName: 'Gemini 3 Flash (Preview)',
134
144
  description: 'Preview - may have stability issues',
135
145
  provider: 'gemini',
146
+ supportsImages: true,
136
147
  thinkingFormat: 'gemini-v3',
137
148
  status: 'supported',
138
149
  contextWindow: 1000000,
@@ -143,6 +154,7 @@ export const MODEL_REGISTRY = [
143
154
  displayName: 'Gemini 3 Pro (Preview)',
144
155
  description: 'Preview - may have stability issues',
145
156
  provider: 'gemini',
157
+ supportsImages: true,
146
158
  thinkingFormat: 'gemini-v3',
147
159
  status: 'supported',
148
160
  contextWindow: 1000000,
@@ -156,6 +168,7 @@ export const MODEL_REGISTRY = [
156
168
  displayName: 'GPT-4o',
157
169
  description: 'Balanced (recommended)',
158
170
  provider: 'openai',
171
+ supportsImages: true,
159
172
  defaultTier: 'balanced',
160
173
  thinkingFormat: 'none',
161
174
  status: 'supported',
@@ -166,6 +179,7 @@ export const MODEL_REGISTRY = [
166
179
  displayName: 'GPT-4o Mini',
167
180
  description: 'Fast, low cost',
168
181
  provider: 'openai',
182
+ supportsImages: true,
169
183
  defaultTier: 'fast',
170
184
  thinkingFormat: 'none',
171
185
  status: 'supported',
@@ -176,6 +190,7 @@ export const MODEL_REGISTRY = [
176
190
  displayName: 'GPT-4 Turbo',
177
191
  description: 'Most capable',
178
192
  provider: 'openai',
193
+ supportsImages: true,
179
194
  defaultTier: 'powerful',
180
195
  thinkingFormat: 'none',
181
196
  status: 'supported',
@@ -187,6 +202,7 @@ export const MODEL_REGISTRY = [
187
202
  displayName: 'GPT-5 Nano',
188
203
  description: 'Fast, experimental',
189
204
  provider: 'openai',
205
+ supportsImages: true,
190
206
  thinkingFormat: 'none',
191
207
  status: 'experimental',
192
208
  contextWindow: 128000,
@@ -197,6 +213,7 @@ export const MODEL_REGISTRY = [
197
213
  displayName: 'GPT-5 Mini',
198
214
  description: 'Balanced, experimental',
199
215
  provider: 'openai',
216
+ supportsImages: true,
200
217
  thinkingFormat: 'none',
201
218
  status: 'experimental',
202
219
  contextWindow: 128000,
@@ -207,6 +224,7 @@ export const MODEL_REGISTRY = [
207
224
  displayName: 'GPT-5.2',
208
225
  description: 'Most capable, experimental',
209
226
  provider: 'openai',
227
+ supportsImages: true,
210
228
  thinkingFormat: 'none',
211
229
  status: 'experimental',
212
230
  contextWindow: 128000,
@@ -499,6 +517,27 @@ export function isModelSupported(modelId) {
499
517
  const model = getModelInfo(modelId);
500
518
  return model?.status === 'supported';
501
519
  }
520
+ /**
521
+ * Whether a model accepts image inputs (vision / multimodal).
522
+ *
523
+ * Uses the registry's `supportsImages` when set; otherwise infers from the
524
+ * provider/id — all modern Claude and Gemini are multimodal, as are OpenAI
525
+ * GPT-4o / GPT-4-turbo / GPT-4.1 / GPT-5; local/text llama·mistral·qwen·deepseek
526
+ * (and search models) are not. Defaults to FALSE for anything unrecognized —
527
+ * safe, because the caller then surfaces a warning rather than silently
528
+ * dropping an attached image (canvas-robustness: image on a text-only model).
529
+ */
530
+ export function modelSupportsImages(modelId) {
531
+ const info = getModelInfo(modelId);
532
+ if (info?.supportsImages !== undefined)
533
+ return info.supportsImages;
534
+ const id = modelId.toLowerCase();
535
+ if (id.includes('claude') || id.includes('gemini'))
536
+ return true;
537
+ if (/gpt-4o|gpt-4-turbo|gpt-4\.1|gpt-5/.test(id))
538
+ return true;
539
+ return false;
540
+ }
502
541
  /**
503
542
  * Get the thinking format for a model.
504
543
  * Returns 'none' for unknown models (safe default).
@@ -11,7 +11,7 @@
11
11
  * (ICanvasRepository); the current project is resolved like the document tools.
12
12
  */
13
13
  import type { PlatformToolsConfig } from '../context.js';
14
- import type { ControlManifest } from '../../canvas/types.js';
14
+ import type { CanvasType, ControlManifest } from '../../canvas/types.js';
15
15
  /**
16
16
  * Guard the `html` arg against shapes that CANNOT render in the sandboxed
17
17
  * canvas iframe (canvas-robustness-spec §3.3). Returns a corrective error
@@ -24,6 +24,12 @@ import type { ControlManifest } from '../../canvas/types.js';
24
24
  * CSP actually blocks.
25
25
  */
26
26
  export declare function validateCanvasHtml(html: string): string | null;
27
+ export interface CanvasIssue {
28
+ level: 'error' | 'warn';
29
+ message: string;
30
+ }
31
+ /** Run deterministic static checks on canvas content. No rendering. */
32
+ export declare function runCanvasChecks(html: string, type: CanvasType): CanvasIssue[];
27
33
  export declare function createCanvasTools(config: PlatformToolsConfig): (import("@compilr-dev/agents").Tool<{
28
34
  type: string;
29
35
  title: string;
@@ -95,6 +95,113 @@ export function validateCanvasHtml(html) {
95
95
  }
96
96
  return null;
97
97
  }
98
+ /** Read a numeric `prop: Npx` from an inline style string. */
99
+ function pxProp(style, prop) {
100
+ const m = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*(-?[\\d.]+)px`, 'i').exec(style);
101
+ return m ? parseFloat(m[1]) : null;
102
+ }
103
+ /** Parse a board's declared width/height from its `data-board` element's style. */
104
+ function parseBoardBounds(tag) {
105
+ const styleM = /\bstyle\s*=\s*(['"])([\s\S]*?)\1/i.exec(tag);
106
+ if (!styleM)
107
+ return null;
108
+ const w = pxProp(styleM[2], 'width');
109
+ const h = pxProp(styleM[2], 'height');
110
+ return w !== null && h !== null ? { w, h } : null;
111
+ }
112
+ /** Best-effort: inline-positioned nodes (left+top px) that fall outside bounds. */
113
+ function offCanvasNodes(src, bounds) {
114
+ const out = [];
115
+ const re = /style\s*=\s*(['"])([^'"]*?\bleft\s*:\s*-?[\d.]+px[^'"]*?)\1/gi;
116
+ let m;
117
+ let seen = 0;
118
+ while ((m = re.exec(src)) !== null && seen < 200 && out.length < 5) {
119
+ seen++;
120
+ const style = m[2];
121
+ const left = pxProp(style, 'left');
122
+ const top = pxProp(style, 'top');
123
+ if (left === null || top === null)
124
+ continue;
125
+ const w = pxProp(style, 'width') ?? 0;
126
+ const h = pxProp(style, 'height') ?? 0;
127
+ if (left < 0 || top < 0 || left + w > bounds.w + 1 || top + h > bounds.h + 1) {
128
+ out.push(`A node at (${String(Math.round(left))}, ${String(Math.round(top))})` +
129
+ (w || h ? ` sized ${String(Math.round(w))}×${String(Math.round(h))}` : '') +
130
+ ` extends outside the ${String(bounds.w)}×${String(bounds.h)} board — move it inside or enlarge data-board.`);
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+ /** Run deterministic static checks on canvas content. No rendering. */
136
+ export function runCanvasChecks(html, type) {
137
+ const issues = [];
138
+ const src = html.trim();
139
+ if (!src) {
140
+ issues.push({ level: 'error', message: 'Canvas content is empty.' });
141
+ return issues;
142
+ }
143
+ const hasTag = /<[a-z][\s\S]*>/i.test(src);
144
+ if (!hasTag) {
145
+ issues.push({
146
+ level: 'error',
147
+ message: 'No HTML/SVG tags — the canvas renders raw HTML/SVG, not plain text or Markdown.',
148
+ });
149
+ }
150
+ // Won't-render shapes (Mermaid / Markdown / external CDN) — reuse the write guard.
151
+ const shape = validateCanvasHtml(src);
152
+ if (shape)
153
+ issues.push({ level: 'error', message: shape });
154
+ // Sparse / likely-incomplete.
155
+ const elementCount = (src.match(/<[a-z][a-z0-9]*\b/gi) ?? []).length;
156
+ if (hasTag && (src.length < 200 || elementCount < 3)) {
157
+ issues.push({
158
+ level: 'warn',
159
+ message: `Looks sparse (${String(elementCount)} elements, ${String(src.length)} chars) — is the canvas fully composed?`,
160
+ });
161
+ }
162
+ // Theme-awareness: hardcoded colors with no theme tokens won't adapt to the user's theme.
163
+ const hardColors = (src.match(/#[0-9a-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\s*\(/gi) ?? []).length;
164
+ const themeTokens = (src.match(/var\(\s*--canvas-/gi) ?? []).length;
165
+ if (hardColors >= 6 && themeTokens === 0) {
166
+ issues.push({
167
+ level: 'warn',
168
+ message: `${String(hardColors)} hardcoded colors and no theme tokens — the canvas won't adapt to the user's theme. ` +
169
+ 'Use var(--canvas-accent | secondary | fg | bg | muted | border | card) for themeable surfaces (keep a fixed color only where one is intended).',
170
+ });
171
+ }
172
+ // Type-specific structure.
173
+ if (type === 'board') {
174
+ const board = /<[^>]*\bdata-board\b[^>]*>/i.exec(src);
175
+ if (!board) {
176
+ issues.push({
177
+ level: 'error',
178
+ message: 'A board must wrap its nodes in <div data-board style="position:relative;width:Wpx;height:Hpx"> — those bounds drive Fit and Export.',
179
+ });
180
+ }
181
+ else {
182
+ const bounds = parseBoardBounds(board[0]);
183
+ if (!bounds) {
184
+ issues.push({
185
+ level: 'warn',
186
+ message: 'data-board has no parseable width/height in its style — Fit/Export fall back to a default. Set width:Wpx;height:Hpx.',
187
+ });
188
+ }
189
+ else {
190
+ for (const msg of offCanvasNodes(src, bounds))
191
+ issues.push({ level: 'warn', message: msg });
192
+ }
193
+ }
194
+ }
195
+ else if (type === 'carousel') {
196
+ if (!/\bdata-sheet\b/i.test(src)) {
197
+ issues.push({
198
+ level: 'warn',
199
+ message: 'No [data-sheet] elements — a carousel paginates on data-sheet blocks; without them it renders as one sheet.',
200
+ });
201
+ }
202
+ }
203
+ return issues;
204
+ }
98
205
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
99
206
  export function createCanvasTools(config) {
100
207
  const ctx = config.context;
@@ -396,5 +503,85 @@ export function createCanvasTools(config) {
396
503
  }
397
504
  },
398
505
  });
399
- return [canvasWriteTool, canvasEditTool, canvasListTool, canvasGetTool, canvasDeleteTool];
506
+ // ---------------------------------------------------------------------------
507
+ // canvas_validate — deterministic self-review
508
+ // ---------------------------------------------------------------------------
509
+ const canvasValidateTool = defineTool({
510
+ name: 'canvas_validate',
511
+ description: 'Self-review a canvas for problems — a fast, deterministic STATIC check (no rendering). ' +
512
+ 'Run it right after canvas_write to catch structural/technical faults: content that cannot render ' +
513
+ '(Mermaid/Markdown/external CDN), a board missing its data-board bounds or with nodes off-canvas, ' +
514
+ 'sparse/empty output, and hardcoded colors that ignore the theme tokens. Pass canvas_id to check a ' +
515
+ 'stored canvas, or html+type to check before writing. It does NOT judge visual/aesthetic quality — ' +
516
+ 'errors mean "likely renders broken", warnings are quality nudges.',
517
+ inputSchema: {
518
+ type: 'object',
519
+ properties: {
520
+ canvas_id: {
521
+ type: 'number',
522
+ description: 'Stored canvas to validate. Omit to validate html+type directly.',
523
+ },
524
+ html: {
525
+ type: 'string',
526
+ description: 'Raw HTML/SVG to validate (when not using canvas_id).',
527
+ },
528
+ type: {
529
+ type: 'string',
530
+ enum: CANVAS_TYPES,
531
+ description: 'Canvas type (required when passing html; defaults to infographic).',
532
+ },
533
+ },
534
+ required: [],
535
+ },
536
+ execute: async (input) => {
537
+ try {
538
+ let html;
539
+ let type;
540
+ if (input.canvas_id !== undefined) {
541
+ const c = await canvases.getById(input.canvas_id);
542
+ if (!c)
543
+ return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
544
+ html = c.content;
545
+ type = c.type;
546
+ }
547
+ else if (input.html !== undefined) {
548
+ html = input.html;
549
+ type = input.type ?? 'infographic';
550
+ }
551
+ else {
552
+ return createErrorResult('Provide canvas_id (a stored canvas) or html (+ type) to validate.');
553
+ }
554
+ const issues = runCanvasChecks(html, type);
555
+ if (issues.length === 0) {
556
+ return createSuccessResult('✓ Passed all static checks — no structural/technical issues. (Visual/aesthetic review is a separate pass.)');
557
+ }
558
+ const errors = issues.filter((i) => i.level === 'error');
559
+ const warns = issues.filter((i) => i.level === 'warn');
560
+ const lines = [];
561
+ if (errors.length > 0) {
562
+ lines.push(`${String(errors.length)} error(s) — likely to render broken:`);
563
+ for (const e of errors)
564
+ lines.push(` ✗ ${e.message}`);
565
+ }
566
+ if (warns.length > 0) {
567
+ lines.push(`${String(warns.length)} warning(s):`);
568
+ for (const w of warns)
569
+ lines.push(` ⚠ ${w.message}`);
570
+ }
571
+ lines.push('', 'Fix the errors (they break rendering); address warnings where they apply, then re-validate.');
572
+ return createSuccessResult(lines.join('\n'));
573
+ }
574
+ catch (error) {
575
+ return createErrorResult(`Failed to validate canvas: ${error instanceof Error ? error.message : String(error)}`);
576
+ }
577
+ },
578
+ });
579
+ return [
580
+ canvasWriteTool,
581
+ canvasEditTool,
582
+ canvasListTool,
583
+ canvasGetTool,
584
+ canvasDeleteTool,
585
+ canvasValidateTool,
586
+ ];
400
587
  }
@@ -67,7 +67,9 @@ export const canvasSkill = defineSkill({
67
67
 
68
68
  4. **To EDIT an existing canvas, do NOT re-send the whole document.** First call \`canvas_get\` with \`outline=true\` (or a \`startLine\`/\`maxLines\` slice) to find the exact snippet, then \`canvas_edit\` with \`operation=str_replace\` (old_str must be unique — include surrounding context — or set replace_all=true), or append/prepend. Reserve \`canvas_write\` with \`canvas_id\` for a full intentional rewrite.
69
69
 
70
- 5. **After writing, tell the user what you made in one line** and point them at the Tweaks they can adjust. The canvas opens in its own tab.
70
+ 5. **Validate before you hand it off.** Once the canvas is composed, call \`canvas_validate\` (canvas_id) — a fast static check that catches content that won't render (Mermaid/Markdown/CDN), a board missing its \`data-board\` bounds or with nodes off-canvas, sparse output, and hardcoded colors that ignore the theme tokens. Fix any **errors** (they render broken) and address applicable **warnings**, then move on. (It doesn't judge looks — that's your eye.)
71
+
72
+ 6. **After writing, tell the user what you made in one line** and point them at the Tweaks they can adjust. The canvas opens in its own tab.
71
73
 
72
74
  ## Rules
73
75
  - CALL THE TOOL — don't describe the HTML you "would" write and stop. A canvas only exists once the tool succeeds.
@@ -288,7 +288,14 @@ export const TOOL_GROUPS = {
288
288
  label: 'Visual Canvas',
289
289
  // Canvas tools are defined in @compilr-dev/sdk (createPlatformTools), not in
290
290
  // the agents-lib TOOL_NAMES registry, so they are referenced by literal name.
291
- tools: ['canvas_write', 'canvas_edit', 'canvas_list', 'canvas_get', 'canvas_delete'],
291
+ tools: [
292
+ 'canvas_write',
293
+ 'canvas_edit',
294
+ 'canvas_list',
295
+ 'canvas_get',
296
+ 'canvas_delete',
297
+ 'canvas_validate',
298
+ ],
292
299
  readOnly: false,
293
300
  tier: 'meta',
294
301
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,6 +25,10 @@
25
25
  "./canvas": {
26
26
  "types": "./dist/canvas/index.d.ts",
27
27
  "import": "./dist/canvas/index.js"
28
+ },
29
+ "./models": {
30
+ "types": "./dist/models/index.d.ts",
31
+ "import": "./dist/models/index.js"
28
32
  }
29
33
  },
30
34
  "files": [