@compilr-dev/sdk 0.17.2 → 0.17.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.
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Canvas Tools — agent authoring + management of visual canvases.
3
3
  *
4
- * 4 tools: canvas_write (create/update), canvas_list, canvas_get, canvas_delete.
4
+ * 5 tools: canvas_write (create/full replace), canvas_edit (server-side
5
+ * str_replace/append/prepend — the token-cheap edit path), canvas_list,
6
+ * canvas_get (targeted read: outline / line slice), canvas_delete.
5
7
  *
6
8
  * `content` is the agent's raw HTML/SVG; the host renderer injects the CSP +
7
9
  * bridge and renders it in a sandboxed iframe. `controls` is the optional Tweaks
@@ -19,4 +21,14 @@ export declare function createCanvasTools(config: PlatformToolsConfig): (import(
19
21
  project_id?: number;
20
22
  }> | import("@compilr-dev/agents").Tool<{
21
23
  canvas_id: number;
24
+ outline?: boolean;
25
+ startLine?: number;
26
+ maxLines?: number;
27
+ }> | import("@compilr-dev/agents").Tool<{
28
+ canvas_id: number;
29
+ operation: "str_replace" | "append" | "prepend";
30
+ old_str?: string;
31
+ new_str?: string;
32
+ content?: string;
33
+ replace_all?: boolean;
22
34
  }>)[];
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Canvas Tools — agent authoring + management of visual canvases.
3
3
  *
4
- * 4 tools: canvas_write (create/update), canvas_list, canvas_get, canvas_delete.
4
+ * 5 tools: canvas_write (create/full replace), canvas_edit (server-side
5
+ * str_replace/append/prepend — the token-cheap edit path), canvas_list,
6
+ * canvas_get (targeted read: outline / line slice), canvas_delete.
5
7
  *
6
8
  * `content` is the agent's raw HTML/SVG; the host renderer injects the CSP +
7
9
  * bridge and renders it in a sandboxed iframe. `controls` is the optional Tweaks
@@ -13,6 +15,43 @@ import { truncateContent } from './truncate.js';
13
15
  import { seedValues } from '../../canvas/types.js';
14
16
  import { validateControlManifest } from '../../canvas/validate.js';
15
17
  const CANVAS_TYPES = ['infographic', 'carousel', 'board'];
