@columenlabs/lumen-ai 0.1.134 → 0.1.135

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/README.md CHANGED
@@ -99,7 +99,7 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
99
99
  - **ChatGPT sign-in.** In place of their own OpenAI key a person may sign in with their ChatGPT plan (`POST /my-signin`, then `POST /my-signin/finish {url}`): the sign-in opens in a new tab and ends on `http://127.0.0.1:1455/auth/callback`, a page that does not load, whose address the person pastes back. The tokens are sealed as their own key and refreshed before they run out; answers on the plan count tokens and no dollars.
100
100
  - **Who pays.** The person's pick (the shared keys by default) while it has a key for the tier's provider and money left this month under its cap (an admin sets the shared keys' cap per person, `PUT /app`), else the other; with neither the panel says why.
101
101
  - **Tools.** Each group is Off, Read, Read + draft or Read + write per person. A tool is a read when its MCP `readOnlyHint` is set. On Read + draft a write only drafts a card, and runs as the person when they press Save, after access is checked again. Read + write is offered only for a group with `writes: true`: its writes run at once, and with `undo` set each one's line in the chat has Undo, which calls `undo(caller, { tool, args, result })` (`POST /chats/:id/writes/:step/undo`); the model is told what was undone. Cards drafted one after another show as one stack with Save all. Use `mcpServerTools` (an SDK `McpServer`), `mcpFetchTools` (a server served as a fetch handler, any SDK), `remoteMcpTools` or `plainTools`.
102
- - **Web.** `fetch` is a plain GET from the Worker (10 s, 2 MB, never the app's own hosts or private addresses). ScrapeQL's own tools join over its MCP API once an admin pastes a Cloudflare Access service token (`PUT /web/scrapeql`).
102
+ - **Web.** `fetch` is a plain GET from the Worker (10 s, 2 MB, never the app's own hosts or private addresses). ScrapeQL's own tools join over its MCP API once an admin pastes a Cloudflare Access service token (`PUT /web/scrapeql`). A Worker on ScrapeQL's own Cloudflare account passes its service binding instead, `scrapeql: { url: 'https://scrapeql/mcp', fetch: env.SCRAPEQL.fetch.bind(env.SCRAPEQL) }`: no token is stored, the tools are always on and Settings shows the link with a Test.
103
103
  - **Hand off and advice.** The `escalate` tool hands one task to a higher tier (the person sets next, any or only when asked); Anthropic's advisor tool lets a higher tier send a plan, at most two an answer.
104
104
  - **Chats** stream as server-sent events (`POST /chat`); closing the request stops the answer and keeps what was said. Past 150k tokens, or on `/compact`, the history sent to the model becomes a summary. Run `assistant.purge()` once a day to drop chats 30 days after their last message.
105
105
 
@@ -123,9 +123,13 @@ export interface AssistantOptions {
123
123
  web?: {
124
124
  /** `fetch` options, or false for no fetch. */
125
125
  fetch?: WebFetchOptions | false;
126
- /** ScrapeQL's MCP address; its tools join once an admin adds the Access token. */
126
+ /**
127
+ * ScrapeQL's MCP address; its tools join once an admin adds the Access token. With `fetch`
128
+ * (a service binding's), ScrapeQL is reached through it: no token, its tools always on.
129
+ */
127
130
  scrapeql?: {
128
131
  url: string;
132
+ fetch?: typeof fetch;
129
133
  };
130
134
  };
131
135
  store: AssistantStore;
@@ -252,8 +256,10 @@ export declare class Assistant {
252
256
  detail: string;
253
257
  at: number;
254
258
  }>;
255
- /** Lists ScrapeQL's tools with the token: works or why not. */
256
- testScrapeql(token: AccessToken): Promise<{
259
+ /** ScrapeQL is reached over a service binding, so it needs no token. */
260
+ get boundScrapeql(): boolean;
261
+ /** Lists ScrapeQL's tools with the token (none over a binding): works or why not. */
262
+ testScrapeql(token: AccessToken | null): Promise<{
257
263
  ok: boolean;
258
264
  detail: string;
259
265
  at: number;
@@ -357,9 +357,11 @@ export class Assistant {
357
357
  web: {
358
358
  fetch: p.web ?? true,
359
359
  scrapeql: this.o.web?.scrapeql
360
- ? caller.admin
361
- ? keyState(scrapeql, (d) => d.client_id)
362
- : { set: !!scrapeql }
360
+ ? this.o.web.scrapeql.fetch
361
+ ? { set: true, bound: true }
362
+ : caller.admin
363
+ ? keyState(scrapeql, (d) => d.client_id)
364
+ : { set: !!scrapeql }
363
365
  : null,
364
366
  },
365
367
  providers,
@@ -525,12 +527,13 @@ export class Assistant {
525
527
  access: 'read',
526
528
  source: webFetchTools(this.o.web?.fetch || {}),
527
529
  });
528
- const sq = this.o.web?.scrapeql ? await this.state.scrapeql() : null;
529
- if (sq && this.o.web?.scrapeql)
530
+ const sq = this.o.web?.scrapeql;
531
+ const token = sq && !sq.fetch ? await this.state.scrapeql() : null;
532
+ if (sq && (sq.fetch || token))
530
533
  sources.push({
531
534
  group: 'web',
532
535
  access: 'read',
533
- source: scrapeqlTools(this.o.web.scrapeql.url, sq, this.o.fetch),
536
+ source: scrapeqlTools(sq.url, token, sq.fetch ?? this.o.fetch),
534
537
  });
535
538
  }
536
539
  const results = await Promise.allSettled(sources.map((s) => s.source()));
@@ -1636,20 +1639,30 @@ export class Assistant {
1636
1639
  return { ok: false, detail: shownError(err).message, at };
1637
1640
  }
1638
1641
  }
1639
- /** Lists ScrapeQL's tools with the token: works or why not. */
1642
+ /** ScrapeQL is reached over a service binding, so it needs no token. */
1643
+ get boundScrapeql() {
1644
+ return !!this.o.web?.scrapeql?.fetch;
1645
+ }
1646
+ /** Lists ScrapeQL's tools with the token (none over a binding): works or why not. */
1640
1647
  async testScrapeql(token) {
1641
1648
  const at = Date.now();
1642
- const url = this.o.web?.scrapeql?.url;
1643
- if (!url)
1649
+ const sq = this.o.web?.scrapeql;
1650
+ if (!sq)
1644
1651
  return { ok: false, detail: 'ScrapeQL is not set up in this app.', at };
1645
1652
  try {
1646
- const open = await scrapeqlTools(url, token, this.o.fetch)();
1653
+ const open = await scrapeqlTools(sq.url, sq.fetch ? null : token, sq.fetch ?? this.o.fetch)();
1647
1654
  const n = open.tools.length;
1648
1655
  await open.close();
1649
1656
  return { ok: true, detail: `ScrapeQL answered with ${n} tool${n === 1 ? '' : 's'}.`, at };
1650
1657
  }
1651
1658
  catch {
1652
- return { ok: false, detail: 'ScrapeQL refused the token or could not be reached.', at };
1659
+ return {
1660
+ ok: false,
1661
+ detail: sq.fetch
1662
+ ? 'ScrapeQL could not be reached over the link.'
1663
+ : 'ScrapeQL refused the token or could not be reached.',
1664
+ at,
1665
+ };
1653
1666
  }
1654
1667
  }
1655
1668
  // ---------------------------------------------------------------- drafts
@@ -273,6 +273,8 @@ export function assistantRoutes(app, o) {
273
273
  const no = admin();
274
274
  if (no)
275
275
  return no;
276
+ if (a.boundScrapeql)
277
+ return fail("ScrapeQL is reached over the app's link; it needs no token.", 409, 'bound');
276
278
  const b = await body(req);
277
279
  const client_id = str(b.client_id, 200);
278
280
  const client_secret = str(b.client_secret, 400);
@@ -300,6 +302,8 @@ export function assistantRoutes(app, o) {
300
302
  const no = admin();
301
303
  if (no)
302
304
  return no;
305
+ if (a.boundScrapeql)
306
+ return json(await a.testScrapeql(null));
303
307
  const d = await a.state.scrapeql();
304
308
  if (!d)
305
309
  return fail('No ScrapeQL token is set.', 404);
@@ -41,5 +41,8 @@ export interface AccessToken {
41
41
  client_id: string;
42
42
  client_secret: string;
43
43
  }
44
- /** ScrapeQL's MCP tools over its API, with an Access service token. Every one of them reads. */
45
- export declare function scrapeqlTools(url: string, token: AccessToken, f?: typeof fetch): ToolSource;
44
+ /**
45
+ * ScrapeQL's MCP tools over its API, with an Access service token, or none over a service
46
+ * binding (`f` is then the binding's fetch). Every one of them reads.
47
+ */
48
+ export declare function scrapeqlTools(url: string, token: AccessToken | null, f?: typeof fetch): ToolSource;
@@ -155,14 +155,21 @@ export function webFetchTools(o = {}) {
155
155
  return open;
156
156
  };
157
157
  }
158
- /** ScrapeQL's MCP tools over its API, with an Access service token. Every one of them reads. */
158
+ /**
159
+ * ScrapeQL's MCP tools over its API, with an Access service token, or none over a service
160
+ * binding (`f` is then the binding's fetch). Every one of them reads.
161
+ */
159
162
  export function scrapeqlTools(url, token, f) {
160
163
  const remote = remoteMcpTools({
161
164
  url,
162
- headers: {
163
- 'CF-Access-Client-Id': token.client_id,
164
- 'CF-Access-Client-Secret': token.client_secret,
165
- },
165
+ ...(token
166
+ ? {
167
+ headers: {
168
+ 'CF-Access-Client-Id': token.client_id,
169
+ 'CF-Access-Client-Secret': token.client_secret,
170
+ },
171
+ }
172
+ : {}),
166
173
  ...(f ? { fetch: f } : {}),
167
174
  });
168
175
  return async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.134",
3
+ "version": "0.1.135",
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",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@anthropic-ai/sdk": "0.133.0",
44
- "@columenlabs/lumen-ops": "0.1.134",
45
- "@columenlabs/lumen-ui-types": "0.1.134",
44
+ "@columenlabs/lumen-ops": "0.1.135",
45
+ "@columenlabs/lumen-ui-types": "0.1.135",
46
46
  "openai": "7.32.0",
47
47
  "zod": "^4.3.6"
48
48
  },