dsh-artcraft-plugin-soundcraft 0.2.1 → 0.2.2
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 +1 -1
- package/src/kit/exec.js +65 -0
- package/src/kit/tools.js +143 -19
- package/src/kit/types.js +4 -0
package/package.json
CHANGED
package/src/kit/exec.js
CHANGED
|
@@ -155,6 +155,71 @@ export async function runCraft(options )
|
|
|
155
155
|
})
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Run one craft process in the background and stream its output to a log file.
|
|
160
|
+
*
|
|
161
|
+
* Long operations — a 4K render, a texture bake, an audio bounce — outlast any
|
|
162
|
+
* reasonable tool timeout, and killing them at the timer loses the work. A job
|
|
163
|
+
* keeps the process alive after the tool returns, so the caller can poll or wait
|
|
164
|
+
* and still read the output afterwards.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
/** A started job, including the process handle the runtime uses to observe it. */
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/** Live job state kept by the runtime. */
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Start a craft CLI in the background.
|
|
199
|
+
* @param options - profile, binary, argv, cwd and the log file to write.
|
|
200
|
+
* @returns a handle the caller can poll through its runtime.
|
|
201
|
+
*/
|
|
202
|
+
export function startCraftJob(options ) {
|
|
203
|
+
const { profile, binary, argv, cwd, logPath, id } = options
|
|
204
|
+
const child = spawn(binary.path, [...argv], {
|
|
205
|
+
cwd,
|
|
206
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
207
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
208
|
+
windowsHide: true,
|
|
209
|
+
})
|
|
210
|
+
const write = (chunk ) => {
|
|
211
|
+
try {
|
|
212
|
+
appendFileSync(logPath, chunk)
|
|
213
|
+
} catch {
|
|
214
|
+
// A full disk must not take the tool down; the process keeps running.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
child.stdout?.on('data', write)
|
|
218
|
+
child.stderr?.on('data', write)
|
|
219
|
+
void profile
|
|
220
|
+
return { id, pid: child.pid, logPath, startedAt: Date.now(), argv, child }
|
|
221
|
+
}
|
|
222
|
+
|
|
158
223
|
/**
|
|
159
224
|
* Compose the argv for one headless `run` invocation.
|
|
160
225
|
*
|
package/src/kit/tools.js
CHANGED
|
@@ -12,11 +12,11 @@ import { CraftMcpClient, renderSchema } from './mcp.js'
|
|
|
12
12
|
|
|
13
13
|
import { groupsOf, loadCatalog, score } from './catalog.js'
|
|
14
14
|
import { craftVersion, discoverCraft, missingBinaryMessage } from './discover.js'
|
|
15
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
16
16
|
import { tmpdir } from 'node:os'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
|
-
import { DEFAULT_TIMEOUT_MS, buildRunArgv, runCraft } from './exec.js'
|
|
19
|
-
|
|
18
|
+
import { DEFAULT_TIMEOUT_MS, buildRunArgv, killTree, runCraft, startCraftJob } from './exec.js'
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -159,7 +159,53 @@ export function createRuntime(profile , packageRoot , config
|
|
|
159
159
|
let catalogCache
|
|
160
160
|
const mcpClients = new Map ()
|
|
161
161
|
const artifacts = config.artifactsDir ?? join(tmpdir(), 'dsh-artcraft', profile.id)
|
|
162
|
+
const jobs = new Map ()
|
|
163
|
+
const jobChildren = new Map ()
|
|
164
|
+
let jobCounter = 0
|
|
162
165
|
return {
|
|
166
|
+
startJob(binary, argv) {
|
|
167
|
+
jobCounter += 1
|
|
168
|
+
const id = `j${jobCounter}`
|
|
169
|
+
const jobDir = join(artifacts, 'jobs')
|
|
170
|
+
mkdirSync(jobDir, { recursive: true })
|
|
171
|
+
const logPath = join(jobDir, `${profile.id}-${id}.log`)
|
|
172
|
+
const started = startCraftJob({ profile, binary, argv, cwd: process.cwd(), logPath, id })
|
|
173
|
+
const state = { ...started, running: true, argv: started.argv, logPath, id, pid: started.pid, startedAt: started.startedAt }
|
|
174
|
+
jobs.set(id, state)
|
|
175
|
+
jobChildren.set(id, started.child)
|
|
176
|
+
started.child.on('close', (code, signal) => {
|
|
177
|
+
const settled = {
|
|
178
|
+
...state, running: false, exitCode: code, signal,
|
|
179
|
+
durationMs: Date.now() - state.startedAt,
|
|
180
|
+
}
|
|
181
|
+
jobs.set(id, settled)
|
|
182
|
+
jobChildren.delete(id)
|
|
183
|
+
})
|
|
184
|
+
started.child.unref()
|
|
185
|
+
return started
|
|
186
|
+
},
|
|
187
|
+
jobState(id) {
|
|
188
|
+
return jobs.get(id)
|
|
189
|
+
},
|
|
190
|
+
jobs() {
|
|
191
|
+
return [...jobs.values()].sort((a, b) => a.startedAt - b.startedAt)
|
|
192
|
+
},
|
|
193
|
+
async waitForJob(id, timeoutMs) {
|
|
194
|
+
const state = jobs.get(id)
|
|
195
|
+
if (state === undefined) return undefined
|
|
196
|
+
const deadline = Date.now() + Math.max(0, timeoutMs)
|
|
197
|
+
for (;;) {
|
|
198
|
+
const current = jobs.get(id)
|
|
199
|
+
if (current === undefined || !current.running) return current
|
|
200
|
+
if (Date.now() >= deadline) return current
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
killJob(id) {
|
|
205
|
+
const child = jobChildren.get(id)
|
|
206
|
+
if (child !== undefined) killTree(child)
|
|
207
|
+
return child !== undefined
|
|
208
|
+
},
|
|
163
209
|
artifactsDir() {
|
|
164
210
|
return artifacts
|
|
165
211
|
},
|
|
@@ -264,6 +310,8 @@ export function renderCatalog(
|
|
|
264
310
|
|
|
265
311
|
|
|
266
312
|
|
|
313
|
+
|
|
314
|
+
|
|
267
315
|
|
|
268
316
|
|
|
269
317
|
/** One tool's definition: name/description/schema plus its executor. */
|
|
@@ -322,6 +370,25 @@ export function createToolSet(runtime ) {
|
|
|
322
370
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
323
371
|
const catalogLimit = config.catalogLimit ?? 60
|
|
324
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Run now, or start a background job when `background` is set.
|
|
375
|
+
*
|
|
376
|
+
* Renders, bakes and bounces outlast the tool timeout; a job keeps the process
|
|
377
|
+
* alive after the call returns and leaves its output in a log file.
|
|
378
|
+
*/
|
|
379
|
+
const runOrStart = async (binary , argv , background ) => {
|
|
380
|
+
if (background === true) {
|
|
381
|
+
const job = runtime.startJob(binary, argv)
|
|
382
|
+
return [
|
|
383
|
+
`started ${profile.id} job ${job.id}${job.pid === undefined ? '' : ` (pid ${job.pid})`}`,
|
|
384
|
+
`log: ${job.logPath}`,
|
|
385
|
+
`poll with \`${profile.id}_job\` {"action":"status","job":"${job.id}"}`,
|
|
386
|
+
`or wait with {"action":"wait","job":"${job.id}","timeoutMs":300000}`,
|
|
387
|
+
].join('\n')
|
|
388
|
+
}
|
|
389
|
+
return renderRun(await runCraft({ profile, binary, argv, cwd: process.cwd(), timeoutMs }), profile)
|
|
390
|
+
}
|
|
391
|
+
|
|
325
392
|
/** Guard every tool: a missing binary is an answer, never a crash. */
|
|
326
393
|
const requireBinary = () => {
|
|
327
394
|
const binary = runtime.binary()
|
|
@@ -335,13 +402,14 @@ export function createToolSet(runtime ) {
|
|
|
335
402
|
input ,
|
|
336
403
|
sample ,
|
|
337
404
|
commands ,
|
|
405
|
+
background ,
|
|
338
406
|
) => {
|
|
339
407
|
const argv = buildRunArgv(profile, {
|
|
340
408
|
...input !== undefined ? { input } : {},
|
|
341
409
|
...sample !== undefined ? { sample } : {},
|
|
342
410
|
commands,
|
|
343
411
|
})
|
|
344
|
-
return
|
|
412
|
+
return await runOrStart(binary, argv, background)
|
|
345
413
|
}
|
|
346
414
|
|
|
347
415
|
const session = { commands: [], stale: false }
|
|
@@ -373,6 +441,7 @@ export function createToolSet(runtime ) {
|
|
|
373
441
|
+ `${profile.blurb} Files: ${profile.extensions.join(', ')}.`,
|
|
374
442
|
parameters: {
|
|
375
443
|
input: { type: 'string', description: `Absolute or workspace-relative path to an existing ${profile.extensions[0]} document. Mutually exclusive with \`sample\`.` },
|
|
444
|
+
background: { type: 'boolean', description: 'Start the work as a background job and return immediately; poll it with the job tool. Use for long renders, bakes and bounces.' },
|
|
376
445
|
commands: {
|
|
377
446
|
type: 'array',
|
|
378
447
|
description: 'Ordered commands to apply. Each is { "id": "cell.set", "json": { "cell": "A1", "input": "x" } }. An entry may also be a raw CLI string.',
|
|
@@ -466,8 +535,7 @@ export function createToolSet(runtime ) {
|
|
|
466
535
|
}
|
|
467
536
|
writeFileSync(scriptPath, typeof body === 'string' ? body : JSON.stringify(body, null, 2) + '\n')
|
|
468
537
|
const extra = Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : []
|
|
469
|
-
|
|
470
|
-
return renderRun(scriptResult, profile)
|
|
538
|
+
return await runOrStart(binary, [...argv, ...extra], args.background)
|
|
471
539
|
}
|
|
472
540
|
if (profile.run.scriptFlag !== undefined) {
|
|
473
541
|
const scriptPath = join(craftWorkDir(profile), `steps-${Date.now()}.json`)
|
|
@@ -482,8 +550,7 @@ export function createToolSet(runtime ) {
|
|
|
482
550
|
profile.run.scriptFlag, scriptPath,
|
|
483
551
|
...Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : [],
|
|
484
552
|
]
|
|
485
|
-
|
|
486
|
-
return renderRun(result, profile)
|
|
553
|
+
return await runOrStart(binary, [profile.run.verb, ...extra], args.background)
|
|
487
554
|
}
|
|
488
555
|
if (profile.run.scriptMode === true) {
|
|
489
556
|
const scriptPath = join(craftWorkDir(profile), `run-${Date.now()}.jsonl`)
|
|
@@ -495,12 +562,7 @@ export function createToolSet(runtime ) {
|
|
|
495
562
|
scriptPath,
|
|
496
563
|
...Array.isArray(args.extraArgs) ? args.extraArgs.filter((a) => typeof a === 'string') : [],
|
|
497
564
|
]
|
|
498
|
-
|
|
499
|
-
profile, binary,
|
|
500
|
-
argv: [...globalFlagsFor(profile, args), profile.run.verb, ...extra],
|
|
501
|
-
cwd: process.cwd(), timeoutMs,
|
|
502
|
-
})
|
|
503
|
-
return renderRun(result, profile)
|
|
565
|
+
return await runOrStart(binary, [...globalFlagsFor(profile, args), profile.run.verb, ...extra], args.background)
|
|
504
566
|
}
|
|
505
567
|
// PdfCraft's `run` accepts exactly one tool per invocation; sending a
|
|
506
568
|
// list would produce "expected key=value" from the CLI, so the extra
|
|
@@ -511,7 +573,7 @@ export function createToolSet(runtime ) {
|
|
|
511
573
|
// whole list in a single process; the history recorded above is replayed
|
|
512
574
|
// through that path, so a second call continues the same document.
|
|
513
575
|
if (profile.oneToolPerRun === true && profile.run.scriptFlag === undefined && rest.length > 0) {
|
|
514
|
-
const only = await runSingle(binary, input, sample, single)
|
|
576
|
+
const only = await runSingle(binary, input, sample, single, args.background)
|
|
515
577
|
return `${only}\n\n[${profile.product} runs one tool per call: ${rest.length} further command(s) were not sent — ${rest.map(c => c.id).join(', ')}. Call this tool again for each.]`
|
|
516
578
|
}
|
|
517
579
|
const argv = buildRunArgv(profile, {
|
|
@@ -529,8 +591,7 @@ export function createToolSet(runtime ) {
|
|
|
529
591
|
...args.print === true ? { print: true } : {},
|
|
530
592
|
...Array.isArray(args.extraArgs) ? { extra: args.extraArgs.filter((a) => typeof a === 'string') } : {},
|
|
531
593
|
})
|
|
532
|
-
|
|
533
|
-
return renderRun(result, profile)
|
|
594
|
+
return await runOrStart(binary, argv, args.background)
|
|
534
595
|
},
|
|
535
596
|
}
|
|
536
597
|
|
|
@@ -644,8 +705,7 @@ export function createToolSet(runtime ) {
|
|
|
644
705
|
else if (argMode === 'section') argv.push(typeof args.section === 'string' && args.section.length > 0 ? args.section : inspectSpec.section ?? 'project')
|
|
645
706
|
if (inspectSpec.projectFlag !== undefined && typeof args.project === 'string') argv.push(inspectSpec.projectFlag, args.project)
|
|
646
707
|
argv.push(...inspectSpec.extraArgs ?? [], ...extra)
|
|
647
|
-
|
|
648
|
-
return renderRun(result, profile)
|
|
708
|
+
return await runOrStart(binary, argv, args.background)
|
|
649
709
|
},
|
|
650
710
|
}
|
|
651
711
|
|
|
@@ -670,6 +730,69 @@ export function createToolSet(runtime ) {
|
|
|
670
730
|
},
|
|
671
731
|
} : undefined
|
|
672
732
|
|
|
733
|
+
const job = {
|
|
734
|
+
kind: 'job',
|
|
735
|
+
name: `${profile.id}_job`,
|
|
736
|
+
description:
|
|
737
|
+
`Track background work started by \`${profile.id}_run\` with \`background: true\`. `
|
|
738
|
+
+ `Long renders, bakes and bounces outlive a tool timeout; the job keeps running and logs to a file. `
|
|
739
|
+
+ `Actions: \`list\` every job, \`status\` one (with the log tail), \`wait\` for it to finish, \`kill\` it.`,
|
|
740
|
+
parameters: {
|
|
741
|
+
action: { type: 'string', enum: ['list', 'status', 'wait', 'kill'], description: 'What to do. Defaults to list.' },
|
|
742
|
+
job: { type: 'string', description: 'Job id returned by the run call, e.g. "j1".' },
|
|
743
|
+
timeoutMs: { type: 'number', description: 'For action "wait": milliseconds to block (default 60000, at most 600000).' },
|
|
744
|
+
tailLines: { type: 'number', description: 'For "status"/"wait": how many log lines to show (default 20).' },
|
|
745
|
+
},
|
|
746
|
+
async execute(args) {
|
|
747
|
+
const action = typeof args.action === 'string' ? args.action : 'list'
|
|
748
|
+
const tailLines = typeof args.tailLines === 'number' && args.tailLines > 0 ? Math.min(Math.floor(args.tailLines), 200) : 20
|
|
749
|
+
/** Last lines of a job's log, with a byte cap so a huge log cannot flood the model. */
|
|
750
|
+
const tail = (path ) => {
|
|
751
|
+
try {
|
|
752
|
+
const text = readFileSync(path, 'utf8')
|
|
753
|
+
const lines = text.split('\n')
|
|
754
|
+
const shown = lines.slice(Math.max(0, lines.length - tailLines)).join('\n').trim()
|
|
755
|
+
return shown.length > 8000 ? `…${shown.slice(-8000)}` : shown
|
|
756
|
+
} catch {
|
|
757
|
+
return '(no log yet)'
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/** A job's argv, shortened: a 200-command render would otherwise print pages. */
|
|
761
|
+
const commandLine = (argv ) => {
|
|
762
|
+
const text = argv.join(' ')
|
|
763
|
+
return text.length > 240 ? `${text.slice(0, 240)}… (${argv.length} arguments)` : text
|
|
764
|
+
}
|
|
765
|
+
const describe = (state ) => [
|
|
766
|
+
`${state.id}${state.running ? ' (running)' : ` (exit ${state.exitCode ?? state.signal ?? '?'})`}`
|
|
767
|
+
+ ` — ${state.durationMs === undefined ? `${Math.round((Date.now() - state.startedAt) / 1000)}s so far` : `${Math.round(state.durationMs / 1000)}s`}`,
|
|
768
|
+
` command: ${commandLine(state.argv)}`,
|
|
769
|
+
` log: ${state.logPath}`,
|
|
770
|
+
].join('\n')
|
|
771
|
+
|
|
772
|
+
if (action === 'list') {
|
|
773
|
+
const all = runtime.jobs()
|
|
774
|
+
if (all.length === 0) return `${profile.product}: no background jobs in this session.`
|
|
775
|
+
return [`${profile.product} background jobs: ${all.length}`, ...all.map(describe)].join('\n')
|
|
776
|
+
}
|
|
777
|
+
const id = typeof args.job === 'string' ? args.job.trim() : ''
|
|
778
|
+
if (id.length === 0) throw new Error('job is required for action ' + JSON.stringify(action))
|
|
779
|
+
if (action === 'kill') {
|
|
780
|
+
const killed = runtime.killJob(id)
|
|
781
|
+
const state = runtime.jobState(id)
|
|
782
|
+
return killed ? `killed ${id}` : `${id} is not running${state === undefined ? ' (unknown job)' : ''}`
|
|
783
|
+
}
|
|
784
|
+
if (action === 'wait') {
|
|
785
|
+
const timeout = typeof args.timeoutMs === 'number' && args.timeoutMs > 0 ? Math.min(Math.floor(args.timeoutMs), 600_000) : 60_000
|
|
786
|
+
const state = await runtime.waitForJob(id, timeout)
|
|
787
|
+
if (state === undefined) return `No job named ${JSON.stringify(id)}.`
|
|
788
|
+
return [describe(state), '', tail(state.logPath)].join('\n')
|
|
789
|
+
}
|
|
790
|
+
const state = runtime.jobState(id)
|
|
791
|
+
if (state === undefined) return `No job named ${JSON.stringify(id)}.`
|
|
792
|
+
return [describe(state), '', tail(state.logPath)].join('\n')
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
|
|
673
796
|
const mcp = runtime.mcp()
|
|
674
797
|
const idleMs = config.mcpIdleMs ?? 300_000
|
|
675
798
|
|
|
@@ -790,6 +913,7 @@ export function createToolSet(runtime ) {
|
|
|
790
913
|
...mcpTools !== undefined ? { mcpTools } : {},
|
|
791
914
|
...mcpCall !== undefined ? { mcpCall } : {},
|
|
792
915
|
...mcpStatus !== undefined ? { mcpStatus } : {},
|
|
916
|
+
job,
|
|
793
917
|
}
|
|
794
918
|
}
|
|
795
919
|
|