acrylblends 0.0.0-stage → 0.3.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/bin/acrylblends.js +4 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +11 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +123 -0
- package/plugins/acryl-inspect/src/service.ts +66 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +64 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +120 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +21 -0
- package/plugins/acryl-node-server/plugin.json +12 -0
- package/plugins/acryl-node-server/src/index.ts +94 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +105 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +21 -0
- package/plugins/acryl-router/plugin.json +12 -0
- package/plugins/acryl-router/src/compose.ts +21 -0
- package/plugins/acryl-router/src/context.ts +59 -0
- package/plugins/acryl-router/src/helpers/index.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-body.ts +59 -0
- package/plugins/acryl-router/src/helpers/sse.ts +83 -0
- package/plugins/acryl-router/src/helpers/validate.ts +44 -0
- package/plugins/acryl-router/src/http-error.ts +21 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/body-limit.ts +25 -0
- package/plugins/acryl-router/src/middleware/cors.ts +46 -0
- package/plugins/acryl-router/src/middleware/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/logger.ts +21 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +82 -0
- package/plugins/acryl-router/src/middleware/request-id.ts +24 -0
- package/plugins/acryl-router/src/middleware/secure-headers.ts +23 -0
- package/plugins/acryl-router/src/pattern.ts +89 -0
- package/plugins/acryl-router/src/registrar.ts +90 -0
- package/plugins/acryl-router/src/service.ts +164 -0
- package/plugins/acryl-router/src/table.ts +156 -0
- package/plugins/acryl-router/src/types.ts +73 -0
- package/plugins/acryl-router/src/util.ts +23 -0
- package/plugins/acryl-router/tests/api.spec.ts +223 -0
- package/plugins/acryl-router/tests/conformance.ts +170 -0
- package/plugins/acryl-router/tests/helpers.spec.ts +172 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +71 -0
- package/plugins/acryl-router/tests/middleware.spec.ts +183 -0
- package/plugins/acryl-router/tests/service.spec.ts +211 -0
- package/plugins/acryl-router/tests/table.spec.ts +47 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +11 -0
- package/plugins/acryl-router-http/src/bridge.ts +52 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +170 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +99 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +101 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +20 -0
- package/plugins/agent/agent.ts +152 -0
- package/plugins/agent/index.ts +20 -0
- package/plugins/agent/memory.ts +24 -0
- package/plugins/agent/model.ts +79 -0
- package/plugins/agent/plugin.json +10 -0
- package/plugins/agent/tools.ts +50 -0
- package/plugins/builder/index.ts +193 -0
- package/plugins/builder/plugin.json +11 -0
- package/plugins/builder/script.ts +161 -0
- package/plugins/extensions/index.ts +755 -0
- package/plugins/extensions/ledger.ts +39 -0
- package/plugins/extensions/plugin.json +12 -0
- package/plugins/extensions/resolve.ts +30 -0
- package/plugins/extensions/stage.ts +44 -0
- package/plugins/instance/index.ts +167 -0
- package/plugins/instance/plugin.json +10 -0
- package/plugins/manifest/index.ts +635 -0
- package/plugins/manifest/lock.ts +94 -0
- package/plugins/manifest/plugin.json +13 -0
- package/plugins/manifest/upgrade-cli.ts +70 -0
- package/plugins/manifest/upgrade.ts +97 -0
- package/plugins/openai/index.ts +159 -0
- package/plugins/openai/plugin.json +11 -0
- package/plugins/openai/prompt.ts +35 -0
- package/plugins/sandbox/index.ts +150 -0
- package/plugins/sandbox/plugin.json +8 -0
- package/plugins/sandbox/runner.mjs +18 -0
- package/plugins/save/cli.ts +19 -0
- package/plugins/save/index.ts +55 -0
- package/plugins/save/plugin.json +12 -0
- package/plugins/settings/index.ts +270 -0
- package/plugins/settings/merge.ts +79 -0
- package/plugins/settings/plugin.json +13 -0
- package/plugins/tasks/index.ts +246 -0
- package/plugins/tasks/plugin.json +11 -0
- package/plugins/tasks/store.ts +86 -0
- package/plugins/tsconfig.json +27 -0
- package/templates/agent/Dockerfile +15 -0
- package/templates/agent/README.md +52 -0
- package/templates/agent/blend.yaml +56 -0
- package/templates/agent/dockerignore +3 -0
- package/templates/agent/gitignore +2 -0
- package/templates/agent/package.json +23 -0
- package/templates/agent/src/app.ts +98 -0
- package/templates/agent/src/main.ts +25 -0
- package/templates/agent/src/routes.ts +53 -0
- package/templates/agent/template.json +16 -0
- package/templates/agent/test/agent.test.ts +45 -0
- package/templates/agent/test/app.test.ts +46 -0
- package/templates/agent/test/builder.test.ts +194 -0
- package/templates/agent/test/model.test.ts +88 -0
- package/templates/agent/tsconfig.json +19 -0
- package/templates/blank/README.md +26 -0
- package/templates/blank/blend.yaml +23 -0
- package/templates/blank/gitignore +2 -0
- package/templates/blank/package.json +23 -0
- package/templates/blank/src/app.ts +61 -0
- package/templates/blank/src/main.ts +20 -0
- package/templates/blank/src/routes.ts +14 -0
- package/templates/blank/template.json +1 -0
- package/templates/blank/test/app.test.ts +56 -0
- package/templates/blank/tsconfig.json +19 -0
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +96 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +19 -0
- package/templates/todo/upstream.json +12 -0
- package/README.md +0 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
|
|
3
|
+
/** One tool: what it does (for the model) and how to run it. */
|
|
4
|
+
export interface Tool {
|
|
5
|
+
readonly description: string
|
|
6
|
+
run(input: unknown): unknown | Promise<unknown>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The tool registry. A plugin registers its tools inside `ctx.effect`, so they leave with it. */
|
|
10
|
+
export interface Tools {
|
|
11
|
+
/** Register a tool. Returns the function that removes it. */
|
|
12
|
+
register(name: string, tool: Tool): () => void
|
|
13
|
+
list(): Array<{ name: string; description: string }>
|
|
14
|
+
call(name: string, input: unknown): Promise<unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module '@deepseek-ai/cordis' {
|
|
18
|
+
interface Context {
|
|
19
|
+
tools: Tools
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Provides the registry, with one example tool (`add`). */
|
|
24
|
+
export function tools(ctx: Context) {
|
|
25
|
+
const table = new Map<string, Tool>()
|
|
26
|
+
const registry: Tools = {
|
|
27
|
+
register(name, tool) {
|
|
28
|
+
if (table.has(name)) throw new Error(`tool "${name}" is already registered`)
|
|
29
|
+
table.set(name, tool)
|
|
30
|
+
return () => void table.delete(name)
|
|
31
|
+
},
|
|
32
|
+
list: () =>
|
|
33
|
+
[...table.entries()].map(([name, tool]) => ({ name, description: tool.description })),
|
|
34
|
+
async call(name, input) {
|
|
35
|
+
const tool = table.get(name)
|
|
36
|
+
if (!tool) throw new Error(`unknown tool "${name}"`)
|
|
37
|
+
return await tool.run(input)
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
ctx.provide('tools', registry)
|
|
41
|
+
ctx.effect(() =>
|
|
42
|
+
registry.register('add', {
|
|
43
|
+
description: 'Add two numbers: input [a, b].',
|
|
44
|
+
run: (input) => {
|
|
45
|
+
const [a = 0, b = 0] = Array.isArray(input) ? (input as number[]) : []
|
|
46
|
+
return a + b
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builder: the agent's tools for extending the app it runs in.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ACRYL's agent tools (`acryl_install_plugin` and its extension docs): the agent writes a plugin into the app's own
|
|
5
|
+
* extensions folder, installs it, and gets back the real result (`active`, `pending` with what it waits for, or the error, with the
|
|
6
|
+
* previous version restored). Writes are confined to that folder. The tools register in `ctx.effect`, so they leave with the builder.
|
|
7
|
+
*/
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import { scripted, type Tools } from '../agent/index.ts'
|
|
12
|
+
import { NAME } from '../extensions/index.ts'
|
|
13
|
+
import type {} from '../instance/index.ts'
|
|
14
|
+
import { builderRules } from './script.ts'
|
|
15
|
+
|
|
16
|
+
export interface BuilderConfig {
|
|
17
|
+
/** Where the agent writes extensions. Defaults to `<project>/extensions`. Must be one of the extensions sources. */
|
|
18
|
+
dir?: string
|
|
19
|
+
/** Where `package_plugin` writes standalone packages. Defaults to `<project>/plugin-packages`. */
|
|
20
|
+
packagesDir?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The package.json of a standalone plugin package: the host framework is a peer, never a second installed copy. */
|
|
24
|
+
export function packageManifest(name: string, description: string): Record<string, unknown> {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
version: '0.1.0',
|
|
28
|
+
description,
|
|
29
|
+
type: 'module',
|
|
30
|
+
exports: { '.': './index.ts', './package.json': './package.json' },
|
|
31
|
+
files: ['*.ts', '**/*.ts', 'README.md'],
|
|
32
|
+
// ACRYL measured it: a host package listed under dependencies brings a second copy of the framework, and its services stop
|
|
33
|
+
// matching after a restart. A peer names the version and installs nothing.
|
|
34
|
+
peerDependencies: { '@deepseek-ai/cordis': '4.0.4' },
|
|
35
|
+
keywords: ['cordis', 'cordis-plugin', 'acrylblends'],
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const EXTENSIONS = new Set(['.ts', '.js', '.mjs', '.json', '.md'])
|
|
40
|
+
const MAX_BYTES = 256 * 1024
|
|
41
|
+
|
|
42
|
+
export class BuilderError extends Error {
|
|
43
|
+
override name = 'BuilderError'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pluginName(input: unknown): string {
|
|
47
|
+
const name = (input as { name?: unknown } | undefined)?.name
|
|
48
|
+
if (typeof name !== 'string' || !NAME.test(name))
|
|
49
|
+
throw new BuilderError('name must be lowercase letters, digits and dashes')
|
|
50
|
+
return name
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A relative file path inside a plugin folder, or an error. */
|
|
54
|
+
function safeFile(path: string): string {
|
|
55
|
+
const clean = normalize(path)
|
|
56
|
+
if (isAbsolute(path) || clean.startsWith('..') || clean.split(sep).includes('..'))
|
|
57
|
+
throw new BuilderError(`"${path}" must be a path inside the plugin folder`)
|
|
58
|
+
if (!EXTENSIONS.has(extname(clean)))
|
|
59
|
+
throw new BuilderError(`"${path}": only ${[...EXTENSIONS].join(', ')} files may be written`)
|
|
60
|
+
return clean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const builder = {
|
|
64
|
+
name: 'builder',
|
|
65
|
+
inject: ['extensions', 'tools', 'appInstance'],
|
|
66
|
+
apply(ctx: Context, config: BuilderConfig) {
|
|
67
|
+
const dir = resolve(config?.dir ?? join(ctx.appInstance.root, 'extensions'))
|
|
68
|
+
if (!ctx.extensions.sources.some((source) => resolve(source.dir) === dir)) {
|
|
69
|
+
throw new BuilderError(
|
|
70
|
+
`the builder writes into ${dir}, which is not an extensions source; add it to the extensions plugin's sources`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const folder = (name: string) => join(dir, name)
|
|
74
|
+
const tools: Tools = ctx.tools
|
|
75
|
+
const register = (name: string, description: string, run: (input: unknown) => unknown) =>
|
|
76
|
+
ctx.effect(() => tools.register(name, { description, run }))
|
|
77
|
+
|
|
78
|
+
register(
|
|
79
|
+
'list_plugins',
|
|
80
|
+
'List installed extensions with their state, and folders not installed yet.',
|
|
81
|
+
() => ({
|
|
82
|
+
installed: ctx.extensions.list(),
|
|
83
|
+
notInstalled: ctx.extensions.pending(),
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
register(
|
|
88
|
+
'read_plugin',
|
|
89
|
+
'Read a file of an extension: input { name, file? } (default index.ts).',
|
|
90
|
+
(input) => {
|
|
91
|
+
const name = pluginName(input)
|
|
92
|
+
const file = safeFile((input as { file?: string }).file ?? 'index.ts')
|
|
93
|
+
const path = join(folder(name), file)
|
|
94
|
+
if (!existsSync(path)) throw new BuilderError(`${name}/${file} does not exist`)
|
|
95
|
+
return { name, file, text: readFileSync(path, 'utf8') }
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
register(
|
|
100
|
+
'write_plugin',
|
|
101
|
+
'Write files of an extension: input { name, files: { "index.ts": "..." } }. The folder is created; other files are kept.',
|
|
102
|
+
(input) => {
|
|
103
|
+
const name = pluginName(input)
|
|
104
|
+
const files = (input as { files?: unknown }).files
|
|
105
|
+
if (!files || typeof files !== 'object')
|
|
106
|
+
throw new BuilderError('files must be an object of path to text')
|
|
107
|
+
const entries = Object.entries(files as Record<string, unknown>)
|
|
108
|
+
const bytes = entries.reduce(
|
|
109
|
+
(total, [, text]) => total + Buffer.byteLength(String(text)),
|
|
110
|
+
0,
|
|
111
|
+
)
|
|
112
|
+
if (entries.length === 0) throw new BuilderError('files is empty')
|
|
113
|
+
if (bytes > MAX_BYTES)
|
|
114
|
+
throw new BuilderError(`files are ${bytes} bytes; the limit is ${MAX_BYTES}`)
|
|
115
|
+
const written: string[] = []
|
|
116
|
+
for (const [path, text] of entries) {
|
|
117
|
+
if (typeof text !== 'string') throw new BuilderError(`${path}: the content must be text`)
|
|
118
|
+
const file = safeFile(path)
|
|
119
|
+
const target = join(folder(name), file)
|
|
120
|
+
if (relative(folder(name), target).startsWith('..'))
|
|
121
|
+
throw new BuilderError(`"${path}" leaves the plugin folder`)
|
|
122
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
123
|
+
writeFileSync(target, text)
|
|
124
|
+
written.push(file)
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, name, dir: folder(name), written }
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
register(
|
|
131
|
+
'install_plugin',
|
|
132
|
+
'Install or update an extension from its folder: input { name }. Returns its real state.',
|
|
133
|
+
(input) => ctx.extensions.install(folder(pluginName(input))),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
register(
|
|
137
|
+
'remove_plugin',
|
|
138
|
+
'Remove an installed extension: input { name, deleteSource? }. deleteSource also deletes its folder.',
|
|
139
|
+
async (input) => {
|
|
140
|
+
const name = pluginName(input)
|
|
141
|
+
const result = await ctx.extensions.remove(name)
|
|
142
|
+
if (
|
|
143
|
+
(input as { deleteSource?: unknown }).deleteSource === true &&
|
|
144
|
+
existsSync(folder(name))
|
|
145
|
+
) {
|
|
146
|
+
rmSync(folder(name), { recursive: true, force: true })
|
|
147
|
+
}
|
|
148
|
+
return result
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const packagesDir = resolve(
|
|
153
|
+
config?.packagesDir ?? join(ctx.appInstance.root, 'plugin-packages'),
|
|
154
|
+
)
|
|
155
|
+
register(
|
|
156
|
+
'package_plugin',
|
|
157
|
+
'Turn an extension into a standalone plugin package other apps can use: input { name, description? }. Returns its folder.',
|
|
158
|
+
(input) => {
|
|
159
|
+
const name = pluginName(input)
|
|
160
|
+
if (
|
|
161
|
+
!existsSync(join(folder(name), 'index.ts')) &&
|
|
162
|
+
!existsSync(join(folder(name), 'index.js'))
|
|
163
|
+
) {
|
|
164
|
+
throw new BuilderError(`${name} has no index.ts in ${folder(name)}; write it first`)
|
|
165
|
+
}
|
|
166
|
+
const description = String(
|
|
167
|
+
(input as { description?: unknown }).description ?? `The ${name} Cordis plugin.`,
|
|
168
|
+
)
|
|
169
|
+
const target = join(packagesDir, name)
|
|
170
|
+
rmSync(target, { recursive: true, force: true })
|
|
171
|
+
mkdirSync(packagesDir, { recursive: true })
|
|
172
|
+
cpSync(folder(name), target, {
|
|
173
|
+
recursive: true,
|
|
174
|
+
filter: (path) => !path.includes('node_modules'),
|
|
175
|
+
})
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(target, 'package.json'),
|
|
178
|
+
`${JSON.stringify(packageManifest(name, description), null, 2)}\n`,
|
|
179
|
+
)
|
|
180
|
+
if (!existsSync(join(target, 'README.md'))) {
|
|
181
|
+
writeFileSync(
|
|
182
|
+
join(target, 'README.md'),
|
|
183
|
+
`# ${name}\n\n${description}\n\nA Cordis plugin. In an ACRYLBLENDS app: \`acrylblends add <this folder>\`.\n`,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, name, dir: target }
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const model = scripted(ctx.get('model'))
|
|
191
|
+
if (model) ctx.effect(() => model.teach(builderRules))
|
|
192
|
+
},
|
|
193
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "builder",
|
|
3
|
+
"description": "The agent's tools for building the app it runs in: write, install, read, list and remove extensions, with real results.",
|
|
4
|
+
"requires": ["agent", "extensions", "sandbox"],
|
|
5
|
+
"inject": ["extensions", "tools", "appInstance"],
|
|
6
|
+
"provides": [],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
+
},
|
|
10
|
+
"mount": "mount it after agent and extensions: root.plugin(builder, { dir: join(root, 'extensions') }); dir must be an extensions source"
|
|
11
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builder's commands for the offline scripted model, so the whole build-install-update-remove loop runs without a network.
|
|
3
|
+
* A real model reads the tool descriptions instead and decides for itself.
|
|
4
|
+
*
|
|
5
|
+
* create route <name> <path> <text...> write a plugin that answers GET <path> with { text }, then install it
|
|
6
|
+
* update route <name> <path> <text...> the same, over the existing plugin
|
|
7
|
+
* break plugin <name> write a version whose apply throws, then try to install it
|
|
8
|
+
* remove plugin <name> remove it and delete its folder
|
|
9
|
+
* create tool <name> <factor> write a sandboxed tool that multiplies input.n by factor (its example is its check), install it
|
|
10
|
+
* run tool <name> <n> call an installed tool
|
|
11
|
+
* spy tool <name> write a tool that tries to read /etc/hosts, to show the sandbox refusing it
|
|
12
|
+
* list plugins list installed extensions
|
|
13
|
+
* package plugin <name> make it a standalone package other apps can add
|
|
14
|
+
*/
|
|
15
|
+
import type { Rule, Step } from '../agent/index.ts'
|
|
16
|
+
|
|
17
|
+
const PATH = /^\/[a-z0-9/-]*$/
|
|
18
|
+
|
|
19
|
+
export function routeSource(name: string, path: string, text: string): string {
|
|
20
|
+
return `import type { Context } from '@deepseek-ai/cordis'
|
|
21
|
+
|
|
22
|
+
/** Written by the builder: answers GET ${path}, and checks that it does before the install counts. */
|
|
23
|
+
export const name = '${name}'
|
|
24
|
+
export const inject = ['router']
|
|
25
|
+
|
|
26
|
+
interface Router {
|
|
27
|
+
get(path: string, handler: (c: { json(body: unknown, status?: number): Response }) => Response): unknown
|
|
28
|
+
request(path: string): Promise<Response>
|
|
29
|
+
}
|
|
30
|
+
export function apply(ctx: Context) {
|
|
31
|
+
// A route registered through this plugin's context is removed when the plugin is unloaded.
|
|
32
|
+
(ctx as unknown as { router: Router }).router.get(${JSON.stringify(path)}, (c) => c.json({ text: ${JSON.stringify(text)} }))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function check(ctx: Context) {
|
|
36
|
+
const reply = await (ctx.get('router') as Router).request(${JSON.stringify(path)})
|
|
37
|
+
if (!reply.ok) throw new Error(\`GET ${path} answered \${reply.status}\`)
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function brokenSource(name: string): string {
|
|
43
|
+
return `export const name = '${name}'
|
|
44
|
+
|
|
45
|
+
export function apply() {
|
|
46
|
+
throw new Error('${name} refuses to start')
|
|
47
|
+
}
|
|
48
|
+
`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summary(result: unknown): string {
|
|
52
|
+
const waiting = (result as { stage?: string; name?: string } | undefined)?.stage === 'approval'
|
|
53
|
+
if (waiting) return `${(result as { name: string }).name}: waiting for the owner's approval.`
|
|
54
|
+
const answer = result as {
|
|
55
|
+
ok?: boolean
|
|
56
|
+
name?: string
|
|
57
|
+
action?: string
|
|
58
|
+
state?: string
|
|
59
|
+
errors?: string[]
|
|
60
|
+
restoredPrevious?: boolean
|
|
61
|
+
}
|
|
62
|
+
if (answer?.ok === true && answer.action)
|
|
63
|
+
return `${answer.name}: ${answer.action}, ${answer.state}.`
|
|
64
|
+
if (answer?.ok === true) return `${answer.name}: done.`
|
|
65
|
+
const restored =
|
|
66
|
+
answer?.restoredPrevious === undefined
|
|
67
|
+
? ''
|
|
68
|
+
: ` Previous version restored: ${String(answer.restoredPrevious)}.`
|
|
69
|
+
return `Refused: ${(answer?.errors ?? ['unknown error']).join('; ')}.${restored}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Write, then install, then report. */
|
|
73
|
+
function writeThenInstall(
|
|
74
|
+
step: number,
|
|
75
|
+
toolResult: unknown,
|
|
76
|
+
name: string,
|
|
77
|
+
files: Record<string, string>,
|
|
78
|
+
): Step {
|
|
79
|
+
if (step === 0) return { tool: 'write_plugin', input: { name, files } }
|
|
80
|
+
if (step === 1) {
|
|
81
|
+
const written = toolResult as { ok?: boolean; error?: string }
|
|
82
|
+
if (written?.ok !== true)
|
|
83
|
+
return { text: `Could not write ${name}: ${written?.error ?? 'unknown error'}` }
|
|
84
|
+
return { tool: 'install_plugin', input: { name } }
|
|
85
|
+
}
|
|
86
|
+
return { text: summary(toolResult) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toolFiles(factor: number): Record<string, string> {
|
|
90
|
+
return {
|
|
91
|
+
'index.ts': `/** Written by the builder: multiplies input.n by ${factor}. Runs only in the sandbox. */\nexport function run(input: { n: number }) {\n return { n: input.n * ${factor} }\n}\n`,
|
|
92
|
+
'tool.json': JSON.stringify({
|
|
93
|
+
description: `Multiply a number by ${factor}: input { n }.`,
|
|
94
|
+
examples: [{ input: { n: 2 }, output: { n: 2 * factor } }],
|
|
95
|
+
}),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function spyFiles(): Record<string, string> {
|
|
100
|
+
return {
|
|
101
|
+
'index.ts':
|
|
102
|
+
"import { readFileSync } from 'node:fs'\nexport function run() {\n return readFileSync('/etc/hosts', 'utf8').length\n}\n",
|
|
103
|
+
'tool.json': JSON.stringify({
|
|
104
|
+
description: 'Reads a file it should not.',
|
|
105
|
+
examples: [{ input: {}, output: 1 }],
|
|
106
|
+
}),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const builderRules: Rule = ({ question, step, toolResult }) => {
|
|
111
|
+
const words = question.trim().split(/\s+/)
|
|
112
|
+
const [verb, noun, name, path, ...text] = words
|
|
113
|
+
if (
|
|
114
|
+
(verb === 'create' || verb === 'update') &&
|
|
115
|
+
noun === 'route' &&
|
|
116
|
+
name &&
|
|
117
|
+
path &&
|
|
118
|
+
PATH.test(path)
|
|
119
|
+
) {
|
|
120
|
+
return writeThenInstall(step, toolResult, name, {
|
|
121
|
+
'index.ts': routeSource(name, path, text.join(' ') || name),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
if (verb === 'create' && noun === 'tool' && name && path && Number.isFinite(Number(path))) {
|
|
125
|
+
return writeThenInstall(step, toolResult, name, toolFiles(Number(path)))
|
|
126
|
+
}
|
|
127
|
+
if (verb === 'spy' && noun === 'tool' && name)
|
|
128
|
+
return writeThenInstall(step, toolResult, name, spyFiles())
|
|
129
|
+
if (verb === 'run' && noun === 'tool' && name && path && Number.isFinite(Number(path))) {
|
|
130
|
+
return step === 0
|
|
131
|
+
? { tool: name, input: { n: Number(path) } }
|
|
132
|
+
: { text: JSON.stringify(toolResult) }
|
|
133
|
+
}
|
|
134
|
+
if (verb === 'break' && noun === 'plugin' && name)
|
|
135
|
+
return writeThenInstall(step, toolResult, name, { 'index.ts': brokenSource(name) })
|
|
136
|
+
if (verb === 'remove' && noun === 'plugin' && name) {
|
|
137
|
+
return step === 0
|
|
138
|
+
? { tool: 'remove_plugin', input: { name, deleteSource: true } }
|
|
139
|
+
: { text: summary(toolResult) }
|
|
140
|
+
}
|
|
141
|
+
if (verb === 'package' && noun === 'plugin' && name) {
|
|
142
|
+
if (step === 0) return { tool: 'package_plugin', input: { name } }
|
|
143
|
+
const packaged = toolResult as { ok?: boolean; dir?: string; error?: string }
|
|
144
|
+
return {
|
|
145
|
+
text: packaged?.ok
|
|
146
|
+
? `${name}: packaged in ${packaged.dir}.`
|
|
147
|
+
: `Could not package ${name}: ${packaged?.error ?? 'unknown error'}`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (verb === 'list' && noun === 'plugins') {
|
|
151
|
+
if (step === 0) return { tool: 'list_plugins', input: {} }
|
|
152
|
+
const installed =
|
|
153
|
+
(toolResult as { installed?: Array<{ name: string; state: string }> })?.installed ?? []
|
|
154
|
+
return {
|
|
155
|
+
text: installed.length
|
|
156
|
+
? installed.map((entry) => `${entry.name} (${entry.state})`).join(', ')
|
|
157
|
+
: 'No plugins installed.',
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}
|