acrylblends 1.4.0 → 1.5.1

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 (57) hide show
  1. package/README.md +5 -2
  2. package/dist/index.d.ts +7 -4
  3. package/dist/index.js +191 -62
  4. package/dist/index.js.map +1 -1
  5. package/legacy/README.md +1 -1
  6. package/legacy/agent/46c6eadf9230/src/app.ts +293 -0
  7. package/legacy/agent/46c6eadf9230/src/routes.ts +163 -0
  8. package/legacy/blank/a53d41e6ef42/src/app.ts +160 -0
  9. package/legacy/blank/a53d41e6ef42/src/routes.ts +79 -0
  10. package/legacy/layouts.json +30 -0
  11. package/lib/{client-modules-Bkdr83jO.js → client-modules-C5qiVroz.js} +1 -1
  12. package/lib/cordis/door.js +1 -1
  13. package/lib/node-server/register.js +6 -1
  14. package/lib/node-server.js +1 -1
  15. package/lib/{transpile-QUK9b3eG.js → transpile-CRRaixyS.js} +19 -4
  16. package/package.json +1 -1
  17. package/plugins/acryl-door/src/cli/acryl.ts +6 -2
  18. package/plugins/acryl-node-server/src/register.ts +10 -1
  19. package/plugins/acryl-node-server/src/transpile.ts +25 -6
  20. package/plugins/acryl-page/src/favicon.ts +19 -0
  21. package/plugins/acryl-page/src/index.ts +18 -13
  22. package/plugins/agent-panel/client/main.ts +9 -0
  23. package/plugins/agent-panel/headless.ts +2 -2
  24. package/plugins/desktop/app-page.ts +151 -0
  25. package/plugins/desktop/assets/acrylblends-logo.png +0 -0
  26. package/plugins/desktop/brand.ts +164 -0
  27. package/plugins/desktop/frame.ts +88 -7
  28. package/plugins/desktop/git.ts +44 -0
  29. package/plugins/desktop/host-supervisor.ts +63 -0
  30. package/plugins/desktop/launch.ts +17 -1
  31. package/plugins/desktop/main.ts +268 -17
  32. package/plugins/desktop/preload.cjs +8 -0
  33. package/plugins/desktop/product.ts +117 -0
  34. package/plugins/desktop/window.ts +4 -0
  35. package/plugins/desktop-web/main.ts +3 -2
  36. package/plugins/desktop-web/plugin.json +1 -1
  37. package/plugins/extensions/index.ts +52 -7
  38. package/plugins/instance/archive.ts +395 -0
  39. package/plugins/instance/bundle-overlay.ts +10 -0
  40. package/plugins/instance/bundle.ts +201 -0
  41. package/plugins/instance/home.ts +395 -0
  42. package/plugins/instance/index.ts +132 -24
  43. package/plugins/instance/storage.ts +413 -0
  44. package/shared/skills/acryl-remove-plugin/SKILL.md +1 -1
  45. package/templates/agent/README.md +1 -1
  46. package/templates/agent/gitignore +2 -0
  47. package/templates/blank/README.md +1 -1
  48. package/templates/blank/gitignore +2 -0
  49. package/templates/blank/src/routes.ts +9 -5
  50. package/templates/chat/gitignore +2 -0
  51. package/templates/chat/src/chat-app.ts +7 -3
  52. package/templates/chat/src/llm.ts +7 -1
  53. package/templates/chat/test/chat.test.ts +16 -1
  54. package/templates/nodejs/gitignore +2 -0
  55. package/templates/notes/gitignore +2 -0
  56. package/templates/todo/README.md +1 -1
  57. package/templates/todo/gitignore +2 -0
@@ -2,3 +2,5 @@ node_modules/
2
2
  .acrylblends/
3
3
  .acryl/
4
4
  package-lock.json
