agent-canvas 0.2.0 → 0.6.0
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/README.md +42 -17
- package/bin/agent-canvas.js +7 -7
- package/bin/agent-canvas.ts +48 -37
- package/bin/lib/commands/daemon.ts +46 -43
- package/bin/lib/commands/exec.ts +100 -100
- package/bin/lib/commands/fetch.ts +15 -15
- package/bin/lib/commands/install.ts +36 -36
- package/bin/lib/commands/instructions.ts +51 -51
- package/bin/lib/commands/push.ts +115 -97
- package/bin/lib/commands/review.ts +190 -0
- package/bin/lib/commands/watch.ts +24 -24
- package/bin/lib/config.ts +12 -12
- package/bin/lib/daemon-lifecycle.ts +47 -51
- package/bin/lib/helpers.ts +82 -76
- package/bin/lib/instructions/app-review.md +88 -0
- package/bin/lib/instructions/component-checklist.md +7 -5
- package/bin/lib/instructions/component-custom.md +20 -14
- package/bin/lib/instructions/component-diff.md +4 -3
- package/bin/lib/instructions/component-item.md +18 -8
- package/bin/lib/instructions/component-markdown.md +4 -2
- package/bin/lib/instructions/component-mermaid.md +12 -6
- package/bin/lib/instructions/component-multichoice.md +6 -2
- package/bin/lib/instructions/component-note.md +1 -1
- package/bin/lib/instructions/component-priority.md +1 -1
- package/bin/lib/instructions/component-rangeinput.md +9 -2
- package/bin/lib/instructions/component-secretinput.md +12 -11
- package/bin/lib/instructions/component-svg.md +62 -16
- package/bin/lib/instructions/component-table.md +6 -6
- package/bin/lib/instructions/component-usefeedback.md +8 -8
- package/bin/lib/instructions/component-userinput.md +6 -2
- package/bin/lib/instructions/flow-feature.md +22 -5
- package/bin/lib/instructions/flow-plan.md +5 -5
- package/bin/lib/instructions/flow-runbook.md +28 -25
- package/bin/lib/instructions/overview.md +27 -18
- package/bin/lib/instructions/theming.md +5 -0
- package/daemon/build.ts +171 -152
- package/daemon/client/App.tsx +1205 -1077
- package/daemon/client/CompareView.tsx +478 -458
- package/daemon/client/FileBrowser.tsx +241 -203
- package/daemon/client/FileViewer.tsx +220 -211
- package/daemon/client/OverviewView.tsx +283 -267
- package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
- package/daemon/client/RevisionSelect.tsx +111 -95
- package/daemon/client/SessionSwitcher.tsx +81 -81
- package/daemon/client/ShareDialog.tsx +211 -212
- package/daemon/client/apiSchemas.ts +78 -0
- package/daemon/client/canvas-runtime.ts +1 -1
- package/daemon/client/clientApi.ts +194 -206
- package/daemon/client/components/SecretInput.tsx +138 -138
- package/daemon/client/components/index.ts +2 -2
- package/daemon/client/domDiff.ts +341 -333
- package/daemon/client/errorMessage.ts +3 -0
- package/daemon/client/exportMarkdown.ts +222 -201
- package/daemon/client/lcs.ts +19 -14
- package/daemon/client/shareCrypto.ts +57 -57
- package/daemon/client/unifiedDiff.ts +660 -649
- package/daemon/dist/annotations-runtime.js +1 -1
- package/daemon/dist/client.css +2 -2
- package/daemon/dist/client.js +77 -80
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/review.js +361 -0
- package/daemon/dist/runtime.js +1 -1
- package/daemon/langMap.ts +24 -6
- package/daemon/review-widget/anchor.ts +187 -0
- package/daemon/review-widget/client.ts +95 -0
- package/daemon/review-widget/index.tsx +36 -0
- package/daemon/review-widget/markup.ts +71 -0
- package/daemon/review-widget/screenshot.ts +91 -0
- package/daemon/review-widget/source.ts +103 -0
- package/daemon/review-widget/styles.css +359 -0
- package/daemon/review-widget/text-modules.d.ts +4 -0
- package/daemon/review-widget/tsconfig.json +16 -0
- package/daemon/review-widget/widget.tsx +436 -0
- package/daemon/src/components.ts +3 -3
- package/daemon/src/handlers/api.ts +330 -328
- package/daemon/src/handlers/files.ts +112 -105
- package/daemon/src/handlers/static.ts +47 -46
- package/daemon/src/handlers/uploads.ts +61 -56
- package/daemon/src/paths.ts +13 -10
- package/daemon/src/remote-feedback.ts +192 -192
- package/daemon/src/review/markdown.ts +52 -0
- package/daemon/src/review/model.ts +132 -0
- package/daemon/src/review/proxy.ts +177 -0
- package/daemon/src/review/server.ts +199 -0
- package/daemon/src/review/store.ts +151 -0
- package/daemon/src/server.ts +127 -86
- package/daemon/src/share.ts +163 -169
- package/daemon/src/stubFetch.ts +7 -0
- package/daemon/tsconfig.json +19 -19
- package/package.json +47 -47
- package/skills/canvas/SKILL.md +9 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- package/daemon/src/remote-feedback.test.ts +0 -165
- package/daemon/src/secrets-api.test.ts +0 -123
- package/daemon/src/share-e2e.test.ts +0 -277
- package/daemon/src/share.test.ts +0 -289
|
@@ -1,113 +1,120 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { LANG_MAP } from
|
|
1
|
+
import type { SessionManager } from '@fabrika/canvas-kernel/server'
|
|
2
|
+
import { jsonResponse, type Route } from '@fabrika/daemon-kit'
|
|
3
|
+
import { readdir } from 'fs/promises'
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from 'path'
|
|
5
|
+
import { LANG_MAP } from '../../langMap'
|
|
6
6
|
|
|
7
7
|
const IMAGE_MIME: Record<string, string> = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
png: 'image/png',
|
|
9
|
+
jpg: 'image/jpeg',
|
|
10
|
+
jpeg: 'image/jpeg',
|
|
11
|
+
gif: 'image/gif',
|
|
12
|
+
svg: 'image/svg+xml',
|
|
13
|
+
webp: 'image/webp',
|
|
14
|
+
ico: 'image/x-icon',
|
|
15
|
+
bmp: 'image/bmp',
|
|
16
|
+
avif: 'image/avif',
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
function resolveSessionPath(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
): { fullPath: string; session: ReturnType<SessionManager[
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
sessionManager: SessionManager,
|
|
21
|
+
sessionId: string,
|
|
22
|
+
relPath: string,
|
|
23
|
+
): { fullPath: string; session: ReturnType<SessionManager['get']> & {} } | Response {
|
|
24
|
+
const session = sessionManager.get(sessionId)
|
|
25
|
+
if (!session) return jsonResponse({ error: 'Session not found' }, 404)
|
|
26
|
+
|
|
27
|
+
const fullPath = resolve(session.projectRoot, relPath)
|
|
28
|
+
const relativePath = relative(resolve(session.projectRoot), fullPath)
|
|
29
|
+
if (!isAbsolute(relPath) && (relativePath === '..' || relativePath.startsWith(`..${sep}`))) {
|
|
30
|
+
return jsonResponse({ error: 'Path traversal rejected' }, 403)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { fullPath, session }
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export function createFileHandlers(sessionManager: SessionManager): Route[] {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
37
|
+
async function handleFileServe(_req: Request, url: URL): Promise<Response> {
|
|
38
|
+
const sessionId = url.searchParams.get('session')
|
|
39
|
+
const relPath = url.searchParams.get('path')
|
|
40
|
+
if (!sessionId || !relPath) return jsonResponse({ error: 'Missing session or path' }, 400)
|
|
41
|
+
|
|
42
|
+
const resolved = resolveSessionPath(sessionManager, sessionId, relPath)
|
|
43
|
+
if (resolved instanceof Response) return resolved
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const file = Bun.file(resolved.fullPath)
|
|
47
|
+
const stat = await file.exists()
|
|
48
|
+
if (!stat) return jsonResponse({ error: 'File not found' }, 404)
|
|
49
|
+
if (file.size > 1024 * 1024) return jsonResponse({ error: 'File too large' }, 413)
|
|
50
|
+
|
|
51
|
+
const content = await file.text()
|
|
52
|
+
const ext = relPath.split('.').pop() || ''
|
|
53
|
+
return jsonResponse({ content, language: LANG_MAP[ext] || 'text' })
|
|
54
|
+
} catch {
|
|
55
|
+
return jsonResponse({ error: 'Failed to read file' }, 500)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleImageServe(_req: Request, url: URL): Promise<Response> {
|
|
60
|
+
const sessionId = url.searchParams.get('session')
|
|
61
|
+
const relPath = url.searchParams.get('path')
|
|
62
|
+
if (!sessionId || !relPath) return jsonResponse({ error: 'Missing session or path' }, 400)
|
|
63
|
+
|
|
64
|
+
const resolved = resolveSessionPath(sessionManager, sessionId, relPath)
|
|
65
|
+
if (resolved instanceof Response) return resolved
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const file = Bun.file(resolved.fullPath)
|
|
69
|
+
if (!(await file.exists())) return jsonResponse({ error: 'File not found' }, 404)
|
|
70
|
+
if (file.size > 10 * 1024 * 1024) return jsonResponse({ error: 'File too large' }, 413)
|
|
71
|
+
|
|
72
|
+
const ext = relPath.split('.').pop()?.toLowerCase() || ''
|
|
73
|
+
const mime = IMAGE_MIME[ext]
|
|
74
|
+
if (!mime) return jsonResponse({ error: 'Unsupported image format' }, 415)
|
|
75
|
+
|
|
76
|
+
return new Response(file, {
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': mime,
|
|
79
|
+
'Cache-Control': 'public, max-age=300',
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
} catch {
|
|
83
|
+
return jsonResponse({ error: 'Failed to read file' }, 500)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleTreeServe(_req: Request, url: URL): Promise<Response> {
|
|
88
|
+
const sessionId = url.searchParams.get('session')
|
|
89
|
+
const relPath = url.searchParams.get('path') || ''
|
|
90
|
+
if (!sessionId) return jsonResponse({ error: 'Missing session' }, 400)
|
|
91
|
+
|
|
92
|
+
const resolved = resolveSessionPath(sessionManager, sessionId, relPath)
|
|
93
|
+
if (resolved instanceof Response) return resolved
|
|
94
|
+
|
|
95
|
+
const ignoreDirs = new Set(['.git', 'node_modules', '.next', '__pycache__', 'dist', '.cache'])
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const dirents = await readdir(resolved.fullPath, { withFileTypes: true })
|
|
99
|
+
const entries = dirents
|
|
100
|
+
.filter((d) => !ignoreDirs.has(d.name) && !d.name.startsWith('.'))
|
|
101
|
+
.map((d) => ({
|
|
102
|
+
name: d.name,
|
|
103
|
+
type: d.isDirectory() ? 'dir' : 'file',
|
|
104
|
+
}))
|
|
105
|
+
.sort((a, b) => {
|
|
106
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
|
|
107
|
+
return a.name.localeCompare(b.name)
|
|
108
|
+
})
|
|
109
|
+
return jsonResponse({ entries })
|
|
110
|
+
} catch {
|
|
111
|
+
return jsonResponse({ error: 'Failed to read directory' }, 500)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [
|
|
116
|
+
{ method: 'GET', pattern: new URLPattern({ pathname: '/api/file' }), handler: handleFileServe },
|
|
117
|
+
{ method: 'GET', pattern: new URLPattern({ pathname: '/api/image' }), handler: handleImageServe },
|
|
118
|
+
{ method: 'GET', pattern: new URLPattern({ pathname: '/api/tree' }), handler: handleTreeServe },
|
|
119
|
+
]
|
|
113
120
|
}
|
|
@@ -1,61 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import type { Route } from '@fabrika/daemon-kit'
|
|
2
|
+
import { dirname, join } from 'path'
|
|
3
|
+
import { DATA_DIR } from '../paths'
|
|
4
4
|
|
|
5
5
|
const MIME_TYPES: Record<string, string> = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
6
|
+
js: 'application/javascript',
|
|
7
|
+
css: 'text/css',
|
|
8
|
+
html: 'text/html',
|
|
9
|
+
svg: 'image/svg+xml',
|
|
10
|
+
png: 'image/png',
|
|
11
|
+
}
|
|
12
12
|
|
|
13
|
-
let cachedDistDir: string | null = null
|
|
13
|
+
let cachedDistDir: string | null = null
|
|
14
14
|
async function getDistDir(): Promise<string> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
if (cachedDistDir) return cachedDistDir
|
|
16
|
+
// import.meta.dir = daemon/src/handlers/, go up two levels to daemon/
|
|
17
|
+
const localDist = join(dirname(import.meta.dir), '..', 'dist')
|
|
18
|
+
if (await Bun.file(join(localDist, 'index.html')).exists()) {
|
|
19
|
+
cachedDistDir = localDist
|
|
20
|
+
} else {
|
|
21
|
+
cachedDistDir = join(DATA_DIR, 'daemon')
|
|
22
|
+
}
|
|
23
|
+
return cachedDistDir
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async function serveSessionPage(): Promise<Response> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const distDir = await getDistDir()
|
|
28
|
+
const file = Bun.file(join(distDir, 'index.html'))
|
|
29
|
+
if (await file.exists()) {
|
|
30
|
+
return new Response(file, { headers: { 'Content-Type': 'text/html' } })
|
|
31
|
+
}
|
|
32
|
+
return new Response('Build not found. Run: bun run daemon/build.ts', { status: 500 })
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function serveStaticAsset(_req: Request, url: URL): Promise<Response> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const path = url.pathname
|
|
37
|
+
const distDir = await getDistDir()
|
|
38
|
+
const assetPath = join(distDir, path.replace('/assets/', ''))
|
|
39
|
+
const file = Bun.file(assetPath)
|
|
40
|
+
if (!(await file.exists())) return new Response('Not found', { status: 404 })
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const ext = path.split('.').pop() || ''
|
|
43
|
+
return new Response(file, {
|
|
44
|
+
// A rebuild keeps the file names, and pages that load the review widget would otherwise keep a stale copy.
|
|
45
|
+
headers: { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', 'Cache-Control': 'no-cache' },
|
|
46
|
+
})
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function createStaticHandlers(): Route[] {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
method: 'GET',
|
|
53
|
+
pattern: new URLPattern({ pathname: '/s/:id' }),
|
|
54
|
+
handler: serveSessionPage,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
method: 'GET',
|
|
58
|
+
pattern: new URLPattern({ pathname: '/assets/*' }),
|
|
59
|
+
handler: serveStaticAsset,
|
|
60
|
+
},
|
|
61
|
+
]
|
|
61
62
|
}
|
|
@@ -1,61 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { UPLOADS_DIR } from
|
|
1
|
+
import type { SessionManager } from '@fabrika/canvas-kernel/server'
|
|
2
|
+
import { jsonResponse, type Route } from '@fabrika/daemon-kit'
|
|
3
|
+
import { writeFileSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { UPLOADS_DIR } from '../paths'
|
|
6
6
|
|
|
7
7
|
const IMAGE_MIME: Record<string, string> = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
png: 'image/png',
|
|
9
|
+
jpg: 'image/jpeg',
|
|
10
|
+
jpeg: 'image/jpeg',
|
|
11
|
+
gif: 'image/gif',
|
|
12
|
+
svg: 'image/svg+xml',
|
|
13
|
+
webp: 'image/webp',
|
|
14
|
+
bmp: 'image/bmp',
|
|
15
|
+
avif: 'image/avif',
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
export function createUploadHandlers(sessionManager: SessionManager): Route[] {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
19
|
+
async function handleUpload(req: Request, _url: URL, match: URLPatternResult): Promise<Response> {
|
|
20
|
+
const sessionId = match.pathname.groups.id!
|
|
21
|
+
if (!sessionManager.get(sessionId)) {
|
|
22
|
+
return jsonResponse({ error: 'Session not found' }, 404)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const formData = await req.formData()
|
|
26
|
+
const file = formData.get('image') as File | null
|
|
27
|
+
if (!file) return jsonResponse({ error: 'No image provided' }, 400)
|
|
28
|
+
if (!file.type.startsWith('image/')) return jsonResponse({ error: 'Not an image' }, 400)
|
|
29
|
+
if (file.size > 10 * 1024 * 1024) return jsonResponse({ error: 'Image too large (max 10MB)' }, 413)
|
|
30
|
+
|
|
31
|
+
const ext = file.name.split('.').pop()?.toLowerCase() || 'png'
|
|
32
|
+
const filename = `${crypto.randomUUID()}.${ext}`
|
|
33
|
+
const filepath = join(UPLOADS_DIR, filename)
|
|
34
|
+
|
|
35
|
+
const buffer = await file.arrayBuffer()
|
|
36
|
+
writeFileSync(filepath, Buffer.from(buffer))
|
|
37
|
+
|
|
38
|
+
return jsonResponse({ path: filepath, filename })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleUploadServe(_req: Request, _url: URL, match: URLPatternResult): Promise<Response> {
|
|
42
|
+
const filename = match.pathname.groups.filename!
|
|
43
|
+
if (filename.includes('/') || filename.includes('..')) {
|
|
44
|
+
return jsonResponse({ error: 'Invalid filename' }, 400)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filepath = join(UPLOADS_DIR, filename)
|
|
48
|
+
const file = Bun.file(filepath)
|
|
49
|
+
if (!(await file.exists())) return jsonResponse({ error: 'Not found' }, 404)
|
|
50
|
+
|
|
51
|
+
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
|
52
|
+
const mime = IMAGE_MIME[ext] || 'application/octet-stream'
|
|
53
|
+
|
|
54
|
+
return new Response(file, {
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': mime,
|
|
57
|
+
'Cache-Control': 'public, max-age=3600',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
{ method: 'POST', pattern: new URLPattern({ pathname: '/api/session/:id/upload' }), handler: handleUpload },
|
|
64
|
+
{ method: 'GET', pattern: new URLPattern({ pathname: '/api/uploads/:filename' }), handler: handleUploadServe },
|
|
65
|
+
]
|
|
61
66
|
}
|
package/daemon/src/paths.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createCanvasPaths } from '@fabrika/canvas-kernel/server'
|
|
2
|
+
import { join } from 'path'
|
|
3
3
|
|
|
4
4
|
/** agent-canvas's on-disk layout. Kept in sync with bin/lib/config.ts. */
|
|
5
|
-
export const paths = createCanvasPaths({ appName:
|
|
5
|
+
export const paths = createCanvasPaths({ appName: 'agent-canvas' })
|
|
6
6
|
|
|
7
|
-
export const DATA_DIR = paths.dataDir
|
|
8
|
-
export const SESSIONS_DIR = paths.sessionsDir
|
|
9
|
-
export const TEMP_DIR = paths.tempDir
|
|
10
|
-
export const PID_FILE = paths.pidFile
|
|
11
|
-
export const COMPILE_TEMP_DIR = paths.compileTempDir
|
|
12
|
-
export const UPLOADS_DIR = paths.uploadsDir
|
|
7
|
+
export const DATA_DIR = paths.dataDir
|
|
8
|
+
export const SESSIONS_DIR = paths.sessionsDir
|
|
9
|
+
export const TEMP_DIR = paths.tempDir
|
|
10
|
+
export const PID_FILE = paths.pidFile
|
|
11
|
+
export const COMPILE_TEMP_DIR = paths.compileTempDir
|
|
12
|
+
export const UPLOADS_DIR = paths.uploadsDir
|
|
13
13
|
|
|
14
14
|
/** Capability used only by the CLI to resolve secret values */
|
|
15
|
-
export const CLI_AUTH_FILE = join(paths.tempDir,
|
|
15
|
+
export const CLI_AUTH_FILE = join(paths.tempDir, 'daemon-auth-token')
|
|
16
|
+
|
|
17
|
+
/** Review-mode threads, one JSON file per project */
|
|
18
|
+
export const REVIEWS_DIR = join(paths.dataDir, 'reviews')
|