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 +22 -14
- package/lib/check.js +5 -3
- package/lib/manifest.js +33 -1
- package/lib/summary.js +2 -0
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
|
|
287
|
-
|
|
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
|
|
295
|
-
|
|
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
|
|
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: `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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": {
|