18
+ /** Render content as numbered lines (1-based), optionally a slice. */
19
+ function numberedLines(content, startLine, maxLines) {
20
+ const lines = content.split('\n');
21
+ const start = Math.max(1, startLine ?? 1);
22
+ const end = maxLines ? Math.min(lines.length, start - 1 + maxLines) : lines.length;
23
+ const out = [];
24
+ for (let i = start; i <= end; i++)
25
+ out.push(`${String(i)}\t${lines[i - 1]}`);
26
+ return out.join('\n');
27
+ }
28
+ /**
29
+ * Lightweight HTML skeleton: lines carrying an id/class on a structural tag, with
30
+ * line numbers — enough for the agent to locate an anchor for a str_replace edit
31
+ * without pulling the whole document.
32
+ */
33
+ function htmlOutline(content) {
34
+ const lines = content.split('\n');
35
+ const rows = [];
36
+ const re = /<(section|div|header|nav|main|footer|aside|article|button|form|ul|ol|table|h[1-6])\b[^>]*\b(id|class)=/i;
37
+ lines.forEach((line, i) => {
38
+ if (re.test(line))
39
+ rows.push(`${String(i + 1)}\t${line.trim().slice(0, 100)}`);
40
+ });
41
+ return rows.length ? rows.join('\n') : '(no structural elements with id/class found)';
42
+ }
43
+ /** Count non-overlapping occurrences of needle in haystack. */
44
+ function countOccurrences(haystack, needle) {
45
+ if (!needle)
46
+ return 0;
47
+ let count = 0;
48
+ let idx = haystack.indexOf(needle);
49
+ while (idx !== -1) {
50
+ count++;
51
+ idx = haystack.indexOf(needle, idx + needle.length);
52
+ }
53
+ return count;
54
+ }
16
55
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
17
56
  export function createCanvasTools(config) {
18
57
  const ctx = config.context;
@@ -29,7 +68,8 @@ export function createCanvasTools(config) {
29
68
  'each control is { type: slider|number|toggle|select|color|text, param, label, default, ...type config }. ' +
30
69
  'Bind params in your HTML via CSS custom properties var(--param), [data-bind="param"] text, and ' +
31
70
  '[data-show="param"] visibility; for computed updates define window.applyParams(values) in a <script>. ' +
32
- 'Pass canvas_id to update an existing canvas; omit it to create a new one.',
71
+ 'Omit canvas_id to create. To EDIT an existing canvas, prefer canvas_edit (str_replace) it avoids ' +
72
+ 're-sending the whole document; only use canvas_write with canvas_id for a full intentional replace.',
33
73
  inputSchema: {
34
74
  type: 'object',
35
75
  properties: {
@@ -139,11 +179,22 @@ export function createCanvasTools(config) {
139
179
  // ---------------------------------------------------------------------------
140
180
  const canvasGetTool = defineTool({
141
181
  name: 'canvas_get',
142
- description: 'Get a canvas by id, including its content, controls manifest, and current values.',
182
+ description: 'Get a canvas by id. Content is returned with line numbers. For large canvases, ' +
183
+ 'use outline=true to see the element skeleton, or startLine/maxLines to read a slice — ' +
184
+ 'then edit with canvas_edit (str_replace) instead of re-sending the whole document.',
143
185
  inputSchema: {
144
186
  type: 'object',
145
187
  properties: {
146
188
  canvas_id: { type: 'number', description: 'Canvas id.' },
189
+ outline: {
190
+ type: 'boolean',
191
+ description: 'If true, return only the structural skeleton (elements with id/class + line numbers).',
192
+ },
193
+ startLine: {
194
+ type: 'number',
195
+ description: 'First line to read (1-based). Use with maxLines.',
196
+ },
197
+ maxLines: { type: 'number', description: 'Max number of lines to read from startLine.' },
147
198
  },
148
199
  required: ['canvas_id'],
149
200
  },
@@ -152,13 +203,23 @@ export function createCanvasTools(config) {
152
203
  const c = await canvases.getById(input.canvas_id);
153
204
  if (!c)
154
205
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
206
+ const totalLines = c.content.split('\n').length;
207
+ const header = `Canvas "${c.title}" (${c.type})\n` +
208
+ `ID: ${String(c.id)} | Controls: ${String(c.controls.controls.length)} | Lines: ${String(totalLines)}\n\n`;
209
+ if (input.outline) {
210
+ return createSuccessResult(`${header}--- outline ---\n${htmlOutline(c.content)}`);
211
+ }
212
+ if (input.startLine !== undefined || input.maxLines !== undefined) {
213
+ return createSuccessResult(`${header}--- content (lines ${String(input.startLine ?? 1)}+) ---\n` +
214
+ numberedLines(c.content, input.startLine, input.maxLines));
215
+ }
216
+ // Full content, numbered. Truncate very large docs with an accurate hint.
155
217
  const tc = truncateContent(c.content);
156
- const output = `Canvas "${c.title}" (${c.type})\n` +
157
- `ID: ${String(c.id)}\n` +
158
- `Controls: ${String(c.controls.controls.length)}\n\n` +
159
- `--- content ---\n` +
160
- tc.content;
161
- return createSuccessResult(output);
218
+ if (tc.truncated) {
219
+ return createSuccessResult(`${header}--- content (truncated; use outline=true or startLine/maxLines) ---\n` +
220
+ numberedLines(c.content, 1, 200));
221
+ }
222
+ return createSuccessResult(`${header}--- content ---\n${numberedLines(c.content)}`);
162
223
  }
163
224
  catch (error) {
164
225
  return createErrorResult(`Failed to get canvas: ${error instanceof Error ? error.message : String(error)}`);
@@ -166,6 +227,85 @@ export function createCanvasTools(config) {
166
227
  },
167
228
  });
168
229
  // ---------------------------------------------------------------------------
230
+ // canvas_edit — server-side edit; only the change goes through context
231
+ // ---------------------------------------------------------------------------
232
+ const canvasEditTool = defineTool({
233
+ name: 'canvas_edit',
234
+ description: 'Edit an existing canvas WITHOUT re-sending its full HTML — prefer this over canvas_write for edits. ' +
235
+ 'The stored content is read and modified server-side; only your small change goes through context. ' +
236
+ 'operation=str_replace finds an exact snippet (old_str) and replaces it with new_str (old_str must be ' +
237
+ 'unique unless replace_all=true); append/prepend add content at the end/start. Use canvas_get ' +
238
+ '(outline or a line slice) first to find the exact snippet to replace.',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ canvas_id: { type: 'number', description: 'Canvas id to edit.' },
243
+ operation: {
244
+ type: 'string',
245
+ enum: ['str_replace', 'append', 'prepend'],
246
+ description: 'str_replace: swap an exact snippet. append/prepend: add content at end/start.',
247
+ },
248
+ old_str: {
249
+ type: 'string',
250
+ description: 'For str_replace: the exact existing snippet to find (include enough surrounding text to be unique).',
251
+ },
252
+ new_str: {
253
+ type: 'string',
254
+ description: 'For str_replace: the replacement snippet.',
255
+ },
256
+ content: {
257
+ type: 'string',
258
+ description: 'For append/prepend: the content to add.',
259
+ },
260
+ replace_all: {
261
+ type: 'boolean',
262
+ description: 'For str_replace: replace every occurrence instead of requiring old_str to be unique.',
263
+ },
264
+ },
265
+ required: ['canvas_id', 'operation'],
266
+ },
267
+ execute: async (input) => {
268
+ try {
269
+ const c = await canvases.getById(input.canvas_id);
270
+ if (!c)
271
+ return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
272
+ let next;
273
+ let summary;
274
+ if (input.operation === 'str_replace') {
275
+ if (input.old_str === undefined || input.new_str === undefined) {
276
+ return createErrorResult('str_replace requires both old_str and new_str.');
277
+ }
278
+ const occurrences = countOccurrences(c.content, input.old_str);
279
+ if (occurrences === 0) {
280
+ return createErrorResult('old_str not found in the canvas content. Use canvas_get to see the exact text.');
281
+ }
282
+ if (occurrences > 1 && !input.replace_all) {
283
+ return createErrorResult(`old_str is not unique (${String(occurrences)} matches). Add surrounding context, or set replace_all=true.`);
284
+ }
285
+ next = input.replace_all
286
+ ? c.content.split(input.old_str).join(input.new_str)
287
+ : c.content.replace(input.old_str, input.new_str);
288
+ summary = `Replaced ${String(input.replace_all ? occurrences : 1)} occurrence(s).`;
289
+ }
290
+ else {
291
+ if (input.content === undefined) {
292
+ return createErrorResult(`${input.operation} requires content.`);
293
+ }
294
+ next =
295
+ input.operation === 'append' ? c.content + input.content : input.content + c.content;
296
+ summary = `${input.operation === 'append' ? 'Appended' : 'Prepended'} ${String(input.content.length)} chars.`;
297
+ }
298
+ const updated = await canvases.update(input.canvas_id, { content: next });
299
+ if (!updated)
300
+ return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
301
+ return createSuccessResult(`Edited canvas "${updated.title}" (${updated.type}). ${summary} Now ${String(next.split('\n').length)} lines.`);
302
+ }
303
+ catch (error) {
304
+ return createErrorResult(`Failed to edit canvas: ${error instanceof Error ? error.message : String(error)}`);
305
+ }
306
+ },
307
+ });
308
+ // ---------------------------------------------------------------------------
169
309
  // canvas_delete
170
310
  // ---------------------------------------------------------------------------
171
311
  const canvasDeleteTool = defineTool({
@@ -190,5 +330,5 @@ export function createCanvasTools(config) {
190
330
  }
191
331
  },
192
332
  });
193
- return [canvasWriteTool, canvasListTool, canvasGetTool, canvasDeleteTool];
333
+ return [canvasWriteTool, canvasEditTool, canvasListTool, canvasGetTool, canvasDeleteTool];
194
334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",