dsh-artcraft-plugin-vectorcraft 0.2.4 → 0.2.8
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.
- package/package.json +4 -1
- package/src/index.js +23 -0
- package/src/index.ts +23 -0
- package/src/kit/preview.js +210 -0
- package/src/kit/tools.js +112 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-artcraft-plugin-vectorcraft",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "DSH plugin: drive VectorCraft (vector art) headlessly through its own CLI and MCP server.",
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
],
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"dsh": "^0.2.1-rc.3"
|
|
45
48
|
}
|
|
46
49
|
}
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
16
16
|
import { applyCraftPlugin, profileOf } from './kit/index.js'
|
|
17
17
|
|
|
18
18
|
import { PLUGIN_ROOT } from './root.js'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
19
20
|
|
|
20
21
|
export const name = 'dsh-artcraft-vectorcraft'
|
|
21
22
|
export const inject = ['tools', 'systemPrompt']
|
|
@@ -26,6 +27,27 @@ export const craft = profileOf('vectorcraft')
|
|
|
26
27
|
/** Configuration is the shared craft config. */
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Commit a rendered preview so the transcript shows the document after the call.
|
|
32
|
+
* The attachment service is optional: without it the path stays in the text.
|
|
33
|
+
*/
|
|
34
|
+
const attachImage = async (ctx , path ) => {
|
|
35
|
+
const attachments = ctx.get('attachments')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if (attachments?.saveImage === undefined) return undefined
|
|
39
|
+
try {
|
|
40
|
+
const ref = await attachments.saveImage({
|
|
41
|
+
data: readFileSync(path),
|
|
42
|
+
mediaType: 'image/png',
|
|
43
|
+
name: path.split(/[\\/]/).pop(),
|
|
44
|
+
})
|
|
45
|
+
return { type: 'image', attachment: ref }
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export function apply(ctx , config = {}) {
|
|
30
52
|
if (craft === undefined) throw new Error('dsh-artcraft-vectorcraft: profile "vectorcraft" is missing from the inlined kernel')
|
|
31
53
|
// `ctx.effect` ties the registration and the MCP server to this plugin's fiber.
|
|
@@ -34,5 +56,6 @@ export function apply(ctx , config = {}) {
|
|
|
34
56
|
packageRoot: PLUGIN_ROOT,
|
|
35
57
|
config,
|
|
36
58
|
defineTool: defineTool ,
|
|
59
|
+
attachImage: (path ) => attachImage(ctx, path),
|
|
37
60
|
}))
|
|
38
61
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
16
16
|
import { applyCraftPlugin, profileOf } from './kit/index.js'
|
|
17
17
|
import type { CraftPluginConfig } from './kit/index.js'
|
|
18
18
|
import { PLUGIN_ROOT } from './root.ts'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
19
20
|
|
|
20
21
|
export const name = 'dsh-artcraft-vectorcraft'
|
|
21
22
|
export const inject = ['tools', 'systemPrompt']
|
|
@@ -26,6 +27,27 @@ export const craft = profileOf('vectorcraft')
|
|
|
26
27
|
/** Configuration is the shared craft config. */
|
|
27
28
|
export type Config = CraftPluginConfig
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Commit a rendered preview so the transcript shows the document after the call.
|
|
32
|
+
* The attachment service is optional: without it the path stays in the text.
|
|
33
|
+
*/
|
|
34
|
+
const attachImage = async (ctx: Context, path: string): Promise<unknown> => {
|
|
35
|
+
const attachments = ctx.get('attachments') as
|
|
36
|
+
| { saveImage?: (input: { data: Buffer; mediaType: string; name?: string }) => Promise<unknown> }
|
|
37
|
+
| undefined
|
|
38
|
+
if (attachments?.saveImage === undefined) return undefined
|
|
39
|
+
try {
|
|
40
|
+
const ref = await attachments.saveImage({
|
|
41
|
+
data: readFileSync(path),
|
|
42
|
+
mediaType: 'image/png',
|
|
43
|
+
name: path.split(/[\\/]/).pop(),
|
|
44
|
+
})
|
|
45
|
+
return { type: 'image', attachment: ref }
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export function apply(ctx: Context, config: Config = {}): void {
|
|
30
52
|
if (craft === undefined) throw new Error('dsh-artcraft-vectorcraft: profile "vectorcraft" is missing from the inlined kernel')
|
|
31
53
|
// `ctx.effect` ties the registration and the MCP server to this plugin's fiber.
|
|
@@ -34,5 +56,6 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
34
56
|
packageRoot: PLUGIN_ROOT,
|
|
35
57
|
config,
|
|
36
58
|
defineTool: defineTool as never,
|
|
59
|
+
attachImage: (path: string) => attachImage(ctx, path),
|
|
37
60
|
}))
|
|
38
61
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a document's current state for the live preview.
|
|
3
|
+
*
|
|
4
|
+
* Three things share this module: the image block a tool returns after an edit,
|
|
5
|
+
* the local panel in `scripts/preview/`, and the DSH client panel: they all read
|
|
6
|
+
* the same state directory (`docs/PREVIEW.md` holds the contract).
|
|
7
|
+
*
|
|
8
|
+
* Rendering goes through the craft's own binary — never a screenshot of a window
|
|
9
|
+
* — so the preview shows what the application itself would show.
|
|
10
|
+
* @module @dsh-artcraft/craft-kit/preview
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { basename, dirname, join } from 'node:path'
|
|
16
|
+
import { runCraft } from './exec.js'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/** Where the previews live when no configuration overrides it. */
|
|
20
|
+
export function previewRoot(configured ) {
|
|
21
|
+
if (configured !== undefined && configured.length > 0) return configured
|
|
22
|
+
const fromEnv = process.env.DSH_ARTCRAFT_PREVIEW_DIR
|
|
23
|
+
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv
|
|
24
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
25
|
+
return join(home, 'artcraft', 'preview')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How one craft renders a document to an image.
|
|
30
|
+
*
|
|
31
|
+
* A craft with no recipe still reports a step in the panel; it just has no
|
|
32
|
+
* picture (SoundCraft has no page, and the MCP-only crafts need their server).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/** Per-craft render recipes, verified against the 2026-11 CLI releases. */
|
|
40
|
+
const RECIPES = {
|
|
41
|
+
// `render FILE OUT [--page N] [--scale S]` draws a page as a PNG.
|
|
42
|
+
wordcraft: { argv: (file, out, scale) => ['render', file, out, '--page', '1', '--scale', String(scale)] },
|
|
43
|
+
// `convert` picks the format from the output extension.
|
|
44
|
+
cadcraft: { argv: (file, out) => ['convert', file, out] },
|
|
45
|
+
figcraft: { argv: (file, out) => ['convert', file, out] },
|
|
46
|
+
// `render FILE [--slide N] [--scale S] OUT` draws one slide as a PNG.
|
|
47
|
+
deckcraft: { argv: (file, out, scale) => ['render', file, '--slide', '0', '--scale', String(scale), out] },
|
|
48
|
+
// VectorCraft exports the active artboard through `run --export`.
|
|
49
|
+
vectorcraft: { argv: (file, out) => ['run', file, '--export', out] },
|
|
50
|
+
// TextureCraft renders the viewport (2D view; the 3D one needs a GPU).
|
|
51
|
+
texturecraft: { argv: (file, out) => ['render', file, out, '--view', '2d'] },
|
|
52
|
+
// PhotoCraft converts the document; `convert` picks the format by extension.
|
|
53
|
+
photocraft: { argv: (file, out) => ['convert', file, out] },
|
|
54
|
+
// EffectCraft renders one frame at t=0 without the render queue.
|
|
55
|
+
effectcraft: { argv: (file, out) => ['render-frame', '--time', '0', '--out', out] },
|
|
56
|
+
// LightCraft develops one file through the same encoder as the export dialog.
|
|
57
|
+
lightcraft: { argv: (file, out) => ['render', file, '-o', out] },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a craft can render a preview at all. */
|
|
61
|
+
export function canPreview(profile ) {
|
|
62
|
+
return RECIPES[profile.id] !== undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One preview step, as documented in docs/PREVIEW.md. */
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One clip on a time-based craft's timeline, in seconds.
|
|
82
|
+
*
|
|
83
|
+
* `timeline` is a property of the DOCUMENT (the whole sequence), not of a step:
|
|
84
|
+
* the panel draws one bar under the current render, and a per-step bar would
|
|
85
|
+
* make it jump between steps that did not change the sequence.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/** A craft's `state.json`. */
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render one document to `<dir>/<craft>/step-NNN.png`.
|
|
108
|
+
*
|
|
109
|
+
* Never throws: a preview is a convenience, and a craft that cannot render must
|
|
110
|
+
* not fail the edit that produced it.
|
|
111
|
+
* @param profile - the craft that wrote the document.
|
|
112
|
+
* @param binary - its resolved binary.
|
|
113
|
+
* @param options - document path, output directory and render scale.
|
|
114
|
+
* @returns the absolute image path, or `undefined` when the craft cannot render.
|
|
115
|
+
*/
|
|
116
|
+
export async function renderPreview(
|
|
117
|
+
profile ,
|
|
118
|
+
binary ,
|
|
119
|
+
options ,
|
|
120
|
+
) {
|
|
121
|
+
const recipe = RECIPES[profile.id]
|
|
122
|
+
if (recipe === undefined) return undefined
|
|
123
|
+
if (!existsSync(options.file)) return undefined
|
|
124
|
+
try {
|
|
125
|
+
mkdirSync(options.dir, { recursive: true })
|
|
126
|
+
const argv = recipe.argv(options.file, options.out, options.scale ?? 1.5)
|
|
127
|
+
const result = await runCraft({ profile, binary, argv, cwd: process.cwd(), timeoutMs: options.timeoutMs ?? 60_000 })
|
|
128
|
+
return result.ok && existsSync(options.out) ? options.out : undefined
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The next step number for a craft, across processes.
|
|
136
|
+
*
|
|
137
|
+
* A plugin process starts its own counter at zero, so numbering from memory
|
|
138
|
+
* alone makes a restarted session overwrite the earlier steps it wrote to disk.
|
|
139
|
+
* The state file is the truth: continue after the highest number recorded.
|
|
140
|
+
* @param root - preview root.
|
|
141
|
+
* @param craft - craft id.
|
|
142
|
+
* @returns the next 1-based step number.
|
|
143
|
+
*/
|
|
144
|
+
export function nextStepNumber(root , craft ) {
|
|
145
|
+
try {
|
|
146
|
+
const state = JSON.parse(readFileSync(join(root, craft, 'state.json'), 'utf8'))
|
|
147
|
+
return state.steps.reduce((highest, step) => Math.max(highest, step.n), 0) + 1
|
|
148
|
+
} catch {
|
|
149
|
+
return 1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Read-modify-write one craft's state and refresh the index. */
|
|
154
|
+
export function recordPreview(
|
|
155
|
+
root ,
|
|
156
|
+
profile ,
|
|
157
|
+
step ,
|
|
158
|
+
file ,
|
|
159
|
+
pages ,
|
|
160
|
+
timeline ,
|
|
161
|
+
) {
|
|
162
|
+
const craftDir = join(root, profile.id)
|
|
163
|
+
mkdirSync(craftDir, { recursive: true })
|
|
164
|
+
const statePath = join(craftDir, 'state.json')
|
|
165
|
+
let previous
|
|
166
|
+
try {
|
|
167
|
+
previous = JSON.parse(readFileSync(statePath, 'utf8'))
|
|
168
|
+
} catch {
|
|
169
|
+
previous = undefined
|
|
170
|
+
}
|
|
171
|
+
const resolved = { ...step, n: previous === undefined ? step.n : Math.max(step.n, (previous.steps.reduce((highest, entry) => Math.max(highest, entry.n), 0)) + 1) }
|
|
172
|
+
const steps = [...(previous?.steps ?? []).filter(entry => entry.n !== resolved.n), resolved]
|
|
173
|
+
const state = {
|
|
174
|
+
craft: profile.id,
|
|
175
|
+
product: profile.product,
|
|
176
|
+
file,
|
|
177
|
+
pages,
|
|
178
|
+
updatedAt: new Date().toISOString(),
|
|
179
|
+
current: resolved.n,
|
|
180
|
+
// Keep the last known timeline: a step that only moved the playhead must
|
|
181
|
+
// not erase the clips a previous step reported.
|
|
182
|
+
...timeline !== undefined ? { timeline } : previous?.timeline !== undefined ? { timeline: previous.timeline } : {},
|
|
183
|
+
steps,
|
|
184
|
+
}
|
|
185
|
+
writeFileSync(statePath, JSON.stringify(state, undefined, 2) + '\n')
|
|
186
|
+
|
|
187
|
+
// index.json is the panel's first read: one entry per craft, no history.
|
|
188
|
+
const indexPath = join(root, 'index.json')
|
|
189
|
+
let index =
|
|
190
|
+
{ updatedAt: state.updatedAt, crafts: {} }
|
|
191
|
+
try {
|
|
192
|
+
index = JSON.parse(readFileSync(indexPath, 'utf8'))
|
|
193
|
+
} catch {
|
|
194
|
+
// First write for this session.
|
|
195
|
+
}
|
|
196
|
+
index.crafts[profile.id] = { file, pages, stepCount: state.steps.length, updatedAt: state.updatedAt }
|
|
197
|
+
index.updatedAt = state.updatedAt
|
|
198
|
+
writeFileSync(indexPath, JSON.stringify(index, undefined, 2) + '\n')
|
|
199
|
+
return state
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A stable name for this step's render, e.g. `step-003.png`. */
|
|
203
|
+
export function stepName(n ) {
|
|
204
|
+
return `step-${String(n).padStart(3, '0')}.png`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The display name of a document path, for the panel's header. */
|
|
208
|
+
export function documentLabel(file ) {
|
|
209
|
+
return basename(dirname(file)) + '/' + basename(file)
|
|
210
|
+
}
|
package/src/kit/tools.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { craftCallView, craftResultView } from './present.js'
|
|
12
|
+
import { canPreview, nextStepNumber, previewRoot, recordPreview, renderPreview, stepName } from './preview.js'
|
|
12
13
|
import { appendJournal } from './journal.js'
|
|
13
14
|
import { CraftMcpClient, renderSchema } from './mcp.js'
|
|
14
15
|
|
|
@@ -161,10 +162,35 @@ export function createRuntime(profile , packageRoot , config
|
|
|
161
162
|
let catalogCache
|
|
162
163
|
const mcpClients = new Map ()
|
|
163
164
|
const artifacts = config.artifactsDir ?? join(tmpdir(), 'dsh-artcraft', profile.id)
|
|
165
|
+
const previewDir = previewRoot(config.previewDir)
|
|
166
|
+
let previewStep = 0
|
|
164
167
|
const jobs = new Map ()
|
|
165
168
|
const jobChildren = new Map ()
|
|
166
169
|
let jobCounter = 0
|
|
167
170
|
return {
|
|
171
|
+
async preview(options) {
|
|
172
|
+
if (config.preview === 'off') return undefined
|
|
173
|
+
if (!canPreview(profile)) return undefined
|
|
174
|
+
const binary = this.binary()
|
|
175
|
+
if (binary === undefined) return undefined
|
|
176
|
+
previewStep = nextStepNumber(previewDir, profile.id)
|
|
177
|
+
const n = previewStep
|
|
178
|
+
const dir = join(previewDir, profile.id)
|
|
179
|
+
const out = join(dir, stepName(n))
|
|
180
|
+
const scale = options.scale ?? config.previewScale ?? 1.5
|
|
181
|
+
const rendered = await renderPreview(profile, binary, { file: options.file, dir, out, scale })
|
|
182
|
+
const relative = rendered === undefined ? undefined : stepName(n)
|
|
183
|
+
recordPreview(previewDir, profile, {
|
|
184
|
+
n,
|
|
185
|
+
at: new Date().toISOString(),
|
|
186
|
+
tool: options.tool,
|
|
187
|
+
title: options.title,
|
|
188
|
+
summary: options.summary,
|
|
189
|
+
...relative === undefined ? {} : { image: relative, thumb: relative },
|
|
190
|
+
...options.durationMs === undefined ? {} : { durationMs: options.durationMs },
|
|
191
|
+
}, options.file, 1)
|
|
192
|
+
return rendered
|
|
193
|
+
},
|
|
168
194
|
startJob(binary, argv) {
|
|
169
195
|
jobCounter += 1
|
|
170
196
|
const id = `j${jobCounter}`
|
|
@@ -382,7 +408,13 @@ export function createToolSet(runtime ) {
|
|
|
382
408
|
* Renders, bakes and bounces outlast the tool timeout; a job keeps the process
|
|
383
409
|
* alive after the call returns and leaves its output in a log file.
|
|
384
410
|
*/
|
|
385
|
-
const runOrStart = async (
|
|
411
|
+
const runOrStart = async (
|
|
412
|
+
binary ,
|
|
413
|
+
argv ,
|
|
414
|
+
background ,
|
|
415
|
+
document ,
|
|
416
|
+
label ,
|
|
417
|
+
) => {
|
|
386
418
|
if (background === true) {
|
|
387
419
|
const job = runtime.startJob(binary, argv)
|
|
388
420
|
return [
|
|
@@ -392,7 +424,21 @@ export function createToolSet(runtime ) {
|
|
|
392
424
|
`or wait with {"action":"wait","job":"${job.id}","timeoutMs":300000}`,
|
|
393
425
|
].join('\n')
|
|
394
426
|
}
|
|
395
|
-
|
|
427
|
+
const result = await runCraft({ profile, binary, argv, cwd: process.cwd(), timeoutMs })
|
|
428
|
+
let text = renderRun(result, profile)
|
|
429
|
+
// A render after every write is what turns the transcript into a step-by-step
|
|
430
|
+
// view of the document; the same image feeds the preview panel.
|
|
431
|
+
if (result.ok && document !== undefined && config.preview !== 'off') {
|
|
432
|
+
const rendered = await runtime.preview({
|
|
433
|
+
file: document,
|
|
434
|
+
tool: 'run',
|
|
435
|
+
title: label === undefined || label.length === 0 ? `${profile.id}_run` : label,
|
|
436
|
+
summary: `exit ${result.exitCode ?? 0} · ${result.durationMs} ms`,
|
|
437
|
+
durationMs: result.durationMs,
|
|
438
|
+
})
|
|
439
|
+
if (rendered !== undefined) text += `\n[preview] ${rendered}`
|
|
440
|
+
}
|
|
441
|
+
return text
|
|
396
442
|
}
|
|
397
443
|
|
|
398
444
|
/** Guard every tool: a missing binary is an answer, never a crash. */
|
|
@@ -418,6 +464,32 @@ export function createToolSet(runtime ) {
|
|
|
418
464
|
return await runOrStart(binary, argv, background)
|
|
419
465
|
}
|
|
420
466
|
|
|
467
|
+
/** A short, human step label: the first command ids, not the tool name. */
|
|
468
|
+
const labelOf = (args ) => {
|
|
469
|
+
const raw = args.commands
|
|
470
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined
|
|
471
|
+
const ids = raw.map((entry) => {
|
|
472
|
+
if (typeof entry === 'string') return entry.trim().split(/\s+/)[0] ?? ''
|
|
473
|
+
if (typeof entry === 'object' && entry !== null) {
|
|
474
|
+
const id = (entry ).id
|
|
475
|
+
return typeof id === 'string' ? id : ''
|
|
476
|
+
}
|
|
477
|
+
return ''
|
|
478
|
+
}).filter(id => id.length > 0)
|
|
479
|
+
if (ids.length === 0) return undefined
|
|
480
|
+
const shown = ids.slice(0, 3).join(', ')
|
|
481
|
+
return `${profile.product} · ${ids.length <= 3 ? shown : `${shown} +${ids.length - 3}`}`
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** The document this call wrote or opened, when it named one. */
|
|
485
|
+
const documentOf = (args ) => {
|
|
486
|
+
for (const key of ['save', 'export', 'input'] ) {
|
|
487
|
+
const value = args[key]
|
|
488
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
489
|
+
}
|
|
490
|
+
return undefined
|
|
491
|
+
}
|
|
492
|
+
|
|
421
493
|
const session = { commands: [], stale: false }
|
|
422
494
|
|
|
423
495
|
/**
|
|
@@ -541,7 +613,7 @@ export function createToolSet(runtime ) {
|
|
|
541
613
|
}
|
|
542
614
|
writeFileSync(scriptPath, typeof body === 'string' ? body : JSON.stringify(body, null, 2) + '\n')
|
|
543
615
|
const extra = Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : []
|
|
544
|
-
return await runOrStart(binary, [...argv, ...extra], args.background)
|
|
616
|
+
return await runOrStart(binary, [...argv, ...extra], args.background, documentOf(args), labelOf(args))
|
|
545
617
|
}
|
|
546
618
|
if (profile.run.scriptFlag !== undefined) {
|
|
547
619
|
const scriptPath = join(craftWorkDir(profile), `steps-${Date.now()}.json`)
|
|
@@ -556,7 +628,7 @@ export function createToolSet(runtime ) {
|
|
|
556
628
|
profile.run.scriptFlag, scriptPath,
|
|
557
629
|
...Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : [],
|
|
558
630
|
]
|
|
559
|
-
return await runOrStart(binary, [profile.run.verb, ...extra], args.background)
|
|
631
|
+
return await runOrStart(binary, [profile.run.verb, ...extra], args.background, documentOf(args), labelOf(args))
|
|
560
632
|
}
|
|
561
633
|
if (profile.run.scriptMode === true) {
|
|
562
634
|
const scriptPath = join(craftWorkDir(profile), `run-${Date.now()}.jsonl`)
|
|
@@ -568,7 +640,7 @@ export function createToolSet(runtime ) {
|
|
|
568
640
|
scriptPath,
|
|
569
641
|
...Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : [],
|
|
570
642
|
]
|
|
571
|
-
return await runOrStart(binary, [...globalFlagsFor(profile, args), profile.run.verb, ...extra], args.background)
|
|
643
|
+
return await runOrStart(binary, [...globalFlagsFor(profile, args), profile.run.verb, ...extra], args.background, documentOf(args), labelOf(args))
|
|
572
644
|
}
|
|
573
645
|
// PdfCraft's `run` accepts exactly one tool per invocation; sending a
|
|
574
646
|
// list would produce "expected key=value" from the CLI, so the extra
|
|
@@ -597,7 +669,7 @@ export function createToolSet(runtime ) {
|
|
|
597
669
|
...args.print === true ? { print: true } : {},
|
|
598
670
|
...Array.isArray(args.extraArgs) ? { extra: args.extraArgs.filter((a) => typeof a === 'string') } : {},
|
|
599
671
|
})
|
|
600
|
-
return await runOrStart(binary, argv, args.background)
|
|
672
|
+
return await runOrStart(binary, argv, args.background, documentOf(args), labelOf(args))
|
|
601
673
|
},
|
|
602
674
|
}
|
|
603
675
|
|
|
@@ -711,7 +783,7 @@ export function createToolSet(runtime ) {
|
|
|
711
783
|
else if (argMode === 'section') argv.push(typeof args.section === 'string' && args.section.length > 0 ? args.section : inspectSpec.section ?? 'project')
|
|
712
784
|
if (inspectSpec.projectFlag !== undefined && typeof args.project === 'string') argv.push(inspectSpec.projectFlag, args.project)
|
|
713
785
|
argv.push(...inspectSpec.extraArgs ?? [], ...extra)
|
|
714
|
-
return await runOrStart(binary, argv, args.background)
|
|
786
|
+
return await runOrStart(binary, argv, args.background, documentOf(args), labelOf(args))
|
|
715
787
|
},
|
|
716
788
|
}
|
|
717
789
|
|
|
@@ -938,7 +1010,7 @@ export const CRAFT_SECTION_ORDER = 120
|
|
|
938
1010
|
|
|
939
1011
|
|
|
940
1012
|
|
|
941
|
-
|
|
1013
|
+
|
|
942
1014
|
|
|
943
1015
|
|
|
944
1016
|
|
|
@@ -999,6 +1071,12 @@ export function applyCraftPlugin(
|
|
|
999
1071
|
|
|
1000
1072
|
|
|
1001
1073
|
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
|
|
1002
1080
|
,
|
|
1003
1081
|
) {
|
|
1004
1082
|
const { profile, packageRoot, config = {}, defineTool } = options
|
|
@@ -1012,8 +1090,20 @@ export function applyCraftPlugin(
|
|
|
1012
1090
|
description: spec.description,
|
|
1013
1091
|
parameters: spec.parameters,
|
|
1014
1092
|
output: {
|
|
1015
|
-
schema: {
|
|
1016
|
-
|
|
1093
|
+
schema: {
|
|
1094
|
+
type: 'object',
|
|
1095
|
+
additionalProperties: false,
|
|
1096
|
+
properties: {
|
|
1097
|
+
text: { type: 'string' },
|
|
1098
|
+
images: { type: 'array', items: { type: 'json' } },
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
render: (_args , value ) => {
|
|
1102
|
+
const blocks = []
|
|
1103
|
+
if (value.text.length > 0) blocks.push({ type: 'text' , text: value.text })
|
|
1104
|
+
for (const image of value.images ?? []) if (image !== undefined) blocks.push(image)
|
|
1105
|
+
return blocks.length > 0 ? blocks : [{ type: 'text' , text: '' }]
|
|
1106
|
+
},
|
|
1017
1107
|
},
|
|
1018
1108
|
async execute(args ) {
|
|
1019
1109
|
const typed = args
|
|
@@ -1021,7 +1111,18 @@ export function applyCraftPlugin(
|
|
|
1021
1111
|
try {
|
|
1022
1112
|
const text = await spec.execute(typed)
|
|
1023
1113
|
appendJournal(profile, { at: startedAt, kind: spec.kind, summary: journalSummary(spec.kind, typed, text, Date.now() - startedAt) })
|
|
1024
|
-
|
|
1114
|
+
// A preview line becomes a real image block when the plugin can commit
|
|
1115
|
+
// one: that is what makes the transcript show each step of the work.
|
|
1116
|
+
const previewPaths = [...text.matchAll(/^\[preview\] (.+)$/gm)].map(match => match[1].trim())
|
|
1117
|
+
if (previewPaths.length > 0 && options.attachImage !== undefined) {
|
|
1118
|
+
const images = []
|
|
1119
|
+
for (const path of previewPaths) {
|
|
1120
|
+
const block = await options.attachImage(path)
|
|
1121
|
+
if (block !== undefined) images.push(block)
|
|
1122
|
+
}
|
|
1123
|
+
return { text: text.replace(/^\[preview\] .+$/gm, '').trim(), images }
|
|
1124
|
+
}
|
|
1125
|
+
return { text, images: [] }
|
|
1025
1126
|
} catch (error) {
|
|
1026
1127
|
appendJournal(profile, {
|
|
1027
1128
|
at: startedAt,
|