@cerebriline/core 4.100.247 → 4.100.249

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.
Files changed (31) hide show
  1. package/assets/prompt-templates/claude.md +9 -0
  2. package/assets/prompt-templates/deepseek.md +9 -0
  3. package/assets/prompt-templates/default.md +49 -1
  4. package/assets/prompt-templates/gemma.md +9 -0
  5. package/assets/prompt-templates/glm.md +9 -0
  6. package/assets/prompt-templates/kimi-k3.md +9 -0
  7. package/assets/prompt-templates/kimi.md +9 -0
  8. package/assets/prompt-templates/minimax.md +12 -1
  9. package/assets/prompt-templates/nemotron.md +9 -0
  10. package/assets/prompt-templates/qwen.md +9 -0
  11. package/assets/skills/media-audio/SKILL.md +55 -0
  12. package/assets/skills/media-images/SKILL.md +102 -0
  13. package/assets/skills/media-video/SKILL.md +53 -0
  14. package/dist/cloud/index.js +16 -16
  15. package/dist/extensions/context/capped-thinking.d.ts +14 -0
  16. package/dist/extensions/context/compaction-shared.d.ts +46 -0
  17. package/dist/extensions/context/continuation-compaction.d.ts +18 -0
  18. package/dist/extensions/tools/icon-generation.d.ts +74 -0
  19. package/dist/extensions/tools/image-generation.d.ts +25 -1
  20. package/dist/extensions/tools/media-tools.d.ts +8 -0
  21. package/dist/extensions/tools/render-image.d.ts +139 -0
  22. package/dist/extensions/tools/transform-image.d.ts +148 -0
  23. package/dist/hub/client/index.d.ts +13 -0
  24. package/dist/hub/daemon/entry.js +576 -572
  25. package/dist/hub/index.js +552 -548
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +1088 -896
  28. package/dist/remote/remote-helper-entry.js +4 -4
  29. package/dist/remote/remote-helper.js +4 -4
  30. package/dist/types/config.d.ts +7 -0
  31. package/package.json +4 -4
@@ -169,6 +169,20 @@ type PrepareTurnResult = {
169
169
  messages?: readonly MessageWithMetadata[];
170
170
  } | undefined;
171
171
  type PrepareTurn = (context: never) => PrepareTurnResult | Promise<PrepareTurnResult>;
172
+ /** A request answered by the conversation itself, or `undefined` to decline. */
173
+ type ConversationAsk = (systemPrompt: string, request: string, maxTokens: number, what: string) => Promise<string | undefined>;
174
+ /**
175
+ * The discarded turn's notes, asked of the conversation as its next turn
176
+ * where that can be done (`DiscardedTurnInput.continueConversation`).
177
+ *
178
+ * Declines -- and the note is asked for in a request of its own, as before --
179
+ * when the runtime offered no way to continue, when a separate summarizer
180
+ * model is configured (it is not the conversation's model), when the provider
181
+ * keeps no prefix to share, and when the instruction and its answer do not fit
182
+ * in what the last request left of the window. A continuation that fails or
183
+ * says nothing declines too: a note is never lost to the attempt.
184
+ */
185
+ export declare function createConversationAsk(input: DiscardedTurnInput, config: CappedThinkingCondenserConfig, providerConfig: ProviderConfig): ConversationAsk | undefined;
172
186
  /**
173
187
  * Condense reasoning that is about to be thrown away rather than sent again.
174
188
  *
@@ -112,6 +112,39 @@ export type MeasureContextTokens = (message: MessageWithMetadata, index: number)
112
112
  * the context as it stands, which is the question the reservation is asking.
113
113
  */
114
114
  export declare function resolveObservedOutputTokens(messages: readonly MessageWithMetadata[], measure: MeasureContextTokens, sampleTurns?: number): number | undefined;
