acrylblends 0.0.0-stage → 0.2.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 +117 -0
- package/dist/index.js +434 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
- 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 +162 -0
- package/plugins/events/index.ts +42 -0
- package/plugins/events/plugin.json +9 -0
- package/plugins/extensions/index.ts +738 -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/http/index.ts +64 -0
- package/plugins/http/plugin.json +10 -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 +31 -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/server/index.ts +138 -0
- package/plugins/server/plugin.json +10 -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 +232 -0
- package/plugins/tasks/plugin.json +11 -0
- package/plugins/tasks/store.ts +86 -0
- package/plugins/tsconfig.json +19 -0
- package/templates/agent/Dockerfile +15 -0
- package/templates/agent/README.md +52 -0
- package/templates/agent/blend.yaml +58 -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 +62 -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 +186 -0
- package/templates/agent/test/model.test.ts +89 -0
- package/templates/agent/tsconfig.json +19 -0
- package/templates/app/README.md +26 -0
- package/templates/app/blend.yaml +23 -0
- package/templates/app/gitignore +2 -0
- package/templates/app/package.json +23 -0
- package/templates/app/src/app.ts +61 -0
- package/templates/app/src/main.ts +20 -0
- package/templates/app/src/routes.ts +14 -0
- package/templates/app/template.json +1 -0
- package/templates/app/test/app.test.ts +56 -0
- package/templates/app/tsconfig.json +19 -0
- package/README.md +0 -3
|
@@ -0,0 +1,162 @@
|
|
|
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 = ['http']
|
|
25
|
+
|
|
26
|
+
interface Http {
|
|
27
|
+
route(method: string, path: string, handler: () => { body: unknown }): () => void
|
|
28
|
+
handle(request: { method: string; path: string; body: unknown }): Promise<{ status: number; body: unknown }>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function apply(ctx: Context) {
|
|
32
|
+
const http = ctx.get('http') as Http
|
|
33
|
+
ctx.effect(() => http.route('GET', ${JSON.stringify(path)}, () => ({ body: { text: ${JSON.stringify(text)} } })))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function check(ctx: Context) {
|
|
37
|
+
const reply = await (ctx.get('http') as Http).handle({ method: 'GET', path: ${JSON.stringify(path)}, body: undefined })
|
|
38
|
+
if (reply.status !== 200) throw new Error(\`GET ${path} answered \${reply.status}\`)
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function brokenSource(name: string): string {
|
|
44
|
+
return `export const name = '${name}'
|
|
45
|
+
|
|
46
|
+
export function apply() {
|
|
47
|
+
throw new Error('${name} refuses to start')
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summary(result: unknown): string {
|
|
53
|
+
const waiting = (result as { stage?: string; name?: string } | undefined)?.stage === 'approval'
|
|
54
|
+
if (waiting) return `${(result as { name: string }).name}: waiting for the owner's approval.`
|
|
55
|
+
const answer = result as {
|
|
56
|
+
ok?: boolean
|
|
57
|
+
name?: string
|
|
58
|
+
action?: string
|
|
59
|
+
state?: string
|
|
60
|
+
errors?: string[]
|
|
61
|
+
restoredPrevious?: boolean
|
|
62
|
+
}
|
|
63
|
+
if (answer?.ok === true && answer.action)
|
|
64
|
+
return `${answer.name}: ${answer.action}, ${answer.state}.`
|
|
65
|
+
if (answer?.ok === true) return `${answer.name}: done.`
|
|
66
|
+
const restored =
|
|
67
|
+
answer?.restoredPrevious === undefined
|
|
68
|
+
? ''
|
|
69
|
+
: ` Previous version restored: ${String(answer.restoredPrevious)}.`
|
|
70
|
+
return `Refused: ${(answer?.errors ?? ['unknown error']).join('; ')}.${restored}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Write, then install, then report. */
|
|
74
|
+
function writeThenInstall(
|
|
75
|
+
step: number,
|
|
76
|
+
toolResult: unknown,
|
|
77
|
+
name: string,
|
|
78
|
+
files: Record<string, string>,
|
|
79
|
+
): Step {
|
|
80
|
+
if (step === 0) return { tool: 'write_plugin', input: { name, files } }
|
|
81
|
+
if (step === 1) {
|
|
82
|
+
const written = toolResult as { ok?: boolean; error?: string }
|
|
83
|
+
if (written?.ok !== true)
|
|
84
|
+
return { text: `Could not write ${name}: ${written?.error ?? 'unknown error'}` }
|
|
85
|
+
return { tool: 'install_plugin', input: { name } }
|
|
86
|
+
}
|
|
87
|
+
return { text: summary(toolResult) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toolFiles(factor: number): Record<string, string> {
|
|
91
|
+
return {
|
|
92
|
+
'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`,
|
|
93
|
+
'tool.json': JSON.stringify({
|
|
94
|
+
description: `Multiply a number by ${factor}: input { n }.`,
|
|
95
|
+
examples: [{ input: { n: 2 }, output: { n: 2 * factor } }],
|
|
96
|
+
}),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function spyFiles(): Record<string, string> {
|
|
101
|
+
return {
|
|
102
|
+
'index.ts':
|
|
103
|
+
"import { readFileSync } from 'node:fs'\nexport function run() {\n return readFileSync('/etc/hosts', 'utf8').length\n}\n",
|
|
104
|
+
'tool.json': JSON.stringify({
|
|
105
|
+
description: 'Reads a file it should not.',
|
|
106
|
+
examples: [{ input: {}, output: 1 }],
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const builderRules: Rule = ({ question, step, toolResult }) => {
|
|
112
|
+
const words = question.trim().split(/\s+/)
|
|
113
|
+
const [verb, noun, name, path, ...text] = words
|
|
114
|
+
if (
|
|
115
|
+
(verb === 'create' || verb === 'update') &&
|
|
116
|
+
noun === 'route' &&
|
|
117
|
+
name &&
|
|
118
|
+
path &&
|
|
119
|
+
PATH.test(path)
|
|
120
|
+
) {
|
|
121
|
+
return writeThenInstall(step, toolResult, name, {
|
|
122
|
+
'index.ts': routeSource(name, path, text.join(' ') || name),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
if (verb === 'create' && noun === 'tool' && name && path && Number.isFinite(Number(path))) {
|
|
126
|
+
return writeThenInstall(step, toolResult, name, toolFiles(Number(path)))
|
|
127
|
+
}
|
|
128
|
+
if (verb === 'spy' && noun === 'tool' && name)
|
|
129
|
+
return writeThenInstall(step, toolResult, name, spyFiles())
|
|
130
|
+
if (verb === 'run' && noun === 'tool' && name && path && Number.isFinite(Number(path))) {
|
|
131
|
+
return step === 0
|
|
132
|
+
? { tool: name, input: { n: Number(path) } }
|
|
133
|
+
: { text: JSON.stringify(toolResult) }
|
|
134
|
+
}
|
|
135
|
+
if (verb === 'break' && noun === 'plugin' && name)
|
|
136
|
+
return writeThenInstall(step, toolResult, name, { 'index.ts': brokenSource(name) })
|
|
137
|
+
if (verb === 'remove' && noun === 'plugin' && name) {
|
|
138
|
+
return step === 0
|
|
139
|
+
? { tool: 'remove_plugin', input: { name, deleteSource: true } }
|
|
140
|
+
: { text: summary(toolResult) }
|
|
141
|
+
}
|
|
142
|
+
if (verb === 'package' && noun === 'plugin' && name) {
|
|
143
|
+
if (step === 0) return { tool: 'package_plugin', input: { name } }
|
|
144
|
+
const packaged = toolResult as { ok?: boolean; dir?: string; error?: string }
|
|
145
|
+
return {
|
|
146
|
+
text: packaged?.ok
|
|
147
|
+
? `${name}: packaged in ${packaged.dir}.`
|
|
148
|
+
: `Could not package ${name}: ${packaged?.error ?? 'unknown error'}`,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (verb === 'list' && noun === 'plugins') {
|
|
152
|
+
if (step === 0) return { tool: 'list_plugins', input: {} }
|
|
153
|
+
const installed =
|
|
154
|
+
(toolResult as { installed?: Array<{ name: string; state: string }> })?.installed ?? []
|
|
155
|
+
return {
|
|
156
|
+
text: installed.length
|
|
157
|
+
? installed.map((entry) => `${entry.name} (${entry.state})`).join(', ')
|
|
158
|
+
: 'No plugins installed.',
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events as a stream (`GET /events`, server-sent events). A client connects once and receives every change as it happens: the
|
|
3
|
+
* event name is the Cordis event (`tasks/changed`, `extensions/changed`, any you list), the data is `{ "args": [...] }` with the
|
|
4
|
+
* event's arguments as JSON. Desktop, web and terminal surfaces can all render the same stream (the Pi Durable "one place to
|
|
5
|
+
* observe" idea); nothing here knows about tasks or extensions. Each connection's listeners are removed when the client leaves.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
8
|
+
import type {} from '../http/index.ts'
|
|
9
|
+
|
|
10
|
+
export interface EventsConfig {
|
|
11
|
+
/** Cordis events to stream. */
|
|
12
|
+
topics?: readonly string[]
|
|
13
|
+
/** Where the stream is served. Default `/events`. */
|
|
14
|
+
path?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const events = {
|
|
18
|
+
name: 'events',
|
|
19
|
+
inject: ['http'],
|
|
20
|
+
apply(ctx: Context, config?: EventsConfig) {
|
|
21
|
+
const topics = config?.topics ?? []
|
|
22
|
+
const path = config?.path ?? '/events'
|
|
23
|
+
const listen = ctx.on as unknown as (
|
|
24
|
+
topic: string,
|
|
25
|
+
listener: (...args: unknown[]) => void,
|
|
26
|
+
) => () => void
|
|
27
|
+
ctx.effect(() =>
|
|
28
|
+
ctx.http.route('GET', path, () => ({
|
|
29
|
+
body: null,
|
|
30
|
+
stream: (send) => {
|
|
31
|
+
const leaves = topics.map((topic) =>
|
|
32
|
+
listen.call(ctx, topic, (...args: unknown[]) => send(topic, { args })),
|
|
33
|
+
)
|
|
34
|
+
send('connected', { topics })
|
|
35
|
+
return () => {
|
|
36
|
+
for (const leave of leaves) leave()
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
})),
|
|
40
|
+
)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "events",
|
|
3
|
+
"description": "A live stream of Cordis events over server-sent events, so a surface renders state instead of polling.",
|
|
4
|
+
"requires": ["http"],
|
|
5
|
+
"inject": ["http"],
|
|
6
|
+
"provides": [],
|
|
7
|
+
"dependencies": { "@deepseek-ai/cordis": "4.0.4" },
|
|
8
|
+
"mount": "add a row to blend.yaml: events, with config { topics: ['tasks/changed', 'extensions/changed'] }"
|
|
9
|
+
}
|