toolaby 1.8.3 → 1.8.4

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
@@ -1,6 +1,6 @@
1
1
  # toolaby
2
2
 
3
- The Toolaby command line. It puts the Wall — sign-in, a Free plan, paid plans, licences and subscriptions — into a Chrome extension you already have, or makes a new one for you.
3
+ The Toolaby command line. It puts Toolaby — sign-in, a Free plan, paid plans, licences and subscriptions — into a Chrome extension you already have, or makes a new one for you.
4
4
 
5
5
  ```sh
6
6
  npm i -g toolaby # once; or run every command through `npx -y toolaby@latest …`
@@ -12,18 +12,18 @@ The command is `toolaby-cli`: on Windows, the Command Prompt runs a `toolaby.js`
12
12
 
13
13
  In a repository you did not write, use the installed command: `npx` can run a `toolaby` the project carries, or fetch one from a registry the project's `.npmrc` names.
14
14
 
15
- Every command acts on the test Wall (`toolaby.app/test`) unless told `--live` (`toolaby.app`), as Stripe's command line does. `toolaby tools` lists your ids on either side.
15
+ Every command acts on Test (`toolaby.app/test`) unless told `--live` (`toolaby.app`), as Stripe's command line does. `toolaby tools` lists your ids on either side.
16
16
 
17
17
  ## What it does
18
18
 
19
- **`toolaby login`** signs you in. It shows a code and opens the Toolaby dashboard; you approve the code there — as the account the dashboard is signed in to — and the command is signed in as that account from then on. It works the way GitHub's and Stripe's command lines sign in, and only ever acts on that account's own tools. It opens only the Wall's own page, over https (or on `localhost`, for a Wall on your machine); any other address is refused and nothing is signed in.
19
+ **`toolaby login`** signs you in. It shows a code and opens the Toolaby dashboard; you approve the code there — as the account the dashboard is signed in to — and the command is signed in as that account from then on. It works the way GitHub's and Stripe's command lines sign in, and only ever acts on that account's own tools. It opens only Toolaby's own page, over https (or on `localhost`, for a server on your machine); any other address is refused and nothing is signed in.
20
20
 
21
21
  **`toolaby wire <tool-id>`**, run in your extension's folder, wires that extension to your tool:
22
22
 
23
- - it adds the Wall's files — a small client library, a config with your tool's id and public key, and the Wall's popup, which stands in front of yours;
24
- - it adds three lines to your `manifest.json` (a permission and your Wall address) and creates a background file if you have none;
25
- - it puts the Wall's background in front of yours, `toolaby.background.js`: yours runs as it did — a classic one's `importScripts()` included — and the Wall starts with it;
26
- - your own code does not change. When your popup opens, the Wall's page checks what the Free plan holds and either sends the person straight to your popup or shows sign-in, the free-use meter or the plans.
23
+ - it adds Toolaby's files — a small client library, a config with your tool's id and public key, and Toolaby's popup, which stands in front of yours;
24
+ - it adds three lines to your `manifest.json` (a permission and your Toolaby address) and creates a background file if you have none;
25
+ - it puts Toolaby's background in front of yours, `toolaby.background.js`: yours runs as it did — a classic one's `importScripts()` included — and Toolaby starts with it;
26
+ - your own code does not change. When your popup opens, Toolaby's page checks what the Free plan holds and either sends the person straight to your popup or shows sign-in, the free-use meter or the plans.
27
27
 
28
28
  It prints what it did in a few rows — `✓` done, `!` worth a look, `→` a step for you, with the exact code — and what to do next, in colour in a terminal (`NO_COLOR` turns it off). When your code sets or reads the popup itself (`chrome.action.setPopup()`, `getPopup()`), it says where; `--own-popup` keeps your popup in front.
29
29
 
@@ -38,8 +38,8 @@ If the extension is built with WXT, CRXJS or Plasmo, the command puts the same f
38
38
  ## Where things live
39
39
 
40
40
  - Your session: `~/.config/toolaby/credentials.json`, readable by you alone (mode 600). `toolaby logout` deletes it after revoking it.
41
- - The session is per side. `TOOLABY_TOKEN` stands in for the file where there is no home directory (CI), and is sent to one Wall only: Live (`toolaby.app`), or the one `TOOLABY_TOKEN_WALL` names — `TOOLABY_TOKEN_WALL=https://toolaby.app/test` for a Test session. If that Wall refuses it, the session `toolaby login` stored for that Wall is used instead. `TOOLABY_WALL` or `--wall <url>` names any other Wall.
41
+ - The session is per side. `TOOLABY_TOKEN` stands in for the file where there is no home directory (CI), and is sent to one server only: Live (`toolaby.app`), or the one `TOOLABY_TOKEN_WALL` names — `TOOLABY_TOKEN_WALL=https://toolaby.app/test` for a Test session. If that server refuses it, the session `toolaby login` stored for that server is used instead. `TOOLABY_WALL` or `--wall <url>` names any other server.
42
42
 
43
- `wire` and `create` write only inside the folder they work on, and never through a symbolic link. A manifest, a framework's config or a Wall that names a place outside it (`../`, an absolute path, a link) is refused before anything is written. `create` writes only the files it knows a new extension has.
43
+ `wire` and `create` write only inside the folder they work on, and never through a symbolic link. A manifest, a framework's config or a server that names a place outside it (`../`, an absolute path, a link) is refused before anything is written. `create` writes only the files it knows a new extension has.
44
44
 
45
- The files the command writes into your extension contain nothing secret: your tool's id, your Wall's address and the *public* key your extension verifies licences with — the same things every copy of your extension carries once it is published.
45
+ The files the command writes into your extension contain nothing secret: your tool's id, your Toolaby address and the *public* key your extension verifies licences with — the same things every copy of your extension carries once it is published.
package/bin.mjs CHANGED
@@ -44,7 +44,7 @@ import { runMcp } from './lib/mcp.js';
44
44
  import { runCheck } from './lib/check.js';
45
45
  import { inside, signInPage, devicePolling, opener, privateKind, privateContent, privateCopies, googleKey, projectRoot, gitDirectory, gitObject, gitObjects, gitFile, notAnImage, unpacked, COMPRESSED, publicLinks, privateLinks, oneLine, fetchBounded, fetchHere, readBounded, readText, SCAFFOLD_FILES } from './lib/safe.js';
46
46
 
47
- const VERSION = '1.8.3';
47
+ const VERSION = '1.8.4';
48
48
  const CLIENT_ID = 'toolaby-cli';
49
49
  const argv = process.argv.slice(2);
50
50
  // `--version` and `--help` are commands of their own; every other command is the first bare word.
