toolaby 1.8.4 → 1.8.5

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/bin.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  // toolaby pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's key left out
9
9
  // toolaby check [--into <folder>] whether the extension here is wired right; --browser loads it in Chromium too
10
10
  // toolaby mcp the Wall for coding agents: an MCP server on stdio (lib/mcp.js)
11
- // toolaby webhooks events the six events, with an example of each
11
+ // toolaby webhooks events the six events and what each means
12
12
  // toolaby webhooks trigger <event> send an example delivery: to your registered endpoints, or with --to straight to a local server
13
13
  // toolaby webhooks secret this machine's development secret, which --to signs with
14
14
  // toolaby webhooks endpoints the workspace's endpoints
@@ -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, sitePattern, 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, parseManifest, notManifestV3 } 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.8.4';
47
+ const VERSION = '1.8.5';
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.
@@ -211,7 +211,7 @@ async function tools() {
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
213
  if (!res.ok) fail(`Toolaby answered HTTP ${res.status}.`);
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; }
214
+ if (!res.json.tools.length) { out(`\n No tools on ${side} yet. Make one in the dashboard, then \`npx -y toolaby@latest 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)}`);
217
217
  out();
@@ -269,7 +269,7 @@ async function filesFor(slug, scaffold, manifestKey = null, surfaces = null) {
269
269
  if (res.status === 404) {
270
270
  // Say who the command is: the tool may well exist — in a workspace of another account of theirs.
271
271
  const me = await api('/api/cli/me', { token }).catch(() => null);
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.` : '';
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? \`npx -y toolaby@latest 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
275
  if (!res.ok) fail(oneLine(res.json.message ?? `Toolaby answered HTTP ${res.status}.`));
@@ -283,16 +283,23 @@ async function wire() {
283
283
  if (!existsSync(dir)) fail(`${dir} does not exist`);
284
284
  const tree = listTree(dir, 3);
285
285
  const has = (p) => tree.has(p.replace(/\/$/, ''));
286
- const manifest = has('manifest.json') ? readJson(join(dir, 'manifest.json')) : null;
287
- if (has('manifest.json') && !manifest) fail('manifest.json could not be read as JSON.');
286
+ // Read as Chrome reads it: comments and a trailing comma are fine there. Written back, it is plain JSON (said below).
287
+ const read = has('manifest.json') ? parseManifest(readOr(join(dir, 'manifest.json'), '')) : null;
288
+ const manifest = read?.manifest ?? null;
289
+ if (read && !manifest) fail(`manifest.json could not be read: ${oneLine(read.error ?? 'it is not JSON')}. Fix it, then wire again. Nothing was written.`);
290
+ // A Manifest V2 extension was wired as if it were V3 — its background page swapped for a service worker Chrome then
291
+ // refused to load (1 Oct 2026). Chrome runs V3 alone: said, and nothing written.
292
+ if (manifest && notManifestV3(manifest)) fail(`${notManifestV3(manifest)}\n Nothing was written.`);
288
293
  // A key the manifest already has is the identity the platform adopts; otherwise the platform's goes in.
289
294
  const plan = await filesFor(slug, false, typeof manifest?.key === 'string' ? manifest.key : null);
290
295
  const site = plan.site;
291
296
  const manifestKey = plan.identity?.manifestKey ?? null;
292
297
  const configSource = ['manifest.config.ts', 'manifest.config.js', 'manifest.ts', 'src/manifest.ts'].find(has);
293
298
  const project = detectProject({ has, packageJson: has('package.json') ? readJson(join(dir, 'package.json')) : null, manifest, manifestSource: configSource ? readOr(join(dir, configSource)) : null, site, tree, manifestKey });
294
- // Nothing recognised: nothing is written and nothing guessed. A manifest below the top is where to point --into; otherwise the Set up page's Do it myself has the files and the three things to paste.
295
- if (project.kind === 'unknown') fail(`${dir} has no manifest.json at the top and is not a WXT, CRXJS or Plasmo project${project.foundManifest ? ` — there is one at ${oneLine(project.foundManifest)}: run this with --into ${oneLine(dirname(project.foundManifest)) || '.'}` : ''}.\n Nothing was written. For a layout of your own, the tool's Set up page → Do it myself has the files and the three things to paste.`);
299
+ // Nothing recognised: nothing is written and nothing guessed. A manifest below the top is where to point --into — unless a
300
+ // build makes the background it names (Vite's public/), where the wiring's background would stand in its way; otherwise
301
+ // the Set up page's Wire it by hand has the files and the three things to paste.
302
+ if (project.kind === 'unknown') fail(`${dir} has no manifest.json at the top and is not a WXT, CRXJS or Plasmo project${project.foundManifest ? ` — there is one at ${oneLine(project.foundManifest)}: run this with --into ${oneLine(dirname(project.foundManifest)) || '.'}, unless your build makes the background it names (then wire it by hand)` : ''}.\n Nothing was written. For a layout of your own, the tool's Set up page → Wire it by hand has the files and the three things to paste.`);
296
303
 
297
304
  // The extension's own popup and side panel: the manifest's, or — wired before — what the existing configuration remembers.
298
305
  const configPath = join(dir, `${project.codeDir}toolaby.config.js`);
@@ -312,7 +319,7 @@ async function wire() {
312
319
  // The background the manifest names, read from inside the folder only: whether it starts the Wall, or is the Wall's.
313
320
  const namedWorker = workerIn(manifest) ?? '';
314
321
  const workerSource = namedWorker && has(namedWorker) && !inside(dir, namedWorker).why ? readOr(join(dir, namedWorker), '') : null;
315
- const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, apex: new URL(wall).host, has, remembered, rememberedSidePanel, manifestKey, ownInFront, workerSource }) : null;
322
+ const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, apex: new URL(wall).host, has, remembered, rememberedSidePanel, manifestKey, ownInFront, workerSource, built: project.built }) : null;
316
323
  const appPopup = wiring ? wiring.appPopup : project.wrapper ? (project.appPopup ?? remembered) : null;
