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.
Files changed (74) hide show
  1. package/bin/acrylblends.js +4 -0
  2. package/dist/index.d.ts +117 -0
  3. package/dist/index.js +434 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +50 -4
  6. package/plugins/agent/agent.ts +152 -0
  7. package/plugins/agent/index.ts +20 -0
  8. package/plugins/agent/memory.ts +24 -0
  9. package/plugins/agent/model.ts +79 -0
  10. package/plugins/agent/plugin.json +10 -0
  11. package/plugins/agent/tools.ts +50 -0
  12. package/plugins/builder/index.ts +193 -0
  13. package/plugins/builder/plugin.json +11 -0
  14. package/plugins/builder/script.ts +162 -0
  15. package/plugins/events/index.ts +42 -0
  16. package/plugins/events/plugin.json +9 -0
  17. package/plugins/extensions/index.ts +738 -0
  18. package/plugins/extensions/ledger.ts +39 -0
  19. package/plugins/extensions/plugin.json +12 -0
  20. package/plugins/extensions/resolve.ts +30 -0
  21. package/plugins/extensions/stage.ts +44 -0
  22. package/plugins/http/index.ts +64 -0
  23. package/plugins/http/plugin.json +10 -0
  24. package/plugins/instance/index.ts +167 -0
  25. package/plugins/instance/plugin.json +10 -0
  26. package/plugins/manifest/index.ts +635 -0
  27. package/plugins/manifest/lock.ts +94 -0
  28. package/plugins/manifest/plugin.json +13 -0
  29. package/plugins/manifest/upgrade-cli.ts +70 -0
  30. package/plugins/manifest/upgrade.ts +97 -0
  31. package/plugins/openai/index.ts +159 -0
  32. package/plugins/openai/plugin.json +11 -0
  33. package/plugins/openai/prompt.ts +31 -0
  34. package/plugins/sandbox/index.ts +150 -0
  35. package/plugins/sandbox/plugin.json +8 -0
  36. package/plugins/sandbox/runner.mjs +18 -0
  37. package/plugins/save/cli.ts +19 -0
  38. package/plugins/save/index.ts +55 -0
  39. package/plugins/save/plugin.json +12 -0
  40. package/plugins/server/index.ts +138 -0
  41. package/plugins/server/plugin.json +10 -0
  42. package/plugins/settings/index.ts +270 -0
  43. package/plugins/settings/merge.ts +79 -0
  44. package/plugins/settings/plugin.json +13 -0
  45. package/plugins/tasks/index.ts +232 -0
  46. package/plugins/tasks/plugin.json +11 -0
  47. package/plugins/tasks/store.ts +86 -0
  48. package/plugins/tsconfig.json +19 -0
  49. package/templates/agent/Dockerfile +15 -0
  50. package/templates/agent/README.md +52 -0
  51. package/templates/agent/blend.yaml +58 -0
  52. package/templates/agent/dockerignore +3 -0
  53. package/templates/agent/gitignore +2 -0
  54. package/templates/agent/package.json +23 -0
  55. package/templates/agent/src/app.ts +98 -0
  56. package/templates/agent/src/main.ts +25 -0
  57. package/templates/agent/src/routes.ts +62 -0
  58. package/templates/agent/template.json +16 -0
  59. package/templates/agent/test/agent.test.ts +45 -0
  60. package/templates/agent/test/app.test.ts +46 -0
  61. package/templates/agent/test/builder.test.ts +186 -0
  62. package/templates/agent/test/model.test.ts +89 -0
  63. package/templates/agent/tsconfig.json +19 -0
  64. package/templates/app/README.md +26 -0
  65. package/templates/app/blend.yaml +23 -0
  66. package/templates/app/gitignore +2 -0
  67. package/templates/app/package.json +23 -0
  68. package/templates/app/src/app.ts +61 -0
  69. package/templates/app/src/main.ts +20 -0
  70. package/templates/app/src/routes.ts +14 -0
  71. package/templates/app/template.json +1 -0
  72. package/templates/app/test/app.test.ts +56 -0
  73. package/templates/app/tsconfig.json +19 -0
  74. 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
+ }