toolaby 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,14 +3,16 @@
3
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.
4
4
 
5
5
  ```sh
6
- npm i -g toolaby # once (or run every command through `npx -y toolaby@latest …`); on Windows, type toolaby-cli
7
- toolaby wire <tool-id> # in your extension's folder, on Test; signs you in first if it must
8
- toolaby wire <tool-id> --live # the same, on Live: the store build
6
+ npm i -g toolaby # once; or run every command through `npx -y toolaby@latest …`
7
+ toolaby-cli wire <tool-id> # in your extension's folder, on Test; signs you in first if it must
8
+ toolaby-cli wire <tool-id> --live # the same, on Live: the store build
9
9
  ```
10
10
 
11
+ The command is `toolaby-cli`: on Windows, the Command Prompt runs a `toolaby.js` in the current folder before a command named `toolaby`, and every wired folder has one. Below, `toolaby <command>` names each command.
12
+
11
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.
12
14
 
13
- Every command acts on the test Wall (`test.toolaby.app`) unless told `--live` (`toolaby.app`), as Stripe's command line does. `toolaby tools` lists your ids on either side.
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.
14
16
 
15
17
  ## What it does
16
18
 
package/bin.mjs CHANGED
@@ -17,7 +17,7 @@
17
17
  // toolaby whoami · logout · help
18
18
  //
19
19
  // Test and live, as Stripe's command line has them: every command acts on
20
- // the TEST Wall (test.toolaby.app) unless told `--live`, so the everyday
20
+ // the TEST Wall (toolaby.app/test) unless told `--live`, so the everyday
21
21
  // command is the safe one and going live is a word you type on purpose.
22
22
  // Signed in, the command acts as you and only on your tools, as Clerk's and
23
23
  // Stripe's do: nothing is fetched for a tool your workspaces do not own. The
@@ -37,14 +37,14 @@ import { deflateRawSync } from 'node:zlib';
37
37
  import { homedir } from 'node:os';
38
38
  import { spawn } from 'node:child_process';
39
39
  import { createHmac, randomBytes } from 'node:crypto';
40
- import { wireManifest, drawsItself, WORKER_LINES, wallWorkerSource, configValues, wrappedBy, namedWorker as workerIn, startsPlatform as startsWall, CLASSIC_CLIENT } from './lib/manifest.js';
40
+ import { wireManifest, sitePattern, drawsItself, WORKER_LINES, wallWorkerSource, configValues, wrappedBy, namedWorker as workerIn, startsPlatform as startsWall, CLASSIC_CLIENT } from './lib/manifest.js';
41
41
  import { wiringSummary, popupSwapIn, gatesIn, ownCode } from './lib/summary.js';
42
42
  import { detectProject, CODE_FILES, POPUP_FILES, SIDE_PANEL_FILES, filesToWrite, fillPages, scaffoldManifest, scaffoldFilesFor, surfacesOf, hasPopup, hasSidePanel, SURFACES, STALE_FILES, BUILD_COPY_NOTE, AGENT_FILES, AGENT_ENTRY_FILES } from './lib/project.js';
43
43
  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.7.1';
47
+ const VERSION = '1.8.0';
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.
@@ -54,7 +54,8 @@ const positional = rest.filter((a) => !a.startsWith('--'));
54
54
  const opt = (k, d = null) => { const i = rest.indexOf(k); return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : d; };
55
55
  // `upgrade` is `wire --force`: the platform's files replaced with the current ones, the extension's own untouched.
56
56
  const flag = (k) => rest.includes(k) || (k === '--force' && command === 'upgrade');
57
- const WALLS = { test: 'https://test.toolaby.app', live: 'https://toolaby.app' };
57
+ // Test shares Live's addresses under /test, as Stripe keeps both modes on its own hosts.
58
+ const WALLS = { test: 'https://toolaby.app/test', live: 'https://toolaby.app' };
58
59
  const live = flag('--live');
59
60
  // An address as fetch() reads it: a URL loses its tabs and line breaks when it is parsed, and kept in the name every
60
61
  // message prints, a --wall or a TOOLABY_WALL with one in it drew lines of its own (25 Sep 2026).
@@ -167,11 +168,11 @@ async function login() {
167
168
  }
168
169
  const why = poll.json.error;
169
170
  if (why === 'slow_down') every += 5000;
170
- else if (why === 'expired_token') fail('The code expired before it was approved. Run `toolaby login` again.');
171
+ else if (why === 'expired_token') fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
171
172
  else if (why === 'access_denied') fail('The sign-in was denied in the browser. Nothing was signed in.');
172
173
  else if (why && why !== 'authorization_pending') fail(`The Wall answered "${oneLine(why)}"${poll.json.error_description ? `: ${oneLine(poll.json.error_description)}` : ''}.`);
173
174
  }
174
- fail('The code expired before it was approved. Run `toolaby login` again.');
175
+ fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
175
176
  }
176
177
 
177
178
  /** A token to act with: the stored one, else a sign-in right here — `wire` in a fresh terminal is one command, not two. */
@@ -183,9 +184,9 @@ async function tokenOrLogin() {
183
184
 
184
185
  async function whoami() {
185
186
  const token = storedToken();
186
- if (!token) fail(`Not signed in to ${side} (${wall}). Run \`toolaby login${live ? ' --live' : ''}\`.${envElsewhere()}`);
187
+ if (!token) fail(`Not signed in to ${side} (${wall}). Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.${envElsewhere()}`);
187
188
  const me = await api('/api/cli/me', { token });
188
- if (me.status === 401) fail(`The session for ${wall} is no longer valid. Run \`toolaby login\`.`);
189
+ if (me.status === 401) fail(`The session for ${wall} is no longer valid. Run \`npx -y toolaby@latest login\`.`);
189
190
  if (!me.ok) fail(`The Wall answered HTTP ${me.status}.`);
190
191
  out(`\n ${oneLine(me.json.email)} · ${side} · ${wall}\n`);
191
192
  for (const w of me.json.workspaces) out(` ${oneLine(w.name)} (${oneLine(w.slug)}, ${oneLine(w.role)}) ${oneLine(w.site)}`);
@@ -207,7 +208,7 @@ async function logout() {
207
208
  async function tools() {
208
209
  const token = await tokenOrLogin();
209
210
  const res = await api('/api/cli/tools', { token });
210
- if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`toolaby login${live ? ' --live' : ''}\`.`);
211
+ if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
211
212
  if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
212
213
  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; }
213
214
  out(`\n ${side}\n`);
@@ -276,7 +277,7 @@ async function filesFor(slug, scaffold, manifestKey = null, surfaces = null) {
276
277
 
277
278
  async function wire() {
278
279
  const slug = positional[0];
279
- if (!isId(slug)) fail(command === 'upgrade' ? 'usage: toolaby upgrade <tool-id> [--into <folder>] — `toolaby tools` lists your ids' : 'usage: toolaby wire <tool-id> [--into <folder>] [--force] [--adopt-key] — `toolaby tools` lists your ids');
280
+ if (!isId(slug)) fail(command === 'upgrade' ? 'usage: npx -y toolaby@latest upgrade <tool-id> [--into <folder>] — `npx -y toolaby@latest tools` lists your ids' : 'usage: npx -y toolaby@latest wire <tool-id> [--into <folder>] [--force] [--adopt-key] — `npx -y toolaby@latest tools` lists your ids');
280
281
  const dir = resolve(opt('--into') ?? '.');
281
282
  if (!existsSync(dir)) fail(`${dir} does not exist`);
282
283
  const tree = listTree(dir, 3);
@@ -371,7 +372,7 @@ async function wire() {
371
372
 
372
373
  async function create() {
373
374
  const slug = positional[0];
374
- if (!isId(slug)) fail('usage: toolaby create <tool-id> [--dir <folder>] — `toolaby tools` lists your ids');
375
+ if (!isId(slug)) fail('usage: npx -y toolaby@latest create <tool-id> [--dir <folder>] — `npx -y toolaby@latest tools` lists your ids');
375
376
  const dir = resolve(opt('--dir') ?? slug);
376
377
  if (there(dir)) fail(`${dir} already exists`);
377
378
  // Which surfaces this extension is made with, asked first: the files for coding agents name only what is written.
@@ -442,7 +443,7 @@ function devSecret() {
442
443
  }
443
444
  async function catalogue() {
444
445
  const res = await api('/api/cli/webhooks/events', { token: await tokenOrLogin() });
445
- if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`toolaby login${live ? ' --live' : ''}\`.`);
446
+ if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
446
447
  if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
447
448
  return res.json.events;
448
449
  }
@@ -462,7 +463,7 @@ async function webhooks() {
462
463
  }
463
464
  if (sub === 'trigger') {
464
465
  const type = positional[1];
465
- if (!type) fail('usage: toolaby webhooks trigger <event> [--to <url>] [--secret <whsec_…>] — `toolaby webhooks events` lists them');
466
+ if (!type) fail('usage: npx -y toolaby@latest webhooks trigger <event> [--to <url>] [--secret <whsec_…>] — `npx -y toolaby@latest webhooks events` lists them');
466
467
  const to = opt('--to');
467
468
  if (!to) {
468
469
  const res = await api('/api/cli/webhooks/trigger', { method: 'POST', body: { type, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
@@ -472,7 +473,7 @@ async function webhooks() {
472
473
  }
473
474
  const events = await catalogue();
474
475
  const event = events.find((e) => e.type === type);
475
- if (!event) fail(`No event "${type}". \`toolaby webhooks events\` lists them.`);
476
+ if (!event) fail(`No event "${type}". \`npx -y toolaby@latest webhooks events\` lists them.`);
476
477
  const secret = opt('--secret') ?? devSecret();
477
478
  if (!/^whsec_[A-Za-z0-9+/=]+$/.test(secret)) fail('--secret must be a Standard Webhooks secret: whsec_ followed by base64.');
478
479
  const id = `msg_${randomBytes(12).toString('base64url')}`;
@@ -494,14 +495,14 @@ async function webhooks() {
494
495
  const res = await api(`/api/cli/webhooks/endpoints${ws ? `?workspace=${encodeURIComponent(ws)}` : ''}`, { token: await tokenOrLogin() });
495
496
  if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
496
497
  out(`\n ${oneLine(res.json.workspace)} · ${side}\n`);
497
- if (res.json.endpoints.length === 0) out(' None yet. `toolaby webhooks add <url>` registers one.');
498
+ if (res.json.endpoints.length === 0) out(' None yet. `npx -y toolaby@latest webhooks add <url>` registers one.');
498
499
  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(', '))})` : ''}`);
499
500
  out();
500
501
  return;
501
502
  }
502
503
  if (sub === 'add') {
503
504
  const url = positional[1];
504
- if (!url) fail('usage: toolaby webhooks add <url> [--description <text>] [--events a.b,c.d] [--workspace <slug>]');
505
+ if (!url) fail('usage: npx -y toolaby@latest webhooks add <url> [--description <text>] [--events a.b,c.d] [--workspace <slug>]');
505
506
  const events = (opt('--events') ?? '').split(',').map((e) => e.trim()).filter(Boolean);
506
507
  const res = await api('/api/cli/webhooks/endpoints', { method: 'POST', body: { url, description: opt('--description') ?? '', events, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
507
508
  if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
@@ -510,13 +511,13 @@ async function webhooks() {
510
511
  }
511
512
  if (sub === 'remove') {
512
513
  const id = positional[1];
513
- if (!id) fail('usage: toolaby webhooks remove <ep_…> [--workspace <slug>] — `toolaby webhooks endpoints` lists them');
514
+ if (!id) fail('usage: npx -y toolaby@latest webhooks remove <ep_…> [--workspace <slug>] — `npx -y toolaby@latest webhooks endpoints` lists them');
514
515
  const res = await api('/api/cli/webhooks/endpoints', { method: 'DELETE', body: { id, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
515
516
  if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
516
517
  out(`\n Removed ${oneLine(res.json.removed)} · ${oneLine(res.json.workspace)} · ${side}. Deliveries to it stop at once.\n`);
517
518
  return;
518
519
  }
519
- fail('usage: toolaby webhooks events | trigger <event> [--to <url>] | endpoints | add <url> | remove <id>');
520
+ fail('usage: npx -y toolaby@latest webhooks events | trigger <event> [--to <url>] | endpoints | add <url> | remove <id>');
520
521
  }
521
522
 
522
523
  // ── pack: the zip the Chrome Web Store takes ──────────────────────────────────────────────────────────────────────────
@@ -732,7 +733,7 @@ function pack() {
732
733
  // The Wall the key names must be reachable from the extension, or sign-in and the purchase handoff fail.
733
734
  for (const site of sites) {
734
735
  const hosts = manifest.host_permissions ?? []; const ec = manifest.externally_connectable?.matches ?? [];
735
- if (!hosts.includes(`${site}/*`) || !ec.includes(`${site}/*`)) problems.push(`The manifest does not name ${oneLine(site)}/* in both host_permissions and externally_connectable: sign-in and the purchase would not reach the extension. \`npx -y toolaby@latest wire <tool-id> --live --force\` adds them.`);
736
+ if (!hosts.includes(sitePattern(site)) || !ec.includes(sitePattern(site))) problems.push(`The manifest does not name ${oneLine(sitePattern(site))} in both host_permissions and externally_connectable: sign-in and the purchase would not reach the extension. \`npx -y toolaby@latest wire <tool-id> --live --force\` adds them.`);
736
737
  }
737
738
  if (problems.length) fail(`Not packed:\n\n${problems.map((p) => ` · ${p}`).join('\n\n')}`);
738
739
 
@@ -784,35 +785,37 @@ function help() {
784
785
  out(`
785
786
  toolaby — the Toolaby command line (${VERSION})
786
787
 
787
- toolaby wire <tool-id> wire the extension in this folder to your tool, on Test
788
- toolaby wire <tool-id> --live the same, on Live: the store build
788
+ npx -y toolaby@latest <command>, or installed (npm i -g toolaby): toolaby-cli <command>
789
+
790
+ wire <tool-id> wire the extension in this folder to your tool, on Test
791
+ wire <tool-id> --live the same, on Live: the store build
789
792
  --into <folder> another folder --force replace the platform's files
790
793
  --own-popup keep your popup in front (the Wall's opens from toolaby.showPaywall())
791
- toolaby upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
792
- toolaby create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
794
+ upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
795
+ create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
793
796
  --surfaces both|popup|side-panel|none what it opens (default: both)
794
- toolaby tools your tools, with their ids
795
- toolaby pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
797
+ tools your tools, with their ids
798
+ pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
796
799
  refuses a Test key or localhost inside --out <file.zip>
797
800
  leaves out keys, certificates, .env and credential files, source maps, secrets,
798
801
  git's files, a build's copy of a private file (compressed, UTF-16 or base64),
799
802
  an image that is none; refuses a build if one is a file the manifest names
800
803
  --allow-private packs them
801
- toolaby check [folder] whether the extension here is wired right: files, key, manifest, client, features;
804
+ check [folder] whether the extension here is wired right: files, key, manifest, client, features;
802
805
  --browser loads it in Chromium (needs Playwright in the project)
803
- toolaby mcp the Wall for coding agents, an MCP server on stdio:
806
+ mcp the Wall for coding agents, an MCP server on stdio:
804
807
  claude mcp add toolaby -- npx -y toolaby@latest mcp
805
- toolaby webhooks events the six events, with an example of each
806
- toolaby webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
807
- toolaby webhooks endpoints your endpoints; add <url> registers one (secret shown once), remove <id> removes one
808
- toolaby login · whoami · logout the session, per side; a command that needs one signs you in itself
808
+ webhooks events the six events, with an example of each
809
+ webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
810
+ webhooks endpoints your endpoints; add <url> registers one (secret shown once), remove <id> removes one
811
+ login · whoami · logout the session, per side; a command that needs one signs you in itself
809
812
 
810
- Every command acts on Test (test.toolaby.app) unless told --live (toolaby.app).
813
+ Every command acts on Test (toolaby.app/test) unless told --live (toolaby.app).
811
814
  --wall <url> or TOOLABY_WALL names any other Wall; TOOLABY_TOKEN a session, for CI —
812
- sent to Live alone, or to the one Wall TOOLABY_TOKEN_WALL names (https://test.toolaby.app for Test).
815
+ sent to Live alone, or to the one Wall TOOLABY_TOKEN_WALL names (https://toolaby.app/test for Test).
813
816
 
814
- Not installed: npx -y toolaby@latest <command>. In a repository you did not write, install it (npm i -g toolaby)
815
- and type toolaby: npx can run a toolaby the project carries, or fetch one from a registry its .npmrc names.
817
+ In a repository you did not write, install it (npm i -g toolaby) and type toolaby-cli: npx can run a toolaby
818
+ the project carries, or fetch one from a registry its .npmrc names.
816
819
  `);
817
820
  }
818
821
 
package/lib/check.js CHANGED
@@ -8,7 +8,7 @@ import { join, relative, resolve } from 'node:path';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { createRequire } from 'node:module';
10
10
  import { oneLine, keySite, fetchBounded, readText } from './safe.js';
11
- import { wrappedBy, namedWorker, startsPlatform as startsWall } from './manifest.js';
11
+ import { wrappedBy, namedWorker, startsPlatform as startsWall, sitePattern } from './manifest.js';
12
12
  import { popupSwapIn, ownCode } from './summary.js';
13
13
 
14
14
  const KEY = /tk_(test|live|dev)_[A-Za-z0-9_-]{20,}/;
@@ -118,7 +118,7 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
118
118
  else ok('The manifest has the permissions and host permissions the Wall 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
- } else warn(`The manifest is generated by ${project.label}: build, then check the built manifest has ${key.site}/* in host_permissions and externally_connectable.`);
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.`);
122
122
 
123
123
  // 4b. The background starts the Wall: the popup and every page ask it everything, so without it nothing answers.
124
124
  const upgrade = `\`npx -y toolaby@latest upgrade ${key.slug}\``;
package/lib/manifest.js CHANGED
@@ -69,6 +69,19 @@ export const loadableName = (file) => {
69
69
  };
70
70
  /** The background the manifest names, as the folder spells it: "./background.js" is background.js. */
71
71
  export const namedWorker = (manifest) => typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
72
+ /**
73
+ * The match pattern a manifest names the Wall by: the origin of the key's site, whatever path follows it. Test shares
74
+ * Live's hosts under /test (`https://northwind.toolaby.app/test`), so one pattern — `https://northwind.toolaby.app/*` —
75
+ * lets the same build reach either side, and a Live wiring's manifest is the one it always was.
76
+ */
77
+ export function sitePattern(site) {
78
+ try {
79
+ return `${new URL(site).origin}/*`;
80
+ }
81
+ catch {
82
+ return `${site.replace(/\/+$/, '')}/*`;
83
+ }
84
+ }
72
85
  /**
73
86
  * Whether an earlier wiring left the extension's own pages drawing the panel: a configuration that says '' for both
74
87
  * pages while the manifest names none of the Wall's. That is what `create` makes, and the "your popup first" shape
@@ -110,16 +123,17 @@ export function wireManifest(input, o) {
110
123
  did.push('the "storage" permission');
111
124
  added.push('storage');
112
125
  }
113
- if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(`${o.site}/*`))) {
114
- manifest.host_permissions = add(manifest.host_permissions, `${o.site}/*`);
115
- did.push(`a host permission for ${o.site}`);
126
+ const pattern = sitePattern(o.site);
127
+ if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(pattern))) {
128
+ manifest.host_permissions = add(manifest.host_permissions, pattern);
129
+ did.push(`a host permission for ${host}`);
116
130
  if (!added.includes(host))
117
131
  added.push(host);
118
132
  }
119
133
  const ec = manifest.externally_connectable ?? {};
120
- if (!(Array.isArray(ec.matches) && ec.matches.includes(`${o.site}/*`))) {
121
- manifest.externally_connectable = { ...ec, matches: add(ec.matches, `${o.site}/*`) };
122
- did.push(`externally_connectable for ${o.site}`);
134
+ if (!(Array.isArray(ec.matches) && ec.matches.includes(pattern))) {
135
+ manifest.externally_connectable = { ...ec, matches: add(ec.matches, pattern) };
136
+ did.push(`externally_connectable for ${host}`);
123
137
  if (!added.includes(host))
124
138
  added.push(host);
125
139
  }
package/lib/project.js CHANGED
@@ -1,4 +1,21 @@
1
1
  // Generated from wall/src/packages/wiring/project.ts by wall/scripts/pack-create.mjs — edit there.
2
+ /**
3
+ * What kind of extension project a folder is, and where the platform's
4
+ * files go in it — the same files for every kind. A plain extension has
5
+ * its manifest.json at the root and everything beside it. A framework
6
+ * (WXT, Plasmo, CRXJS) generates the manifest at build time, bundles the
7
+ * background, and copies a folder of files as they are: the client goes
8
+ * where the build can import it, the Wall's popup — and its side panel,
9
+ * where the extension has one — where the build copies verbatim. What the manifest and the background need is the framework's
10
+ * own extension point where it has one — WXT loads a module from modules/
11
+ * by itself, so the wiring writes one and nothing is pasted — and, where it
12
+ * has none (Plasmo, CRXJS), a snippet for its config: shown, never written
13
+ * into someone's TypeScript. Pure:
14
+ * given what is in the folder, it says what to write and what to say.
15
+ * Shared by the browser (rb/WireExtension) and the terminal
16
+ * (`toolaby wire`, which carries a transpiled copy).
17
+ */
18
+ import { sitePattern } from './manifest.js';
2
19
  /** What to tell a developer whose build ships the extension — the Wire button, the command and the Set up page all say it in these words. */
3
20
  export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies every toolaby* file to its output — toolaby.js, toolaby.config.js, toolaby-popup.html and the rest — the way it copies your popup.html.';
4
21
  /** Always written, where the build imports from: the platform as one file (the client and the popup UI), its config, its types. */
@@ -109,7 +126,7 @@ export function detectProject(o) {
109
126
  const worker = first(has, [`${src}background.ts`, `${src}background.js`, `${src}background/index.ts`, `${src}background/index.js`]);
110
127
  const workerFile = worker ?? `${src}background.ts`;
111
128
  const workerLines = `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`;
112
- const code = JSON.stringify({ manifest: { ...(o.manifestKey ? { key: o.manifestKey } : {}), permissions: ['storage'], host_permissions: [`${site}/*`], externally_connectable: { matches: [`${site}/*`] } } }, null, 2).replace(/^\{\n|\n\}$/g, '');
129
+ const code = JSON.stringify({ manifest: { ...(o.manifestKey ? { key: o.manifestKey } : {}), permissions: ['storage'], host_permissions: [sitePattern(site)], externally_connectable: { matches: [sitePattern(site)] } } }, null, 2).replace(/^\{\n|\n\}$/g, '');
113
130
  return {
114
131
  kind: 'plasmo', label: 'Plasmo', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, appSidePanel: null, worker, built: false, foundManifest: null,
115
132
  createWorker: worker ? null : { file: workerFile, content: workerLines },
package/lib/safe.js CHANGED
@@ -60,15 +60,17 @@ export const oneLine = (value) => String(value).replace(/\s+/g, ' ').slice(0, 50
60
60
  const wallScheme = (url) => url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.test(url.hostname));
61
61
 
62
62
  /**
63
- * The Wall a tool key names, when `check` may ask it anything: an origin, https or on this machine, and nothing else —
64
- * no path, no credentials, no other scheme — or null. The key is read from the project's own toolaby.config.js, and a
63
+ * The Wall a tool key names, when `check` may ask it anything: an origin, https or on this machine, with nothing after
64
+ * it but Test's `/test`, where Test shares Live's hosts — no other path, no query, no credentials, no other scheme.
65
+ * Null otherwise. The key is read from the project's own toolaby.config.js, and a
65
66
  * cloned project's key sent `check`'s requests wherever it named: a server inside the developer's network, any path on
66
67
  * any host (25 Sep 2026).
67
68
  */
68
69
  export function keySite(site) {
69
70
  let url;
70
71
  try { url = new URL(site); } catch { return null; }
71
- return url.origin === site && wallScheme(url) ? site : null;
72
+ const path = url.pathname === '/' ? '' : url.pathname;
73
+ return (path === '' || path === '/test') && `${url.origin}${path}` === site && wallScheme(url) ? site : null;
72
74
  }
73
75
 
74
76
  /**
@@ -673,13 +675,17 @@ function publicLinkEntries(root, packed = null) {
673
675
  /**
674
676
  * The page `login` opens: the one the Wall gave, when it is the Wall's own over https — or, for a Wall on this
675
677
  * machine, a page on this machine. Anything else is null, and is not opened: an address elsewhere, a downgrade to
676
- * http, or a scheme such as file: that opens whatever it names.
678
+ * http, or a scheme such as file: that opens whatever it names. A Wall under a path gives a page under it: Test at
679
+ * toolaby.app/test approves its codes at toolaby.app/test/…, and Live's page of the same name knows none of them (a
680
+ * root path resolved against Test's own base loses /test: 29 Sep 2026).
677
681
  */
678
682
  export function signInPage(link, wall) {
679
683
  if (typeof link !== 'string') return null;
680
684
  let home; let page;
681
685
  try { home = new URL(wall); page = new URL(link, home); } catch { return null; }
682
686
  if (page.username || page.password) return null;
687
+ const base = home.pathname.replace(/\/+$/, '');
688
+ if (base && page.pathname !== base && !page.pathname.startsWith(`${base}/`)) return null;
683
689
  if (page.protocol === 'https:' && page.origin === home.origin) return page.href;
684
690
  const web = page.protocol === 'http:' || page.protocol === 'https:';
685
691
  return web && LOOPBACK.test(home.hostname) && LOOPBACK.test(page.hostname) ? page.href : null;
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
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": {
7
- "toolaby-cli": "bin.mjs",
8
- "toolaby": "bin.mjs"
7
+ "toolaby-cli": "bin.mjs"
9
8
  },
10
9
  "files": [
11
10
  "bin.mjs",