317
324
  const appSidePanel = wiring ? wiring.appSidePanel : project.wrapper ? (project.appSidePanel ?? rememberedSidePanel) : null;
318
325
  const targets = filesToWrite(project, appPopup, appSidePanel, { popup: !!wiring?.ownPopup, sidePanel: !!wiring?.ownSidePanel });
@@ -367,6 +374,7 @@ async function wire() {
367
374
  if (r.code) say(`\n${r.code.split('\n').map((l) => ` ${cyan(l)}`).join('\n')}\n`);
368
375
  }
369
376
  if (ownAgents) say(` ${dim('Your AGENTS.md is your own: add "Before changing anything paid, read TOOLABY.md."')}`);
377
+ if (read?.comments && wiring?.changed) say(` ${dim('manifest.json had comments: it is written back as plain JSON, without them.')}`);
370
378
  if (stale.length) say(` ${dim(`Removed: ${stale.join(', ')} (now inside toolaby.js)`)}`);
371
379
  say(`\n ${bold('Next')} ${summary.rows.some((r) => r.again) ? `${plain(summary.next.replace(/wire again\.$/, 'run'))} ${cyan(again)}` : plain(summary.next)}\n`);
372
380
  }
@@ -454,11 +462,11 @@ async function webhooks() {
454
462
  const events = await catalogue();
455
463
  out(`\n ${side}\n`);
456
464
  for (const e of events) out(` ${oneLine(e.type).padEnd(26)} ${oneLine(e.description)}`);
457
- out(`\n toolaby webhooks trigger <event> → your registered endpoints\n toolaby webhooks trigger <event> --to <url> → a local server, signed with this machine's development secret\n toolaby webhooks secret → that secret, for the local server's WALL_WEBHOOK_SECRET\n`);
465
+ out(`\n toolaby webhooks trigger <event> → your registered endpoints\n toolaby webhooks trigger <event> --to <url> → a local server, signed with this machine's development secret\n toolaby webhooks secret → that secret, for the local server's TOOLABY_WEBHOOK_SECRET\n`);
458
466
  return;
459
467
  }
