mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
feat(artifact): add extractArtifacts message scanner
Scans Message[] for artifact tool_use/tool_result pairs, parses URL/id/expires from the upload response string, and returns ArtifactInfo[] newest-first. Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
120
src/commands/artifacts/__tests__/scanner.test.ts
Normal file
120
src/commands/artifacts/__tests__/scanner.test.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
import { extractArtifacts } from '../scanner.js'
|
||||||
|
import type { Message } from 'src/types/message.js'
|
||||||
|
|
||||||
|
function assistantToolUse(id: string, input: Record<string, unknown>): Message {
|
||||||
|
return {
|
||||||
|
type: 'assistant',
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: [{ type: 'tool_use' as const, id, name: 'artifact', input }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userToolResult(id: string, content: string, isError = false): Message {
|
||||||
|
return {
|
||||||
|
type: 'user',
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_result' as const,
|
||||||
|
tool_use_id: id,
|
||||||
|
content,
|
||||||
|
is_error: isError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('extractArtifacts', () => {
|
||||||
|
test('returns empty list when no artifact tool_use messages', () => {
|
||||||
|
expect(extractArtifacts([])).toEqual([])
|
||||||
|
expect(
|
||||||
|
extractArtifacts([
|
||||||
|
{
|
||||||
|
type: 'user',
|
||||||
|
uuid: crypto.randomUUID(),
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text' as const, text: 'hi' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pairs a successful tool_use with its tool_result and returns parsed fields', () => {
|
||||||
|
const messages: Message[] = [
|
||||||
|
assistantToolUse('tu1', { file_path: '/tmp/report.html', ttl: 7 }),
|
||||||
|
userToolResult(
|
||||||
|
'tu1',
|
||||||
|
'Artifact uploaded: https://x.test/7d/abc.html (id: abc, expires: 2026-06-27T10:00:00.000Z)',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = extractArtifacts(messages)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
filePath: '/tmp/report.html',
|
||||||
|
hash: 'abc',
|
||||||
|
url: 'https://x.test/7d/abc.html',
|
||||||
|
expiresAt: '2026-06-27T10:00:00.000Z',
|
||||||
|
basename: 'report.html',
|
||||||
|
isError: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('skips artifact tool_use without a matching tool_result', () => {
|
||||||
|
const messages: Message[] = [
|
||||||
|
assistantToolUse('tu1', { file_path: '/tmp/report.html', ttl: 7 }),
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(extractArtifacts(messages)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps error results with isError=true and no parsed fields', () => {
|
||||||
|
const messages: Message[] = [
|
||||||
|
assistantToolUse('tu1', { file_path: '/tmp/missing.html', ttl: 7 }),
|
||||||
|
userToolResult(
|
||||||
|
'tu1',
|
||||||
|
'File does not exist or is not readable: /tmp/missing.html',
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = extractArtifacts(messages)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
filePath: '/tmp/missing.html',
|
||||||
|
basename: 'missing.html',
|
||||||
|
isError: true,
|
||||||
|
})
|
||||||
|
expect(result[0].url).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('orders newest first (last in conversation appears at top)', () => {
|
||||||
|
const messages: Message[] = [
|
||||||
|
assistantToolUse('tu1', { file_path: '/tmp/a.html', ttl: 7 }),
|
||||||
|
userToolResult(
|
||||||
|
'tu1',
|
||||||
|
'Artifact uploaded: https://x.test/7d/a.html (id: a, expires: 2026-06-27T10:00:00.000Z)',
|
||||||
|
),
|
||||||
|
assistantToolUse('tu2', { file_path: '/tmp/b.html', ttl: 7 }),
|
||||||
|
userToolResult(
|
||||||
|
'tu2',
|
||||||
|
'Artifact uploaded: https://x.test/7d/b.html (id: b, expires: 2026-06-27T10:00:00.000Z)',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = extractArtifacts(messages)
|
||||||
|
|
||||||
|
expect(result.map(r => r.basename)).toEqual(['b.html', 'a.html'])
|
||||||
|
})
|
||||||
|
})
|
||||||
97
src/commands/artifacts/scanner.ts
Normal file
97
src/commands/artifacts/scanner.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { basename } from 'path'
|
||||||
|
import type { Message } from 'src/types/message.js'
|
||||||
|
|
||||||
|
export type ArtifactInfo = {
|
||||||
|
toolUseId: string
|
||||||
|
filePath: string
|
||||||
|
basename: string
|
||||||
|
hash?: string
|
||||||
|
url?: string
|
||||||
|
expiresAt?: string
|
||||||
|
rawContent: string
|
||||||
|
isError: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const URL_REGEX = /https?:\/\/\S+\.html\b/
|
||||||
|
const ID_REGEX = /\bid:\s*([A-Za-z0-9_-]+)/
|
||||||
|
const EXPIRES_REGEX = /\bexpires:\s*([0-9T:.Z+-]+)/
|
||||||
|
|
||||||
|
export function extractArtifacts(messages: Message[]): ArtifactInfo[] {
|
||||||
|
const results: ArtifactInfo[] = []
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.type !== 'assistant') continue
|
||||||
|
const content = message.message?.content
|
||||||
|
if (!Array.isArray(content)) continue
|
||||||
|
|
||||||
|
for (const block of content) {
|
||||||
|
if (typeof block !== 'object' || block === null) continue
|
||||||
|
if (!('type' in block)) continue
|
||||||
|
const b = block as Record<string, unknown>
|
||||||
|
if (b.type !== 'tool_use') continue
|
||||||
|
if (b.name !== 'artifact') continue
|
||||||
|
|
||||||
|
const toolUseId = b.id as string
|
||||||
|
const input = b.input as { file_path?: string } | undefined
|
||||||
|
const filePath = input?.file_path ?? '<unknown>'
|
||||||
|
|
||||||
|
const resultBlock = findToolResult(messages, toolUseId)
|
||||||
|
if (!resultBlock) continue
|
||||||
|
|
||||||
|
const rawContent =
|
||||||
|
typeof resultBlock.content === 'string'
|
||||||
|
? resultBlock.content
|
||||||
|
: Array.isArray(resultBlock.content)
|
||||||
|
? resultBlock.content
|
||||||
|
.map(c =>
|
||||||
|
typeof c === 'string'
|
||||||
|
? c
|
||||||
|
: 'text' in c
|
||||||
|
? (c as { text: string }).text
|
||||||
|
: '',
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const isError = resultBlock.is_error === true
|
||||||
|
const urlMatch = rawContent.match(URL_REGEX)
|
||||||
|
const idMatch = rawContent.match(ID_REGEX)
|
||||||
|
const expiresMatch = rawContent.match(EXPIRES_REGEX)
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
toolUseId,
|
||||||
|
filePath,
|
||||||
|
basename: basename(filePath),
|
||||||
|
hash: idMatch?.[1],
|
||||||
|
url: urlMatch?.[0],
|
||||||
|
expiresAt: expiresMatch?.[1],
|
||||||
|
rawContent,
|
||||||
|
isError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newest first
|
||||||
|
return results.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
function findToolResult(
|
||||||
|
messages: Message[],
|
||||||
|
toolUseId: string,
|
||||||
|
): { content: unknown; is_error?: boolean } | null {
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.type !== 'user') continue
|
||||||
|
const content = message.message?.content
|
||||||
|
if (!Array.isArray(content)) continue
|
||||||
|
|
||||||
|
for (const block of content) {
|
||||||
|
if (typeof block !== 'object' || block === null) continue
|
||||||
|
if (!('type' in block)) continue
|
||||||
|
const b = block as Record<string, unknown>
|
||||||
|
if (b.type !== 'tool_result') continue
|
||||||
|
if (b.tool_use_id !== toolUseId) continue
|
||||||
|
return { content: b.content, is_error: b.is_error as boolean | undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user