@columenlabs/lumen-ai 0.1.193 → 0.1.195

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.
@@ -13,6 +13,8 @@
13
13
  * PUT /workspaces/:id/tabs/:tab {name?, config?}
14
14
  * DELETE /workspaces/:id/tabs/:tab
15
15
  * GET /frames every frame (for + Data)
16
+ * POST /frames {name, recipe} a frame from a recipe, for a card to link (reuses one
17
+ * that runs the same recipe)
16
18
  * GET /frames/:id rows, meta and hand edits
17
19
  * POST /frames/:id/refresh runs its recipe again
18
20
  * GET|PUT /frames/:id/script its recipe
@@ -13,6 +13,8 @@
13
13
  * PUT /workspaces/:id/tabs/:tab {name?, config?}
14
14
  * DELETE /workspaces/:id/tabs/:tab
15
15
  * GET /frames every frame (for + Data)
16
+ * POST /frames {name, recipe} a frame from a recipe, for a card to link (reuses one
17
+ * that runs the same recipe)
16
18
  * GET /frames/:id rows, meta and hand edits
17
19
  * POST /frames/:id/refresh runs its recipe again
18
20
  * GET|PUT /frames/:id/script its recipe
@@ -120,8 +122,17 @@ export function workspaceRoutes(opts) {
120
122
  if (!id) {
121
123
  if (m === 'GET')
122
124
  return json(await s.frames(owner));
123
- if (m === 'POST')
124
- return fail('Your own columns are not offered here yet.', 501);
125
+ if (m === 'POST') {
126
+ const raw = await req
127
+ .clone()
128
+ .json()
129
+ .catch(() => null);
130
+ if (!raw || typeof raw !== 'object' || !('recipe' in raw))
131
+ return fail('Your own columns are not offered here yet.', 501);
132
+ const p = await body(addTabSchema);
133
+ await allowed(p.recipe);
134
+ return json(await s.makeFrame(owner, p));
135
+ }
125
136
  }
126
137
  else if (!sub && m === 'GET') {
127
138
  return json(await s.frame(owner, id));
@@ -105,6 +105,14 @@ export declare function createWorkspaces(opts: WorkspacesOptions): {
105
105
  frames(owner: string): Promise<{
106
106
  frames: FrameChoice[];
107
107
  }>;
108
+ /**
109
+ * A frame from a recipe the product offers (+ Data's app recipes), for a card to link. One that
110
+ * already runs the same recipe is reused, so picking it twice keeps one copy.
111
+ */
112
+ makeFrame(owner: string, p: {
113
+ name: string;
114
+ recipe: FrameScript;
115
+ }): Promise<FrameChoice>;
108
116
  frame(owner: string, id: string): Promise<FrameData>;
109
117
  refresh: (owner: string, id: string, by?: 'you' | 'assistant') => Promise<FrameData>;
110
118
  script(owner: string, id: string): Promise<{
@@ -189,9 +189,37 @@ export function createWorkspaces(opts) {
189
189
  throw err instanceof FrameError ? err : new FrameError(error, 502);
190
190
  }
191
191
  }
192
+ /** A new frame from a recipe, run once (a failed run keeps the frame with its error). */
193
+ async function newFrame(owner, name, recipe, by) {
194
+ const [{ n } = { n: 0 }] = await db.all(`SELECT COUNT(*) AS n FROM (SELECT 1 FROM ${t.frames} WHERE owner = ? LIMIT ${MAX_FRAMES})`, owner);
195
+ if (Number(n) >= MAX_FRAMES)
196
+ throw new FrameError(`You keep ${MAX_FRAMES} tabs of data, the most: delete some first.`, 409);
197
+ const head = {
198
+ id: newId('fr'),
199
+ name,
200
+ columns: [],
201
+ rows: 0,
202
+ mode: 'live',
203
+ kind: 'snapshot',
204
+ at: now(),
205
+ origin: 'assistant',
206
+ script: recipe,
207
+ };
208
+ await runInto(owner, head, [], recipe, by, []);
209
+ return (await heads(owner, [head.id])).get(head.id) ?? head;
210
+ }
211
+ const choiceOf = (h) => ({
212
+ id: h.id,
213
+ name: h.name,
214
+ columns: h.columns,
215
+ at: h.at,
216
+ origin: h.origin ?? 'assistant',
217
+ });
218
+ /** A frame no tab shows but a card links (a recipe added from + Data) still counts as used. */
192
219
  async function dropFrameIfUnused(owner, frame) {
193
220
  const rows = await db.all(`SELECT tabs FROM ${t.spaces} WHERE owner = ? LIMIT ${MAX_WORKSPACES}`, owner);
194
- const used = rows.some((r) => parse(r.tabs, []).some((x) => 'frame' in x.source && x.source.frame === frame));
221
+ const used = rows.some((r) => parse(r.tabs, []).some((x) => ('frame' in x.source && x.source.frame === frame) ||
222
+ JSON.stringify(x.config ?? null).includes(frame)));
195
223
  if (!used)
196
224
  await db.run(`DELETE FROM ${t.frames} WHERE owner = ? AND id = ?`, owner, frame);
197
225
  }
@@ -257,21 +285,7 @@ export function createWorkspaces(opts) {
257
285
  const w = await getSpace(owner, id);
258
286
  if (w.tabs.length >= MAX_TABS)
259
287
  throw new FrameError(`${w.name} has ${MAX_TABS} tabs, the most: delete one first.`, 409);
260
- const [{ n } = { n: 0 }] = await db.all(`SELECT COUNT(*) AS n FROM (SELECT 1 FROM ${t.frames} WHERE owner = ? LIMIT ${MAX_FRAMES})`, owner);
261
- if (Number(n) >= MAX_FRAMES)
262
- throw new FrameError(`You keep ${MAX_FRAMES} tabs of data, the most: delete some first.`, 409);
263
- const head = {
264
- id: newId('fr'),
265
- name: p.name,
266
- columns: [],
267
- rows: 0,
268
- mode: 'live',
269
- kind: 'snapshot',
270
- at: now(),
271
- origin: 'assistant',
272
- script: p.recipe,
273
- };
274
- await runInto(owner, head, [], p.recipe, p.by ?? 'you', []);
288
+ const head = await newFrame(owner, p.name, p.recipe, p.by ?? 'you');
275
289
  const tab = {
276
290
  id: newId('tb'),
277
291
  name: p.name,
@@ -336,18 +350,21 @@ export function createWorkspaces(opts) {
336
350
  async frames(owner) {
337
351
  const rows = await db.all(`SELECT head FROM ${t.frames} WHERE owner = ? LIMIT ${MAX_FRAMES}`, owner);
338
352
  return {
339
- frames: rows.map((r) => {
340
- const h = parse(r.head, null);
341
- return {
342
- id: h.id,
343
- name: h.name,
344
- columns: h.columns,
345
- at: h.at,
346
- origin: h.origin ?? 'assistant',
347
- };
348
- }),
353
+ frames: rows.map((r) => choiceOf(parse(r.head, null))),
349
354
  };
350
355
  },
356
+ /**
357
+ * A frame from a recipe the product offers (+ Data's app recipes), for a card to link. One that
358
+ * already runs the same recipe is reused, so picking it twice keeps one copy.
359
+ */
360
+ async makeFrame(owner, p) {
361
+ const rows = await db.all(`SELECT head FROM ${t.frames} WHERE owner = ? LIMIT ${MAX_FRAMES}`, owner);
362
+ const want = recipeText(p.recipe);
363
+ const same = rows
364
+ .map((r) => parse(r.head, null))
365
+ .find((h) => h?.script && recipeText(h.script) === want);
366
+ return choiceOf(same ?? (await newFrame(owner, p.name, p.recipe, 'you')));
367
+ },
351
368
  async frame(owner, id) {
352
369
  return (await getFrame(owner, id)).data;
353
370
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.193",
3
+ "version": "0.1.195",
4
4
  "description": "Lumen's assistant and agent parts: a chat assistant's server (Claude tiers, tools over MCP, the web, hand off, spend caps), serving an MCP server from a Worker, read-only tool sets and metered tool calls.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,8 +52,8 @@
52
52
  "@ai-sdk/openai": "4.0.108",
53
53
  "@anthropic-ai/sdk": "0.133.0",
54
54
  "@cloudflare/think": "0.23.0",
55
- "@columenlabs/lumen-ops": "0.1.193",
56
- "@columenlabs/lumen-ui-types": "0.1.193",
55
+ "@columenlabs/lumen-ops": "0.1.195",
56
+ "@columenlabs/lumen-ui-types": "0.1.195",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",