dsh-artcraft-plugin-pdfcraft 0.2.2 → 0.2.4
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/journal.js +55 -0
- package/src/kit/present.js +206 -0
- package/src/kit/tools.js +59 -2
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable work journal for the craft tools.
|
|
3
|
+
*
|
|
4
|
+
* The harness shows each tool call, but a session that builds a report spans a
|
|
5
|
+
* dozen calls across four applications, and reconstructing what actually happened
|
|
6
|
+
* means scrolling a transcript. Every call also appends one line to a per-craft
|
|
7
|
+
* journal in the workspace (`.artcraft/journal/`), so the user can open one file
|
|
8
|
+
* and read the story: which craft, which operation, how long, what came out.
|
|
9
|
+
*
|
|
10
|
+
* A journal line is best-effort and never fails a tool call: a read-only
|
|
11
|
+
* workspace loses the record, not the work.
|
|
12
|
+
* @module @dsh-artcraft/craft-kit/journal
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/** One recorded event. */
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/** Where the journals live, relative to the workspace the agent runs in. */
|
|
32
|
+
export function journalDir(cwd = process.cwd()) {
|
|
33
|
+
return join(cwd, '.artcraft', 'journal')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append one entry to the craft's journal.
|
|
38
|
+
* @param profile - the craft that ran.
|
|
39
|
+
* @param entry - what happened.
|
|
40
|
+
* @param cwd - workspace root; defaults to the process cwd.
|
|
41
|
+
*/
|
|
42
|
+
export function appendJournal(profile , entry , cwd = process.cwd()) {
|
|
43
|
+
try {
|
|
44
|
+
const dir = journalDir(cwd)
|
|
45
|
+
mkdirSync(dir, { recursive: true })
|
|
46
|
+
const time = new Date(entry.at).toTimeString().slice(0, 8)
|
|
47
|
+
const artifacts = entry.artifacts !== undefined && entry.artifacts.length > 0
|
|
48
|
+
? `\n → ${entry.artifacts.join(', ')}`
|
|
49
|
+
: ''
|
|
50
|
+
appendFileSync(join(dir, `${profile.id}.md`), `${time} · ${entry.kind} · ${entry.summary}${artifacts}\n`)
|
|
51
|
+
appendFileSync(join(dir, 'events.jsonl'), JSON.stringify({ craft: profile.id, ...entry }) + '\n')
|
|
52
|
+
} catch {
|
|
53
|
+
// A journal is a convenience; never let it fail the call that produced it.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render intents for the craft tools: what a call looks like *while running* and
|
|
3
|
+
* what its result looks like afterwards.
|
|
4
|
+
*
|
|
5
|
+
* A craft run is one call that performs a dozen steps, and a raw JSONL dump is a
|
|
6
|
+
* poor card: the reader cannot see how many steps there are, which one failed, or
|
|
7
|
+
* what files came out. These presenters turn the same result into a titled card
|
|
8
|
+
* (a UI that understands {@link ToolCallView}/{@link ToolResultView} shows it as a
|
|
9
|
+
* tool row with a header and a short body).
|
|
10
|
+
*
|
|
11
|
+
* The kernel stays DSH-free: these are plain objects shaped like the harness's
|
|
12
|
+
* view contracts, and the calling plugin passes them to `defineTool`.
|
|
13
|
+
* @module @dsh-artcraft/craft-kit/present
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/** One text block, as the harness's content-block shape expects. */
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/** A file the call touches, for editor follow-along. */
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/** Pending-call card: `{card:'generic'}` with a title and an optional body. */
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/** Completed-call card: a replacement title and reformatted content. */
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/** Arguments the model passed, as far as these presenters care. */
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/** First `count` entries of a list, plus a count when longer. */
|
|
51
|
+
function summarise (items , render , count = 8) {
|
|
52
|
+
const shown = items.slice(0, count).map(render)
|
|
53
|
+
if (items.length > shown.length) shown.push(`…and ${items.length - shown.length} more`)
|
|
54
|
+
return shown.length === 0 ? [{ type: 'text', text: '(no commands)' }] : shown.map(text => ({ type: 'text', text }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The command list a run call carries, whether objects or raw strings. */
|
|
58
|
+
function commandLabels(raw ) {
|
|
59
|
+
if (!Array.isArray(raw)) return []
|
|
60
|
+
return raw.map((entry) => {
|
|
61
|
+
if (typeof entry === 'string') return entry
|
|
62
|
+
if (typeof entry === 'object' && entry !== null) {
|
|
63
|
+
const record = entry
|
|
64
|
+
const id = typeof record.id === 'string' ? record.id : '(unnamed)'
|
|
65
|
+
const json = record.json === undefined ? '' : ` ${JSON.stringify(record.json)}`
|
|
66
|
+
return `${id}${json.length > 60 ? `${json.slice(0, 60)}…` : json}`
|
|
67
|
+
}
|
|
68
|
+
return String(entry)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A path from the arguments, when the call writes one. */
|
|
73
|
+
function outputPath(args ) {
|
|
74
|
+
for (const key of ['save', 'export', 'output', 'out'] ) {
|
|
75
|
+
const value = args[key]
|
|
76
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A path from the arguments, when the call reads one. */
|
|
82
|
+
function inputPath(args ) {
|
|
83
|
+
const value = args.input ?? args.file
|
|
84
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The pending-call card for one tool.
|
|
89
|
+
* @param profile - the craft, for the title.
|
|
90
|
+
* @param kind - which tool of the set is being called.
|
|
91
|
+
* @param args - the model's arguments.
|
|
92
|
+
* @returns a generic call view.
|
|
93
|
+
*/
|
|
94
|
+
export function craftCallView(profile , kind , args ) {
|
|
95
|
+
const name = profile.product
|
|
96
|
+
switch (kind) {
|
|
97
|
+
case 'run': {
|
|
98
|
+
const labels = commandLabels(args.commands)
|
|
99
|
+
const target = outputPath(args)
|
|
100
|
+
return {
|
|
101
|
+
card: 'generic',
|
|
102
|
+
kind: 'execute',
|
|
103
|
+
title: `${name} · ${labels.length} command${labels.length === 1 ? '' : 's'}${target === undefined ? '' : ` → ${target}`}`,
|
|
104
|
+
content: summarise(labels, label => `• ${label}`),
|
|
105
|
+
...target === undefined ? {} : { locations: [{ path: target }] },
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
case 'commands': {
|
|
109
|
+
const search = typeof args.search === 'string' && args.search.length > 0 ? ` "${args.search}"` : ''
|
|
110
|
+
return { card: 'generic', kind: 'search', title: `${name} · command catalogue${search}` }
|
|
111
|
+
}
|
|
112
|
+
case 'describe': {
|
|
113
|
+
const id = typeof args.id === 'string' ? args.id : ''
|
|
114
|
+
return { card: 'generic', kind: 'read', title: `${name} · describe ${id}` }
|
|
115
|
+
}
|
|
116
|
+
case 'inspect': {
|
|
117
|
+
const file = inputPath(args) ?? '(default)'
|
|
118
|
+
return { card: 'generic', kind: 'read', title: `${name} · inspect ${file}`, locations: [{ path: file }] }
|
|
119
|
+
}
|
|
120
|
+
case 'convert': {
|
|
121
|
+
const input = inputPath(args) ?? ''
|
|
122
|
+
const output = typeof args.output === 'string' ? args.output : ''
|
|
123
|
+
return {
|
|
124
|
+
card: 'generic',
|
|
125
|
+
kind: 'execute',
|
|
126
|
+
title: `${name} · convert ${input}${output.length === 0 ? '' : ` → ${output}`}`,
|
|
127
|
+
locations: output.length === 0 ? [] : [{ path: output }],
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
case 'mcpCall': {
|
|
131
|
+
const tool = typeof args.tool === 'string' ? args.tool : '(tool)'
|
|
132
|
+
const attach = typeof args.attach === 'number' ? ` · attached :${args.attach}` : ''
|
|
133
|
+
return { card: 'generic', kind: 'execute', title: `${name} · ${tool}${attach}`, rawInput: args.arguments }
|
|
134
|
+
}
|
|
135
|
+
case 'mcpTools': {
|
|
136
|
+
const search = typeof args.search === 'string' && args.search.length > 0 ? ` "${args.search}"` : ''
|
|
137
|
+
return { card: 'generic', kind: 'search', title: `${name} · MCP tools, resources and prompts${search}` }
|
|
138
|
+
}
|
|
139
|
+
case 'mcpStatus':
|
|
140
|
+
return { card: 'generic', kind: 'read', title: `${name} · MCP bridge status` }
|
|
141
|
+
case 'job': {
|
|
142
|
+
const action = typeof args.action === 'string' ? args.action : 'list'
|
|
143
|
+
const job = typeof args.job === 'string' ? ` ${args.job}` : ''
|
|
144
|
+
return { card: 'generic', kind: 'other', title: `${name} · job ${action}${job}` }
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
return { card: 'generic', kind: 'other', title: `${name} · ${kind}` }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What a run result text says: exit status, duration, artifacts, per-step failures. */
|
|
152
|
+
function summariseRun(profile , text ) {
|
|
153
|
+
const steps = text.split('\n').filter(line => line.startsWith('{"command"') || line.startsWith('{"step"'))
|
|
154
|
+
let ok = 0
|
|
155
|
+
let failed = 0
|
|
156
|
+
let firstFailure = ''
|
|
157
|
+
for (const line of steps) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(line)
|
|
160
|
+
if (parsed.ok === false) {
|
|
161
|
+
failed += 1
|
|
162
|
+
if (firstFailure.length === 0) firstFailure = `${parsed.command ?? 'step'}: ${parsed.error ?? 'failed'}`
|
|
163
|
+
} else ok += 1
|
|
164
|
+
} catch {
|
|
165
|
+
// A craft that prints non-JSON progress lines still runs; the exit line below decides.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const exit = /exited (\d+) in (\d+)ms/.exec(text)
|
|
169
|
+
const artifacts = [...text.matchAll(/^(?:wrote|saved|exported) (.+)$/gm)].map(match => match[1])
|
|
170
|
+
const title = `${profile.product} · ${steps.length > 0 ? `${ok} ok${failed > 0 ? `, ${failed} failed` : ''}` : 'finished'}${exit === null ? '' : ` · exit ${exit[1]}`}`
|
|
171
|
+
const body = []
|
|
172
|
+
if (failed > 0) body.push(`first failure — ${firstFailure}`)
|
|
173
|
+
if (artifacts.length > 0) body.push(`wrote ${artifacts.join(', ')}`)
|
|
174
|
+
if (exit !== null) body.push(`took ${exit[2]} ms`)
|
|
175
|
+
if (body.length === 0) body.push(text.trim().split('\n')[0] ?? '(no output)')
|
|
176
|
+
return { card: 'generic', title, content: body.map(line => ({ type: 'text', text: line })) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The completed-call card for one tool.
|
|
181
|
+
* @param profile - the craft, for the title.
|
|
182
|
+
* @param kind - which tool ran.
|
|
183
|
+
* @param text - the model-facing result.
|
|
184
|
+
* @param isError - whether the call failed.
|
|
185
|
+
* @returns a generic result view.
|
|
186
|
+
*/
|
|
187
|
+
export function craftResultView(profile , kind , text , isError ) {
|
|
188
|
+
if (isError) {
|
|
189
|
+
return {
|
|
190
|
+
card: 'generic',
|
|
191
|
+
title: `${profile.product} · ${kind} failed`,
|
|
192
|
+
content: [{ type: 'text', text: text.split('\n').slice(0, 4).join('\n') }],
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (kind === 'run') return summariseRun(profile, text)
|
|
196
|
+
const all = text.trim().split('\n').filter(line => line.trim().length > 0)
|
|
197
|
+
// A few lines are usually the whole story (job status, an image path and its
|
|
198
|
+
// dimensions); keep them rather than collapsing to one line and a count.
|
|
199
|
+
const shown = all.slice(0, 4)
|
|
200
|
+
if (all.length > shown.length) shown.push(`…and ${all.length - shown.length} more lines`)
|
|
201
|
+
return {
|
|
202
|
+
card: 'generic',
|
|
203
|
+
title: `${profile.product} · ${kind}`,
|
|
204
|
+
content: (shown.length === 0 ? ['(no output)'] : shown).map(line => ({ type: 'text', text: line })),
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/kit/tools.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @module @dsh-artcraft/craft-kit/tools
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { craftCallView, craftResultView } from './present.js'
|
|
12
|
+
import { appendJournal } from './journal.js'
|
|
11
13
|
import { CraftMcpClient, renderSchema } from './mcp.js'
|
|
12
14
|
|
|
13
15
|
import { groupsOf, loadCatalog, score } from './catalog.js'
|
|
@@ -314,13 +316,17 @@ export function renderCatalog(
|
|
|
314
316
|
|
|
315
317
|
|
|
316
318
|
|
|
317
|
-
/** One tool's definition: name/description/schema plus its executor. */
|
|
319
|
+
/** One tool's definition: name/description/schema plus its executor and views. */
|
|
318
320
|
|
|
319
321
|
|
|
320
322
|
|
|
321
323
|
|
|
322
324
|
|
|
323
325
|
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
324
330
|
|
|
325
331
|
|
|
326
332
|
/**
|
|
@@ -918,6 +924,7 @@ export function createToolSet(runtime ) {
|
|
|
918
924
|
}
|
|
919
925
|
|
|
920
926
|
|
|
927
|
+
|
|
921
928
|
/** Prompt-section slot: after the built-in tool sections, app prose follows. */
|
|
922
929
|
export const CRAFT_SECTION_ORDER = 120
|
|
923
930
|
|
|
@@ -934,8 +941,34 @@ export const CRAFT_SECTION_ORDER = 120
|
|
|
934
941
|
|
|
935
942
|
|
|
936
943
|
|
|
944
|
+
|
|
945
|
+
|
|
937
946
|
|
|
938
947
|
|
|
948
|
+
/** One-line journal summary for a finished call. */
|
|
949
|
+
function journalSummary(kind , args , text , elapsedMs ) {
|
|
950
|
+
const output = ['save', 'export', 'output', 'out'].map(key => args[key]).find(value => typeof value === 'string' && value.length > 0)
|
|
951
|
+
switch (kind) {
|
|
952
|
+
case 'run': {
|
|
953
|
+
const count = Array.isArray(args.commands) ? args.commands.length : 0
|
|
954
|
+
const exit = /exited (\d+)/.exec(text)
|
|
955
|
+
return `${count} command(s)${output === undefined ? '' : ` → ${String(output)}`}`
|
|
956
|
+
+ `${exit === null ? '' : ` · exit ${exit[1]}`} · ${elapsedMs}ms`
|
|
957
|
+
}
|
|
958
|
+
case 'mcpCall':
|
|
959
|
+
return `tool ${String(args.tool ?? '?')} · ${elapsedMs}ms`
|
|
960
|
+
case 'job':
|
|
961
|
+
return `job ${String(args.action ?? 'list')}${typeof args.job === 'string' ? ` ${args.job}` : ''}`
|
|
962
|
+
case 'inspect':
|
|
963
|
+
case 'convert':
|
|
964
|
+
return `${String(args.file ?? args.input ?? '')}${args.output === undefined ? '' : ` → ${String(args.output)}`}`
|
|
965
|
+
default: {
|
|
966
|
+
const first = text.trim().split('\n')[0] ?? ''
|
|
967
|
+
return first.length > 120 ? `${first.slice(0, 120)}…` : first
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
939
972
|
/** The `ctx` surface the kernel uses; kept structural for the same reason. */
|
|
940
973
|
|
|
941
974
|
|
|
@@ -983,8 +1016,32 @@ export function applyCraftPlugin(
|
|
|
983
1016
|
render: (_args , value ) => [{ type: 'text' , text: value }],
|
|
984
1017
|
},
|
|
985
1018
|
async execute(args ) {
|
|
986
|
-
|
|
1019
|
+
const typed = args
|
|
1020
|
+
const startedAt = Date.now()
|
|
1021
|
+
try {
|
|
1022
|
+
const text = await spec.execute(typed)
|
|
1023
|
+
appendJournal(profile, { at: startedAt, kind: spec.kind, summary: journalSummary(spec.kind, typed, text, Date.now() - startedAt) })
|
|
1024
|
+
return text
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
appendJournal(profile, {
|
|
1027
|
+
at: startedAt,
|
|
1028
|
+
kind: spec.kind,
|
|
1029
|
+
summary: `failed: ${String((error ).message ?? error).slice(0, 140)}`,
|
|
1030
|
+
})
|
|
1031
|
+
throw error
|
|
1032
|
+
}
|
|
987
1033
|
},
|
|
1034
|
+
// Cards: a run is one call with a dozen steps, so the raw JSONL is a poor
|
|
1035
|
+
// row. These turn it into a titled card — what the call does, then what it
|
|
1036
|
+
// produced — and the journal keeps the same story on disk.
|
|
1037
|
+
presentCall: (args ) => craftCallView(profile, spec.kind, (args ?? {}) ),
|
|
1038
|
+
presentResult: (args , result ) =>
|
|
1039
|
+
craftResultView(
|
|
1040
|
+
profile,
|
|
1041
|
+
spec.kind,
|
|
1042
|
+
(result.content ?? []).filter(block => block.type === 'text').map(block => block.text ?? '').join('\n'),
|
|
1043
|
+
result.isError === true,
|
|
1044
|
+
),
|
|
988
1045
|
})))
|
|
989
1046
|
}
|
|
990
1047
|
ctx.systemPrompt.section({
|