460
468
  if (sub === 'secret') {
461
- // For the server on this machine: `WALL_WEBHOOK_SECRET=$(npx -y toolaby@latest webhooks secret) node server.mjs`.
469
+ // For the server on this machine: `TOOLABY_WEBHOOK_SECRET=$(npx -y toolaby@latest webhooks secret) node server.mjs`.
462
470
  process.stdout.write(devSecret() + '\n');
463
471
  return;
464
472
  }
@@ -487,7 +495,7 @@ async function webhooks() {
487
495
  } catch (e) { fail(`Could not reach ${to}: ${e.message}`); }
488
496
  const text = (await res.text()).slice(0, 200);
489
497
  out(`\n POST ${to} → ${res.status}${text ? ` ${oneLine(text)}` : ''}`);
490
- out(` ${type}, signed with ${opt('--secret') ? 'your secret' : "this machine's development secret — `npx -y toolaby@latest webhooks secret` prints it for WALL_WEBHOOK_SECRET"}.\n`);
498
+ out(` ${type}, signed with ${opt('--secret') ? 'your secret' : "this machine's development secret — `npx -y toolaby@latest webhooks secret` prints it for TOOLABY_WEBHOOK_SECRET"}.\n`);
491
499
  if (res.status >= 300) out(' A real delivery would be retried until your server answers 2xx.\n');
492
500
  return;
493
501
  }
@@ -823,7 +831,7 @@ function help() {
823
831
  --browser loads it in Chromium (needs Playwright in the project)
824
832
  mcp Toolaby for coding agents, an MCP server on stdio:
825
833
  claude mcp add toolaby -- npx -y toolaby@latest mcp
826
- webhooks events the six events, with an example of each
834
+ webhooks events the six events and what each means
827
835
  webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
828
836
  webhooks endpoints your endpoints; add <url> registers one (secret shown once), remove <id> removes one
829
837
  login · whoami · logout the session, per side; a command that needs one signs you in itself
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, sitePattern } from './manifest.js';
11
+ import { wrappedBy, namedWorker, startsPlatform as startsWall, sitePattern, parseManifest, notManifestV3 } 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,}/;
@@ -69,8 +69,10 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
69
69
  if (!existsSync(dir)) { bad(`${dir} does not exist.`); return false; }
70
70
  const tree = listTree(dir, 3);
71
71
  const has = (p) => tree.has(p.replace(/\/$/, ''));
72
- const manifest = has('manifest.json') ? readJson(join(dir, 'manifest.json')) : null;
72
+ // As Chrome reads it (comments allowed), and only a Manifest V3 extension: Chrome runs no other.
73
+ const manifest = has('manifest.json') ? parseManifest((() => { try { return readText(join(dir, 'manifest.json')); } catch { return ''; } })()).manifest : null;
73
74
  if (has('manifest.json') && !manifest) { bad('manifest.json is not valid JSON.'); return false; }
75
+ if (manifest && notManifestV3(manifest)) { bad(notManifestV3(manifest)); return false; }
74
76
  const configSource = ['manifest.config.ts', 'manifest.config.js', 'manifest.ts', 'src/manifest.ts'].find(has);
75
77
  const readOr = (p, none = null) => { try { return readText(p); } catch { return none; } };
76
78
  const project = detectProject({ has, packageJson: has('package.json') ? readJson(join(dir, 'package.json')) : null, manifest, manifestSource: configSource ? readOr(join(dir, configSource)) : null, site: 'https://example.invalid', tree });
@@ -117,7 +119,7 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
117
119
  if (needed.length) bad(`The manifest lacks what Toolaby needs: ${needed.map(oneLine).join('; ')}. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
118
120
  else ok('The manifest has the permissions and host permissions Toolaby needs.');
119
121
  // 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
- 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.`);
122
+ 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 Set up page. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
121
123
  } 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