5
+ .env
6
+ .env.*
@@ -9,7 +9,7 @@ export const APP_NAME = '__APP_NAME__'
9
9
  /**
10
10
  * The only place the app's mount prefix is named. On its own server (production, `npm start`) the app mounts at the root and
11
11
  * its public URLs are `/`, `/about`, `/health`; when the ACRYL frame hosts it (`npm run dev`) the routes mount here, and the
12
- * frame opens its tile at `/app/`. Every page link and asset URL this template writes is derived from the prefix it is
12
+ * frame opens its tile at `/app` (`/app/` answers too). Every page link and asset URL this template writes is derived from the prefix it is
13
13
  * mounted at, so the same source serves both.
14
14
  */
15
15
  export const FRAME_MOUNT_PREFIX = '/app'
@@ -50,14 +50,14 @@ const homePage = (name: string, widgets: string): string => `<!doctype html>
50
50
  * The app's own pages and routes, mountable at any prefix: the standalone server mounts it at the root, the frame host at {@link FRAME_MOUNT_PREFIX}.
51
51
  * The first screen shows the widgets of the app's plugins (`ctx.home`), read at each request, so a plugin switched on or off shows at the next load.
52
52
  */
53
- export function makeApp(home: Home): Acryl {
53
+ export function makeApp(home: Home, name: () => string = () => APP_NAME): Acryl {
54
54
  return (
55
55
  new Acryl()
56
56
  // A browser gets the first screen; an API client gets the JSON hello.
57
57
  .get('/', async (c) =>
58
58
  (c.req.header('accept') ?? '').includes('text/html')
59
- ? c.html(homePage(APP_NAME, await renderWidgets(home)))
60
- : c.json({ app: APP_NAME, ok: true }),
59
+ ? c.html(homePage(name(), await renderWidgets(home)))
60
+ : c.json({ app: name(), ok: true }),
61
61
  )
62
62
  .get('/health', (c) => c.json({ ok: true }))
63
63
  .get('/about', (c) => c.html(aboutPage()))
@@ -70,6 +70,10 @@ export const routes = {
70
70
  inject: ['router', 'home'],
71
71
  apply(ctx: Context, options: { readonly prefix?: string } = {}) {
72
72
  const prefix = options.prefix ?? ''
73
- ctx.router.route(prefix === '' ? '/' : prefix, makeApp(ctx.home))
73
+ // The name the app has now (Settings > App renames it), else the one the scaffold was made with.
74
+ ctx.router.route(
75
+ prefix === '' ? '/' : prefix,
76
+ makeApp(ctx.home, () => ctx.get('appInstance')?.name ?? APP_NAME),
77
+ )
74
78
  },
75
79
  }
@@ -5,3 +5,5 @@ data/
5
5
  *.sqlite
6
6
  *.sqlite-*
7
7
  package-lock.json
8
+ .env
9
+ .env.*
@@ -7,7 +7,7 @@ import {
7
7
  import { Acryl } from '../plugins/acryl-router/src/index.ts'
8
8
  import { type Access, accessGate } from './access.ts'
9
9
  import { chatStore } from './db.ts'
10
- import { type Llm, openAiCompatible } from './llm.ts'
10
+ import { type Llm, MissingKey, openAiCompatible } from './llm.ts'
11
11
  import { chatPage } from './page.tsx'
12
12
 
13
13
  interface ChatInput {
@@ -99,10 +99,14 @@ export function createApp(
99
99
  } catch (error) {
100
100
  if (!signal.aborted) {
101
101
  console.error(error)
102
- // The reason stays in the log; the page gets a plain message.
102
+ // The reason stays in the log; the page gets a plain message, or how to set the missing key.
103
103
  await stream.writeSSE({
104
104
  event: 'error',
105
- data: JSON.stringify('The model could not answer. Try again.'),
105
+ data: JSON.stringify(
106
+ error instanceof MissingKey
107
+ ? error.message
108
+ : 'The model could not answer. Try again.',
109
+ ),
106
110
  })
107
111
  }
108
112
  } finally {
@@ -4,13 +4,19 @@ export type Llm = (
4
4
  signal: AbortSignal,
5
5
  ) => AsyncIterable<string>
6
6
 
7
+ /** The model has no key: the one failure whose message the page may show, since it names the fix and holds no detail from the service. */
8
+ export class MissingKey extends Error {}
9
+
7
10
  /**
8
11
  * An OpenAI-compatible chat completions API with `stream: true` (DeepSeek by default). The key is read from DEEPSEEK_API_KEY at the moment of the call and
9
12
  * is never stored; DEEPSEEK_BASE_URL and DEEPSEEK_MODEL choose the service.
10
13
  */
11
14
  export const openAiCompatible: Llm = async function* (messages, signal) {
12
15
  const key = process.env.DEEPSEEK_API_KEY
13
- if (!key) throw new Error('DEEPSEEK_API_KEY is not set')
16
+ if (!key)
17
+ throw new MissingKey(
18
+ 'DEEPSEEK_API_KEY is not set: set it in the terminal that runs the app, then start the app again.',
19
+ )
14
20
  const base = (process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com').replace(/\/$/, '')
15
21
  const response = await fetch(`${base}/chat/completions`, {
16
22
  method: 'POST',
@@ -12,7 +12,7 @@ import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
12
12
  import { chatAccess } from '../src/access.ts'
13
13
  import { createApp as boot } from '../src/app.ts'
14
14
  import { createApp } from '../src/chat-app.ts'
15
- import type { Llm } from '../src/llm.ts'
15
+ import { type Llm, openAiCompatible } from '../src/llm.ts'
16
16
  import { FRAME_MOUNT_PREFIX } from '../src/routes.ts'
17
17
 
18
18
  /** Throwaway homes and databases live inside the project (`.acrylblends/` is ignored by git), because a sandboxed agent may not write to the system temp folder. */
@@ -155,6 +155,21 @@ test('a failing model sends an error event with a plain message and no detail',
155
155
  assert.match(last?.[1] ?? '', /could not answer/)
156
156
  })
157
157
 
158
+ test('a model without its key tells the page which variable to set', async () => {
159
+ const saved = process.env.DEEPSEEK_API_KEY
160
+ delete process.env.DEEPSEEK_API_KEY
161
+ try {
162
+ const app = createApp(await memoryDb(), openAiCompatible)
163
+ const last = events(await (await app.request('/api/chat', post({ message: 'hi' }))).text()).at(
164
+ -1,
165
+ )
166
+ assert.equal(last?.[0], 'error')
167
+ assert.match(last?.[1] ?? '', /DEEPSEEK_API_KEY is not set/)
168
+ } finally {
169
+ if (saved !== undefined) process.env.DEEPSEEK_API_KEY = saved
170
+ }
171
+ })
172
+
158
173
  test('when the client goes away the model call is aborted and what arrived is kept', async () => {
159
174
  let sawAbort: (value: boolean) => void = () => {}
160
175
  const aborted = new Promise<boolean>((resolve) => {
@@ -2,3 +2,5 @@ node_modules/
2
2
  .acrylblends/
3
3
  .acryl/
4
4
  package-lock.json
5
+ .env
6
+ .env.*
@@ -5,3 +5,5 @@ data/
5
5
  *.sqlite
6
6
  *.sqlite-*
7
7
  package-lock.json
8
+ .env
9
+ .env.*
@@ -21,7 +21,7 @@ curl -N $URL/todo/events
21
21
 
22
22
  The routes only answer a caller on this machine: a request from another site's page (a foreign `Origin`, or `Origin: null`) or addressed to another host name gets `403`.
23
23
 
24
- Open `$URL/todo` for the page. Stop the app and start it again: the list is still there (`<home>/todo.json`, `.acrylblends/` unless `ACRYLBLENDS_HOME` says otherwise).
24
+ Open `$URL/todo` for the page. Stop the app and start it again: the list is still there (`<home>/todo.json`, `~/.acryl/apps/<id>/data/` unless `ACRYLBLENDS_HOME` says otherwise).
25
25
 
26
26
  - `blend.yaml` is the app as data. `npm run lock` records what it is built from; `npm run save` commits it, refusing secrets and public remotes for a private app.
27
27
  - The framework plugins are in `plugins/acryl-*/src`. Do not edit them: they are ACRYL's, vendored; the text that explains them is in the ACRYLBLENDS docs.
@@ -2,3 +2,5 @@ node_modules/
2
2
  .acrylblends/
3
3
  .acryl/
4
4
  package-lock.json
5
+ .env
6
+ .env.*