115
+ /**
116
+ * What a compaction written as the conversation's next turn needs on top of
117
+ * the conversation: its instruction and its summary.
118
+ *
119
+ * One function because two things ask it. The trigger asks before the fact,
120
+ * so that compaction starts while this still fits; the continuation asks at
121
+ * the moment it would start, and steps aside when it does not. Harness run
122
+ * 0432 (2026-10-11) is what two different answers cost: the trigger waited
123
+ * for 90% of an 85,106-token window, the writer's request then measured
124
+ * 92,738, and each such compaction fell back to a summary that did not fit
125
+ * beside the conversation either, released the conversation's window and made
126
+ * the next turn read its whole prompt again.
127
+ */
128
+ export declare const CONTINUATION_WRITER_INSTRUCTION_TOKENS = 3072;
129
+ export declare function resolveContinuationWriterRoomTokens(contextWindow: number | undefined): number;
130
+ /**
131
+ * What the conversation has gained since its last request was measured: the
132
+ * newest assistant message and the results that followed it.
133
+ *
134
+ * The provider's count is of the request that produced that message, so it
135
+ * holds neither. In run 0432 the count was 78,895 and the conversation the
136
+ * writer had to continue was about 90,000.
137
+ */
138
+ export declare function resolvePendingTurnTokens(messages: readonly MessageWithMetadata[], measure: MeasureContextTokens): number;
139
+ /**
140
+ * How much one turn adds to the conversation, from this session's own turns:
141
+ * an assistant message and everything up to the next one.
142
+ *
143
+ * Read the way {@link resolveObservedOutputTokens} reads output, for the same
144
+ * reason: the newest turns count in full, and past them a single large turn is
145
+ * an outlier and the second largest sets the figure.
146
+ */
147
+ export declare function resolveObservedTurnGrowthTokens(messages: readonly MessageWithMetadata[], measure: MeasureContextTokens, sampleTurns?: number): number | undefined;
115
148
  /**
116
149
  * The largest prompt that still leaves the model room to answer.
117
150
  *
@@ -150,6 +183,19 @@ export declare function resolveCompactionTriggerTokens(input: {
150
183
  modelMaxTokens?: number;
151
184
  /** The high-water output of recent turns, from {@link resolveObservedOutputTokens}. */
152
185
  observedOutputTokens?: number;
186
+ /**
187
+ * Set where compaction would be written as the conversation's next turn.
188
+ * The trigger then also holds back what that turn needs, so compaction
189
+ * starts while it still fits: the writer's own room, what the conversation
190
+ * has gained since the count the trigger compares was taken, and one more
191
+ * turn like this session's.
192
+ */
193
+ continuation?: {
194
+ /** {@link resolvePendingTurnTokens}; 0 when the compared count is current. */
195
+ pendingTokens: number;
196
+ /** {@link resolveObservedTurnGrowthTokens}. */
197
+ turnGrowthTokens?: number;
198
+ };
153
199
  }): number;