124
 
123
125
  // 4b. The background starts the Wall: the popup and every page ask it everything, so without it nothing answers.
package/lib/manifest.js CHANGED
@@ -33,6 +33,33 @@ const SCRIPT_PATH = /^(?!.*\.\.)(?!.*\/\/)[A-Za-z0-9_][A-Za-z0-9_\-./]*\.m?js$/;
33
33
  export function withoutComments(text) {
34
34
  return text.replace(/("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m, str) => (str ? m : ''));
35
35
  }
36
+ /**
37
+ * manifest.json as Chrome reads it: JSON that may carry comments and a trailing comma (Vimium's does; 1 Oct 2026), which
38
+ * JSON.parse refuses. `comments` says whether the text needed them gone: written back, the manifest is plain JSON.
39
+ */
40
+ export function parseManifest(text) {
41
+ const clean = String(text ?? '').replace(/^\uFEFF/, '');
42
+ try {
43
+ return { manifest: JSON.parse(clean), comments: false };
44
+ }
45
+ catch { /* JSON with comments, or not JSON at all */ }
46
+ const plain = withoutComments(clean).replace(/("(?:[^"\\\n]|\\.)*")|,(\s*[}\]])/g, (m, str, tail) => (str ? m : tail));
47
+ try {
48
+ return { manifest: JSON.parse(plain), comments: true };
49
+ }
50
+ catch (e) {
51
+ return { manifest: null, comments: false, error: e.message };
52
+ }
53
+ }
54
+ /** Why Chrome would not run this manifest as a Manifest V3 extension, or null: what wire and check refuse before anything else. */
55
+ export function notManifestV3(manifest) {
56
+ const version = manifest?.manifest_version;
57
+ if (version === 3)
58
+ return null;
59
+ if (version === 2)
60
+ return 'This is a Manifest V2 extension (manifest_version 2), and Chrome runs Manifest V3 extensions only. Move it to Manifest V3 first, then wire it: https://developer.chrome.com/docs/extensions/develop/migrate';
61
+ return `manifest.json says manifest_version ${JSON.stringify(version ?? null)}: Chrome runs Manifest V3 extensions, with "manifest_version": 3.`;
62
+ }
36
63
  /** Whether a background already starts the Wall itself: then it is left as it is. */
37
64
  export const startsPlatform = (source) => /\b(?:startBackground|ensureBackground)\s*\(/.test(withoutComments(source));
38
65
  /** The extension's own background the Wall's runs, read from the Wall's; null for any other file. */
@@ -205,7 +232,12 @@ export function wireManifest(input, o) {
205
232
  const named = namedWorker(manifest);
206
233
  let background;
207
234
  let workerMoved = false;
208
- if (!named || !o.has(named)) {
235
+ if (named && !o.has(named) && o.built) {
236
+ // Vite's or webpack's output (Linkwarden, wired on 1 Oct 2026): a two-line background written at that name stood where
237
+ // the build writes the extension's own, and copied as the summary says, it would have replaced it.
238
+ background = { kind: 'built', file: named };
239
+ }
240
+ else if (!named || !o.has(named)) {
209
241
  const file = named ?? (o.has('background.js') ? WALL_WORKER : 'background.js');
210
242
  manifest.background = { service_worker: file, type: 'module' };
211
243
  did.push(`a service worker, ${file} (two lines: it starts the platform)`);
package/lib/summary.js CHANGED
@@ -46,6 +46,8 @@ export function wiringSummary(o) {
46
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
+ else if (b.kind === 'built')
50
+ rows.push({ tone: 'todo', label: 'Background', text: `Your build makes ${b.file}. Add these 2 lines to the top of its source, with the path from there to toolaby.js:`, code: `import { toolaby } from './toolaby.js';\n\ntoolaby.startBackground();` });
49
51
  else if (b.kind === 'gone')
50
52
  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
53
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
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": {