@compilr-dev/sdk 0.18.11 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Canvas assets — images stored beside a canvas, not inside it.
3
+ *
4
+ * ⚠️ WHY NOT JUST A data: URI IN THE HTML. The sandbox CSP is `img-src data: blob:`, so a
5
+ * data URI is the ONLY thing that renders — but the canvas document is stored in sqlite
6
+ * and edited with `canvas_edit`'s str_replace. Four product photos inline is a few hundred
7
+ * KB of base64 sitting in the middle of the document the agent has to read an outline of
8
+ * and patch by hand. The editing discipline that makes canvases reliable stops working.
9
+ *
10
+ * So the markup carries a REFERENCE and the bytes live in a side table:
11
+ *
12
+ * <img src="asset:hero" alt="…">
13
+ *
14
+ * and the host substitutes real data URIs at the two moments the canvas becomes pixels:
15
+ * rendering into the iframe, and exporting to PDF/HTML/PNG. `resolveCanvasAssets` below is
16
+ * that substitution, shared by both so they cannot disagree — the same reason the token
17
+ * block had to be extracted.
18
+ *
19
+ * ⚠️ `asset:` IS NOT A REAL SCHEME. If a host forgets to substitute, the CSP blocks it and
20
+ * the image silently does not render. That is the failure mode to design against, which is
21
+ * why `canvas_validate` errors on a reference with no matching asset, and why the renderer
22
+ * and exporter share one function rather than each implementing the regex.
23
+ */
24
+ /** An image stored against a canvas. */
25
+ export interface CanvasAsset {
26
+ id: number;
27
+ canvasId: number;
28
+ /** The handle the markup references as `asset:<ref>`. */
29
+ ref: string;
30
+ mediaType: string;
31
+ /** base64, no data: prefix. */
32
+ data: string;
33
+ width?: number;
34
+ height?: number;
35
+ /** Encoded size in bytes — what the canvas actually costs to render. */
36
+ bytes: number;
37
+ /** Where it came from, for provenance when someone asks "which file is this?". */
38
+ sourcePath?: string;
39
+ createdAt: Date;
40
+ }
41
+ export interface CreateCanvasAssetInput {
42
+ canvasId: number;
43
+ ref: string;
44
+ mediaType: string;
45
+ data: string;
46
+ width?: number;
47
+ height?: number;
48
+ bytes: number;
49
+ sourcePath?: string;
50
+ }
51
+ /**
52
+ * A ref is a short handle an agent types into markup, so it has to be safe to embed in an
53
+ * attribute and easy to get right: lowercase word characters and dashes.
54
+ */
55
+ export declare const ASSET_REF: RegExp;
56
+ export declare const isValidAssetRef: (ref: string) => boolean;
57
+ /** Every asset ref the markup uses, deduplicated, in document order. */
58
+ export declare function referencedAssets(html: string): string[];
59
+ /**
60
+ * Replace every `asset:<ref>` with a real data URI.
61
+ *
62
+ * A reference with no matching asset is LEFT ALONE rather than blanked: the CSP will drop
63
+ * it either way, and leaving it makes the cause visible in devtools instead of presenting
64
+ * an empty `src` that looks like an authoring mistake.
65
+ */
66
+ export declare function resolveCanvasAssets(html: string, assets: readonly Pick<CanvasAsset, 'ref' | 'mediaType' | 'data'>[]): string;
67
+ /**
68
+ * Refs used by the markup that no asset satisfies.
69
+ *
70
+ * ⚠️ This is the check that makes the indirection safe. Without it a typo produces a
71
+ * canvas that renders with a hole in it and no error anywhere.
72
+ */
73
+ export declare function missingAssetRefs(html: string, assets: readonly Pick<CanvasAsset, 'ref'>[]): string[];
74
+ /**
75
+ * The total encoded weight of the assets a canvas actually uses.
76
+ * Unreferenced assets are excluded — they cost storage, not render time.
77
+ */
78
+ export declare function assetPayloadBytes(html: string, assets: readonly Pick<CanvasAsset, 'ref' | 'bytes'>[]): number;
79
+ /**
80
+ * ⚠️ A BUDGET, because every byte here is paid twice — once in the stored canvas and once
81
+ * in every export. Four photos at card size land around 200KB; a single un-resized phone
82
+ * photo is 4MB on its own. The tool refuses above this and says what to do.
83
+ */
84
+ export declare const ASSET_BUDGET_BYTES = 2000000;
85
+ /** Display width an image is resized to before storing, unless the caller says otherwise. */
86
+ export declare const DEFAULT_ASSET_WIDTH = 640;
87
+ /**
88
+ * The inverse of `resolveCanvasAssets`: turn embedded data URIs back into `asset:<ref>`.
89
+ *
90
+ * ⚠️ THIS IS NOT OPTIONAL, it is what keeps the indirection from unravelling. The host
91
+ * bridge serializes the live iframe's DOM back to be persisted after a visual edit — and
92
+ * that DOM holds the RESOLVED data URIs. Without this pass, one visual edit writes every
93
+ * image into the stored document as base64: exactly the state the asset table exists to
94
+ * prevent, arrived at by the back door, and silently.
95
+ *
96
+ * Matching is on the base64 payload rather than the whole URI, so a media type rewritten
97
+ * by the browser still maps home.
98
+ */
99
+ export declare function dereferenceCanvasAssets(html: string, assets: readonly Pick<CanvasAsset, 'ref' | 'data'>[]): string;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Canvas assets — images stored beside a canvas, not inside it.
3
+ *
4
+ * ⚠️ WHY NOT JUST A data: URI IN THE HTML. The sandbox CSP is `img-src data: blob:`, so a
5
+ * data URI is the ONLY thing that renders — but the canvas document is stored in sqlite
6
+ * and edited with `canvas_edit`'s str_replace. Four product photos inline is a few hundred
7
+ * KB of base64 sitting in the middle of the document the agent has to read an outline of
8
+ * and patch by hand. The editing discipline that makes canvases reliable stops working.
9
+ *
10
+ * So the markup carries a REFERENCE and the bytes live in a side table:
11
+ *
12
+ * <img src="asset:hero" alt="…">
13
+ *
14
+ * and the host substitutes real data URIs at the two moments the canvas becomes pixels:
15
+ * rendering into the iframe, and exporting to PDF/HTML/PNG. `resolveCanvasAssets` below is
16
+ * that substitution, shared by both so they cannot disagree — the same reason the token
17
+ * block had to be extracted.
18
+ *
19
+ * ⚠️ `asset:` IS NOT A REAL SCHEME. If a host forgets to substitute, the CSP blocks it and
20
+ * the image silently does not render. That is the failure mode to design against, which is
21
+ * why `canvas_validate` errors on a reference with no matching asset, and why the renderer
22
+ * and exporter share one function rather than each implementing the regex.
23
+ */
24
+ /**
25
+ * A ref is a short handle an agent types into markup, so it has to be safe to embed in an
26
+ * attribute and easy to get right: lowercase word characters and dashes.
27
+ */
28
+ export const ASSET_REF = /^[a-z0-9][a-z0-9-]{0,39}$/;
29
+ export const isValidAssetRef = (ref) => ASSET_REF.test(ref);
30
+ /**
31
+ * ⚠️ Matches `asset:<ref>` ONLY inside an attribute value, not anywhere in the document.
32
+ * A canvas that mentions `asset:hero` in a code sample or a caption must not have its text
33
+ * rewritten into a 200KB base64 blob.
34
+ */
35
+ const REFERENCE = /(["'])asset:([a-z0-9][a-z0-9-]{0,39})\1/gi;
36
+ /** Every asset ref the markup uses, deduplicated, in document order. */
37
+ export function referencedAssets(html) {
38
+ const seen = new Set();
39
+ for (const m of html.matchAll(REFERENCE))
40
+ seen.add(m[2].toLowerCase());
41
+ return [...seen];
42
+ }
43
+ /**
44
+ * Replace every `asset:<ref>` with a real data URI.
45
+ *
46
+ * A reference with no matching asset is LEFT ALONE rather than blanked: the CSP will drop
47
+ * it either way, and leaving it makes the cause visible in devtools instead of presenting
48
+ * an empty `src` that looks like an authoring mistake.
49
+ */
50
+ export function resolveCanvasAssets(html, assets) {
51
+ if (assets.length === 0)
52
+ return html;
53
+ const byRef = new Map(assets.map((a) => [a.ref.toLowerCase(), a]));
54
+ return html.replace(REFERENCE, (whole, quote, ref) => {
55
+ const asset = byRef.get(ref.toLowerCase());
56
+ if (!asset)
57
+ return whole;
58
+ return `${quote}data:${asset.mediaType};base64,${asset.data}${quote}`;
59
+ });
60
+ }
61
+ /**
62
+ * Refs used by the markup that no asset satisfies.
63
+ *
64
+ * ⚠️ This is the check that makes the indirection safe. Without it a typo produces a
65
+ * canvas that renders with a hole in it and no error anywhere.
66
+ */
67
+ export function missingAssetRefs(html, assets) {
68
+ const have = new Set(assets.map((a) => a.ref.toLowerCase()));
69
+ return referencedAssets(html).filter((ref) => !have.has(ref));
70
+ }
71
+ /**
72
+ * The total encoded weight of the assets a canvas actually uses.
73
+ * Unreferenced assets are excluded — they cost storage, not render time.
74
+ */
75
+ export function assetPayloadBytes(html, assets) {
76
+ const used = new Set(referencedAssets(html));
77
+ return assets
78
+ .filter((a) => used.has(a.ref.toLowerCase()))
79
+ .reduce((total, a) => total + a.bytes, 0);
80
+ }
81
+ /**
82
+ * ⚠️ A BUDGET, because every byte here is paid twice — once in the stored canvas and once
83
+ * in every export. Four photos at card size land around 200KB; a single un-resized phone
84
+ * photo is 4MB on its own. The tool refuses above this and says what to do.
85
+ */
86
+ export const ASSET_BUDGET_BYTES = 2_000_000;
87
+ /** Display width an image is resized to before storing, unless the caller says otherwise. */
88
+ export const DEFAULT_ASSET_WIDTH = 640;
89
+ /**
90
+ * The inverse of `resolveCanvasAssets`: turn embedded data URIs back into `asset:<ref>`.
91
+ *
92
+ * ⚠️ THIS IS NOT OPTIONAL, it is what keeps the indirection from unravelling. The host
93
+ * bridge serializes the live iframe's DOM back to be persisted after a visual edit — and
94
+ * that DOM holds the RESOLVED data URIs. Without this pass, one visual edit writes every
95
+ * image into the stored document as base64: exactly the state the asset table exists to
96
+ * prevent, arrived at by the back door, and silently.
97
+ *
98
+ * Matching is on the base64 payload rather than the whole URI, so a media type rewritten
99
+ * by the browser still maps home.
100
+ */
101
+ export function dereferenceCanvasAssets(html, assets) {
102
+ let out = html;
103
+ for (const asset of assets) {
104
+ if (!asset.data)
105
+ continue;
106
+ // `data:<anything>;base64,<payload>` → asset:<ref>, in any attribute quoting.
107
+ const pattern = new RegExp(`data:[^"'\\s]*?base64,${asset.data.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g');
108
+ out = out.replace(pattern, `asset:${asset.ref}`);
109
+ }
110
+ return out;
111
+ }
@@ -10,3 +10,5 @@ export { runQualityChecks, checkSurfaceTokens, checkTextContrast, checkHueCount,
10
10
  export type { QualityIssue } from './quality-checks.js';
11
11
  export { deriveSurfaceRamp, withLightness } from './ramp.js';
12
12
  export type { SurfaceRamp } from './ramp.js';
13
+ export { resolveCanvasAssets, dereferenceCanvasAssets, referencedAssets, missingAssetRefs, assetPayloadBytes, isValidAssetRef, ASSET_BUDGET_BYTES, DEFAULT_ASSET_WIDTH, } from './assets.js';
14
+ export type { CanvasAsset, CreateCanvasAssetInput } from './assets.js';
@@ -14,3 +14,9 @@ export * from './validate.js';
14
14
  export { parseColor, relativeLuminance, contrastRatio, oklabLightness, lightnessDeltaPct, isNeutral, hueAngle, AA_TEXT, SURFACE_DELTA_MIN, SURFACE_DELTA_TARGET, SURFACE_DELTA_MAX, HAIRLINE_MIN_PCT, HAIRLINE_MAX_PCT, } from './color.js';
15
15
  export { runQualityChecks, checkSurfaceTokens, checkTextContrast, checkHueCount, checkDeadTweaks, primaryVarNames, } from './quality-checks.js';
16
16
  export { deriveSurfaceRamp, withLightness } from './ramp.js';
17
+ /*
18
+ Assets — images stored beside a canvas. `resolveCanvasAssets` is renderer-safe and is
19
+ shared by the host's live iframe AND its export path; they must not each implement the
20
+ substitution, for the same reason the theme token block had to be extracted.
21
+ */
22
+ export { resolveCanvasAssets, dereferenceCanvasAssets, referencedAssets, missingAssetRefs, assetPayloadBytes, isValidAssetRef, ASSET_BUDGET_BYTES, DEFAULT_ASSET_WIDTH, } from './assets.js';
@@ -245,6 +245,9 @@ export const CAPABILITY_PACKS = {
245
245
  'canvas_validate',
246
246
  'canvas_screenshot',
247
247
  'canvas_guide',
248
+ 'canvas_asset_add',
249
+ 'canvas_asset_list',
250
+ 'canvas_asset_delete',
248
251
  ],
249
252
  readOnly: false,
250
253
  promptModules: ['platform-tool-hints'],
@@ -255,6 +258,9 @@ export const CAPABILITY_PACKS = {
255
258
  'START by calling canvas_guide("canvas") — the design rules live there, not here, and a ' +
256
259
  'canvas authored without them renders correctly and still reads as generic. Then ' +
257
260
  'canvas_guide on the type you pick. ' +
261
+ // ⚠️ Real images go in as ASSETS, never as base64 in the markup.
262
+ 'When the user has supplied real images, put them in with canvas_asset_add and reference ' +
263
+ 'them as <img src="asset:REF"> — never paste base64, never draw a grey box instead. ' +
258
264
  '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
265
  estimatedPromptTokens: 190,
260
266
  estimatedToolTokens: 1500,
package/dist/index.d.ts CHANGED
@@ -35,8 +35,8 @@
35
35
  */
36
36
  export { createCompilrAgent } from './agent.js';
37
37
  export type { CompilrAgentConfig, CompilrAgent, RunOptions, RunResult, ToolCallRecord, ToolConfig, UsageInfo, ProviderType, PermissionCallback, GuardrailConfig, ContextConfig, CapabilitiesConfig, } from './config.js';
38
- export { AgentTeam, TeamAgent, SharedContextManager, ArtifactStore, DelegationTracker, ContextResolver, createDelegationStatusTool, createHandoffTool, createDelegateTool, createDelegateBackgroundTool, buildHandoffTaskMessage, validateHandoffIntent, HandoffStash, createConsultTool, buildConsultQuestionMessage, ROLE_NAME_ALIASES, normalizeRoleName, } from './team/index.js';
39
- export type { AgentTeamConfig, TeamAgentConfig, ITeamPersistence, IArtifactStorage, ISessionRegistry, CustomAgentDefinition, AgentTemplate, AgentWorkshopData, WorkshopRoleDef, WorkshopToolProfile, WorkshopModelTier, WorkshopSkillDef, PlanSubmitInfo, PlanSubmitResult, PlanModeExitInfo, PlanModeCallbacks, ToolConfig as TeamToolConfig, ToolTier, ToolGroup, ProfileInfo, } from './team/index.js';
38
+ export { AgentTeam, TeamAgent, SharedContextManager, ArtifactStore, DelegationTracker, ContextResolver, createDelegationStatusTool, createHandoffTool, createDelegateTool, createDelegateBackgroundTool, buildHandoffTaskMessage, validateHandoffIntent, HandoffStash, createConsultTool, buildConsultQuestionMessage, ROLE_NAME_ALIASES, normalizeRoleName, isNarrowing, } from './team/index.js';
39
+ export type { AgentTeamConfig, TeamAgentConfig, TeamAgentUpdate, AgentUpdateResult, ITeamPersistence, IArtifactStorage, ISessionRegistry, CustomAgentDefinition, AgentTemplate, AgentWorkshopData, WorkshopRoleDef, WorkshopToolProfile, WorkshopModelTier, WorkshopSkillDef, PlanSubmitInfo, PlanSubmitResult, PlanModeExitInfo, PlanModeCallbacks, ToolConfig as TeamToolConfig, ToolTier, ToolGroup, ProfileInfo, } from './team/index.js';
40
40
  export type { AgentRole, RoleMetadata, ToolProfile, MascotExpression, BackgroundSessionInfo, SerializedTeam, SerializedTeamAgent, TeamMetadata, TeamEvent, TeamEventType, TeamEventHandler, Artifact, ArtifactType as TeamArtifactType, ArtifactSummary as TeamArtifactSummary, CreateArtifactOptions, UpdateArtifactOptions, SerializedArtifact, SharedContext, SharedProjectInfo, SharedTeamInfo, TeamRosterEntry, TeamActivity, TeamActivityType, SharedDecision, TokenBudget, SerializedSharedContext, ParsedMention, ParsedInput, ResolvedMention, ResolveOptions, ResolutionSource, Delegation, DelegationStatus, DelegationResult, CompletionEvent, CreateDelegationOptions, DelegationStats, DelegationTrackerEvents, HandoffResult, HandoffToolConfig, HandoffIntent, HandoffValidationResult, DelegateResult, DelegateToolConfig, DelegateBackgroundResult, DelegateBackgroundToolConfig, ConsultInput, ConsultResult, ConsultToolConfig, NormalizedRole, SkillToolRequirement, } from './team/index.js';
41
41
  export { ROLE_METADATA, ROLE_EXPERTISE, ROLE_GROUPS, PREDEFINED_ROLE_IDS, TOOL_GROUPS, TOOL_PROFILES, PROFILE_INFO, SKILL_REQUIREMENTS, CUSTOM_MASCOTS, buildAgentWorkshopData, buildSuggestedRolesMap, PLAN_MODE_BLOCKED_TOOLS, PLAN_MODE_DENIAL_MESSAGE, PLAN_MODE_PROMPT, isToolAllowedInPlanMode, getPlanModePrompt, } from './team/index.js';
42
42
  export { getToolsForProfile, detectProfileFromTools, isProfileReadOnly, generateToolAwarenessPrompt, generateCoordinatorGuidance, generateSpecialistGuidance, createDefaultToolConfig, validateToolConfig, getAllGroupIds, getGroupInfo, getGroupsByTier, getGroupsForProfile, assignMascot, generateCustomAgentSystemPrompt, getCustomAgentToolFilter, getCustomAgentProfileLabel, validateAgentId, isAgentIdTaken, createCustomAgentDefinition, listTemplates, getTemplate, saveTemplate, updateTemplate, deleteTemplate, createAgentFromTemplate, parseInputForMentions, getReferencedAgents, hasReferences, buildMessageWithContext, buildContextMap, findAgentForRole, findAgentById, getAvailableSpecialists, getSpecialistsSummary, hasSpecialists, suggestOwner, suggestOwners, matchesAgentExpertise, wouldCreateLoop, recordAssignment, getAssignmentHistory, clearAssignmentHistory, clearAllAssignmentHistory, canReassign, resolveAgentIdCollision, setActiveSharedContext, getActiveSharedContext, recordTeamActivity, getDefinedSkillNames, getSkillRequirements, checkSkillCompatibility, getCompatibleSkills, getAllRequiredTools, getSkillsByCategory, } from './team/index.js';
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ export { createCompilrAgent } from './agent.js';
41
41
  // Multi-Agent Team Orchestration
42
42
  // =============================================================================
43
43
  // Core classes
44
- export { AgentTeam, TeamAgent, SharedContextManager, ArtifactStore, DelegationTracker, ContextResolver, createDelegationStatusTool, createHandoffTool, createDelegateTool, createDelegateBackgroundTool, buildHandoffTaskMessage, validateHandoffIntent, HandoffStash, createConsultTool, buildConsultQuestionMessage, ROLE_NAME_ALIASES, normalizeRoleName, } from './team/index.js';
44
+ export { AgentTeam, TeamAgent, SharedContextManager, ArtifactStore, DelegationTracker, ContextResolver, createDelegationStatusTool, createHandoffTool, createDelegateTool, createDelegateBackgroundTool, buildHandoffTaskMessage, validateHandoffIntent, HandoffStash, createConsultTool, buildConsultQuestionMessage, ROLE_NAME_ALIASES, normalizeRoleName, isNarrowing, } from './team/index.js';
45
45
  // Constants
46
46
  export { ROLE_METADATA, ROLE_EXPERTISE, ROLE_GROUPS, PREDEFINED_ROLE_IDS, TOOL_GROUPS, TOOL_PROFILES, PROFILE_INFO, SKILL_REQUIREMENTS, CUSTOM_MASCOTS, buildAgentWorkshopData, buildSuggestedRolesMap,
47
47
  // Plan mode
@@ -45,8 +45,22 @@ export interface PlatformHooks {
45
45
  export interface PlatformToolsConfig {
46
46
  /** Data access layer */
47
47
  context: PlatformContext;
48
- /** Working directory override (defaults to process.cwd()) */
49
- cwd?: string;
48
+ /**
49
+ * Working directory override (defaults to process.cwd()).
50
+ * Accepts a getter, because platform tools are memoised and a host's active
51
+ * project can change after they are built.
52
+ */
53
+ cwd?: string | (() => string | undefined | null);
50
54
  /** Optional hooks for CLI-specific side effects */
51
55
  hooks?: PlatformHooks;
52
56
  }
57
+ /**
58
+ * Resolve a `cwd` that may be a value or a getter.
59
+ *
60
+ * ⚠️ Platform tools are built once and memoised by every host, so a plain string is
61
+ * captured at construction. In Desktop that meant relative paths resolved against
62
+ * whichever project was open when the tools were first requested — for the rest of the
63
+ * session, across every project switch. Hosts whose working directory moves pass a
64
+ * getter; this is the one place that unwraps it.
65
+ */
66
+ export declare function resolveCwd(cwd?: string | (() => string | undefined | null)): string;
@@ -4,4 +4,16 @@
4
4
  * Provides a single entry point for data access, abstracting over the
5
5
  * underlying storage backend (SQLite in CLI, PostgreSQL in web/API).
6
6
  */
7
- export {};
7
+ /**
8
+ * Resolve a `cwd` that may be a value or a getter.
9
+ *
10
+ * ⚠️ Platform tools are built once and memoised by every host, so a plain string is
11
+ * captured at construction. In Desktop that meant relative paths resolved against
12
+ * whichever project was open when the tools were first requested — for the rest of the
13
+ * session, across every project switch. Hosts whose working directory moves pass a
14
+ * getter; this is the one place that unwraps it.
15
+ */
16
+ export function resolveCwd(cwd) {
17
+ const value = typeof cwd === 'function' ? cwd() : cwd;
18
+ return value ?? process.cwd();
19
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { Project, WorkItem, WorkItemComment, ProjectDocument, Plan, PlanSummary, PlanWithWorkItem, HistoryEntry, CreateProjectInput, UpdateProjectInput, ProjectListOptions, CreateWorkItemInput, UpdateWorkItemInput, QueryWorkItemsInput, CreateCommentInput, UpdateCommentInput, CreateDocumentInput, UpdateDocumentInput, CreatePlanInput, UpdatePlanInput, ListPlansOptions, WorkItemQueryResult, ProjectListResult, BulkCreateItem, ProjectStatus, WorkItemType, WorkItemStatus, DocumentType, PlanStatus } from './types.js';
9
9
  import type { CanvasRecord, CanvasSummary, CreateCanvasInput, UpdateCanvasInput } from '../canvas/types.js';
10
+ import type { CanvasAsset, CreateCanvasAssetInput } from '../canvas/assets.js';
10
11
  export interface IProjectRepository {
11
12
  create(input: CreateProjectInput): Promise<Project>;
12
13
  getById(id: number): Promise<Project | null>;
@@ -53,6 +54,9 @@ export interface ICanvasRepository {
53
54
  listByProject(projectId: number): Promise<CanvasSummary[]>;
54
55
  update(id: number, input: UpdateCanvasInput): Promise<CanvasRecord | null>;
55
56
  delete(id: number): Promise<boolean>;
57
+ addAsset?(input: CreateCanvasAssetInput): Promise<CanvasAsset>;
58
+ listAssets?(canvasId: number): Promise<CanvasAsset[]>;
59
+ deleteAsset?(canvasId: number, ref: string): Promise<boolean>;
56
60
  }
57
61
  export interface IPlanRepository {
58
62
  create(input: CreatePlanInput): Promise<Plan>;
@@ -5,6 +5,7 @@
5
5
  * SQL reserved word, so it is always quoted as "values" in statements.
6
6
  */
7
7
  import type Database from 'better-sqlite3';
8
+ import type { CanvasAsset, CreateCanvasAssetInput } from '../../canvas/assets.js';
8
9
  import type { ICanvasRepository } from '../repositories.js';
9
10
  import type { CanvasRecord, CanvasSummary, CreateCanvasInput, UpdateCanvasInput } from '../../canvas/types.js';
10
11
  export declare class SQLiteCanvasRepository implements ICanvasRepository {
@@ -15,4 +16,12 @@ export declare class SQLiteCanvasRepository implements ICanvasRepository {
15
16
  listByProject(projectId: number): Promise<CanvasSummary[]>;
16
17
  update(id: number, input: UpdateCanvasInput): Promise<CanvasRecord | null>;
17
18
  delete(id: number): Promise<boolean>;
19
+ /**
20
+ * ⚠️ UPSERT on (canvas_id, ref). Re-adding the same handle REPLACES the image rather
21
+ * than failing on the unique index — an agent correcting a photo should not have to
22
+ * delete first, and a half-applied fix is worse than an overwrite.
23
+ */
24
+ addAsset(input: CreateCanvasAssetInput): Promise<CanvasAsset>;
25
+ listAssets(canvasId: number): Promise<CanvasAsset[]>;
26
+ deleteAsset(canvasId: number, ref: string): Promise<boolean>;
18
27
  }
@@ -92,4 +92,56 @@ export class SQLiteCanvasRepository {
92
92
  const result = this.db.prepare('DELETE FROM canvases WHERE id = ?').run(id);
93
93
  return Promise.resolve(result.changes > 0);
94
94
  }
95
+ // ---------------------------------------------------------------------------
96
+ // Assets — images stored beside the canvas, referenced from its markup as
97
+ // `asset:<ref>` and substituted by the host at render and export.
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * ⚠️ UPSERT on (canvas_id, ref). Re-adding the same handle REPLACES the image rather
101
+ * than failing on the unique index — an agent correcting a photo should not have to
102
+ * delete first, and a half-applied fix is worse than an overwrite.
103
+ */
104
+ addAsset(input) {
105
+ this.db
106
+ .prepare(`INSERT INTO canvas_assets (canvas_id, ref, media_type, data, width, height, bytes, source_path)
107
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
108
+ ON CONFLICT(canvas_id, ref) DO UPDATE SET
109
+ media_type = excluded.media_type,
110
+ data = excluded.data,
111
+ width = excluded.width,
112
+ height = excluded.height,
113
+ bytes = excluded.bytes,
114
+ source_path = excluded.source_path`)
115
+ .run(input.canvasId, input.ref, input.mediaType, input.data, input.width ?? null, input.height ?? null, input.bytes, input.sourcePath ?? null);
116
+ const row = this.db
117
+ .prepare('SELECT * FROM canvas_assets WHERE canvas_id = ? AND ref = ?')
118
+ .get(input.canvasId, input.ref);
119
+ return Promise.resolve(toAsset(row));
120
+ }
121
+ listAssets(canvasId) {
122
+ const rows = this.db
123
+ .prepare('SELECT * FROM canvas_assets WHERE canvas_id = ? ORDER BY id')
124
+ .all(canvasId);
125
+ return Promise.resolve(rows.map(toAsset));
126
+ }
127
+ deleteAsset(canvasId, ref) {
128
+ const result = this.db
129
+ .prepare('DELETE FROM canvas_assets WHERE canvas_id = ? AND ref = ?')
130
+ .run(canvasId, ref);
131
+ return Promise.resolve(result.changes > 0);
132
+ }
133
+ }
134
+ function toAsset(row) {
135
+ return {
136
+ id: row.id,
137
+ canvasId: row.canvas_id,
138
+ ref: row.ref,
139
+ mediaType: row.media_type,
140
+ data: row.data,
141
+ width: row.width ?? undefined,
142
+ height: row.height ?? undefined,
143
+ bytes: row.bytes,
144
+ sourcePath: row.source_path ?? undefined,
145
+ createdAt: new Date(row.created_at),
146
+ };
95
147
  }
@@ -180,4 +180,33 @@ function runMigrations(db, fromVersion, toVersion) {
180
180
  db.exec(`ALTER TABLE canvases ADD COLUMN page_size TEXT;`);
181
181
  db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(9);
182
182
  }
183
+ if (fromVersion < 10 && toVersion >= 10) {
184
+ /*
185
+ Canvas assets — images stored BESIDE a canvas rather than inside it.
186
+
187
+ ⚠️ The markup references `asset:<ref>` and the host substitutes a data URI at render
188
+ and at export. Inlining base64 into `content` would also render (the CSP allows
189
+ data:) but would put hundreds of KB in the middle of a document agents read as an
190
+ outline and patch with str_replace — the editing discipline that keeps canvas
191
+ authoring reliable stops working at that size.
192
+ */
193
+ db.exec(`
194
+ CREATE TABLE IF NOT EXISTS canvas_assets (
195
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
196
+ canvas_id INTEGER NOT NULL,
197
+ ref TEXT NOT NULL,
198
+ media_type TEXT NOT NULL,
199
+ data TEXT NOT NULL,
200
+ width INTEGER,
201
+ height INTEGER,
202
+ bytes INTEGER NOT NULL,
203
+ source_path TEXT,
204
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
205
+ FOREIGN KEY (canvas_id) REFERENCES canvases(id) ON DELETE CASCADE
206
+ );
207
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_canvas_assets_ref
208
+ ON canvas_assets(canvas_id, ref);
209
+ `);
210
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(10);
211
+ }
183
212
  }
@@ -4,7 +4,7 @@
4
4
  * Shared between CLI and Desktop — both access ~/.compilr-dev/projects.db
5
5
  * Schema version must be kept in sync across all consumers.
6
6
  */
7
- export declare const SCHEMA_VERSION = 9;
7
+ export declare const SCHEMA_VERSION = 10;
8
8
  export declare const SCHEMA_SQL = "\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Projects table\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n display_name TEXT NOT NULL,\n description TEXT,\n type TEXT DEFAULT 'general',\n status TEXT DEFAULT 'active',\n path TEXT NOT NULL,\n docs_path TEXT,\n repo_pattern TEXT DEFAULT 'single',\n language TEXT,\n framework TEXT,\n package_manager TEXT,\n runtime_version TEXT,\n commands TEXT,\n git_remote TEXT,\n git_branch TEXT DEFAULT 'main',\n workflow_mode TEXT DEFAULT 'flexible',\n lifecycle_state TEXT DEFAULT 'setup',\n current_item_id TEXT,\n last_context TEXT,\n metadata TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_activity_at DATETIME\n);\n\n-- Work items (backlog items, tasks, bugs)\nCREATE TABLE IF NOT EXISTS work_items (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n item_number INTEGER NOT NULL,\n item_id TEXT NOT NULL,\n type TEXT NOT NULL,\n status TEXT DEFAULT 'backlog',\n priority TEXT DEFAULT 'medium',\n guided_step TEXT,\n owner TEXT,\n title TEXT NOT NULL,\n description TEXT,\n estimated_effort TEXT,\n actual_minutes INTEGER,\n completed_at DATETIME,\n completed_by TEXT,\n commit_hash TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n UNIQUE (project_id, item_id)\n);\n\n-- Project documents (PRD, architecture, plans, etc.)\nCREATE TABLE IF NOT EXISTS project_documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n doc_type TEXT NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n status TEXT,\n work_item_id INTEGER,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE SET NULL\n);\n\n-- Work item history (audit trail)\nCREATE TABLE IF NOT EXISTS work_item_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n work_item_id INTEGER NOT NULL,\n project_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT,\n notes TEXT,\n changed_by TEXT,\n changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path);\nCREATE INDEX IF NOT EXISTS idx_projects_docs_path ON projects(docs_path);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_project ON work_items(project_id);\nCREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_priority ON work_items(priority);\nCREATE INDEX IF NOT EXISTS idx_work_items_owner ON work_items(owner);\nCREATE INDEX IF NOT EXISTS idx_project_documents_project ON project_documents(project_id);\nCREATE INDEX IF NOT EXISTS idx_project_documents_type ON project_documents(doc_type);\nCREATE INDEX IF NOT EXISTS idx_project_documents_status ON project_documents(status);\nCREATE INDEX IF NOT EXISTS idx_project_documents_work_item ON project_documents(work_item_id);\nCREATE INDEX IF NOT EXISTS idx_work_item_history_item ON work_item_history(work_item_id);\n\n-- Terminal sessions (multi-terminal awareness)\nCREATE TABLE IF NOT EXISTS terminal_sessions (\n id TEXT PRIMARY KEY,\n project_id INTEGER,\n pid INTEGER NOT NULL,\n tty_path TEXT,\n label TEXT,\n started_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,\n active_agent TEXT DEFAULT 'default',\n agents_json TEXT DEFAULT '[]',\n status TEXT DEFAULT 'active',\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL\n);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_project ON terminal_sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_status ON terminal_sessions(status);\n\n-- File locks (multi-terminal file lock awareness)\nCREATE TABLE IF NOT EXISTS file_locks (\n path TEXT NOT NULL,\n project_id INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n locked_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (path, project_id),\n FOREIGN KEY (session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_file_locks_session ON file_locks(session_id);\n\n-- Session notifications (cross-session notifications)\nCREATE TABLE IF NOT EXISTS session_notifications (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n from_session_id TEXT NOT NULL,\n to_session_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n message TEXT,\n payload_json TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n read_at DATETIME,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (from_session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_project ON session_notifications(project_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_to_session ON session_notifications(to_session_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_unread ON session_notifications(read_at);\n\n-- Work item comments\nCREATE TABLE IF NOT EXISTS work_item_comments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n work_item_id INTEGER NOT NULL,\n project_id INTEGER NOT NULL,\n author TEXT NOT NULL,\n content TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_work_item_comments_work_item ON work_item_comments(work_item_id);\nCREATE INDEX IF NOT EXISTS idx_work_item_comments_project ON work_item_comments(project_id);\n\n-- Canvases (visual reasoning surfaces: infographic / carousel / board)\nCREATE TABLE IF NOT EXISTS canvases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n controls TEXT NOT NULL DEFAULT '{\"controls\":[]}',\n \"values\" TEXT NOT NULL DEFAULT '{}',\n page_size TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_canvases_project ON canvases(project_id);\n";
9
9
  export interface ProjectRecord {
10
10
  id: number;
@@ -4,7 +4,7 @@
4
4
  * Shared between CLI and Desktop — both access ~/.compilr-dev/projects.db
5
5
  * Schema version must be kept in sync across all consumers.
6
6
  */
7
- export const SCHEMA_VERSION = 9;
7
+ export const SCHEMA_VERSION = 10;
8
8
  export const SCHEMA_SQL = `
9
9
  -- Schema version tracking
10
10
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -10,6 +10,7 @@
10
10
  * manifest (validated here). Persistence is delegated to ctx.canvases
11
11
  * (ICanvasRepository); the current project is resolved like the document tools.
12
12
  */
13
+ import type { ImageResizer } from './image-tools.js';
13
14
  import type { PlatformToolsConfig } from '../context.js';
14
15
  import type { CanvasType, ControlManifest } from '../../canvas/types.js';
15
16
  /**
@@ -30,7 +31,9 @@ export interface CanvasIssue {
30
31
  }
31
32
  /** Run deterministic static checks on canvas content. No rendering. */
32
33
  export declare function runCanvasChecks(html: string, type: CanvasType, manifest?: ControlManifest): CanvasIssue[];
33
- export declare function createCanvasTools(config: PlatformToolsConfig): (import("@compilr-dev/agents").Tool<{
34
+ export declare function createCanvasTools(config: PlatformToolsConfig, imageConfig?: {
35
+ resizer?: ImageResizer;
36
+ }): (import("@compilr-dev/agents").Tool<{
34
37
  type: string;
35
38
  title: string;
36
39
  html: string;
@@ -52,4 +55,9 @@ export declare function createCanvasTools(config: PlatformToolsConfig): (import(
52
55
  replace_all?: boolean;
53
56
  }> | import("@compilr-dev/agents").Tool<{
54
57
  topic: string;
58
+ }> | import("@compilr-dev/agents").Tool<{
59
+ canvas_id: number;
60
+ path: string;
61
+ ref: string;
62
+ max_width?: number;
55
63
  }>)[];