154
200
  export interface FileOperationSummary {
155
201
  readFiles: string[];
@@ -186,6 +186,12 @@ export interface CompactionContinuation {
186
186
  * where the engine books.
187
187
  */
188
188
  fresh(purpose: "retrospective" | "synthesizer", systemPrompt: string, request: string, maxTokens: number, providerConfig: ProviderConfig, reservation?: PPrimeReservation): Promise<ContinuationCallResult>;
189
+ /**
190
+ * Whether a retrospective of about `chars` characters would run as the
191
+ * session's own next turn. It has to be asked for before {@link release}
192
+ * then: afterwards there is no session to continue.
193
+ */
194
+ retrospectiveFitsInSession(chars: number, maxTokens: number): boolean;
189
195
  /**
190
196
  * Give the session's cells back: the critics, P', P, then the session's
191
197
  * own slot. Idempotent; run before the synthesizer, and again by
@@ -216,6 +222,12 @@ export interface PrepareContinuationInput {
216
222
  requestTokens: number;
217
223
  /** The last request's own prompt count, when one was measured. */
218
224
  observedRequestTokens?: number;
225
+ /**
226
+ * What the conversation has gained since `requestTokens` was counted: the
227
+ * newest assistant message and the results after it. 0 when the count is
228
+ * an estimate of the conversation as it stands.
229
+ */
230
+ pendingTokens?: number;
219
231
  reasoning: ContinuationReasoning;
220
232
  /** A summarizer model of its own is configured: it cannot continue this one. */
221
233
  separateSummarizer: boolean;
@@ -238,6 +250,12 @@ export interface PrepareContinuationInput {
238
250
  }
239
251
  /** Whether a failed call is the server saying "not now". */
240
252
  export declare function isInfraFault(error: unknown): boolean;
253
+ /**
254
+ * Why compaction on this connection is never written as a continuation, or
255
+ * `undefined` when it can be. The part of the answer that does not depend on
256
+ * the turn, so the trigger can ask it before there is a compaction to prepare.
257
+ */
258
+ export declare function continuationCompactionExcluded(config: ProviderConfig, separateSummarizer: boolean): string | undefined;
241
259
  /**
242
260
  * Whether compaction on this connection can run as a continuation, and in
243
261
  * which shape. `undefined` continuation with the reason when it cannot.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Icons: several proposals to choose from, then every file an icon is.
3
+ *
4
+ * `generate_image` can draw an icon, and the first person to use it for one
5
+ * was pleased that it could. What it left him with was one PNG at the size the
6
+ * model draws, when an icon is a choice between candidates and then a dozen
7
+ * files: an `.ico` holding seven sizes, an `.icns`, and the PNGs a web page and
8
+ * a phone ask for by size.
9
+ *
10
+ * So this is two steps under one name. With a `prompt` it draws several
11
+ * proposals and shows them on one numbered sheet, for the user to pick from.
12
+ * With `pick` it writes the chosen one out in every format. The model asks the
13
+ * question in between: choosing is the user's part.
14
+ */
15
+ import { type AgentTool } from "@cerebriline/shared";
16
+ import { type ImageGenerationEndpoint } from "./image-generation";
17
+ import { type RenderDriver } from "./render-image";
18
+ export declare const GENERATE_ICON_TOOL_NAME = "generate_icon";
19
+ export declare const GENERATE_ICON_TOOL_DESCRIPTION = "Make an icon for an application, a site or a file type: several proposals for the user to choose from, then the chosen one saved in every format an icon needs. Two steps, one call each.\n\n1. Proposals. Give `prompt`: what the icon shows and the feeling it should have (\"a paper plane, friendly, blue on white\"). Several candidates are drawn and returned on one numbered sheet, each saved as its own file. Show the user the sheet and ask which one they want (`ask_question`, with the numbers as the options); do not choose for them. If none is right, call again with a changed prompt.\n2. Files. Give `pick`: the path of the chosen proposal (or of any square image in the workspace), and `path`: where the icon belongs, without an extension, e.g. `assets/icon`. That writes `assets/icon.ico`, `assets/icon.icns`, `assets/icon.png` and `assets/icon-<size>.png` for the usual sizes.\n\nArguments:\n- `prompt` \u2014 step 1: the subject and the mood. The rules of an icon (one centred subject, bold shapes, no text) are added for you.\n- `count` \u2014 step 1: how many proposals, 1 to 6. Optional, default 4.\n- `style` \u2014 step 1: e.g. `flat vector`, `glossy 3D`, `line art`, `pixel art`. Optional, default flat vector.\n- `pick` \u2014 step 2: workspace path of the image to make the icon from.\n- `path` \u2014 step 2: where to save, without an extension.\n- `formats` \u2014 step 2: which of `ico`, `icns`, `png`, `webp` to write. Optional, default `ico,icns,png`.\n- `sizes` \u2014 step 2: the PNG sizes to write beside the main one. Optional, default `16,32,48,64,128,180,192,256,512`.\n- `background` \u2014 step 2: a CSS colour to put behind the icon. Optional; left as the proposal has it.\n\nEach proposal costs what an image costs: seconds to a minute, and money on a hosted backend.";
20
+ export declare const GENERATE_ICON_TOOL_INPUT_SCHEMA: {
21
+ readonly type: "object";
22
+ readonly properties: {
23
+ readonly prompt: {
24
+ readonly type: "string";
25
+ readonly description: "Step 1: what the icon shows, and its mood.";
26
+ };
27
+ readonly count: {
28
+ readonly type: "number";
29
+ readonly description: "Step 1: how many proposals, 1 to 6. Default 4.";
30
+ };
31
+ readonly style: {
32
+ readonly type: "string";
33
+ readonly description: "Step 1: the drawing style. Default flat vector.";
34
+ };
35
+ readonly pick: {
36
+ readonly type: "string";
37
+ readonly description: "Step 2: workspace path of the chosen image.";
38
+ };
39
+ readonly path: {
40
+ readonly type: "string";
41
+ readonly description: "Step 2: where the icon is saved, without an extension, e.g. assets/icon.";
42
+ };
43
+ readonly formats: {
44
+ readonly type: "string";
45
+ readonly description: "Step 2: any of ico, icns, png, webp. Default ico,icns,png.";
46
+ };
47
+ readonly sizes: {
48
+ readonly type: "string";
49
+ readonly description: "Step 2: PNG sizes to write beside the main one.";
50
+ };
51
+ readonly background: {
52
+ readonly type: "string";
53
+ readonly description: "Step 2: a CSS colour behind the icon. Optional.";
54
+ };
55
+ };
56
+ readonly required: readonly [];
57
+ };
58
+ export interface GenerateIconToolOptions {
59
+ cwd: string;
60
+ getEndpoint: () => ImageGenerationEndpoint | undefined | Promise<ImageGenerationEndpoint | undefined>;
61
+ createDriver: () => RenderDriver | Promise<RenderDriver>;
62
+ fileExists: (absolutePath: string) => Promise<boolean>;
63
+ writeFile: (absolutePath: string, data: Buffer) => Promise<void>;
64
+ fetchImpl?: typeof fetch;
65
+ timeoutMs?: number;
66
+ now?: () => number;
67
+ onError?: (message: string, error: unknown) => void;
68
+ }
69
+ /** The prompt an icon is drawn from: the subject, then what makes it an icon. */
70
+ export declare function buildIconPrompt(subject: string, style: string, index: number): string;
71
+ /** The proposals on one sheet, each under its number. */
72
+ export declare function buildProposalSheet(fileUrls: string[]): string;
73
+ /** Create the `generate_icon` tool. */
74
+ export declare function createGenerateIconTool(options: GenerateIconToolOptions): AgentTool;
@@ -38,7 +38,7 @@ export type ToolOutput = string | Array<{
38
38
  mediaType: string;
39
39
  }>;
40
40
  export declare const GENERATE_IMAGE_TOOL_NAME = "generate_image";
41
- export declare const GENERATE_IMAGE_TOOL_DESCRIPTION = "Generate an image from a text description and save it into the workspace. Use it for visual work you would otherwise have to ask the user to do elsewhere: an app icon, a placeholder texture or sprite, a logo, a background, or a mockup of a layout or theme you are about to build.\n\nThe image is written to a file and, if you can see images, returned to you as well \u2014 so you can look at what you made and generate again with a changed prompt if it is wrong.\n\nArguments:\n- `prompt` \u2014 what to draw. Describe the subject, the style and the background. Say \"flat vector icon, solid background, no text\" rather than \"an icon\": these models render text badly, so ask for lettering only when you must.\n- `path` \u2014 where to save it, relative to the workspace. Optional; defaults to a file under `.cline/generated-images/`. Give a real path when the image is an asset the project will use.\n- `size` \u2014 `WxH` in pixels, e.g. `1024x1024`. Optional, and the backend may round it.\n\nThis costs real time \u2014 seconds to a minute per image \u2014 and on a hosted backend it costs money. Generate one image and look at it before generating variations.";
41
+ export declare const GENERATE_IMAGE_TOOL_DESCRIPTION = "Generate an image from a text description and save it into the workspace. Use it for visual work you would otherwise have to ask the user to do elsewhere: an app icon, a placeholder texture or sprite, a logo, a background, an illustration or a photograph.\n\nIt paints pixels: it cannot write legible text, and a picture of an interface or a diagram made this way has garbled labels. For a screen, a mockup, a workflow diagram, a chart or anything else with words in it, write it as HTML, SVG or Mermaid and use `render_image` when you have it.\n\nThe image is written to a file and, if you can see images, returned to you as well \u2014 so you can look at what you made and generate again with a changed prompt if it is wrong.\n\nArguments:\n- `prompt` \u2014 what to draw. Describe the subject, the style and the background. Say \"flat vector icon, solid background, no text\" rather than \"an icon\": these models render text badly, so ask for lettering only when you must.\n- `path` \u2014 where to save it, relative to the workspace. Optional; defaults to a file under `.cline/generated-images/`. Give a real path when the image is an asset the project will use.\n- `size` \u2014 `WxH` in pixels, e.g. `1024x1024`. Optional, and the backend may round it.\n\nThis costs real time \u2014 seconds to a minute per image \u2014 and on a hosted backend it costs money. Generate one image and look at it before generating variations.";
42
42
  export declare const GENERATE_IMAGE_TOOL_INPUT_SCHEMA: {
43
43
  readonly type: "object";
44
44
  readonly properties: {
@@ -172,6 +172,30 @@ export declare function saveAndReportImage(input: {
172
172
  emitUpdate?: (update: unknown) => void;
173
173
  } | undefined;
174
174
  }): Promise<ToolOutput>;
175
+ /**
176
+ * One image from the endpoint, or why not, in words for the model.
177
+ *
178
+ * Shared by every tool that asks for a picture: the wire format, the busy
179
+ * retry and the reading of the answer are the same whatever it is for.
180
+ * A timeout and an unreachable server are thrown, as the request throws them.
181
+ */
182
+ export declare function requestGeneratedImage(input: {
183
+ endpoint: ImageGenerationEndpoint;
184
+ prompt: string;
185
+ size?: string;
186
+ fetchImpl: typeof fetch;
187
+ timeoutMs: number;
188
+ signal?: AbortSignal;
189
+ onBusy?: (waitMs: number) => void;
190
+ }): Promise<{
191
+ image: {
192
+ data: Buffer;
193
+ mediaType: string;
194
+ };
195
+ revisedPrompt?: string;
196
+ } | {
197
+ error: string;
198
+ }>;
175
199
  /**
176
200
  * Create the `generate_image` tool.
177
201
  *
@@ -17,6 +17,7 @@ import type { AgentTool } from "@cerebriline/shared";
17
17
  import { type SpeechEndpoint, type TranscriptionEndpoint } from "./audio-tools";
18
18
  import { type ImageGenerationEndpoint } from "./image-generation";
19
19
  import { type MediaEndpointProbe, type MediaSessionProvider, type ResolvedMediaEndpoint } from "./media-endpoint";
20
+ import type { RenderDriver } from "./render-image";
20
21
  import { type VideoGenerationEndpoint } from "./video-generation";
21
22
  /** What every media section holds. */
22
23
  export interface MediaSectionSettings {
@@ -100,6 +101,13 @@ export interface CreateMediaToolsOptions {
100
101
  readFile: (absolutePath: string) => Promise<Buffer>;
101
102
  /** Must create the directory: the default paths are under `.cline/`. */
102
103
  writeFile: (absolutePath: string, data: Buffer) => Promise<void>;
104
+ /**
105
+ * The host's browser, where it has one. With it and `fileExists`,
106
+ * `generate_icon` is offered beside `generate_image`: drawing the proposals
107
+ * is the endpoint's work, and writing an `.ico` from one is the browser's.
108
+ */
109
+ createRenderDriver?: () => RenderDriver | Promise<RenderDriver>;
110
+ fileExists?: (absolutePath: string) => Promise<boolean>;
103
111
  fetchImpl?: typeof fetch;
104
112
  onError?: (message: string, error: unknown) => void;
105
113
  /** One line per tool: where it goes, or why it was left out. */
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A tool that draws what the model writes.
3
+ *
4
+ * `generate_image` paints pixels, and pixels that look like an interface are
5
+ * not one: asked for "rendered images showing the workflow of the
6
+ * application", a model on xOllama produced two dashboards whose tabs read
7
+ * "Dasihorad" and "Documets" (2026-10-12). No prompt mends that. The models
8
+ * that cannot draw a word can write one, and they write HTML, CSS, SVG and
9
+ * diagram notation well. So this tool takes the source and a real browser
10
+ * renders it: every label is the text that was typed, and a screen can be
11
+ * built from the application's own markup and stylesheet.
12
+ *
13
+ * The picture is as large as what was drawn unless a size is asked for. The
14
+ * first diagram rendered the other way was 1180 by 370 pixels in a fixed 1280
15
+ * by 800 capture, a third of it empty.
16
+ */
17
+ import { type AgentTool } from "@cerebriline/shared";
18
+ export declare const RENDER_IMAGE_TOOL_NAME = "render_image";
19
+ export declare const RENDER_IMAGE_TOOL_DESCRIPTION = "Render HTML, SVG or a Mermaid diagram that you write into an image file in the workspace. Use it for anything with text or structure in it: application screens and mockups, workflow and architecture diagrams, charts, annotated figures. An image model cannot write legible text or draw a real interface; this draws exactly what you write.\n\nThe image is saved and, if you can see images, returned to you, so you can check the layout and render again.\n\n- `source` \u2014 what to draw: HTML with its CSS (a fragment or a whole document), an SVG document, or Mermaid notation (`flowchart LR`, `sequenceDiagram`, ...). Relative URLs resolve against the workspace root, so a screen of the application can link the project's own stylesheet and images: reuse its markup and class names with sample data and the picture is the real interface.\n- `kind` \u2014 `html`, `svg` or `mermaid`. Optional; read from the source.\n- `path` \u2014 where to save, relative to the workspace: `.png`, or `.svg` to keep an SVG or Mermaid drawing as vectors. Optional; defaults to a PNG under `.cline/rendered-images/`.\n- `width`, `height` \u2014 page size in CSS pixels. Optional. Leave both out and the image is exactly as large as what was drawn; `width` alone fixes where the layout wraps.\n- `scale` \u2014 pixel density, 1 to 4, default 2.\n- `crop` \u2014 `\"x,y,width,height\"` in CSS pixels, to keep one region of the page.\n- `background` \u2014 a CSS colour, or `transparent`.\n- `theme` \u2014 Mermaid only: `default`, `dark`, `neutral` or `forest`.\n\nA README on GitHub renders a ```mermaid block as it is; render a diagram to a file when it must show somewhere that does not. Remote fonts, scripts and images load only if the machine reaches them quickly: inline what matters.";
20
+ export declare const RENDER_IMAGE_TOOL_INPUT_SCHEMA: {
21
+ readonly type: "object";
22
+ readonly properties: {
23
+ readonly source: {
24
+ readonly type: "string";
25
+ readonly description: "HTML (with its CSS), an SVG document, or Mermaid notation, written out in full.";
26
+ };
27
+ readonly kind: {
28
+ readonly type: "string";
29
+ readonly enum: readonly ["html", "svg", "mermaid"];
30
+ readonly description: "Optional; read from the source when left out.";
31
+ };
32
+ readonly path: {
33
+ readonly type: "string";
34
+ readonly description: "Where to save, relative to the workspace (.png, or .svg for an SVG or Mermaid drawing). Optional.";
35
+ };
36
+ readonly width: {
37
+ readonly type: "number";
38
+ readonly description: "Page width in CSS pixels. Optional.";
39
+ };
40
+ readonly height: {
41
+ readonly type: "number";
42
+ readonly description: "Page height in CSS pixels. Optional; without it the height is what was drawn.";
43
+ };
44
+ readonly scale: {
45
+ readonly type: "number";
46
+ readonly description: "Pixel density, 1 to 4. Optional, default 2.";
47
+ };
48
+ readonly crop: {
49
+ readonly type: "string";
50
+ readonly description: "\"x,y,width,height\" in CSS pixels. Optional.";
51
+ };
52
+ readonly background: {
53
+ readonly type: "string";
54
+ readonly description: "A CSS colour, or \"transparent\". Optional.";
55
+ };
56
+ readonly theme: {
57
+ readonly type: "string";
58
+ readonly description: "Mermaid only: default, dark, neutral or forest.";
59
+ };
60
+ };
61
+ readonly required: readonly ["source"];
62
+ };
63
+ export type RenderKind = "html" | "svg" | "mermaid";
64
+ export interface RenderRegion {
65
+ x: number;
66
+ y: number;
67
+ width: number;
68
+ height: number;
69
+ }
70
+ /** One render, as the host's browser is asked for it. */
71
+ export interface RenderRequest {
72
+ /** A whole HTML document. */
73
+ html: string;
74
+ /** The folder relative URLs in it resolve against. */
75
+ baseDir: string;
76
+ /** Present for a diagram: the browser draws it before the capture. */
77
+ mermaid?: {
78
+ theme: string;
79
+ };
80
+ /** The page's width; what a layout wraps at. */
81
+ width: number;
82
+ /** The page's height. Absent: as tall as what was drawn. */
83
+ height?: number;
84
+ /** No size was asked for: capture what was drawn and nothing around it. */
85
+ fitContent: boolean;
86
+ scale: number;
87
+ crop?: RenderRegion;
88
+ transparent: boolean;
89
+ /** Also hand back the drawing as SVG text, where the page holds one. */
90
+ wantSvg: boolean;
91
+ /** What the capture is encoded as. Absent: PNG. */
92
+ format?: "png" | "jpeg" | "webp";
93
+ /** 1 to 100, for the two formats that have one. */
94
+ quality?: number;
95
+ signal?: AbortSignal;
96
+ }
97
+ export interface RenderResult {
98
+ /** The capture, in the format asked for. */
99
+ data: Buffer;
100
+ /** The captured region, in CSS pixels. */
101
+ width: number;
102
+ height: number;
103
+ svg?: string;
104
+ }
105
+ /** The host's browser. Structural, as the browser tool's driver is. */
106
+ export interface RenderDriver {
107
+ render(request: RenderRequest): Promise<RenderResult>;
108
+ }
109
+ export interface RenderImageToolOptions {
110
+ cwd: string;
111
+ /** Built on first use: a session that never renders never starts a browser. */
112
+ createDriver: () => RenderDriver | Promise<RenderDriver>;
113
+ writeFile: (absolutePath: string, data: Buffer) => Promise<void>;
114
+ onError?: (message: string, error: unknown) => void;
115
+ }
116
+ /** The id of the element a fragment is measured by. */
117
+ export declare const RENDER_ROOT_ID = "cerebriline-render-root";
118
+ /** What a source is, when the call does not say. */
119
+ export declare function detectRenderKind(source: string): RenderKind;
120
+ /** A fenced block's contents, when the source arrives wrapped in one. */
121
+ export declare function unfence(source: string): string;
122
+ /** `"x,y,width,height"`, or the same four as a list. */
123
+ export declare function parseCrop(value: unknown): RenderRegion | undefined;
124
+ /**
125
+ * The page a source is rendered as.
126
+ *
127
+ * A whole document goes in as it is. Anything else is set in a page with no
128
+ * margin, inside one inline block the capture is measured by: a block-level
129
+ * wrapper would be as wide as the page whatever it held.
130
+ */
131
+ export declare function buildRenderDocument(input: {
132
+ kind: RenderKind;
133
+ source: string;
134
+ background?: string;
135
+ /** The folder relative URLs resolve against: the workspace. */
136
+ baseDir?: string;
137
+ }): string;
138
+ /** Create the `render_image` tool. */
139
+ export declare function createRenderImageTool(options: RenderImageToolOptions): AgentTool;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Resize, crop and convert an image that is already a file.
3
+ *
4
+ * Asked for by the first person to generate images with this in earnest: "the
5
+ * harness needs the ability to dynamically change the image size", and to crop.
6
+ * An image model draws at the sizes it knows, a README wants 1200 pixels across
7
+ * and an installer wants an `.ico`, and until now the model's only way from one
8
+ * to the other was a shell command for a program that might not be installed.
9
+ *
10
+ * The work is done by the browser `render_image` already uses: it decodes
11
+ * every format a page can show and encodes the three that matter. So there is
12
+ * no image library here, and nothing new to ship.
13
+ */
14
+ import { type AgentTool } from "@cerebriline/shared";
15
+ import { type RenderDriver, type RenderRegion, type RenderResult } from "./render-image";
16
+ export declare const TRANSFORM_IMAGE_TOOL_NAME = "transform_image";
17
+ export declare const TRANSFORM_IMAGE_TOOL_DESCRIPTION = "Resize, crop or convert an image file in the workspace and save the result as a new file. Use it to change an image's dimensions, cut a region out of it, change its format, or make an icon file from it. The source is never modified.\n\n- `image` \u2014 workspace path of the source: PNG, JPEG, WebP, GIF, BMP, AVIF, ICO or SVG.\n- `path` \u2014 where to save, relative to the workspace. The extension chooses the format: `.png`, `.jpg`, `.webp`, `.ico` or `.icns`. Optional; defaults to a file beside the source with the new size in its name.\n- `width`, `height` \u2014 the new size in pixels. Give one and the other follows the proportions.\n- `fit` \u2014 with both given: `inside` (default; the whole image, within the box, proportions kept), `cover` (fills the box, the overflow cut off), `pad` (the whole image, padded to exactly the box) or `stretch`.\n- `crop` \u2014 `\"x,y,width,height\"` in pixels of the source, taken before resizing.\n- `background` \u2014 a CSS colour for padding and for what was transparent. Optional; transparency is kept where the format has it, and JPEG gets white.\n- `quality` \u2014 1 to 100 for `.jpg` and `.webp`, default 90.\n- `sizes` \u2014 for `.ico` and `.icns`, the sizes to hold, e.g. `\"16,32,48,256\"`. Optional; the usual set when left out.";
18
+ export declare const TRANSFORM_IMAGE_TOOL_INPUT_SCHEMA: {
19
+ readonly type: "object";
20
+ readonly properties: {
21
+ readonly image: {
22
+ readonly type: "string";
23
+ readonly description: "Workspace path of the source image.";
24
+ };
25
+ readonly path: {
26
+ readonly type: "string";
27
+ readonly description: "Where to save (.png, .jpg, .webp, .ico or .icns), relative to the workspace. Optional.";
28
+ };
29
+ readonly width: {
30
+ readonly type: "number";
31
+ readonly description: "New width in pixels. Optional.";
32
+ };
33
+ readonly height: {
34
+ readonly type: "number";
35
+ readonly description: "New height in pixels. Optional.";
36
+ };
37
+ readonly fit: {
38
+ readonly type: "string";
39
+ readonly enum: readonly ["inside", "cover", "pad", "stretch"];
40
+ readonly description: "How the image meets a box of both sizes. Optional.";
41
+ };
42
+ readonly crop: {
43
+ readonly type: "string";
44
+ readonly description: "\"x,y,width,height\" in source pixels. Optional.";
45
+ };
46
+ readonly background: {
47
+ readonly type: "string";
48
+ readonly description: "A CSS colour. Optional.";
49
+ };
50
+ readonly quality: {
51
+ readonly type: "number";
52
+ readonly description: "1 to 100, for .jpg and .webp. Optional.";
53
+ };
54
+ readonly sizes: {
55
+ readonly type: "string";
56
+ readonly description: "For .ico and .icns: sizes to hold, e.g. \"16,32,48,256\".";
57
+ };
58
+ };
59
+ readonly required: readonly ["image"];
60
+ };
61
+ export type ImageFit = "inside" | "cover" | "pad" | "stretch";
62
+ export interface TransformPlan {
63
+ /** The part of the source that is drawn. */
64
+ sx: number;
65
+ sy: number;
66
+ sw: number;
67
+ sh: number;
68
+ /** The size of the result. */
69
+ width: number;
70
+ height: number;
71
+ /** Where the source lands in it. */
72
+ dx: number;
73
+ dy: number;
74
+ dw: number;
75
+ dh: number;
76
+ }
77
+ /**
78
+ * Where a source of this size goes in a result of that one.
79
+ *
80
+ * Written to stand alone, with nothing declared outside it and no function
81
+ * inside it: the page that draws the image runs this same text.
82
+ */
83
+ export declare function planTransform(sourceWidth: number, sourceHeight: number, crop: {
84
+ x: number;
85
+ y: number;
86
+ width: number;
87
+ height: number;
88
+ } | null, width: number | null, height: number | null, fit: string): TransformPlan | null;
89
+ export interface TransformSpec {
90
+ crop?: RenderRegion;
91
+ width?: number;
92
+ height?: number;
93
+ fit: ImageFit;
94
+ /** Painted under the image, over the whole result. */
95
+ background?: string;
96
+ }
97
+ /** The page that draws one image at its new size. */
98
+ export declare function buildTransformDocument(sourceUrl: string, spec: TransformSpec): string;
99
+ /** One image, drawn at a new size by the host's browser. */
100
+ export declare function transformWithDriver(driver: RenderDriver, input: {
101
+ sourcePath: string;
102
+ baseDir: string;
103
+ spec: TransformSpec;
104
+ format?: "png" | "jpeg" | "webp";
105
+ quality?: number;
106
+ signal?: AbortSignal;
107
+ }): Promise<RenderResult>;
108
+ /** An `.ico`: a directory of images, each one a PNG. */
109
+ export declare function encodeIco(images: {
110
+ size: number;
111
+ png: Buffer;
112
+ }[]): Buffer;
113
+ /** An `.icns`: the same idea, with a name per size. */
114
+ export declare function encodeIcns(images: {
115
+ size: number;
116
+ png: Buffer;
117
+ }[]): Buffer;
118
+ export declare const ICO_SIZES: number[];
119
+ export declare const ICNS_SIZES: number[];
120
+ /** The sizes an icon file is asked to hold, or why not those. */
121
+ export declare function parseIconSizes(value: unknown, container: "ico" | "icns"): number[] | {
122
+ error: string;
123
+ };
124
+ /** Every size of an icon, from one source, as square PNGs. */
125
+ export declare function renderIconSizes(driver: RenderDriver, input: {
126
+ sourcePath: string;
127
+ baseDir: string;
128
+ sizes: number[];
129
+ crop?: RenderRegion;
130
+ background?: string;
131
+ signal?: AbortSignal;
132
+ }): Promise<{
133
+ size: number;
134
+ png: Buffer;
135
+ }[]>;
136
+ export declare class UnreadableImageError extends Error {
137
+ constructor();
138
+ }
139
+ export interface TransformImageToolOptions {
140
+ cwd: string;
141
+ createDriver: () => RenderDriver | Promise<RenderDriver>;
142
+ /** Whether the source is there: the browser says nothing useful about a missing file. */
143
+ fileExists: (absolutePath: string) => Promise<boolean>;
144
+ writeFile: (absolutePath: string, data: Buffer) => Promise<void>;
145
+ onError?: (message: string, error: unknown) => void;
146
+ }
147
+ /** Create the `transform_image` tool. */
148
+ export declare function createTransformImageTool(options: TransformImageToolOptions): AgentTool;
@@ -61,6 +61,19 @@ export declare class HubCommandError extends Error {
61
61
  }
62
62
  export declare function isHubCommandTimeoutError(error: unknown, command?: HubCommandEnvelope["command"]): boolean;
63
63
  export declare function rememberRecoverableLocalHubUrl(url: string, authToken?: string): string;
64
+ /**
65
+ * Keep a local hub's token for the connections this process makes to it,
66
+ * without marking the hub as one this process may restart.
67
+ *
68
+ * A hub on an explicit endpoint (`CLINE_HUB_PORT`, or a host/port the caller
69
+ * passed) is not "recoverable", and that used to mean its token was not kept
70
+ * either. The caller that had just started it got back a URL, connected with
71
+ * no token and was refused: "WebSocket connection to
72
+ * 'ws://127.0.0.1:25499/hub' failed: Connection ended", on every first start
73
+ * (solidPC and eleven2go, 2026-10-11, three of three). The second start
74
+ * worked because it found the discovery record, which carries the token.
75
+ */
76
+ export declare function rememberLocalHubAuthToken(url: string, authToken?: string): void;
64
77
  export declare class NodeHubClient {
65
78
  private readonly options;
66
79
  private socket;