@@ -104,7 +104,7 @@ const ANSWER_TIME = 30_000;
104
104
  async function api(path, { method = 'GET', body, token = storedToken() } = {}) {
105
105
  // The time ran out (fetchHere's deadline; AbortError where an older Node says so): one line, whichever part was late.
106
106
  // A Wall that answered a byte at a time held `login` and `wire` for ever (round 8, 25 Sep 2026).
107
- const late = (e) => (e?.name === 'TimeoutError' || e?.name === 'AbortError' ? new Error(`The Wall at ${wall} did not answer ${path.split('?')[0]} within ${ANSWER_TIME / 1000} seconds.`) : null);
107
+ const late = (e) => (e?.name === 'TimeoutError' || e?.name === 'AbortError' ? new Error(`Toolaby at ${wall} did not answer ${path.split('?')[0]} within ${ANSWER_TIME / 1000} seconds.`) : null);
108
108
  // Only the Wall answers for the Wall: a redirect elsewhere — Live's /test is Test's — is refused (lib/safe.js).
109
109
  const res = await fetchHere(`${wall}${path}`, {
110
110
  ms: ANSWER_TIME,
@@ -122,7 +122,7 @@ async function api(path, { method = 'GET', body, token = storedToken() } = {}) {
122
122
  // end filled the command's memory, and the MCP server's, which hands what it reads to an agent (25 Sep 2026).
123
123
  // An answer cut short reads as no JSON, as it always has; only one past the limit, or the time, stops the command.
124
124
  const bytes = await readBounded(res, ANSWER_LIMIT).catch((e) => { const why = late(e); if (why) throw why; return Buffer.alloc(0); });
125
- if (!bytes) throw new Error(`The Wall at ${wall} answered ${path.split('?')[0]} with more than ${ANSWER_LIMIT / 1024 / 1024} MB, which no answer of a Wall's is. None of it was used.`);
125
+ if (!bytes) throw new Error(`Toolaby at ${wall} answered ${path.split('?')[0]} with more than ${ANSWER_LIMIT / 1024 / 1024} MB, which no answer of Toolaby's is. None of it was used.`);
126
126
  let json = {};
127
127
  try { json = JSON.parse(bytes.toString('utf8')); } catch { /* not JSON: the status says what happened */ }
128
128
  return { status: res.status, ok: res.ok, json };
@@ -138,16 +138,16 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
138
138
 
139
139
  async function login() {
140
140
  const start = await api('/api/dev-auth/device/code', { method: 'POST', body: { client_id: CLIENT_ID }, token: null });
141
- if (!start.ok || !start.json.device_code) fail(`The Wall at ${wall} did not start a sign-in (HTTP ${start.status}). Is the address right? --wall names another.`);
141
+ if (!start.ok || !start.json.device_code) fail(`Toolaby at ${wall} did not start a sign-in (HTTP ${start.status}). Is the address right? --wall names another.`);
142
142
  const { device_code, user_code, verification_uri, verification_uri_complete } = start.json;
143
143
  const link = verification_uri_complete || `${verification_uri}?user_code=${encodeURIComponent(user_code)}`;
144
144
  // The Wall's own page, over https (http on this machine), or nothing opened and nothing signed in: an address the
145
145
  // Wall hands over is opened on this computer, and a file: or another site's page opens whatever it names.
146
146
  const page = signInPage(link, wall);
147
- if (!page) fail(`The Wall at ${wall} gave ${JSON.stringify(String(link).slice(0, 200))} as its sign-in page, which is not its own address over https.\n It was not opened, and nothing was signed in.`);
147
+ if (!page) fail(`Toolaby at ${wall} gave ${JSON.stringify(String(link).slice(0, 200))} as its sign-in page, which is not its own address over https.\n It was not opened, and nothing was signed in.`);
148
148
  // How often it asks and for how long: what the Wall said, a quarter of an hour at most (lib/safe.js).
149
149
  const { every: first, lasts } = devicePolling(start.json);
150
- out(`\n Sign in to the ${side} Wall to continue.\n\n Open ${page}\n Code ${oneLine(user_code)} (approve it there — it expires in ${Math.round(lasts / 60_000)} minutes)\n`);
150
+ out(`\n Sign in to Toolaby (${side}) to continue.\n\n Open ${page}\n Code ${oneLine(user_code)} (approve it there — it expires in ${Math.round(lasts / 60_000)} minutes)\n`);
151
151
  openBrowser(page);
152
152
  let every = first;
153
153
  const until = Date.now() + lasts;
@@ -171,7 +171,7 @@ async function login() {
171
171
  if (why === 'slow_down') every += 5000;
172
172
  else if (why === 'expired_token') fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
173
173
  else if (why === 'access_denied') fail('The sign-in was denied in the browser. Nothing was signed in.');
174
- else if (why && why !== 'authorization_pending') fail(`The Wall answered "${oneLine(why)}"${poll.json.error_description ? `: ${oneLine(poll.json.error_description)}` : ''}.`);
174
+ else if (why && why !== 'authorization_pending') fail(`Toolaby answered "${oneLine(why)}"${poll.json.error_description ? `: ${oneLine(poll.json.error_description)}` : ''}.`);
175
175
  }
176
176
  fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
177
177
  }
@@ -188,7 +188,7 @@ async function whoami() {
188
188
  if (!token) fail(`Not signed in to ${side} (${wall}). Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.${envElsewhere()}`);
189
189
  const me = await api('/api/cli/me', { token });
190
190
  if (me.status === 401) fail(`The session for ${wall} is no longer valid. Run \`npx -y toolaby@latest login\`.`);
191
- if (!me.ok) fail(`The Wall answered HTTP ${me.status}.`);
191
+ if (!me.ok) fail(`Toolaby answered HTTP ${me.status}.`);
192
192
  out(`\n ${oneLine(me.json.email)} · ${side} · ${wall}\n`);
193
193
  for (const w of me.json.workspaces) out(` ${oneLine(w.name)} (${oneLine(w.slug)}, ${oneLine(w.role)}) ${oneLine(w.site)}`);
194
194
  out();
@@ -202,7 +202,7 @@ async function logout() {
202
202
  const res = await api('/api/dev-auth/sign-out', { method: 'POST', body: {}, token: entry?.token ?? envToken() }).catch(() => ({ ok: false, status: 0 }));
203
203
  delete all[wall];
204
204
  writeCredentials(all);
205
- out(res.ok ? `\n Signed out of ${side} (${wall}): the session was revoked and forgotten.\n` : `\n The session was forgotten here, but the Wall could not be reached to revoke it (HTTP ${res.status}); it expires on its own, or sign it out from the dashboard.\n`);
205
+ out(res.ok ? `\n Signed out of ${side} (${wall}): the session was revoked and forgotten.\n` : `\n The session was forgotten here, but Toolaby could not be reached to revoke it (HTTP ${res.status}); it expires on its own, or sign it out from the dashboard.\n`);
206
206
  if (envToken()) out(' TOOLABY_TOKEN is set in this shell and still authenticates.\n');
207
207
  }
208
208
 
@@ -210,7 +210,7 @@ async function tools() {
210
210
  const token = await tokenOrLogin();
211
211
  const res = await api('/api/cli/tools', { token });
212
212
  if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
213
- if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
213
+ if (!res.ok) fail(`Toolaby answered HTTP ${res.status}.`);
214
214
  if (!res.json.tools.length) { out(`\n No tools on ${side} yet. Make one in the dashboard, then \`toolaby wire <tool-id>${live ? ' --live' : ''}\` in your extension's folder.\n`); return; }
215
215
  out(`\n ${side}\n`);
216
216
  for (const t of res.json.tools) out(` ${oneLine(t.slug).padEnd(40)} ${oneLine(t.name)}${t.bundle ? ' (bundle)' : ''} ${oneLine(t.site)}`);
@@ -272,7 +272,7 @@ async function filesFor(slug, scaffold, manifestKey = null, surfaces = null) {
272
272
  const who = me?.ok ? `\n\n You are signed in as ${oneLine(me.json.email)}${me.json.workspaces.length ? ` (workspaces: ${me.json.workspaces.map((w) => oneLine(w.slug)).join(', ')})` : ' (no workspace yet)'}.\n Another account owns it? \`toolaby logout\`, sign in to the dashboard as that account, then run this again.` : '';
273
273
  fail(`${oneLine(res.json.message ?? `No tool "${slug}" in your workspaces.`)}${who}`);
274
274
  }
275
- if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
275
+ if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
276
276
  return res.json;
277
277
  }
278
278
 
@@ -325,9 +325,9 @@ async function wire() {
325
325
  // The extension's own background, with the Wall's in front of it — and, for a classic one, the client as a script.
326
326
  const wrap = wiring?.background.kind === 'wrap' ? wiring.background : null;
327
327
  const classicClient = wrap?.classic ? `${project.codeDir}${CLASSIC_CLIENT}` : null;
328
- const at = placesIn(dir, [...targets, ...entries, ...stale, ...(worker ? [worker] : []), ...(wrap ? [wrap.file] : []), ...(classicClient ? [classicClient] : []), ...(wiring?.changed ? ['manifest.json'] : [])], { ...(worker ? { [worker]: 'the background the manifest names' } : {}), ...(wrap ? { [wrap.file]: "the Wall's background, beside yours" } : {}) });
328
+ const at = placesIn(dir, [...targets, ...entries, ...stale, ...(worker ? [worker] : []), ...(wrap ? [wrap.file] : []), ...(classicClient ? [classicClient] : []), ...(wiring?.changed ? ['manifest.json'] : [])], { ...(worker ? { [worker]: 'the background the manifest names' } : {}), ...(wrap ? { [wrap.file]: "Toolaby's background, beside yours" } : {}) });
329
329
  // A file of the developer's own where the Wall's background goes is never replaced: only one the Wall wrote.
330
- if (wrap && wrap.file !== namedWorker && there(at[wrap.file]) && !wrappedBy(readOr(at[wrap.file], ''))) fail(`${wrap.file} is already in ${dir}, and it is not the Wall's. Rename it, then run this again. Nothing was written.`);
330
+ if (wrap && wrap.file !== namedWorker && there(at[wrap.file]) && !wrappedBy(readOr(at[wrap.file], ''))) fail(`${wrap.file} is already in ${dir}, and it is not Toolaby's. Rename it, then run this again. Nothing was written.`);
331
331
  // Taken for missing, it may only be out of the listing's sight — spelt "./background.js", in dist/, or deeper down:
332
332
  // the developer's own background is never replaced by two lines of ours.
333
333
  if (worker && there(at[worker])) fail(`${JSON.stringify(worker)}, the background the manifest names, is already in ${dir}; it would have been replaced. Nothing was written.\n Name it in the manifest as the folder spells it — or, if a build writes it, run this with --into the folder whose manifest.json names it.`);
@@ -338,7 +338,7 @@ async function wire() {
338
338
  // The agent files come from Walls that write them; one that does not is older, not newer, than this command.
339
339
  const agentPaths = new Set(AGENT_FILES.map((f) => f.path));
340
340
  const missing = [...targets, ...(classicClient ? [classicClient] : [])].filter((p) => !agentPaths.has(p)).map((p) => p.split('/').pop()).filter((name) => typeof plan.files[name] !== 'string');
341
- if (missing.length) fail(`This command (${VERSION}) is older than the Wall at ${wall}: it expects ${missing.join(', ')}, which the Wall no longer hands out.\n Update it: npm install -g toolaby@latest — or run the Wall's own copy: npx --yes ${wall}/toolaby.tgz ${command} ${slug}`);
341
+ if (missing.length) fail(`This command (${VERSION}) is older than Toolaby at ${wall}: it expects ${missing.join(', ')}, which Toolaby no longer hands out.\n Update it: npm install -g toolaby@latest — or run Toolaby's own copy: npx --yes ${wall}/toolaby.tgz ${command} ${slug}`);
342
342
  const wrote = targets.filter((path) => typeof plan.files[path.split('/').pop()] === 'string');
343
343
  for (const path of wrote) write(path, fillPages(plan.files[path.split('/').pop()], { appPopup, appSidePanel }));
344
344
  for (const f of entries) { write(f, plan.files[f]); wrote.push(f); }
@@ -386,12 +386,12 @@ async function create() {
386
386
  // the folder as it came let a Wall at another address — --wall, TOOLABY_WALL — write any file, anywhere.
387
387
  const scaffold = Array.isArray(plan.scaffold) && plan.scaffold.length ? plan.scaffold : SCAFFOLD_FILES;
388
388
  const unknown = scaffold.filter((f) => !SCAFFOLD_FILES.includes(f));
389
- if (unknown.length) fail(`The Wall at ${wall} named ${unknown.map((f) => JSON.stringify(f)).join(', ')} among a new extension's files: not ${unknown.length > 1 ? 'files' : 'a file'} this command (${VERSION}) writes.\n Nothing was written. If the Wall is newer than this command, update it: npm install -g toolaby@latest`);
389
+ if (unknown.length) fail(`Toolaby at ${wall} named ${unknown.map((f) => JSON.stringify(f)).join(', ')} among a new extension's files: not ${unknown.length > 1 ? 'files' : 'a file'} this command (${VERSION}) writes.\n Nothing was written. If Toolaby is newer than this command, update it: npm install -g toolaby@latest`);
390
390
  // Both surfaces (the default), one of them, or none at all for a tool that works from the background — and beside each,
391
391
  // the Wall's page for it, which toolaby.showPaywall() opens when an action is refused (the extension's own pages stay in front).
392
392
  const wanted = [...CODE_FILES, ...scaffoldFilesFor(surfaces, scaffold), ...(hasPopup(surfaces) ? POPUP_FILES : []), ...(hasSidePanel(surfaces) ? SIDE_PANEL_FILES : [])];
393
393
  const absent = wanted.filter((f) => typeof plan.files[f] !== 'string');
394
- if (absent.length) fail(`The Wall at ${wall} named ${absent.join(', ')} for a new extension but did not hand ${absent.length > 1 ? 'them' : 'it'} over.\n Nothing was written. Update this command: npm install -g toolaby@latest — or run the Wall's own copy: npx --yes ${wall}/toolaby.tgz create ${slug}`);
394
+ if (absent.length) fail(`Toolaby at ${wall} named ${absent.join(', ')} for a new extension but did not hand ${absent.length > 1 ? 'them' : 'it'} over.\n Nothing was written. Update this command: npm install -g toolaby@latest — or run Toolaby's own copy: npx --yes ${wall}/toolaby.tgz create ${slug}`);
395
395
  // What a coding agent reads: the guide with this tool's features and plans, the skill, AGENTS.md and CLAUDE.md.
396
396
  const agentFiles = AGENT_FILES.filter((f) => typeof plan.files[f.name] === 'string');
397
397
  const entries = AGENT_ENTRY_FILES.filter((f) => typeof plan.files[f] === 'string');
@@ -403,7 +403,7 @@ async function create() {
403
403
  const iconBytes = {};
404
404
  for (const icon of icons) {
405
405
  const got = await fetchBounded(`${wall}/boilerplate/${icon}`).catch((e) => ({ why: e.name === 'TimeoutError' ? 'it took longer than 30 seconds' : oneLine(e.message) }));
406
- if (got.why || !got.body) fail(`The Wall at ${wall} did not send ${icon}: ${got.why ?? 'it sent more than a megabyte, which no icon is'}. Nothing was written.`);
406
+ if (got.why || !got.body) fail(`Toolaby at ${wall} did not send ${icon}: ${got.why ?? 'it sent more than a megabyte, which no icon is'}. Nothing was written.`);
407
407
  iconBytes[icon] = got.body;
408
408
  }
409
409
  const write = (rel, content) => { mkdirSync(dirname(at[rel]), { recursive: true }); writeFileSync(at[rel], content); };
@@ -418,7 +418,7 @@ async function create() {
418
418
  for (const f of entries) write(f, plan.files[f]);
419
419
  const clicking = { both: 'Click the icon: the popup shows what the Free plan holds. Open side panel, in it, is the same page at the panel\'s width', popup: 'Click the icon: the popup shows what the Free plan holds', 'side-panel': 'Click the icon: the side panel opens — it has no popup, so the click is the panel\'s', none: 'No popup and no side panel: nothing opens on a click. Your code says when to sell — toolaby.openPaymentPage()' }[surfaces];
420
420
  out(`\n ${oneLine(plan.name)} → ${dir}${surfaces === 'both' ? '' : ` (--surfaces ${surfaces})`}\n\n 1. chrome://extensions → Developer mode → Load unpacked → ${dir}\n 2. ${clicking}; Buy opens ${oneLine(plan.buyer ?? `${plan.site}/tools/${slug}`)}\n\n Edit background.js: your work goes behind toolaby.gate().\n Surfaces, and how to add or drop one later: ${wall}/docs/surfaces\n`);
421
- if (typeof plan.files['TOOLABY.md'] === 'string') out(` With a coding agent: AGENTS.md and TOOLABY.md tell it how this extension sells. For Claude Code, the Wall itself:\n claude mcp add toolaby -- npx -y toolaby@latest mcp${live ? ' --live' : ''}\n`);
421
+ if (typeof plan.files['TOOLABY.md'] === 'string') out(` With a coding agent: AGENTS.md and TOOLABY.md tell it how this extension sells. For Claude Code, Toolaby itself:\n claude mcp add toolaby -- npx -y toolaby@latest mcp${live ? ' --live' : ''}\n`);
422
422
  }
423
423
 
424
424
  // ── webhooks: the catalogue, and a delivery on demand ───────────────────────────────────────────────────────────────
@@ -445,7 +445,7 @@ function devSecret() {
445
445
  async function catalogue() {
446
446
  const res = await api('/api/cli/webhooks/events', { token: await tokenOrLogin() });
447
447
  if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
448
- if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
448
+ if (!res.ok) fail(`Toolaby answered HTTP ${res.status}.`);
449
449
  return res.json.events;
450
450
  }
451
451
  async function webhooks() {
@@ -468,7 +468,7 @@ async function webhooks() {
468
468
  const to = opt('--to');
469
469
  if (!to) {
470
470
  const res = await api('/api/cli/webhooks/trigger', { method: 'POST', body: { type, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
471
- if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
471
+ if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
472
472
  out(`\n Sent ${oneLine(res.json.sent)} to every endpoint of ${oneLine(res.json.workspace)} · ${side}. Its id is ${oneLine(res.json.id)}; the delivery log is under Configure → Webhooks.\n`);
473
473
  return;
474
474
  }
@@ -494,7 +494,7 @@ async function webhooks() {
494
494
  if (sub === 'endpoints') {
495
495
  const ws = opt('--workspace');
496
496
  const res = await api(`/api/cli/webhooks/endpoints${ws ? `?workspace=${encodeURIComponent(ws)}` : ''}`, { token: await tokenOrLogin() });
497
- if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
497
+ if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
498
498
  out(`\n ${oneLine(res.json.workspace)} · ${side}\n`);
499
499
  if (res.json.endpoints.length === 0) out(' None yet. `npx -y toolaby@latest webhooks add <url>` registers one.');
500
500
  for (const e of res.json.endpoints) out(` ${oneLine(e.id)} ${e.disabled ? 'paused ' : 'on '} ${oneLine(e.url)}${e.description ? ` — ${oneLine(e.description)}` : ''}${e.events.length ? ` (${oneLine(e.events.join(', '))})` : ''}`);
@@ -506,7 +506,7 @@ async function webhooks() {
506
506
  if (!url) fail('usage: npx -y toolaby@latest webhooks add <url> [--description <text>] [--events a.b,c.d] [--workspace <slug>]');
507
507
  const events = (opt('--events') ?? '').split(',').map((e) => e.trim()).filter(Boolean);
508
508
  const res = await api('/api/cli/webhooks/endpoints', { method: 'POST', body: { url, description: opt('--description') ?? '', events, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
509
- if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
509
+ if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
510
510
  out(`\n Added ${oneLine(res.json.id)} → ${oneLine(res.json.url)} · ${oneLine(res.json.workspace)} · ${side}\n\n Signing secret, shown once — keep it where your server keeps secrets:\n\n ${oneLine(res.json.secret)}\n`);
511
511
  return;
512
512
  }
@@ -514,7 +514,7 @@ async function webhooks() {
514
514
  const id = positional[1];
515
515
  if (!id) fail('usage: npx -y toolaby@latest webhooks remove <ep_…> [--workspace <slug>] — `npx -y toolaby@latest webhooks endpoints` lists them');
516
516
  const res = await api('/api/cli/webhooks/endpoints', { method: 'DELETE', body: { id, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
517
- if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
517
+ if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
518
518
  out(`\n Removed ${oneLine(res.json.removed)} · ${oneLine(res.json.workspace)} · ${side}. Deliveries to it stop at once.\n`);
519
519
  return;
520
520
  }
@@ -731,8 +731,8 @@ function pack() {
731
731
  try { const facts = JSON.parse(Buffer.from(k.split('_')[2], 'base64url').toString('utf8')); if (typeof facts.s === 'string') sites.add(facts.s.replace(/\/$/, '')); } catch { /* not a key after all */ }
732
732
  }
733
733
  }
734
- if ((modes.has('test') || modes.has('dev')) && !flag('--allow-test')) problems.push(`It carries a ${[...modes].filter((m) => m !== 'live').map((m) => `tk_${m}_`).join(' and ')} key: it would sell on ${modes.has('test') ? 'Test, with test cards' : 'a development Wall'}. Run \`npx -y toolaby@latest wire <tool-id> --live --force\`, build again, then pack. (--allow-test packs it anyway, for a build you will not publish.)`);
735
- if (modes.size === 0) out('\n No tool key found in the build: it is not wired to the Wall, or the key is somewhere this cannot read. Packing anyway.');
734
+ if ((modes.has('test') || modes.has('dev')) && !flag('--allow-test')) problems.push(`It carries a ${[...modes].filter((m) => m !== 'live').map((m) => `tk_${m}_`).join(' and ')} key: it would sell on ${modes.has('test') ? 'Test, with test cards' : 'a development server'}. Run \`npx -y toolaby@latest wire <tool-id> --live --force\`, build again, then pack. (--allow-test packs it anyway, for a build you will not publish.)`);
735
+ if (modes.size === 0) out('\n No tool key found in the build: it is not wired to Toolaby, or the key is somewhere this cannot read. Packing anyway.');
736
736
 
737
737
  // Addresses of this machine have no place in a store build.
738
738
  const local = [...(manifest.host_permissions ?? []), ...(manifest.externally_connectable?.matches ?? [])].filter((m) => typeof m === 'string' && LOCAL.test(m));
@@ -751,7 +751,7 @@ function pack() {
751
751
  const keys = new Set([...sites].map(sitePattern));
752
752
  const strangers = [...new Set([...(manifest.host_permissions ?? []), ...(manifest.externally_connectable?.matches ?? [])])]
753
753
  .filter((m) => typeof m === 'string' && !keys.has(m) && walls.some((w) => { const h = hostOf(m); return !!h && (h === w || h.endsWith(`.${w}`)); }));
754
- if (strangers.length && !flag('--allow-wall-hosts')) problems.push(`The manifest names ${strangers.map(oneLine).join(', ')}: the Wall's, and no key's in this build — another workspace, or the other side's Test address. A store build asks its buyers for ${strangers.length > 1 ? 'these' : 'it'} at install for nothing: take ${strangers.length > 1 ? 'them' : 'it'} out of host_permissions and externally_connectable. (--allow-wall-hosts packs it anyway.)`);
754
+ if (strangers.length && !flag('--allow-wall-hosts')) problems.push(`The manifest names ${strangers.map(oneLine).join(', ')}: Toolaby's, and no key's in this build — another workspace, or the other side's Test address. A store build asks its buyers for ${strangers.length > 1 ? 'these' : 'it'} at install for nothing: take ${strangers.length > 1 ? 'them' : 'it'} out of host_permissions and externally_connectable. (--allow-wall-hosts packs it anyway.)`);
755
755
  if (problems.length) fail(`Not packed:\n\n${problems.map((p) => ` · ${p}`).join('\n\n')}`);
756
756
 
757
757
  const hadKey = typeof manifest.key === 'string';
@@ -782,7 +782,7 @@ function pack() {
782
782
  // Said, not refused: the links are the project's, not the build's, and the build may hold nothing of theirs yet.
783
783
  const linked = project ? publicLinks(project, folder) : [];
784
784
  if (linked.length) out(` Links in the folders a build copies as they are: ${linked.slice(0, 8).map(oneLine).join(', ')}${linked.length > 8 ? ` and ${linked.length - 8} more` : ''}. The build copies what each points at, a private file too: make each the file itself.`);
785
- if (hadKey) out(' The manifest\'s "key" is left out: the store keeps the id it gives the item. Your unpacked copy keeps the key and its own id; the Wall admits both once the listing\'s address is on the tool.');
785
+ if (hadKey) out(' The manifest\'s "key" is left out: the store keeps the id it gives the item. Your unpacked copy keeps the key and its own id; Toolaby admits both once the listing\'s address is on the tool.');
786
786
  out(' Upload it at https://chrome.google.com/webstore/devconsole — a new item, or Package → Upload new package.\n');
787
787
  }
788
788
 
@@ -807,13 +807,13 @@ function help() {
807
807
  wire <tool-id> wire the extension in this folder to your tool, on Test
808
808
  wire <tool-id> --live the same, on Live: the store build
809
809
  --into <folder> another folder --force replace the platform's files
810
- --own-popup keep your popup in front (the Wall's opens from toolaby.showPaywall())
810
+ --own-popup keep your popup in front (Toolaby's opens from toolaby.showPaywall())
811
811
  upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
812
812
  create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
813
813
  --surfaces both|popup|side-panel|none what it opens (default: both)
814
814
  tools your tools, with their ids
815
815
  pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
816
- refuses a Test key, localhost, or another of the Wall's hosts inside
816
+ refuses a Test key, localhost, or another of Toolaby's hosts inside
817
817
  --out <file.zip>
818
818
  leaves out keys, certificates, .env and credential files, source maps, secrets,
819
819
  git's files, a build's copy of a private file (compressed, UTF-16 or base64),
@@ -821,7 +821,7 @@ function help() {
821
821
  --allow-private packs them
822
822
  check [folder] whether the extension here is wired right: files, key, manifest, client, features;
823
823
  --browser loads it in Chromium (needs Playwright in the project)
824
- mcp the Wall for coding agents, an MCP server on stdio:
824
+ mcp Toolaby for coding agents, an MCP server on stdio:
825
825
  claude mcp add toolaby -- npx -y toolaby@latest mcp
826
826
  webhooks events the six events, with an example of each
827
827
  webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
@@ -829,8 +829,8 @@ function help() {
829
829
  login · whoami · logout the session, per side; a command that needs one signs you in itself
830
830
 
831
831
  Every command acts on Test (toolaby.app/test) unless told --live (toolaby.app).
832
- --wall <url> or TOOLABY_WALL names any other Wall; TOOLABY_TOKEN a session, for CI —
833
- sent to Live alone, or to the one Wall TOOLABY_TOKEN_WALL names (https://toolaby.app/test for Test).
832
+ --wall <url> or TOOLABY_WALL names any other server; TOOLABY_TOKEN a session, for CI —
833
+ sent to Live alone, or to the one server TOOLABY_TOKEN_WALL names (https://toolaby.app/test for Test).
834
834
 
835
835
  In a repository you did not write, install it (npm i -g toolaby) and type toolaby-cli: npx can run a toolaby
836
836
  the project carries, or fetch one from a registry its .npmrc names.
package/lib/check.js CHANGED
@@ -81,7 +81,7 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
81
81
  const at = (f) => join(dir, `${project.codeDir}${f}`);
82
82
  const missing = ['toolaby.js', 'toolaby.config.js'].filter((f) => !existsSync(at(f)));
83
83
  if (missing.length) { bad(`${missing.map((f) => `${project.codeDir}${f}`).join(' and ')} missing. Run \`npx -y toolaby@latest wire <tool-id>\` here.`); return false; }
84
- ok(`The Wall's files are in ${project.codeDir || 'the top folder'}.`);
84
+ ok(`Toolaby's files are in ${project.codeDir || 'the top folder'}.`);
85
85
 
86
86
  // 3. The tool key, and the tool it names — from a file: a link to a device or a pipe is read as nothing.
87
87
  const configText = readOr(at('toolaby.config.js'), '');
@@ -94,15 +94,15 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
94
94
  // project's key sent the two requests wherever it named, and a tool id of ".." made the signed-in one a request for
95
95
  // another route (25 Sep 2026).
96
96
  if (!keySite(key.site) || !TOOL_ID.test(key.slug)) {
97
- bad(`The tool key names ${JSON.stringify(key.slug)} at ${JSON.stringify(key.site)}, which is not a tool on a Wall: its address is https (http only on this machine) with nothing after the host, and its id lowercase letters, digits and dashes. Nothing was asked of it. \`npx -y toolaby@latest upgrade <tool-id>\` writes the tool's own key.`);
97
+ bad(`The tool key names ${JSON.stringify(key.slug)} at ${JSON.stringify(key.site)}, which is not a tool on a Toolaby server: its address is https (http only on this machine) with nothing after the host, and its id lowercase letters, digits and dashes. Nothing was asked of it. \`npx -y toolaby@latest upgrade <tool-id>\` writes the tool's own key.`);
98
98
  return false;
99
99
  }
100
- ok(`The tool key names ${key.slug}, on ${mode === 'live' ? 'Live' : mode === 'test' ? 'Test' : 'a development Wall'} (${key.site}).`);
100
+ ok(`The tool key names ${key.slug}, on ${mode === 'live' ? 'Live' : mode === 'test' ? 'Test' : 'a development server'} (${key.site}).`);
101
101
  const pub = await fetchBounded(`${key.site}/api/tools/${encodeURIComponent(key.slug)}`).then((r) => ({ status: r.status, json: jsonOf(r.body) })).catch(() => ({ status: 0, json: null }));
102
102
  const tool = pub.status === 200 ? pub.json : null;
103
- if (!tool) bad(pub.status === 0 ? `The Wall at ${key.site} could not be reached.` : pub.status === 404 ? `The Wall does not sell ${key.slug}: deleted, switched off, or on the other side. \`npx -y toolaby@latest tools${mode === 'live' ? ' --live' : ''}\` lists yours.` : `The Wall answered HTTP ${pub.status} for the tool.`);
103
+ if (!tool) bad(pub.status === 0 ? `Toolaby at ${key.site} could not be reached.` : pub.status === 404 ? `Toolaby does not sell ${key.slug}: deleted, switched off, or on the other side. \`npx -y toolaby@latest tools${mode === 'live' ? ' --live' : ''}\` lists yours.` : `Toolaby answered HTTP ${pub.status} for the tool.`);
104
104
  else {
105
- ok(`The Wall knows it: ${oneLine(tool.name)}.`);
105
+ ok(`Toolaby knows it: ${oneLine(tool.name)}.`);
106
106
  const jwk = tool.publicKeyJwk ?? {};
107
107
  if (key.x && jwk.x && (key.x !== jwk.x || key.y !== jwk.y)) warn('The workspace\'s signing key was rotated after this key was written. Installed copies fetch the new one; `npx -y toolaby@latest upgrade <tool-id>` writes it in.');
108
108
  }
@@ -114,8 +114,8 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
114
114
  const again = wireManifest(manifest, { site: key.site, has, remembered: null, rememberedSidePanel: null, manifestKey: typeof manifest.key === 'string' ? manifest.key : null, workerSource: workerText });
115
115
  // The background is said below, in a line of its own.
116
116
  const needed = (again.did ?? []).filter((d) => !/popup|side panel|service worker/i.test(d));
117
- if (needed.length) bad(`The manifest lacks what the Wall needs: ${needed.map(oneLine).join('; ')}. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
118
- else ok('The manifest has the permissions and host permissions the Wall needs.');
117
+ if (needed.length) bad(`The manifest lacks what Toolaby needs: ${needed.map(oneLine).join('; ')}. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
118
+ else ok('The manifest has the permissions and host permissions Toolaby needs.');
119
119
  // The identity: without the `key`, each copy's id follows its folder, and once the extension is named the tool key refuses every copy not chosen.
120
120
  if (typeof manifest.key !== 'string' && /manifestKey:\s*'[A-Za-z0-9+/]{100,}={0,2}'/.test(configText)) warn(`manifest.json has no "key": each copy's id then depends on its folder, and once your extension is named the tool key refuses any copy you did not choose on the Keys page. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
121
121
  } else warn(`The manifest is generated by ${project.label}: build, then check the built manifest has ${sitePattern(key.site)} in host_permissions and externally_connectable.`);
@@ -128,20 +128,20 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
128
128
  const wraps = wrappedBy(text);
129
129
  // What wiring again would do with it, by the same rules (wiring/manifest).
130
130
  const b = wireManifest(manifest, { site: key.site, has, workerSource: text }).background;
131
- if (!named || text === null) bad(`Nothing starts the Wall: the manifest names no background${named ? ` it can find (${oneLine(named)})` : ''}. ${upgrade} adds one.`);
131
+ if (!named || text === null) bad(`Nothing starts Toolaby: the manifest names no background${named ? ` it can find (${oneLine(named)})` : ''}. ${upgrade} adds one.`);
132
132
  else if (b.kind === 'gone') bad(`${oneLine(named)} runs ${oneLine(b.wraps)}, which is no longer in the folder. Put your background's file name in manifest.json, then ${upgrade}.`);
133
- else if (b.kind === 'rename') bad(`The Wall can't load a file named ${JSON.stringify(oneLine(b.file))}. Rename it to ${oneLine(b.to)}, put that in manifest.json, then ${upgrade}.`);
134
- else if (b.kind === 'lines') bad(`${oneLine(b.file)} never starts the Wall, so the popup cannot reach it. Add at its top: ${oneLine(project.workerLines.trim().replace(/\n+/g, ' '))}`);
133
+ else if (b.kind === 'rename') bad(`Toolaby can't load a file named ${JSON.stringify(oneLine(b.file))}. Rename it to ${oneLine(b.to)}, put that in manifest.json, then ${upgrade}.`);
134
+ else if (b.kind === 'lines') bad(`${oneLine(b.file)} never starts Toolaby, so the popup cannot reach it. Add at its top: ${oneLine(project.workerLines.trim().replace(/\n+/g, ' '))}`);
135
135
  else if (wraps) {
136
136
  // A classic one carries the key it starts with: the one in toolaby.config.js, or the background starts with an old one.
137
137
  const carried = text.match(/"key":"([^"]+)"/)?.[1];
138
- if (carried && carried !== keyText) bad(`${oneLine(named)} starts the Wall with another key than toolaby.config.js. \`npx -y toolaby@latest upgrade ${key.slug}\` writes both again.`);
139
- else ok(`The background starts the Wall, and runs yours (${oneLine(wraps)}).`);
140
- } else if (startsWall(text)) ok(`The background (${oneLine(named)}) starts the Wall.`);
141
- else bad(`${oneLine(named)} never starts the Wall, so the popup cannot reach it. ${upgrade} puts the Wall's in front of it; yours stays as it is.`);
138
+ if (carried && carried !== keyText) bad(`${oneLine(named)} starts Toolaby with another key than toolaby.config.js. \`npx -y toolaby@latest upgrade ${key.slug}\` writes both again.`);
139
+ else ok(`The background starts Toolaby, and runs yours (${oneLine(wraps)}).`);
140
+ } else if (startsWall(text)) ok(`The background (${oneLine(named)}) starts Toolaby.`);
141
+ else bad(`${oneLine(named)} never starts Toolaby, so the popup cannot reach it. ${upgrade} puts Toolaby's in front of it; yours stays as it is.`);
142
142
  } else if (project.worker && !project.extraFiles) {
143
- if (startsWall(readOr(join(dir, project.worker), ''))) ok(`The background (${oneLine(project.worker)}) starts the Wall.`);
144
- else bad(`${oneLine(project.worker)} never starts the Wall, so the popup cannot reach it. Add at its top: ${oneLine(project.workerLines.trim().replace(/\n+/g, ' '))}`);
143
+ if (startsWall(readOr(join(dir, project.worker), ''))) ok(`The background (${oneLine(project.worker)}) starts Toolaby.`);
144
+ else bad(`${oneLine(project.worker)} never starts Toolaby, so the popup cannot reach it. Add at its top: ${oneLine(project.workerLines.trim().replace(/\n+/g, ' '))}`);
145
145
  }
146
146
  // A popup the code swaps while it runs: the Wall's in front of it can be skipped.
147
147
  if (project.manifestFile && manifest?.action?.default_popup === 'toolaby-popup.html') {
@@ -151,9 +151,9 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
151
151
 
152
152
  // 5. The client, as fresh as the Wall's.
153
153
  const client = await fetchBounded(`${key.site}/client/toolaby.js`).then((r) => (r.ok && r.body ? r.body.toString('utf8') : null)).catch(() => null);
154
- if (client === null) warn('The Wall\'s current client could not be read to compare.');
155
- else if (sha(client) !== sha(readOr(at('toolaby.js'), ''))) warn(`toolaby.js is not the Wall's current client. \`npx -y toolaby@latest upgrade ${key.slug}\` replaces it (your own files stay).`);
156
- else ok('toolaby.js is the Wall\'s current client.');
154
+ if (client === null) warn('Toolaby\'s current client could not be read to compare.');
155
+ else if (sha(client) !== sha(readOr(at('toolaby.js'), ''))) warn(`toolaby.js is not Toolaby's current client. \`npx -y toolaby@latest upgrade ${key.slug}\` replaces it (your own files stay).`);
156
+ else ok('toolaby.js is Toolaby\'s current client.');
157
157
 
158
158
  // 6. The features the code gates on, against the tool's; and the page showPaywall() opens.
159
159
  const sources = sourceFiles(dir).map((f) => { try { return readText(f); } catch { return ''; } });
@@ -182,7 +182,7 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
182
182
  else if (facts?.ok) warn('No copy has checked in yet: load it at chrome://extensions and open it once.');
183
183
  // The key carries what the Free plan holds and the kind of plan sold: a change since it was written leaves it behind.
184
184
  const current = facts?.ok ? facts.json?.toolKey : null;
185
- if (typeof current === 'string' && current.split('_')[1] === mode && current !== keyText) warn(`The tool key in toolaby.config.js is older than the Wall's: what the Free plan holds, the plans or the signing key changed since it was written. \`npx -y toolaby@latest upgrade ${key.slug}\` writes the current one.`);
185
+ if (typeof current === 'string' && current.split('_')[1] === mode && current !== keyText) warn(`The tool key in toolaby.config.js is older than Toolaby's: what the Free plan holds, the plans or the signing key changed since it was written. \`npx -y toolaby@latest upgrade ${key.slug}\` writes the current one.`);
186
186
  }
187
187
 
188
188
  // 8. In a browser: the background answers.
@@ -215,7 +215,7 @@ async function browserCheck({ dir, project, manifest, ok, warn, bad }) {
215
215
  chrome.runtime.sendMessage({ action: 'getLicenseStatus' }).then((r) => r ?? null),
216
216
  new Promise((r) => setTimeout(() => r('timeout'), 8000)),
217
217
  ])).catch((e) => `error: ${e.message}`);
218
- if (answer && answer !== 'timeout' && !String(answer).startsWith('error')) ok('The background answers the Wall\'s client: its listeners registered.');
218
+ if (answer && answer !== 'timeout' && !String(answer).startsWith('error')) ok('The background answers Toolaby\'s client: its listeners registered.');
219
219
  else bad(`The background did not answer (${oneLine(answer ?? 'nothing')}): its module threw while loading, so none of its listeners registered.`);
220
220
  await page.close().catch(() => {});
221
221
  // The extension's own pages, each opened in a tab: one whose script throws is a popup that opens blank.
package/lib/manifest.js CHANGED
@@ -49,7 +49,7 @@ export function wallWorkerSource(b, config) {
49
49
  const depth = b.file.split('/').length - 1;
50
50
  const up = '../'.repeat(depth);
51
51
  const own = b.wraps.slice(b.wraps.lastIndexOf('/') + 1);
52
- const head = `// The Wall's background, in front of yours: ${b.wraps} runs as it did, then the Wall starts.\n// Written by the Wall's wiring (toolaby wire): keep it, never edit it. Your code stays in ${b.wraps}.\n// toolaby-wraps: ${b.wraps}\n`;
52
+ const head = `// Toolaby's background, in front of yours: ${b.wraps} runs as it did, then Toolaby starts.\n// Written by Toolaby's wiring (toolaby wire): keep it, never edit it. Your code stays in ${b.wraps}.\n// toolaby-wraps: ${b.wraps}\n`;
53
53
  if (!b.classic)
54
54
  return `${head}import { toolaby } from '${up || './'}toolaby.js';\nimport './${own}';\n\ntoolaby.ensureBackground();\n`;
55
55
  const c = config ?? { key: '', appPopup: '', appSidePanel: '' };
@@ -164,12 +164,12 @@ export function wireManifest(input, o) {
164
164
  const appPopup = o.ownInFront ? null : ownPopup;
165
165
  if (appPopup && declared !== WALL_POPUP) {
166
166
  manifest.action = { ...manifest.action, default_popup: WALL_POPUP };
167
- did.push(`the Wall's popup in front of ${appPopup}: it opens on to yours while the device may, and shows the panel when it may not`);
167
+ did.push(`Toolaby's popup in front of ${appPopup}: it opens on to yours while the device may, and shows the panel when it may not`);
168
168
  }
169
169
  // Asked to keep the extension's own in front after a wiring stood the Wall's there: theirs goes back.
170
170
  if (o.ownInFront && declared === WALL_POPUP && ownPopup) {
171
171
  manifest.action = { ...manifest.action, default_popup: ownPopup };
172
- did.push(`${ownPopup} back in front: toolaby.showPaywall() opens the Wall's popup`);
172
+ did.push(`${ownPopup} back in front: toolaby.showPaywall() opens Toolaby's popup`);
173
173
  }
174
174
  // The extension's own side panel, the same way: the Wall's in front, with the permission a side panel needs.
175
175
  // Chrome spells it side_panel.default_path, Firefox sidebar_action.default_panel; a manifest may carry both.
@@ -183,7 +183,7 @@ export function wireManifest(input, o) {
183
183
  // The extension's own side panel stays in front, as its popup does — and goes back there after a wiring put the Wall's.
184
184
  if (declaredPanel === WALL_SIDE_PANEL && ownSidePanel) {
185
185
  manifest.side_panel = { ...manifest.side_panel, default_path: ownSidePanel };
186
- did.push(`${ownSidePanel} back in front: toolaby.showPaywall() opens the Wall's side panel`);
186
+ did.push(`${ownSidePanel} back in front: toolaby.showPaywall() opens Toolaby's side panel`);
187
187
  }
188
188
  if (declaredSidebar === WALL_SIDE_PANEL && ownSidePanel)
189
189
  manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: ownSidePanel };
@@ -192,12 +192,12 @@ export function wireManifest(input, o) {
192
192
  manifest.side_panel = { ...manifest.side_panel, default_path: WALL_SIDE_PANEL };
193
193
  if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('sidePanel')))
194
194
  manifest.permissions = add(manifest.permissions, 'sidePanel');
195
- did.push(`the Wall's side panel in front of ${declaredPanel}: the same as the popup, at the panel's width`);
195
+ did.push(`Toolaby's side panel in front of ${declaredPanel}: the same as the popup, at the panel's width`);
196
196
  }
197
197
  // Firefox's sidebar, when it names the same page — one that names another is left alone rather than pointed elsewhere.
198
198
  if (!o.ownInFront && theirs(declaredSidebar) && (!declaredPanel || declaredSidebar === declaredPanel)) {
199
199
  manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: WALL_SIDE_PANEL };
200
- did.push(`the Wall's side panel in front of ${declaredSidebar} in Firefox's sidebar_action too`);
200
+ did.push(`Toolaby's side panel in front of ${declaredSidebar} in Firefox's sidebar_action too`);
201
201
  }
202
202
  // The background. None named, or named and not there: one of the Wall's alone, a module. The extension's own: left as
203
203
  // it is, and the Wall's stood in front of it — beside it, a module for a module and a classic script for a classic
package/lib/mcp.js CHANGED
@@ -35,7 +35,7 @@ function toolsFor(side) {
35
35
  {
36
36
  name: 'toolaby_whoami',
37
37
  title: 'Who is signed in',
38
- description: `The developer signed in to the ${side} Wall, and their workspaces.`,
38
+ description: `The developer signed in to Toolaby (${side}), and their workspaces.`,
39
39
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
40
40
  annotations: READS,
41
41
  run: (_a, api) => api('/api/cli/me'),
@@ -43,7 +43,7 @@ function toolsFor(side) {
43
43
  {
44
44
  name: 'toolaby_list_tools',
45
45
  title: 'List tools',
46
- description: `Every tool in the developer's workspaces on the ${side} Wall, with the id the other tools take.`,
46
+ description: `Every tool in the developer's workspaces on Toolaby (${side}), with the id the other tools take.`,
47
47
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
48
48
  annotations: READS,
49
49
  run: (_a, api) => api('/api/cli/tools'),
@@ -51,7 +51,7 @@ function toolsFor(side) {
51
51
  {
52
52
  name: 'toolaby_get_tool',
53
53
  title: 'Read a tool',
54
- description: `One tool on the ${side} Wall: what the Free plan holds (freePlan.holds, in a sentence), its features, its plans with what each unlocks, its tool key, and its extension: identityId is the id of every copy whose manifest has the tool's key (such a copy needs no binding, so boundId stays null), checkedInAt says whether one has checked in, inUse which builds. Then the addresses of its pages.`,
54
+ description: `One tool on Toolaby (${side}): what the Free plan holds (freePlan.holds, in a sentence), its features, its plans with what each unlocks, its tool key, and its extension: identityId is the id of every copy whose manifest has the tool's key (such a copy needs no binding, so boundId stays null), checkedInAt says whether one has checked in, inUse which builds. Then the addresses of its pages.`,
55
55
  inputSchema: { type: 'object', properties: { tool }, required: ['tool'], additionalProperties: false },
56
56
  annotations: READS,
57
57
  run: (a, api) => api(`/api/cli/tools/${encodeURIComponent(a.tool)}`),
@@ -59,7 +59,7 @@ function toolsFor(side) {
59
59
  {
60
60
  name: 'toolaby_create_tool',
61
61
  title: 'Create a tool',
62
- description: `A new tool on the ${side} Wall. This changes what the developer's own workspace sells: one more tool. Its Free plan holds everything until access is set. Then wire an extension to it: \`npx -y toolaby@latest create <id>\` for a new one, \`npx -y toolaby@latest wire <id>\` for an existing one.`,
62
+ description: `A new tool on Toolaby (${side}). This changes what the developer's own workspace sells: one more tool. Its Free plan holds everything until access is set. Then wire an extension to it: \`npx -y toolaby@latest create <id>\` for a new one, \`npx -y toolaby@latest wire <id>\` for an existing one.`,
63
63
  inputSchema: {
64
64
  type: 'object',
65
65
  properties: {
@@ -165,7 +165,7 @@ function toolsFor(side) {
165
165
  {
166
166
  name: 'toolaby_docs',
167
167
  title: 'Read the docs',
168
- description: 'The Wall\'s docs as Markdown. Without a page: the index, every page with what it covers. With a page (like "gating", "pricing", "reference/client"): that page.',
168
+ description: 'Toolaby\'s docs as Markdown. Without a page: the index, every page with what it covers. With a page (like "gating", "pricing", "reference/client"): that page.',
169
169
  inputSchema: { type: 'object', properties: { page: { type: 'string' } }, additionalProperties: false },
170
170
  annotations: READS,
171
171
  run: (a, _api, docs) => docs(a.page),
@@ -177,7 +177,7 @@ function toolsFor(side) {
177
177
  function resultOf(res) {
178
178
  const body = res.json ?? {};
179
179
  if (res.ok) return { content: [{ type: 'text', text: typeof res.text === 'string' ? res.text : JSON.stringify(body, null, 2) }], ...(typeof res.text === 'string' ? {} : { structuredContent: body }) };
180
- const why = body.message ?? body.error ?? `The Wall answered HTTP ${res.status}.`;
180
+ const why = body.message ?? body.error ?? `Toolaby answered HTTP ${res.status}.`;
181
181
  return { content: [{ type: 'text', text: why }], isError: true };
182
182
  }
183
183
 
@@ -195,7 +195,7 @@ export function runMcp({ version, side, wall, api, token }) {
195
195
  const url = clean ? `${wall}/docs/${clean}.md` : `${wall}/llms.txt`;
196
196
  // The Wall's own pages, a few megabytes at most: a redirect elsewhere handed an agent any host's page as the docs (25 Sep 2026).
197
197
  const res = await fetchBounded(url, { limit: 4 * 1024 * 1024 }).catch(() => ({ ok: false, status: 0, body: null }));
198
- if (res.ok && !res.body) return { ok: false, status: 413, json: { message: 'The docs page is larger than any the Wall serves.' } };
198
+ if (res.ok && !res.body) return { ok: false, status: 413, json: { message: 'The docs page is larger than any Toolaby serves.' } };
199
199
  const text = res.body ? res.body.toString('utf8') : '';
200
200
  return res.ok ? { ok: true, status: res.status, text } : { ok: false, status: res.status, json: { message: res.status === 404 ? `No docs page "${clean}". Call toolaby_docs without a page for the index.` : `The docs answered HTTP ${res.status}.` } };
201
201
  };
@@ -208,7 +208,7 @@ export function runMcp({ version, side, wall, api, token }) {
208
208
  return { result: { content: [{ type: 'text', text: 'A tool id is lowercase letters, digits and dashes, as toolaby_list_tools gives it.' }], isError: true } };
209
209
  }
210
210
  if (name !== 'toolaby_docs' && !token()) {
211
- return { result: { content: [{ type: 'text', text: `Not signed in to the ${side} Wall (${wall}). Run \`npx -y toolaby@latest login${side === 'Live' ? ' --live' : ''}\` in a terminal, then call this again.` }], isError: true } };
211
+ return { result: { content: [{ type: 'text', text: `Not signed in to Toolaby (${side}, ${wall}). Run \`npx -y toolaby@latest login${side === 'Live' ? ' --live' : ''}\` in a terminal, then call this again.` }], isError: true } };
212
212
  }
213
213
  try {
214
214
  return { result: resultOf(await t.run(args ?? {}, api, docs)) };
@@ -228,8 +228,8 @@ export function runMcp({ version, side, wall, api, token }) {
228
228
  return send({ id: msg.id, result: {
229
229
  protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
230
230
  capabilities: { tools: { listChanged: false } },
231
- serverInfo: { name: 'toolaby', title: 'Toolaby Wall', version },
232
- instructions: `The Toolaby Wall (${side}, ${wall}): accounts, licences, subscriptions and the paywall for Chrome extensions. Read a tool with toolaby_get_tool before changing it; the extension gates paid work with toolaby.gate({ feature }) and each feature key must exist on the tool (toolaby_set_features). Wire files with the shell: npx -y toolaby@latest create|wire|upgrade <id>, check with npx -y toolaby@latest check. toolaby_docs reads the docs.`,
231
+ serverInfo: { name: 'toolaby', title: 'Toolaby', version },
232
+ instructions: `Toolaby (${side}, ${wall}): accounts, licences, subscriptions and the paywall for Chrome extensions. Read a tool with toolaby_get_tool before changing it; the extension gates paid work with toolaby.gate({ feature }) and each feature key must exist on the tool (toolaby_set_features). Wire files with the shell: npx -y toolaby@latest create|wire|upgrade <id>, check with npx -y toolaby@latest check. toolaby_docs reads the docs.`,
233
233
  } });
234
234
  }
235
235
  case 'notifications/initialized':
package/lib/project.js CHANGED
@@ -131,7 +131,7 @@ export function detectProject(o) {
131
131
  kind: 'plasmo', label: 'Plasmo', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, appSidePanel: null, worker, built: false, foundManifest: null,
132
132
  createWorker: worker ? null : { file: workerFile, content: workerLines },
133
133
  workerLines, snippets: [{ file: 'package.json', language: 'json', code }],
134
- note: 'Plasmo builds only the pages its manifest names, so the Wall\'s popup cannot stand in front of yours: ask the gate in your popup — the lines are in the docs, under Plasmo.',
134
+ note: 'Plasmo builds only the pages its manifest names, so Toolaby\'s popup cannot stand in front of yours: ask the gate in your popup — the lines are in the docs, under Plasmo.',
135
135
  };
136
136
  }
137
137
  if (crxjs && !has('manifest.json')) {
@@ -147,10 +147,10 @@ export function detectProject(o) {
147
147
  const workerFile = worker ?? 'src/background.ts';
148
148
  const workerLines = `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`;
149
149
  const code = [
150
- `// ${configFile ?? 'manifest.config.ts'} — the Wall's needs come from toolaby.config.js: import once, spread inside defineManifest({ … })`,
150
+ `// ${configFile ?? 'manifest.config.ts'} — Toolaby's needs come from toolaby.config.js: import once, spread inside defineManifest({ … })`,
151
151
  `import { manifest as toolaby } from '${importPath(configFile ?? 'manifest.config.ts').replace(/toolaby\.js$/, 'toolaby.manifest.js')}';`,
152
152
  ``,
153
- `...toolaby, // the extension's id, the storage permission, the Wall's address`,
153
+ `...toolaby, // the extension's id, the storage permission, Toolaby's address`,
154
154
  ...(appPopup ? [`action: { default_popup: 'toolaby-popup.html' },`] : []),
155
155
  ...(appSidePanel ? [`side_panel: { default_path: 'toolaby-sidepanel.html' }, // and 'sidePanel' among the permissions`] : []),
156
156
  ...(worker ? [] : [`background: { service_worker: '${workerFile}', type: 'module' },`]),
@@ -159,7 +159,7 @@ export function detectProject(o) {
159
159
  const viteFile = first(has, ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']) ?? 'vite.config.ts';
160
160
  const inputs = [appPopup ? `popup: '${appPopup}'` : null, appSidePanel ? `sidepanel: '${appSidePanel}'` : null].filter(Boolean).join(', ');
161
161
  const vite = [
162
- `// ${viteFile} — CRXJS builds only the pages the manifest names: this keeps ${appPopup && appSidePanel ? 'your popup and side panel' : appSidePanel ? 'your side panel' : 'your popup'} built, for the Wall's to hand over to`,
162
+ `// ${viteFile} — CRXJS builds only the pages the manifest names: this keeps ${appPopup && appSidePanel ? 'your popup and side panel' : appSidePanel ? 'your side panel' : 'your popup'} built, for Toolaby's to hand over to`,
163
163
  `export default defineConfig({`,
164
164
  ` plugins: [crx({ manifest })],`,
165
165
  ` build: { rollupOptions: { input: { ${inputs} } } },`,
package/lib/summary.js CHANGED
@@ -21,51 +21,51 @@ export function wiringSummary(o) {
21
21
  if (o.appPopup) {
22
22
  rows.push(o.popupSwap
23
23
  ? { tone: 'warn', label: 'Popup', text: `Your code handles the popup itself (${o.popupSwap.call} in ${o.popupSwap.file}), so keep yours in front`, fix: 'own-popup' }
24
- : { tone: 'ok', label: 'Popup', text: `The Wall checks first, then opens ${o.appPopup}` });
24
+ : { tone: 'ok', label: 'Popup', text: `Toolaby checks first, then opens ${o.appPopup}` });
25
25
  }
26
26
  else if (ownPopup && o.keptForSwap && o.popupSwap)
27
27
  rows.push({ tone: 'ok', label: 'Popup', text: `Yours stays in front, as your code handles it (${o.popupSwap.call} in ${o.popupSwap.file})` });
28
28
  else if (ownPopup)
29
- rows.push({ tone: 'ok', label: 'Popup', text: `Yours stays in front; toolaby.showPaywall() opens the Wall's` });
29
+ rows.push({ tone: 'ok', label: 'Popup', text: `Yours stays in front; toolaby.showPaywall() opens Toolaby's` });
30
30
  else if (p.note)
31
31
  rows.push({ tone: 'warn', label: 'Popup', text: p.note });
32
32
  else if (!o.appSidePanel && !w?.ownSidePanel)
33
- rows.push({ tone: 'ok', label: 'Popup', text: 'None: the Wall steps in where your code calls toolaby.gate()' });
33
+ rows.push({ tone: 'ok', label: 'Popup', text: 'None: Toolaby steps in where your code calls toolaby.gate()' });
34
34
  if (o.appSidePanel)
35
- rows.push({ tone: 'ok', label: 'Side panel', text: `The Wall checks first, then opens ${o.appSidePanel}` });
35
+ rows.push({ tone: 'ok', label: 'Side panel', text: `Toolaby checks first, then opens ${o.appSidePanel}` });
36
36
  else if (w?.ownSidePanel)
37
- rows.push({ tone: 'ok', label: 'Side panel', text: `Yours stays in front; toolaby.showPaywall() opens the Wall's` });
37
+ rows.push({ tone: 'ok', label: 'Side panel', text: `Yours stays in front; toolaby.showPaywall() opens Toolaby's` });
38
38
  // The background: what starts the Wall.
39
39
  if (w) {
40
40
  const b = w.background;
41
41
  if (b.kind === 'new')
42
- rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts the Wall` });
42
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts Toolaby` });
43
43
  else if (b.kind === 'wrap')
44
- rows.push({ tone: 'ok', label: 'Background', text: `${b.wraps} runs as before, and the Wall starts with it` });
44
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.wraps} runs as before, and Toolaby starts with it` });
45
45
  else if (b.kind === 'started')
46
- rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts the Wall` });
46
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts Toolaby` });
47
47
  else if (b.kind === 'lines')
48
48
  rows.push({ tone: 'todo', label: 'Background', text: `Add these 2 lines to the top of ${b.file}:`, code: `import { toolaby } from '${importPath(b.file)}';\n\ntoolaby.startBackground();` });
49
49
  else if (b.kind === 'gone')
50
50
  rows.push({ tone: 'todo', label: 'Background', text: `${b.wraps} is no longer in the folder. Put your background's file name in manifest.json:`, code: '"background": { "service_worker": "your-background.js" }', again: true });
51
51
  else
52
- rows.push({ tone: 'todo', label: 'Background', text: `The Wall can't load a file named "${b.file}". Rename it to ${b.to}, and in manifest.json:`, code: `"background": { "service_worker": "${b.to}" }`, again: true });
52
+ rows.push({ tone: 'todo', label: 'Background', text: `Toolaby can't load a file named "${b.file}". Rename it to ${b.to}, and in manifest.json:`, code: `"background": { "service_worker": "${b.to}" }`, again: true });
53
53
  }
54
54
  else if (p.extraFiles?.length)
55
- rows.push({ tone: 'ok', label: 'Background', text: `${p.label} starts the Wall (${p.extraFiles.map((f) => f.to).join(', ')})` });
55
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.label} starts Toolaby (${p.extraFiles.map((f) => f.to).join(', ')})` });
56
56
  else if (p.createWorker)
57
- rows.push({ tone: 'ok', label: 'Background', text: `${p.createWorker.file} starts the Wall` });
57
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.createWorker.file} starts Toolaby` });
58
58
  else if (p.worker && !o.workerStarts)
59
59
  rows.push({ tone: 'todo', label: 'Background', text: `Add these 2 lines to the top of ${p.worker}:`, code: p.workerLines.trim() });
60
60
  else if (p.worker)
61
- rows.push({ tone: 'ok', label: 'Background', text: `${p.worker} starts the Wall` });
61
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.worker} starts Toolaby` });
62
62
  // With no page of the Wall's in front, the paywall opens only where the code asks for it: when nothing does, the lines.
63
63
  if (!o.appPopup && !o.appSidePanel && o.gates === false && p.kind === 'plain') {
64
64
  rows.push({ tone: 'todo', label: 'Paywall', text: p.built ? 'Nothing opens it yet. Where your paid action starts, add (the import is the path to toolaby.js):' : 'Nothing opens it yet. Where your paid action starts, in a module script, add:', code: `import { toolaby } from '${p.built ? './toolaby.js' : '/toolaby.js'}';\n\nconst permit = await toolaby.gate();\nif (!permit.allowed) return toolaby.showPaywall(permit);` });
65
65
  }
66
66
  // The manifest: what it gained, or, generated by a framework, what its config needs.
67
67
  if (w)
68
- rows.push({ tone: 'ok', label: 'Manifest', text: w.added.length ? w.added.join(' · ') : 'Has what the Wall needs' });
68
+ rows.push({ tone: 'ok', label: 'Manifest', text: w.added.length ? w.added.join(' · ') : 'Has what Toolaby needs' });
69
69
  for (const sn of p.snippets)
70
70
  rows.push({ tone: 'todo', label: 'Manifest', text: `Add to ${sn.file}:`, code: sn.code.trim() });
71
71
  if (p.built)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "The Toolaby command line: sign in once, then wire any Chrome extension to your tool on the Wall — sign-in, the Free plan, licences and subscriptions — or start a new one; check it, and give coding agents the Wall as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {