toolaby 1.6.5 → 1.7.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
@@ -20,8 +20,11 @@ Every command acts on the test Wall (`test.toolaby.app`) unless told `--live` (`
20
20
 
21
21
  - 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;
22
22
  - it adds three lines to your `manifest.json` (a permission and your Wall address) and creates a background file if you have none;
23
+ - 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;
23
24
  - 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.
24
25
 
26
+ 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.
27
+
25
28
  If the extension is built with WXT, CRXJS or Plasmo, the command puts the same files where that build expects them and prints the few lines to paste into the framework's config, because the framework generates the manifest itself. Nothing is installed from npm.
26
29
 
27
30
  **`toolaby create <tool-id>`** makes a new, minimal extension in a folder named after the tool, ready for *Load unpacked*.
package/bin.mjs CHANGED
@@ -37,13 +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 } from './lib/manifest.js';
40
+ import { wireManifest, drawsItself, WORKER_LINES, wallWorkerSource, configValues, wrappedBy, namedWorker as workerIn, startsPlatform as startsWall, CLASSIC_CLIENT } from './lib/manifest.js';
41
+ import { wiringSummary, popupSwapIn, ownCode } from './lib/summary.js';
41
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';
42
43
  import { runMcp } from './lib/mcp.js';
43
44
  import { runCheck } from './lib/check.js';
44
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';
45
46
 
46
- const VERSION = '1.6.5';
47
+ const VERSION = '1.7.0';
47
48
  const CLIENT_ID = 'toolaby-cli';
48
49
  const argv = process.argv.slice(2);
49
50
  // `--version` and `--help` are commands of their own; every other command is the first bare word.
@@ -66,7 +67,13 @@ const side = wall === WALLS.live ? 'Live' : wall === WALLS.test ? 'Test' : wall;
66
67
  // nor any character that shows nothing — a zero width, a mark of direction, a filler — which could hide a word.
67
68
  const plain = (s) => String(s).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u115f\u1160\u200b-\u200f\u2028-\u202e\u2060-\u2069\u2800\u3164\ufeff\uffa0]/g, '');
68
69
  const out = (s = '') => console.log(plain(s));
69
- const fail = (s) => { console.error(`\n ${plain(s)}\n`); process.exit(1); };
70
+ // Colour, the command's own: put on after a piece is made plain, never taken from what it prints; off when the output is
71
+ // not a terminal, and when NO_COLOR asks (FORCE_COLOR turns it on). A line of tinted pieces goes out through `say`.
72
+ const colour = !process.env.NO_COLOR && (process.env.FORCE_COLOR ? process.env.FORCE_COLOR !== '0' : !!process.stdout.isTTY && process.env.TERM !== 'dumb');
73
+ const tint = (code) => (s) => (colour ? `\x1b[${code}m${plain(s)}\x1b[0m` : plain(s));
74
+ const [green, yellow, red, cyan, bold, dim] = ['32', '33', '31', '36', '1', '2'].map(tint);
75
+ const say = (s = '') => console.log(s);
76
+ const fail = (s) => { console.error(`\n ${red('✗')} ${plain(s)}\n`); process.exit(1); };
70
77
 
71
78
  // ── The session: one per Wall, in the user's config, readable by them alone ─────────────────────────────────────────
72
79
  const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'toolaby');
@@ -224,7 +231,7 @@ const readJson = (path) => { try { return JSON.parse(readText(path)); } catch {
224
231
  /** A project file's text, or `none` where it is no file to read (lib/safe.js readText): a link to a device reads as nothing. */
225
232
  const readOr = (path, none = null) => { try { return readText(path); } catch { return none; } };
226
233
  /** Whether a background already starts the platform, outside its comments: then it needs no lines added. */
227
- const startsPlatform = (path) => { try { return /\bstartBackground\s*\(/.test(readText(path).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '')); } catch { return false; } };
234
+ const startsPlatform = (path) => { try { return startsWall(readText(path)); } catch { return false; } };
228
235
  const isId = (s) => typeof s === 'string' && /^[a-z0-9][a-z0-9-]{1,118}$/.test(s);
229
236
  /** Whether anything is at the path, a link to nowhere included (existsSync follows a link, and says no for a dangling one). */
230
237
  const there = (path) => { try { lstatSync(path); return true; } catch { return false; } };
@@ -290,9 +297,13 @@ async function wire() {
290
297
  const oldConfig = existsSync(configPath) ? readOr(configPath, '') : '';
291
298
  const remembered = oldConfig.match(/appPopup:\s*'([^']*)'/)?.[1] || null;
292
299
  const rememberedSidePanel = oldConfig.match(/appSidePanel:\s*'([^']*)'/)?.[1] || null;
293
- // An extension whose own pages draw the panel — what `create` makes, or "your popup first" — keeps them in front.
294
- const ownInFront = drawsItself(manifest, oldConfig);
295
- const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, has, remembered, rememberedSidePanel, manifestKey, ownInFront }) : null;
300
+ // An extension whose own pages draw the panel — what `create` makes, "your popup first", or asked with --own-popup —
301
+ // keeps them in front.
302
+ const ownInFront = flag('--own-popup') || drawsItself(manifest, oldConfig);
303
+ // The background the manifest names, read from inside the folder only: whether it starts the Wall, or is the Wall's.
304
+ const namedWorker = workerIn(manifest) ?? '';
305
+ const workerSource = namedWorker && has(namedWorker) && !inside(dir, namedWorker).why ? readOr(join(dir, namedWorker), '') : null;
306
+ const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, has, remembered, rememberedSidePanel, manifestKey, ownInFront, workerSource }) : null;
296
307
  const appPopup = wiring ? wiring.appPopup : project.wrapper ? (project.appPopup ?? remembered) : null;
297
308
  const appSidePanel = wiring ? wiring.appSidePanel : project.wrapper ? (project.appSidePanel ?? rememberedSidePanel) : null;
298
309
  const targets = filesToWrite(project, appPopup, appSidePanel, { popup: !!wiring?.ownPopup, sidePanel: !!wiring?.ownSidePanel });
@@ -301,8 +312,13 @@ async function wire() {
301
312
  // What an earlier wiring wrote and this one does not, wherever it put them: taken away, so nothing stale is shipped.
302
313
  const stale = [...new Set([project.codeDir, project.popupDir, 'public/', 'src/public/'])].flatMap((under) => STALE_FILES.map((f) => `${under}${f}`)).filter((p) => existsSync(join(dir, p)));
303
314
  // The background a manifest names and that is not there is written — at the name the manifest gives, which is the repository's to choose.
304
- const worker = wiring ? wiring.writeWorker : project.createWorker?.file ?? null;
305
- const at = placesIn(dir, [...targets, ...entries, ...stale, ...(worker ? [worker] : []), ...(wiring?.did.length ? ['manifest.json'] : [])], worker ? { [worker]: 'the background the manifest names' } : {});
315
+ const worker = wiring ? (wiring.background.kind === 'new' ? wiring.background.file : null) : project.createWorker?.file ?? null;
316
+ // The extension's own background, with the Wall's in front of it — and, for a classic one, the client as a script.
317
+ const wrap = wiring?.background.kind === 'wrap' ? wiring.background : null;
318
+ const classicClient = wrap?.classic ? `${project.codeDir}${CLASSIC_CLIENT}` : null;
319
+ 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" } : {}) });
320
+ // A file of the developer's own where the Wall's background goes is never replaced: only one the Wall wrote.
321
+ 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.`);
306
322
  // Taken for missing, it may only be out of the listing's sight — spelt "./background.js", in dist/, or deeper down:
307
323
  // the developer's own background is never replaced by two lines of ours.
308
324
  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.`);
@@ -312,41 +328,40 @@ async function wire() {
312
328
  // A command older than the Wall asks for files the Wall no longer hands out: said, with the way to the current one, rather than a crash.
313
329
  // The agent files come from Walls that write them; one that does not is older, not newer, than this command.
314
330
  const agentPaths = new Set(AGENT_FILES.map((f) => f.path));
315
- const missing = targets.filter((p) => !agentPaths.has(p)).map((p) => p.split('/').pop()).filter((name) => typeof plan.files[name] !== 'string');
331
+ const missing = [...targets, ...(classicClient ? [classicClient] : [])].filter((p) => !agentPaths.has(p)).map((p) => p.split('/').pop()).filter((name) => typeof plan.files[name] !== 'string');
316
332
  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}`);
317
333
  const wrote = targets.filter((path) => typeof plan.files[path.split('/').pop()] === 'string');
318
334
  for (const path of wrote) write(path, fillPages(plan.files[path.split('/').pop()], { appPopup, appSidePanel }));
319
335
  for (const f of entries) { write(f, plan.files[f]); wrote.push(f); }
320
336
  const ownAgents = has('AGENTS.md') && !readOr(join(dir, 'AGENTS.md'), '').includes('TOOLABY.md');
321
337
  for (const p of stale) unlinkSync(at[p]);
322
- const did = [];
323
- let ownWorker = null;
324
338
  if (wiring) {
325
- if (wiring.writeWorker) write(wiring.writeWorker, WORKER_LINES);
326
- if (wiring.did.length) write('manifest.json', JSON.stringify(wiring.manifest, null, 2) + '\n');
327
- did.push(...wiring.did);
328
- if (wiring.ownWorker && !startsPlatform(join(dir, wiring.ownWorker.file))) ownWorker = wiring.ownWorker;
339
+ if (wiring.background.kind === 'new') write(wiring.background.file, WORKER_LINES);
340
+ // The Wall's background in front of theirs, with what toolaby.config.js starts the Wall with (a classic one carries it).
341
+ if (wrap) write(wrap.file, wallWorkerSource(wrap, wrap.classic ? configValues(fillPages(plan.files['toolaby.config.js'], { appPopup, appSidePanel })) : null));
342
+ if (classicClient) write(classicClient, plan.files[CLASSIC_CLIENT]);
343
+ if (wiring.changed) write('manifest.json', JSON.stringify(wiring.manifest, null, 2) + '\n');
329
344
  } else if (project.createWorker) {
330
345
  write(project.createWorker.file, project.createWorker.content);
331
- did.push(`a background entry, ${project.createWorker.file} (it starts the platform)`);
332
- } else if (project.worker && !project.extraFiles && !startsPlatform(join(dir, project.worker))) {
333
- // A framework with its own extension point (WXT's module) starts the platform in the background itself.
334
- ownWorker = { file: project.worker, classic: false };
335
346
  }
336
-
337
- out(`\n ${oneLine(plan.name)} → ${dir}${project.kind === 'plain' ? '' : ` (${project.label})`} · ${side}${side === 'Test' ? ' (the tk_test_ key; `--live` for the store build)' : ''}\n`);
338
- // Each thing done is a line, and names what the manifest and the Wall named: the popup, the background, the address.
339
- out(` Written: ${wrote.join(', ')}${did.length ? `,\n ${did.map(oneLine).join(',\n ')}` : ''}.\n`);
340
- if (ownAgents) out(' Your AGENTS.md is your own: add a line so coding agents find the Wall\'s guide — "Before changing anything paid, read TOOLABY.md."\n');
341
- if (stale.length) out(` Removed: ${stale.join(', ')} — earlier files, now inside toolaby.js.\n`);
342
- for (const sn of project.snippets) out(` Add to ${sn.file} — yours to paste, the manifest being generated:\n\n${sn.code.split('\n').map((l) => ` ${l}`).join('\n')}\n`);
343
- if (ownWorker) out(` Your background, ${oneLine(ownWorker.file)}: add${ownWorker.classic ? ' — after giving "background" a "type": "module" in manifest.json (importScripts() is not available in a module worker)' : ''}:\n\n${project.workerLines.trimEnd().split('\n').map((l) => ` ${l}`).join('\n')}\n`);
344
- if (project.built) out(` ${BUILD_COPY_NOTE}\n`);
345
- out(` Next: ${project.kind === 'plain' && !project.built ? '' : 'build or start the dev server, then '}chrome://extensions → reload it. The tool's Set up page says "checked in" the moment it starts.`);
346
- if (project.note) out(` ${project.note}\n`);
347
- else if (appPopup || appSidePanel) out(` Your ${appPopup && appSidePanel ? 'popup and side panel open' : appSidePanel ? 'side panel opens' : 'popup opens'} as before; what the Free plan holds (Pricing, in the dashboard) decides when the Wall's panel takes its place.\n`);
348
- else if (ownInFront && (wiring?.ownPopup || wiring?.ownSidePanel)) out(` Your ${wiring.ownPopup && wiring.ownSidePanel ? 'popup and side panel stay' : wiring.ownSidePanel ? 'side panel stays' : 'popup stays'} in front and draw${wiring.ownPopup && wiring.ownSidePanel ? '' : 's'} the panel, as before; toolaby.showPaywall() opens the Wall's page beside it.\n`);
349
- else out(` No popup or side panel to stand in front of: count a use where your feature runs — the Set up page shows the line.\n`);
347
+ // The extension's own code, read for a popup it swaps while it runs: the Wall's in front of it could be skipped then.
348
+ const code = appPopup ? [...tree].filter(ownCode).slice(0, 400).map((path) => { let text = ''; try { if (statSync(join(dir, path)).size < 512_000) text = readOr(join(dir, path), ''); } catch { /* gone */ } return { path, text }; }) : [];
349
+ const summary = wiringSummary({ project, wiring, appPopup, appSidePanel, workerStarts: !!project.worker && startsPlatform(join(dir, project.worker)), popupSwap: popupSwapIn(code) });
350
+
351
+ // Wired again, a folder takes `upgrade` (wire, replacing the platform's files): the command, ready to paste.
352
+ const into = opt('--into');
353
+ const again = `npx -y toolaby@latest upgrade ${slug}${live ? ' --live' : ''}${into ? ` --into ${/^[\w@%+=:,./-]+$/.test(into) ? into : `'${into.replace(/'/g, "'\\''")}'`}` : ''}`;
354
+ say(`\n ${summary.todo ? `${yellow('!')} ${bold('Almost wired')}` : `${green('✓')} ${bold('Wired')}`} ${plain(oneLine(plan.name))} → ${plain(basename(dir))} ${dim(`· ${side}${project.kind === 'plain' ? '' : ` · ${project.label}`}`)}\n`);
355
+ const width = Math.max(...summary.rows.map((r) => r.label.length));
356
+ for (const r of summary.rows) {
357
+ const mark = r.tone === 'ok' ? green('✓') : r.tone === 'warn' ? yellow('!') : cyan('→');
358
+ say(` ${mark} ${bold(r.label.padEnd(width))} ${plain(oneLine(r.text))}`);
359
+ if (r.fix === 'own-popup') say(` ${' '.repeat(width)} ${cyan(`${again} --own-popup`)}`);
360
+ if (r.code) say(`\n${r.code.split('\n').map((l) => ` ${cyan(l)}`).join('\n')}\n`);
361
+ }
362
+ if (ownAgents) say(` ${dim('Your AGENTS.md is your own: add "Before changing anything paid, read TOOLABY.md."')}`);
363
+ if (stale.length) say(` ${dim(`Removed: ${stale.join(', ')} (now inside toolaby.js)`)}`);
364
+ say(`\n ${bold('Next')} ${summary.rows.some((r) => r.again) ? `${plain(summary.next.replace(/wire again\.$/, 'run'))} ${cyan(again)}` : plain(summary.next)}\n`);
350
365
  }
351
366
 
352
367
  async function create() {
@@ -756,7 +771,7 @@ function mcp() {
756
771
  }
757
772
 
758
773
  async function check() {
759
- const passed = await runCheck({ dir: opt('--into') ?? positional[0] ?? '.', out, api: (path, o = {}) => api(path, o), token: storedToken, detectProject, wireManifest, listTree, readJson, browser: flag('--browser') });
774
+ const passed = await runCheck({ mark: (tone, text) => say(` ${tone === 'ok' ? green('✓') : tone === 'warn' ? yellow('!') : red('✗')} ${plain(text)}`), dir: opt('--into') ?? positional[0] ?? '.', out, api: (path, o = {}) => api(path, o), token: storedToken, detectProject, wireManifest, listTree, readJson, browser: flag('--browser') });
760
775
  if (!passed) process.exit(1);
761
776
  }
762
777
 
@@ -767,6 +782,7 @@ function help() {
767
782
  toolaby wire <tool-id> wire the extension in this folder to your tool, on Test
768
783
  toolaby wire <tool-id> --live the same, on Live: the store build
769
784
  --into <folder> another folder --force replace the platform's files
785
+ --own-popup keep your popup in front (the Wall's opens from toolaby.showPaywall())
770
786
  toolaby upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
771
787
  toolaby create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
772
788
  --surfaces both|popup|side-panel|none what it opens (default: both)
package/lib/check.js CHANGED
@@ -8,6 +8,8 @@ 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';
12
+ import { popupSwapIn, ownCode } from './summary.js';
11
13
 
12
14
  const KEY = /tk_(test|live|dev)_[A-Za-z0-9_-]{20,}/;
13
15
  /** A tool id as the Wall makes them (lib/mcp.js has the same): the only thing the check puts after a Wall's address. */
@@ -53,12 +55,14 @@ export function featuresUsed(source) {
53
55
  return keys;
54
56
  }
55
57
 
56
- export async function runCheck({ dir: given, out, api, token, detectProject, wireManifest, listTree, readJson, browser }) {
58
+ export async function runCheck({ dir: given, out, api, token, detectProject, wireManifest, listTree, readJson, browser, mark = null }) {
57
59
  const dir = resolve(given ?? '.');
58
60
  const results = [];
59
- const ok = (s) => { results.push('ok'); out(` ✓ ${s}`); };
60
- const warn = (s) => { results.push('warn'); out(` ! ${s}`); };
61
- const bad = (s) => { results.push('bad'); out(` ✗ ${s}`); };
61
+ // `mark` prints a line with the command's own coloured mark (bin.mjs); without it, the plain one.
62
+ const line = (tone, sign, s) => { results.push(tone); if (mark) mark(tone, s); else out(` ${sign} ${s}`); };
63
+ const ok = (s) => line('ok', '✓', s);
64
+ const warn = (s) => line('warn', '!', s);
65
+ const bad = (s) => line('bad', '✗', s);
62
66
  out(`\n Checking ${dir}\n`);
63
67
 
64
68
  // 1. What the folder is.
@@ -105,14 +109,46 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
105
109
 
106
110
  // 4. The manifest, against the platform's own wiring rules: what wiring it again would add.
107
111
  if (project.manifestFile && manifest) {
108
- const again = wireManifest(manifest, { site: key.site, has, remembered: null, rememberedSidePanel: null, manifestKey: typeof manifest.key === 'string' ? manifest.key : null });
109
- const needed = (again.did ?? []).filter((d) => !/popup|side panel/i.test(d));
112
+ const named = namedWorker(manifest) ?? '';
113
+ const workerText = named && has(named) ? readOr(join(dir, named), null) : null;
114
+ const again = wireManifest(manifest, { site: key.site, has, remembered: null, rememberedSidePanel: null, manifestKey: typeof manifest.key === 'string' ? manifest.key : null, workerSource: workerText });
115
+ // The background is said below, in a line of its own.
116
+ const needed = (again.did ?? []).filter((d) => !/popup|side panel|service worker/i.test(d));
110
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.`);
111
118
  else ok('The manifest has the permissions and host permissions the Wall needs.');
112
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.
113
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.`);
114
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.`);
115
122
 
123
+ // 4b. The background starts the Wall: the popup and every page ask it everything, so without it nothing answers.
124
+ const upgrade = `\`npx -y toolaby@latest upgrade ${key.slug}\``;
125
+ if (project.manifestFile && manifest) {
126
+ const named = namedWorker(manifest) ?? '';
127
+ const text = named && has(named) ? readOr(join(dir, named), null) : null;
128
+ const wraps = wrappedBy(text);
129
+ // What wiring again would do with it, by the same rules (wiring/manifest).
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.`);
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, ' '))}`);
135
+ else if (wraps) {
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
+ 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.`);
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, ' '))}`);
145
+ }
146
+ // A popup the code swaps while it runs: the Wall's in front of it can be skipped.
147
+ if (project.manifestFile && manifest?.action?.default_popup === 'toolaby-popup.html') {
148
+ const swap = popupSwapIn(sourceFiles(dir).map((f) => relative(dir, f).split('\\').join('/')).filter(ownCode).map((path) => ({ path, text: readOr(join(dir, path), '') })));
149
+ if (swap) warn(`Your code handles the popup itself (${oneLine(swap.call)} in ${oneLine(swap.file)}): \`npx -y toolaby@latest upgrade ${key.slug} --own-popup\` keeps yours in front.`);
150
+ }
151
+
116
152
  // 5. The client, as fresh as the Wall's.
117
153
  const client = await fetchBounded(`${key.site}/client/toolaby.js`).then((r) => (r.ok && r.body ? r.body.toString('utf8') : null)).catch(() => null);
118
154
  if (client === null) warn('The Wall\'s current client could not be read to compare.');
package/lib/manifest.js CHANGED
@@ -6,7 +6,9 @@
6
6
  * the storage permission; the platform's address as a host permission and
7
7
  * for externally_connectable (the site's sign-in and purchase handoffs); a
8
8
  * module service worker when the manifest names none, or names one that is
9
- * not there; the Wall's popup in front of the extension's own — and its
9
+ * not there — and in front of the extension's own background, the Wall's,
10
+ * which runs it and then starts the platform (wallWorkerSource), so nothing
11
+ * of theirs is edited; the Wall's popup in front of the extension's own — and its
10
12
  * side panel in front of the extension's side panel — which the
11
13
  * configuration then remembers; and the extension's identity — the `key`
12
14
  * that fixes its id, which the tool key is bound to (identity.ts) — unless
@@ -18,6 +20,55 @@
18
20
  export const WALL_POPUP = 'toolaby-popup.html';
19
21
  export const WALL_SIDE_PANEL = 'toolaby-sidepanel.html';
20
22
  export const WORKER_LINES = "import { toolaby } from './toolaby.js';\n\ntoolaby.startBackground();\n";
23
+ /** The Wall's background: the one it writes when the manifest names none, and the one it stands in front of theirs. */
24
+ export const WALL_WORKER = 'toolaby.background.js';
25
+ /** The client as a classic script (a global `Toolaby`): what a classic background loads with importScripts(). */
26
+ export const CLASSIC_CLIENT = 'toolaby-client.js';
27
+ /** The line in the Wall's background that names the extension's own, which it runs: read back when wired again. */
28
+ const WRAPS = /^\/\/ toolaby-wraps: (.+)$/m;
29
+ /** A background the Wall's can name in the code it writes: a path inside the folder, plain characters, a script. */
30
+ const SCRIPT_PATH = /^(?!.*\.\.)(?!.*\/\/)[A-Za-z0-9_][A-Za-z0-9_\-./]*\.m?js$/;
31
+ /** Code without its comments, strings kept whole, so a `//` inside one is not taken for a comment. */
32
+ export function withoutComments(text) {
33
+ return text.replace(/("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m, str) => (str ? m : ''));
34
+ }
35
+ /** Whether a background already starts the Wall itself: then it is left as it is. */
36
+ export const startsPlatform = (source) => /\b(?:startBackground|ensureBackground)\s*\(/.test(withoutComments(source));
37
+ /** The extension's own background the Wall's runs, read from the Wall's; null for any other file. */
38
+ export const wrappedBy = (source) => source?.match(WRAPS)?.[1]?.trim() || null;
39
+ /**
40
+ * The Wall's background in front of the extension's own (`file`, beside `wraps`, so a relative importScripts() of
41
+ * theirs still finds its files): it runs theirs as before, then starts the Wall unless theirs did — ensureBackground(),
42
+ * so a background that starts it with handlers of its own keeps them. A module background gets a module; a classic
43
+ * one a classic script, which loads the client as one (CLASSIC_CLIENT), so its importScripts() work as they did. The
44
+ * classic one carries the configuration it starts with — the key and the pages of toolaby.config.js — which wiring
45
+ * again rewrites with it.
46
+ */
47
+ export function wallWorkerSource(b, config) {
48
+ const depth = b.file.split('/').length - 1;
49
+ const up = '../'.repeat(depth);
50
+ const own = b.wraps.slice(b.wraps.lastIndexOf('/') + 1);
51
+ 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
+ if (!b.classic)
53
+ return `${head}import { toolaby } from '${up || './'}toolaby.js';\nimport './${own}';\n\ntoolaby.ensureBackground();\n`;
54
+ const c = config ?? { key: '', appPopup: '', appSidePanel: '' };
55
+ return `${head}importScripts('${up}${CLASSIC_CLIENT}');\nself.toolaby = Toolaby(${JSON.stringify({ key: c.key, appPopup: c.appPopup, appSidePanel: c.appSidePanel })});\nimportScripts('${own}');\ntoolaby.ensureBackground();\n`;
56
+ }
57
+ /** What toolaby.config.js starts the Wall with, read from its text: what a classic background of the Wall's carries. */
58
+ export function configValues(text) {
59
+ const code = withoutComments(text);
60
+ const get = (name) => code.match(new RegExp(`${name}:\\s*'([^']*)'`))?.[1];
61
+ const key = get('key');
62
+ return key ? { key, appPopup: get('appPopup') ?? '', appSidePanel: get('appSidePanel') ?? '' } : null;
63
+ }
64
+ /** A name a background can be loaded by, for one that has none: its characters kept where they can be, the rest a dash. */
65
+ export const loadableName = (file) => {
66
+ const clean = file.split('/').filter((p) => p && p !== '.' && p !== '..').map((p) => p.replace(/[^A-Za-z0-9_\-.]+/g, '-').replace(/^[^A-Za-z0-9_]+/, '')).filter(Boolean).join('/');
67
+ const js = /\.m?js$/.test(clean) ? clean : `${clean.replace(/\.[^./]*$/, '') || 'background'}.js`;
68
+ return js.replace(/-+(?=\.m?js$)/, '');
69
+ };
70
+ /** The background the manifest names, as the folder spells it: "./background.js" is background.js. */
71
+ export const namedWorker = (manifest) => typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
21
72
  /**
22
73
  * Whether an earlier wiring left the extension's own pages drawing the panel: a configuration that says '' for both
23
74
  * pages while the manifest names none of the Wall's. That is what `create` makes, and the "your popup first" shape
@@ -37,11 +88,19 @@ export function drawsItself(manifest, oldConfig) {
37
88
  export function wireManifest(input, o) {
38
89
  const manifest = JSON.parse(JSON.stringify(input));
39
90
  const did = [];
91
+ const added = [];
92
+ const host = (() => { try {
93
+ return new URL(o.site).host;
94
+ }
95
+ catch {
96
+ return o.site;
97
+ } })();
40
98
  // The identity: the manifest's own key stays (the platform adopts it); otherwise the platform's goes in, and the unpacked extension's id is known before it loads.
41
99
  const own = typeof manifest.key === 'string' && manifest.key.trim() ? manifest.key.trim() : null;
42
100
  if (!own && o.manifestKey) {
43
101
  manifest.key = o.manifestKey;
44
102
  did.push('a "key", so the extension keeps one id across reloads and machines');
103
+ added.push('key');
45
104
  }
46
105
  const manifestKey = own ?? o.manifestKey ?? null;
47
106
  const add = (list, value) => { const arr = Array.isArray(list) ? [...list] : []; if (!arr.includes(value))
@@ -49,15 +108,20 @@ export function wireManifest(input, o) {
49
108
  if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('storage'))) {
50
109
  manifest.permissions = add(manifest.permissions, 'storage');
51
110
  did.push('the "storage" permission');
111
+ added.push('storage');
52
112
  }
53
113
  if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(`${o.site}/*`))) {
54
114
  manifest.host_permissions = add(manifest.host_permissions, `${o.site}/*`);
55
115
  did.push(`a host permission for ${o.site}`);
116
+ if (!added.includes(host))
117
+ added.push(host);
56
118
  }
57
119
  const ec = manifest.externally_connectable ?? {};
58
120
  if (!(Array.isArray(ec.matches) && ec.matches.includes(`${o.site}/*`))) {
59
121
  manifest.externally_connectable = { ...ec, matches: add(ec.matches, `${o.site}/*`) };
60
122
  did.push(`externally_connectable for ${o.site}`);
123
+ if (!added.includes(host))
124
+ added.push(host);
61
125
  }
62
126
  // The extension's own popup: the manifest's, or the one remembered when the manifest already names the Wall's.
63
127
  const declared = typeof manifest.action?.default_popup === 'string' ? manifest.action.default_popup : null;
@@ -67,6 +131,11 @@ export function wireManifest(input, o) {
67
131
  manifest.action = { ...manifest.action, default_popup: WALL_POPUP };
68
132
  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`);
69
133
  }
134
+ // Asked to keep the extension's own in front after a wiring stood the Wall's there: theirs goes back.
135
+ if (o.ownInFront && declared === WALL_POPUP && ownPopup) {
136
+ manifest.action = { ...manifest.action, default_popup: ownPopup };
137
+ did.push(`${ownPopup} back in front: toolaby.showPaywall() opens the Wall's popup`);
138
+ }
70
139
  // The extension's own side panel, the same way: the Wall's in front, with the permission a side panel needs.
71
140
  // Chrome spells it side_panel.default_path, Firefox sidebar_action.default_panel; a manifest may carry both.
72
141
  const theirs = (v) => (typeof v === 'string' && v !== WALL_SIDE_PANEL ? v : null);
@@ -75,7 +144,15 @@ export function wireManifest(input, o) {
75
144
  const ours = declaredPanel === WALL_SIDE_PANEL || declaredSidebar === WALL_SIDE_PANEL;
76
145
  const ownSidePanel = theirs(declaredPanel) ?? theirs(declaredSidebar) ?? (ours ? o.rememberedSidePanel || null : null);
77
146
  const appSidePanel = o.ownInFront ? null : ownSidePanel;
78
- if (o.ownInFront) { /* the extension's own side panel stays in front, as its popup does */ }
147
+ if (o.ownInFront) {
148
+ // The extension's own side panel stays in front, as its popup does — and goes back there after a wiring put the Wall's.
149
+ if (declaredPanel === WALL_SIDE_PANEL && ownSidePanel) {
150
+ manifest.side_panel = { ...manifest.side_panel, default_path: ownSidePanel };
151
+ did.push(`${ownSidePanel} back in front: toolaby.showPaywall() opens the Wall's side panel`);
152
+ }
153
+ if (declaredSidebar === WALL_SIDE_PANEL && ownSidePanel)
154
+ manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: ownSidePanel };
155
+ }
79
156
  else if (theirs(declaredPanel)) {
80
157
  manifest.side_panel = { ...manifest.side_panel, default_path: WALL_SIDE_PANEL };
81
158
  if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('sidePanel')))
@@ -87,16 +164,40 @@ export function wireManifest(input, o) {
87
164
  manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: WALL_SIDE_PANEL };
88
165
  did.push(`the Wall's side panel in front of ${declaredSidebar} in Firefox's sidebar_action too`);
89
166
  }
90
- const named = typeof manifest.background?.service_worker === 'string' && manifest.background.service_worker.trim() ? manifest.background.service_worker.trim() : null;
91
- let writeWorker = null;
92
- let ownWorker = null;
167
+ // The background. None named, or named and not there: one of the Wall's alone, a module. The extension's own: left as
168
+ // 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
169
+ // one — unless it starts the Wall already. Wired again, the Wall's is written again for the one it names.
170
+ const named = namedWorker(manifest);
171
+ let background;
172
+ let workerMoved = false;
93
173
  if (!named || !o.has(named)) {
94
- writeWorker = named ?? (o.has('background.js') ? 'toolaby.background.js' : 'background.js');
95
- manifest.background = { service_worker: writeWorker, type: 'module' };
96
- did.push(`a service worker, ${writeWorker} (two lines: it starts the platform)`);
174
+ const file = named ?? (o.has('background.js') ? WALL_WORKER : 'background.js');
175
+ manifest.background = { service_worker: file, type: 'module' };
176
+ did.push(`a service worker, ${file} (two lines: it starts the platform)`);
177
+ background = { kind: 'new', file };
97
178
  }
98
179
  else {
99
- ownWorker = { file: named, classic: manifest.background?.type !== 'module' };
180
+ const source = o.workerSource ?? '';
181
+ const wraps = wrappedBy(source);
182
+ const theirs = wraps ?? named;
183
+ const classic = manifest.background?.type !== 'module';
184
+ if (!wraps && startsPlatform(source))
185
+ background = { kind: 'started', file: named };
186
+ else if (wraps && !o.has(wraps))
187
+ background = { kind: 'gone', file: named, wraps };
188
+ else if (/\.(ts|tsx|mts|cts|jsx)$/.test(theirs))
189
+ background = { kind: 'lines', file: theirs };
190
+ else if (!SCRIPT_PATH.test(theirs))
191
+ background = { kind: 'rename', file: theirs, to: loadableName(theirs) };
192
+ else {
193
+ const file = wraps ? named : `${theirs.includes('/') ? theirs.slice(0, theirs.lastIndexOf('/') + 1) : ''}${WALL_WORKER}`;
194
+ if (!wraps) {
195
+ manifest.background = { ...manifest.background, service_worker: file };
196
+ workerMoved = true;
197
+ }
198
+ background = { kind: 'wrap', file, wraps: theirs, classic };
199
+ }
100
200
  }
101
- return { manifest, did, writeWorker, ownWorker, appPopup, appSidePanel, manifestKey, ownPopup, ownSidePanel };
201
+ const changed = did.length > 0 || workerMoved;
202
+ return { manifest, did, added, changed, background, appPopup, appSidePanel, manifestKey, ownPopup, ownSidePanel };
102
203
  }
package/lib/project.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Generated from wall/src/packages/wiring/project.ts by wall/scripts/pack-create.mjs — edit there.
2
2
  /** 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
- export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies the platform\'s files to its output — toolaby.js, toolaby.config.js and toolaby-popup.html (toolaby-sidepanel.html too, with a side panel) — the way it copies your popup.html.';
3
+ 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
4
  /** Always written, where the build imports from: the platform as one file (the client and the popup UI), its config, its types. */
5
5
  export const CODE_FILES = ['toolaby.config.js', 'toolaby.js', 'toolaby.d.ts'];
6
6
  /** Beside the client when the build generates the manifest: the fragment it spreads in, derived from the key (boilerplate/toolaby.manifest.js). */
@@ -161,7 +161,7 @@ export function detectProject(o) {
161
161
  const declared = typeof manifest?.action?.default_popup === 'string' ? manifest.action.default_popup : null;
162
162
  // Chrome's side_panel, else Firefox's sidebar_action: the same page, two spellings.
163
163
  const declaredPanel = typeof manifest?.side_panel?.default_path === 'string' ? manifest.side_panel.default_path : typeof manifest?.sidebar_action?.default_panel === 'string' ? manifest.sidebar_action.default_panel : null;
164
- const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker : null;
164
+ const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
165
165
  const workerFile = named && has(named) ? named : 'background.js';
166
166
  return {
167
167
  kind: crxjs ? 'crxjs' : 'plain', label: crxjs ? 'CRXJS' : 'a plain extension', codeDir: '', popupDir: '', manifestFile: 'manifest.json', wrapper: true, built: crxjs ? false : built, foundManifest: null,
package/lib/summary.js ADDED
@@ -0,0 +1,73 @@
1
+ // Generated from wall/src/packages/wiring/summary.ts by wall/scripts/pack-create.mjs — edit there.
2
+ import { withoutComments } from './manifest.js';
3
+ import { importPath } from './project.js';
4
+ export function popupSwapIn(files) {
5
+ for (const f of files) {
6
+ const m = withoutComments(f.text).match(/\b(chrome|browser)\.(action|browserAction)\.(setPopup|getPopup)\s*\(/);
7
+ if (m)
8
+ return { file: f.path, call: `${m[1]}.${m[2]}.${m[3]}()` };
9
+ }
10
+ return null;
11
+ }
12
+ /** Whether a path is the extension's own code, for popupSwapIn: a script, not the Wall's, not a dependency's or a build's. */
13
+ export const ownCode = (path) => /\.(m?js|jsx|ts|tsx)$/.test(path) && !/(^|\/)toolaby[^/]*$/.test(path) && !/(^|\/)(node_modules|dist|build|\.output|\.wxt|\.plasmo)\//.test(path);
14
+ export function wiringSummary(o) {
15
+ const { project: p, wiring: w } = o;
16
+ const rows = [];
17
+ // The popup: the Wall's in front of the extension's, or theirs in front and the Wall's beside it, or none at all.
18
+ const ownPopup = w?.ownPopup ?? null;
19
+ if (o.appPopup) {
20
+ rows.push(o.popupSwap
21
+ ? { 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' }
22
+ : { tone: 'ok', label: 'Popup', text: `The Wall checks first, then opens ${o.appPopup}` });
23
+ }
24
+ else if (ownPopup)
25
+ rows.push({ tone: 'ok', label: 'Popup', text: `Yours stays in front; toolaby.showPaywall() opens the Wall's` });
26
+ else if (p.note)
27
+ rows.push({ tone: 'warn', label: 'Popup', text: p.note });
28
+ else if (!o.appSidePanel && !w?.ownSidePanel)
29
+ rows.push({ tone: 'ok', label: 'Popup', text: 'None: the Wall steps in where your code calls toolaby.gate()' });
30
+ if (o.appSidePanel)
31
+ rows.push({ tone: 'ok', label: 'Side panel', text: `The Wall checks first, then opens ${o.appSidePanel}` });
32
+ else if (w?.ownSidePanel)
33
+ rows.push({ tone: 'ok', label: 'Side panel', text: `Yours stays in front; toolaby.showPaywall() opens the Wall's` });
34
+ // The background: what starts the Wall.
35
+ if (w) {
36
+ const b = w.background;
37
+ if (b.kind === 'new')
38
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts the Wall` });
39
+ else if (b.kind === 'wrap')
40
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.wraps} runs as before, and the Wall starts with it` });
41
+ else if (b.kind === 'started')
42
+ rows.push({ tone: 'ok', label: 'Background', text: `${b.file} starts the Wall` });
43
+ else if (b.kind === 'lines')
44
+ 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();` });
45
+ else if (b.kind === 'gone')
46
+ 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 });
47
+ else
48
+ 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 });
49
+ }
50
+ else if (p.extraFiles?.length)
51
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.label} starts the Wall (${p.extraFiles.map((f) => f.to).join(', ')})` });
52
+ else if (p.createWorker)
53
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.createWorker.file} starts the Wall` });
54
+ else if (p.worker && !o.workerStarts)
55
+ rows.push({ tone: 'todo', label: 'Background', text: `Add these 2 lines to the top of ${p.worker}:`, code: p.workerLines.trim() });
56
+ else if (p.worker)
57
+ rows.push({ tone: 'ok', label: 'Background', text: `${p.worker} starts the Wall` });
58
+ // The manifest: what it gained, or, generated by a framework, what its config needs.
59
+ if (w)
60
+ rows.push({ tone: 'ok', label: 'Manifest', text: w.added.length ? w.added.join(' · ') : 'Has what the Wall needs' });
61
+ for (const sn of p.snippets)
62
+ rows.push({ tone: 'todo', label: 'Manifest', text: `Add to ${sn.file}:`, code: sn.code.trim() });
63
+ if (p.built)
64
+ rows.push({ tone: 'warn', label: 'Build', text: 'Your build must copy every toolaby* file into its output' });
65
+ const steps = rows.filter((r) => r.tone === 'todo').length;
66
+ const todo = steps > 0;
67
+ const doThem = steps > 1 ? 'Do the steps above' : 'Do the step above';
68
+ const reload = p.kind === 'plain' && !p.built ? 'reload it in chrome://extensions' : 'build it (or start the dev server), then reload it in chrome://extensions';
69
+ const next = rows.some((r) => r.again)
70
+ ? `${doThem}, then wire again.`
71
+ : `${todo ? `${doThem}, then ${reload}` : reload.charAt(0).toUpperCase() + reload.slice(1)}. The Set up page turns green when it checks in.`;
72
+ return { rows, todo, next };
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.6.5",
3
+ "version": "1.7.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": {