@compilr-dev/sdk 0.17.17 → 0.18.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.
@@ -81,6 +81,8 @@ export interface CanvasRecord {
81
81
  controls: ControlManifest;
82
82
  /** Current tweak state; seeded from control defaults on create. */
83
83
  values: Record<string, ParamValue>;
84
+ /** Page size / aspect (carousel slide aspect). Undefined = default 16:9. */
85
+ pageSize?: PageSize;
84
86
  createdAt: Date;
85
87
  updatedAt: Date;
86
88
  }
@@ -92,6 +94,24 @@ export interface CanvasSummary {
92
94
  title: string;
93
95
  updatedAt: Date;
94
96
  }
97
+ /**
98
+ * Page size / aspect for a canvas — mainly the carousel slide aspect (16:9
99
+ * widescreen, 4:3 standard, 1:1 square, 4:5 portrait, 9:16 story). The agent may
100
+ * set it and the user may change it live. Infographic/board ignore it (sized by
101
+ * content / declared bounds). Default `16:9`.
102
+ */
103
+ export type PageSize = '16:9' | '4:3' | '1:1' | '4:5' | '9:16';
104
+ export declare const DEFAULT_PAGE_SIZE: PageSize;
105
+ /** Pixel dimensions per page size (used for render aspect + export sizing). */
106
+ export declare const PAGE_SIZE_DIMENSIONS: Record<PageSize, {
107
+ w: number;
108
+ h: number;
109
+ }>;
110
+ /** Resolve a page size (or undefined) to pixel dimensions, defaulting to 16:9. */
111
+ export declare function pageSizeDimensions(pageSize: PageSize | undefined): {
112
+ w: number;
113
+ h: number;
114
+ };
95
115
  export interface CreateCanvasInput {
96
116
  projectId: number;
97
117
  type: CanvasType;
@@ -99,7 +119,8 @@ export interface CreateCanvasInput {
99
119
  content: string;
100
120
  controls?: ControlManifest;
101
121
  values?: Record<string, ParamValue>;
122
+ pageSize?: PageSize;
102
123
  }
103
- export type UpdateCanvasInput = Partial<Pick<CanvasRecord, 'title' | 'content' | 'controls' | 'values'>>;
124
+ export type UpdateCanvasInput = Partial<Pick<CanvasRecord, 'title' | 'content' | 'controls' | 'values' | 'pageSize'>>;
104
125
  /** Seed a values map from a manifest's control defaults. */
105
126
  export declare function seedValues(manifest: ControlManifest | undefined): Record<string, ParamValue>;
@@ -10,6 +10,19 @@
10
10
  * subpath `@compilr-dev/sdk/canvas` (the Desktop renderer needs the control types
11
11
  * + validation without pulling node-only deps — same pattern as `.../errors`).
12
12
  */
13
+ export const DEFAULT_PAGE_SIZE = '16:9';
14
+ /** Pixel dimensions per page size (used for render aspect + export sizing). */
15
+ export const PAGE_SIZE_DIMENSIONS = {
16
+ '16:9': { w: 1280, h: 720 },
17
+ '4:3': { w: 1024, h: 768 },
18
+ '1:1': { w: 1080, h: 1080 },
19
+ '4:5': { w: 1080, h: 1350 },
20
+ '9:16': { w: 1080, h: 1920 },
21
+ };
22
+ /** Resolve a page size (or undefined) to pixel dimensions, defaulting to 16:9. */
23
+ export function pageSizeDimensions(pageSize) {
24
+ return PAGE_SIZE_DIMENSIONS[pageSize ?? DEFAULT_PAGE_SIZE];
25
+ }
13
26
  /** Seed a values map from a manifest's control defaults. */
14
27
  export function seedValues(manifest) {
15
28
  const values = {};
@@ -30,6 +30,7 @@ function toCanvas(r) {
30
30
  content: r.content,
31
31
  controls: parseControls(r.controls),
32
32
  values: parseValues(r.values),
33
+ pageSize: r.page_size ? r.page_size : undefined,
33
34
  createdAt: new Date(r.created_at),
34
35
  updatedAt: new Date(r.updated_at),
35
36
  };
@@ -53,9 +54,9 @@ export class SQLiteCanvasRepository {
53
54
  const controls = JSON.stringify(input.controls ?? { controls: [] });
54
55
  const values = JSON.stringify(input.values ?? {});
55
56
  const result = this.db
56
- .prepare(`INSERT INTO canvases (project_id, type, title, content, controls, "values", created_at, updated_at)
57
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
58
- .run(input.projectId, input.type, input.title, input.content, controls, values, now, now);
57
+ .prepare(`INSERT INTO canvases (project_id, type, title, content, controls, "values", page_size, created_at, updated_at)
58
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
59
+ .run(input.projectId, input.type, input.title, input.content, controls, values, input.pageSize ?? null, now, now);
59
60
  const record = this.db
60
61
  .prepare('SELECT * FROM canvases WHERE id = ?')
61
62
  .get(Number(result.lastInsertRowid));
@@ -80,9 +81,10 @@ export class SQLiteCanvasRepository {
80
81
  const content = input.content ?? existing.content;
81
82
  const controls = input.controls ? JSON.stringify(input.controls) : existing.controls;
82
83
  const values = input.values ? JSON.stringify(input.values) : existing.values;
84
+ const pageSize = input.pageSize !== undefined ? input.pageSize : existing.page_size;
83
85
  this.db
84
- .prepare(`UPDATE canvases SET title = ?, content = ?, controls = ?, "values" = ?, updated_at = ? WHERE id = ?`)
85
- .run(title, content, controls, values, now, id);
86
+ .prepare(`UPDATE canvases SET title = ?, content = ?, controls = ?, "values" = ?, page_size = ?, updated_at = ? WHERE id = ?`)
87
+ .run(title, content, controls, values, pageSize, now, id);
86
88
  const record = this.db.prepare('SELECT * FROM canvases WHERE id = ?').get(id);
87
89
  return Promise.resolve(toCanvas(record));
88
90
  }
@@ -175,4 +175,9 @@ function runMigrations(db, fromVersion, toVersion) {
175
175
  `);
176
176
  db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(8);
177
177
  }
178
+ if (fromVersion < 9 && toVersion >= 9) {
179
+ // Canvas page size / aspect (carousel slide aspect). NULL = default 16:9.
180
+ db.exec(`ALTER TABLE canvases ADD COLUMN page_size TEXT;`);
181
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(9);
182
+ }
178
183
  }
@@ -4,8 +4,8 @@
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 = 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 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";
7
+ export declare const SCHEMA_VERSION = 9;
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;
11
11
  name: string;
@@ -71,6 +71,7 @@ export interface CanvasDbRecord {
71
71
  content: string;
72
72
  controls: string;
73
73
  values: string;
74
+ page_size: string | null;
74
75
  created_at: string;
75
76
  updated_at: string;
76
77
  }
@@ -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 = 8;
7
+ export const SCHEMA_VERSION = 9;
8
8
  export const SCHEMA_SQL = `
9
9
  -- Schema version tracking
10
10
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -181,6 +181,7 @@ CREATE TABLE IF NOT EXISTS canvases (
181
181
  content TEXT NOT NULL,
182
182
  controls TEXT NOT NULL DEFAULT '{"controls":[]}',
183
183
  "values" TEXT NOT NULL DEFAULT '{}',
184
+ page_size TEXT,
184
185
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
185
186
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
186
187
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
@@ -17,6 +17,7 @@ export declare function createCanvasTools(config: PlatformToolsConfig): (import(
17
17
  title: string;
18
18
  html: string;
19
19
  controls?: ControlManifest;
20
+ page_size?: string;
20
21
  canvas_id?: number;
21
22
  project_id?: number;
22
23
  }> | import("@compilr-dev/agents").Tool<{
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
14
14
  import { truncateContent } from './truncate.js';
15
+ const PAGE_SIZES = ['16:9', '4:3', '1:1', '4:5', '9:16'];
15
16
  import { seedValues } from '../../canvas/types.js';
16
17
  import { validateControlManifest } from '../../canvas/validate.js';
17
18
  const CANVAS_TYPES = ['infographic', 'carousel', 'board'];
@@ -91,6 +92,13 @@ export function createCanvasTools(config) {
91
92
  controls: { type: 'array', items: { type: 'object' } },
92
93
  },
93
94
  },
95
+ page_size: {
96
+ type: 'string',
97
+ enum: PAGE_SIZES,
98
+ description: 'Optional page size / aspect — mainly the CAROUSEL slide aspect: 16:9 (widescreen, default), ' +
99
+ '4:3 (standard), 1:1 (square/Instagram), 4:5 (portrait), 9:16 (story/reel). Ignored for ' +
100
+ 'infographic/board. Set 1:1 or 4:5 for social carousels.',
101
+ },
94
102
  canvas_id: {
95
103
  type: 'number',
96
104
  description: 'Existing canvas id to update. Omit to create a new canvas.',
@@ -119,6 +127,7 @@ export function createCanvasTools(config) {
119
127
  ...(input.controls
120
128
  ? { controls: input.controls, values: seedValues(input.controls) }
121
129
  : {}),
130
+ ...(input.page_size ? { pageSize: input.page_size } : {}),
122
131
  });
123
132
  if (!updated)
124
133
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
@@ -136,6 +145,7 @@ export function createCanvasTools(config) {
136
145
  content: input.html,
137
146
  controls,
138
147
  values: seedValues(controls),
148
+ pageSize: input.page_size,
139
149
  });
140
150
  return createSuccessResult(`Created canvas "${created.title}" (${created.type})\nID: ${String(created.id)}`);
141
151
  }
@@ -40,31 +40,57 @@ export interface ConsultResult {
40
40
  * (target run exceeded max iterations, transport error, etc.). */
41
41
  error?: string;
42
42
  }
43
+ /** Default wall-clock cap for a whole consult sub-run (ms). */
44
+ export declare const DEFAULT_CONSULT_TIMEOUT_MS = 180000;
45
+ /**
46
+ * Options passed to `onConsult` alongside the question. Additive — hosts
47
+ * that predate the timeout work can ignore `signal` and keep compiling,
48
+ * but they will NOT get the hang protection until they forward it into
49
+ * their target `agent.run(..., { signal })`.
50
+ */
51
+ export interface ConsultRunOptions {
52
+ /**
53
+ * Aborts when the wall-clock timeout fires (or the host links its own
54
+ * borrower signal to it). Implementations MUST forward this into the
55
+ * target agent's run so an expired consult actually stops the sub-run
56
+ * instead of orphaning it.
57
+ */
58
+ signal: AbortSignal;
59
+ }
43
60
  /**
44
61
  * Configuration for `createConsultTool`.
45
62
  *
46
63
  * `onConsult` is intentionally platform-shaped — each host runs sub-agents
47
64
  * differently (CLI's REPL sub-context, Desktop's agent-manager, in-process
48
- * Agent.run). The SDK owns validation and message format; the platform
49
- * owns the actual run.
65
+ * Agent.run). The SDK owns validation, message format, and the wall-clock
66
+ * timeout; the platform owns the actual run.
50
67
  */
51
68
  export interface ConsultToolConfig {
52
69
  /** The team instance — used to validate target agent membership. */
53
70
  team: AgentTeam;
54
71
  /** The borrowing agent's ID. */
55
72
  currentAgentId: string;
73
+ /**
74
+ * Wall-clock cap for the whole consult sub-run, in ms. The SDK arms a
75
+ * timer, aborts `signal` on expiry, and returns a recoverable timeout
76
+ * error to the borrower's LLM. Defaults to {@link DEFAULT_CONSULT_TIMEOUT_MS}.
77
+ */
78
+ timeoutMs?: number;
56
79
  /**
57
80
  * Platform-specific consult handler. Called after validation passes.
58
81
  * Should:
59
82
  * 1. Synthesise the question message (use `buildConsultQuestionMessage`).
60
- * 2. Run the target agent against that message (sub-run, not a turn
61
- * in the borrower's conversation).
83
+ * 2. Run the target agent against that message, **history-aware** (the
84
+ * target's real agent/history — NOT a throwaway or snapshot-restored
85
+ * run), forwarding `opts.signal` into the run so the timeout can
86
+ * abort it.
62
87
  * 3. Return the target's final response text.
63
88
  *
64
- * The target's conversation history is NOT mutated by a consult — the
65
- * call is transient.
89
+ * As of consult v2 the target DOES remember the exchange — the historical
90
+ * "transient / history not mutated" contract is removed. See
91
+ * `consult-persistent-conversation-spec.md`.
66
92
  */
67
- onConsult: (targetAgentId: string, question: string, context: string | undefined) => Promise<{
93
+ onConsult: (targetAgentId: string, question: string, context: string | undefined, opts: ConsultRunOptions) => Promise<{
68
94
  answer: string;
69
95
  error?: string;
70
96
  }>;
@@ -21,6 +21,8 @@
21
21
  * /workspace/project-docs/00-requirements/compilr-dev-sdk/consult-tool-spec.md
22
22
  */
23
23
  import { defineTool } from '@compilr-dev/agents';
24
+ /** Default wall-clock cap for a whole consult sub-run (ms). */
25
+ export const DEFAULT_CONSULT_TIMEOUT_MS = 180_000;
24
26
  // =============================================================================
25
27
  // Canonical question-message format
26
28
  // =============================================================================
@@ -63,10 +65,12 @@ export function buildConsultQuestionMessage(input) {
63
65
  */
64
66
  export function createConsultTool(config) {
65
67
  const { team, currentAgentId, onConsult } = config;
68
+ const timeoutMs = config.timeoutMs ?? DEFAULT_CONSULT_TIMEOUT_MS;
66
69
  return defineTool({
67
70
  name: 'consult',
68
- description: 'Ask another specialist a focused question and continue the conversation yourself. ' +
69
- 'Use this when you need a quick opinion or expertise input from a teammate without giving up the conversation. ' +
71
+ description: 'Ask a teammate a focused question and continue your own work. ' +
72
+ 'They answer using their own context and remember the exchange, so a consult is a real interaction in their conversation — not a throwaway. ' +
73
+ 'Use this when you need a quick opinion or expertise from a teammate without giving up the conversation. ' +
70
74
  'Unlike handoff, you keep ownership — the target answers and control returns to you. ' +
71
75
  'Example: $arch can consult $pm on scope, get an answer, and continue designing.',
72
76
  inputSchema: {
@@ -123,8 +127,19 @@ export function createConsultTool(config) {
123
127
  error: `Agent "${targetId}" not found in team. Available specialists: ${available || '(none)'}`,
124
128
  };
125
129
  }
130
+ // Wall-clock cap for the whole sub-run. The SDK owns this so BOTH
131
+ // hosts inherit the hang fix without duplicating timeout logic. On
132
+ // expiry we abort the signal (the host forwards it into the target
133
+ // run) and hand the borrower's LLM a recoverable error rather than
134
+ // hanging "running" forever — the failure mode observed in testing.
135
+ const ac = new AbortController();
136
+ const timer = setTimeout(() => {
137
+ ac.abort(new Error('consult-timeout'));
138
+ }, timeoutMs);
126
139
  try {
127
- const { answer, error } = await onConsult(targetId, question, context);
140
+ const { answer, error } = await onConsult(targetId, question, context, {
141
+ signal: ac.signal,
142
+ });
128
143
  if (error) {
129
144
  return { success: false, error };
130
145
  }
@@ -132,11 +147,23 @@ export function createConsultTool(config) {
132
147
  return { success: true, result };
133
148
  }
134
149
  catch (err) {
150
+ if (ac.signal.aborted) {
151
+ const seconds = String(Math.round(timeoutMs / 1000));
152
+ return {
153
+ success: false,
154
+ error: `$${targetId} didn't respond in time (${seconds}s). ` +
155
+ 'It may have a pending question — the request was surfaced in your chat. ' +
156
+ 'You can try consulting again, ask the user, or proceed without it.',
157
+ };
158
+ }
135
159
  return {
136
160
  success: false,
137
161
  error: `Consult failed: ${err instanceof Error ? err.message : String(err)}`,
138
162
  };
139
163
  }
164
+ finally {
165
+ clearTimeout(timer);
166
+ }
140
167
  },
141
168
  });
142
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.17.17",
3
+ "version": "0.18.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",