clearotron 0.3.0-beta.7 → 0.3.0-beta.9
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 +28 -21
- package/bin/example.mjs +14 -2
- package/bin/key.mjs +23 -5
- package/bin/onboard.mjs +52 -16
- package/bin/start.mjs +95 -35
- package/bin/status.mjs +23 -3
- package/bin/stop.mjs +23 -8
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +20 -0
- package/driver/compose-read.mjs +21 -2
- package/driver/engine/jx-turn.mjs +4 -1
- package/driver/package.json +1 -1
- package/driver/portal-service.mjs +12 -5
- package/driver/suite-census.json +41 -5
- package/driver/systemd/render-units.mjs +7 -13
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DBUVdQT-.js → index-qes7gLpM.js} +2 -1
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/shared/access-audience.mjs +7 -6
- package/shared/client-door.mjs +39 -0
- package/shared/env-file-merge.mjs +24 -0
- package/shared/invocation.mjs +49 -2
- package/shared/permanent-install.mjs +34 -0
- package/shared/running-start.mjs +73 -0
package/bin/stop.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { systemdSaid, looksLikeBusFailure, busRemedy, CAPTURE_STDERR } from "../
|
|
|
23
23
|
import { BACKGROUND_UNITS } from "./start.mjs";
|
|
24
24
|
import { CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
25
25
|
import { invoke } from "../shared/invocation.mjs";
|
|
26
|
+
import { readRunning } from "../shared/running-start.mjs";
|
|
26
27
|
|
|
27
28
|
const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
|
|
28
29
|
const say = (s = "") => console.log(s);
|
|
@@ -91,17 +92,31 @@ for (const u of BACKGROUND_UNITS) {
|
|
|
91
92
|
}
|
|
92
93
|
// THE COMMENT HERE ALREADY NAMED THE CAUSE AND SHRUGGED AT IT. If there is no user bus, the disables
|
|
93
94
|
// above did not happen either — so this is where that is said.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// ONLY WHEN THERE WAS SOMETHING TO RELOAD. On a box with no unit installed the reload has nothing to do,
|
|
96
|
+
// and asking anyway printed a bus error and `su` advice to a reader whose product was running in a
|
|
97
|
+
// terminal — a WSL distribution without systemd meets exactly that (measured 2026-09-11).
|
|
98
|
+
if (found) {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], CAPTURE_STDERR);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const said = systemdSaid(e);
|
|
103
|
+
err(` could not ask systemd to reload its units — ${said}`);
|
|
104
|
+
if (looksLikeBusFailure(said)) err(`\n${busRemedy()}\n`);
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
if (!found) {
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
// A FOREGROUND START IS NOT "NOTHING RUNNING". It is not this verb's to stop — its terminal holds it —
|
|
110
|
+
// but saying nothing was running while it served is the answer that sent a reader looking elsewhere.
|
|
111
|
+
const foreground = readRunning();
|
|
112
|
+
if (foreground.length) {
|
|
113
|
+
say(" No background product is installed, but the product is running in the foreground:");
|
|
114
|
+
for (const r of foreground) say(` ${r.demo ? "the demo" : "the product"} at ${r.url} (pid ${r.pid})`);
|
|
115
|
+
say(" Ctrl-C in the terminal that holds it stops it. This verb stops only what `start --background` installed.");
|
|
116
|
+
} else {
|
|
117
|
+
say(" Nothing was running in the background — no pinned unit is installed on this box.");
|
|
118
|
+
say(" Nothing to do, and nothing was changed.");
|
|
119
|
+
}
|
|
105
120
|
} else if (failures.length) {
|
|
106
121
|
// THE SENTENCE THAT WAS WRONG. "The background product is stopped and the box runs nothing again" was
|
|
107
122
|
// printed unconditionally — including on the run where four services stayed up. A reader who is told
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 86061f4: Fixed: Commands printed while running from npx name the published version, so they still work after npm cleans its cache.
|
|
8
|
+
- ab9dc2d: Fixed: When the engine cannot answer "Describe it", the page names the check that finds out why. It says "just now" only for a usage limit.
|
|
9
|
+
- ab9dc2d: Fixed: `clearotron status` says whether a product started in a terminal is up, and on which addresses. `clearotron stop` says how to stop it.
|
|
10
|
+
- ab9dc2d: Fixed: The demo no longer offers `start --background`, which set up an empty install in your home instead of running the demo.
|
|
11
|
+
- ab9dc2d: Fixed: The key command the demo prints issues a key the demo's client door accepts, run exactly as printed.
|
|
12
|
+
|
|
13
|
+
## 0.3.0-beta.8
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 9181d6a: Fixed: Commands the install, start and the New company screen print now run as printed, from any directory and after npm cleans its cache.
|
|
18
|
+
- 0d75b4a: Fixed: A demo started with npx prints commands that keep working after npm cleans its cache.
|
|
19
|
+
- 9181d6a: Fixed: Doctor, run before the first start, describes the local sign-in that start brings up, and drops checkout-only warnings on a packaged install.
|
|
20
|
+
- 91888b2: Fixed: The package no longer names the hosted service's own hostnames; a deployment names its own.
|
|
21
|
+
- 9181d6a: Fixed: The README installs with `npx clearotron install`, which needs no root, and no longer says the demo opens a browser.
|
|
22
|
+
|
|
3
23
|
## 0.3.0-beta.7
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/driver/compose-read.mjs
CHANGED
|
@@ -325,7 +325,26 @@ export function jsonFromTurnText(text) {
|
|
|
325
325
|
return null;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
|
|
328
|
+
/**
|
|
329
|
+
* What the page says when the engine did not answer a read.
|
|
330
|
+
*
|
|
331
|
+
* "JUST NOW" ONLY WHERE IT IS TRUE. Every failed turn said the reader "could not reach the engine just
|
|
332
|
+
* now", and on a fresh install whose engine was not signed in that sent a stranger to press the button
|
|
333
|
+
* again, forever: the failure was the sign-in, which no retry changes (measured on a published beta,
|
|
334
|
+
* 2026-09-11). A rate limit is the one failure known to pass, so it alone says so, with its reset time
|
|
335
|
+
* when the engine gave one. Everything else names the check that finds the cause — `checkCommand`, in the
|
|
336
|
+
* reader's own invocation and with no machine path, because the page is not a place to print one.
|
|
337
|
+
*/
|
|
338
|
+
export function engineFailureMessage(t, { checkCommand = "clearotron doctor --probe-engine" } = {}) {
|
|
339
|
+
if (t?.rateLimited) {
|
|
340
|
+
const at = t.resetsAt ? new Date(t.resetsAt) : null;
|
|
341
|
+
const when = at && !Number.isNaN(at.getTime()) ? ` It resets at ${at.toISOString().slice(11, 16)} UTC.` : "";
|
|
342
|
+
return `The engine is at its usage limit just now.${when} Try again then, or set the search up below.`;
|
|
343
|
+
}
|
|
344
|
+
return `The request could not be read: the engine did not answer it. \`${checkCommand}\` finds out why — set the search up below meanwhile.`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function makeComposeReader({ turn, now = () => new Date(), checkCommand } = {}) {
|
|
329
348
|
if (typeof turn !== "function") return null;
|
|
330
349
|
return async function read(brief) {
|
|
331
350
|
const text = String(brief ?? "").trim();
|
|
@@ -361,7 +380,7 @@ export function makeComposeReader({ turn, now = () => new Date() } = {}) {
|
|
|
361
380
|
+ `${JSON.stringify(READ_SCHEMA)}\n\nThe brief:\n${text}`,
|
|
362
381
|
});
|
|
363
382
|
if (!t?.ok) {
|
|
364
|
-
return { ok: false, error: "engine", message:
|
|
383
|
+
return { ok: false, error: "engine", message: engineFailureMessage(t, { ...(checkCommand ? { checkCommand } : {}) }),
|
|
365
384
|
cause: t?.cause ?? "the engine turn did not complete", vendor: t?.vendor ?? null, authMode: t?.authMode ?? null,
|
|
366
385
|
engine: t?.engine ?? null, model: t?.model ?? null };
|
|
367
386
|
}
|
|
@@ -94,7 +94,10 @@ export function readJxTuple(tuple, { vendor, authMode, engine }) {
|
|
|
94
94
|
if (tuple?.killed || tuple?.signals?.stalled)
|
|
95
95
|
return { ok: false, cause: "the engine turn was killed before it answered (stall or wall)", truncationObservable: observable, ...base };
|
|
96
96
|
if (tuple?.signals?.rateLimited)
|
|
97
|
-
|
|
97
|
+
// FLAGGED, not only described: a rate limit is the one failure a caller may call passing, and it
|
|
98
|
+
// needs to know that without parsing this sentence. `resetsAt` is the adapter's, when it read one.
|
|
99
|
+
return { ok: false, cause: "the engine turn was rate-limited", rateLimited: true, resetsAt: tuple?.signals?.resetsAt ?? null,
|
|
100
|
+
truncationObservable: observable, ...base };
|
|
98
101
|
if (tuple?.code !== 0 || tuple?.json?.status !== "ok")
|
|
99
102
|
return { ok: false, cause: `the engine turn did not complete cleanly (code ${tuple?.code ?? "?"}, status ${tuple?.json?.status ?? "none"})`,
|
|
100
103
|
truncationObservable: observable, ...base };
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.9",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
@@ -38,10 +38,9 @@ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
|
38
38
|
import { storeInRepo, storeOutsideRepoMessage, makeCommittableAudit, resolveStoreRepoRoot, makeStoreCommit } from "../shared/store-in-repo.mjs"; //,
|
|
39
39
|
import { customerStoreDir, customerStoreLine } from "../shared/customer-store.mjs"; // — one store for the surface and the runs
|
|
40
40
|
import { clientFailureNote } from "../shared/client-failure-note.mjs"; // — one sentence, three surfaces
|
|
41
|
-
import { bareInvocation, invocationPrefix, installRoute } from "../shared/invocation.mjs"; // — and why this one surface is by NAME
|
|
41
|
+
import { bareInvocation, browserCommand, invocationPrefix, installRoute, npxVersionOf } from "../shared/invocation.mjs"; // — and why this one surface is by NAME
|
|
42
42
|
import { stdioConnectOffer, stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs"; // — ONE author for the connect route
|
|
43
43
|
import { isWsl } from "../shared/wsl.mjs"; // — on WSL, the connect lines say where they run
|
|
44
|
-
import { inNpxCache, ownVersion } from "../shared/permanent-install.mjs"; // — a demo from npx is reset through npx
|
|
45
44
|
import { connectOffers, offersForWire } from "../shared/connect-clients.mjs"; // — ONE table, resolved server-side
|
|
46
45
|
|
|
47
46
|
/**
|
|
@@ -1534,6 +1533,10 @@ export function makePortalService({
|
|
|
1534
1533
|
// half its readers. A WORD, never a command line and never a prefix: `invocationForm` can
|
|
1535
1534
|
// answer with this machine's absolute path, and this value is rendered in a browser.
|
|
1536
1535
|
setupRoute: installRoute(),
|
|
1536
|
+
// THE COMMAND THAT FILES AN ORGANISATION, for the screen that cannot create a company without one.
|
|
1537
|
+
// `browserCommand`'s rule: no path on this machine, and from npx's cache the published version
|
|
1538
|
+
// rather than a bare name that reader does not have.
|
|
1539
|
+
organisationCommand: browserCommand('start --organisation "<name>"'),
|
|
1537
1540
|
// — a button that always fails must not render as available. The reason is
|
|
1538
1541
|
// operator-shaped and staff-only; a client reads the generic sentence the button carries.
|
|
1539
1542
|
controls: { stop: { available: stopControl.available !== false,
|
|
@@ -1587,7 +1590,10 @@ export function makePortalService({
|
|
|
1587
1590
|
// A provider outage is not a portal fault and must not read as one. The composer shows the
|
|
1588
1591
|
// sentence and keeps every field the user already typed — the brief is still in the box.
|
|
1589
1592
|
audit({ event: "compose-read", by: principal.email, ok: false, error: String(e?.message ?? e) });
|
|
1590
|
-
|
|
1593
|
+
// NOT "just now": a throw says nothing about whether it will pass, and that wording sent a reader
|
|
1594
|
+
// back to the button for a failure no retry fixes. It names the check instead.
|
|
1595
|
+
return { status: 502, json: { code: "read_failed",
|
|
1596
|
+
error: `The request could not be read: the reader failed. \`${browserCommand("doctor --probe-engine")}\` finds out why — set the search up below meanwhile.` } };
|
|
1591
1597
|
}
|
|
1592
1598
|
// Length, never content. A brief is client material — whose mark, for whose product, before
|
|
1593
1599
|
// whose deadline — and an audit log is a different disclosure surface from a run store.
|
|
@@ -4708,7 +4714,8 @@ const PORT = PORT_CHOICE.port;
|
|
|
4708
4714
|
return null;
|
|
4709
4715
|
}
|
|
4710
4716
|
log(`brief reading ON — engine=${runner.engine} billing=${runner.authMode} model=${model}`);
|
|
4711
|
-
|
|
4717
|
+
// The check a failed read points at, as a reader types it, with no path of this machine in it.
|
|
4718
|
+
return makeComposeReader({ turn: runner.turn, checkCommand: browserCommand("doctor --probe-engine") });
|
|
4712
4719
|
} catch (e) {
|
|
4713
4720
|
// A missing dependency or a bad key shape must not stop the portal booting: everything else on
|
|
4714
4721
|
// this service is the load-bearing product, and the composer degrades to exactly what it did
|
|
@@ -4838,7 +4845,7 @@ const PORT = PORT_CHOICE.port;
|
|
|
4838
4845
|
attempts: makeAttemptLimiter({ max: 10, windowMs: 5 * 60 * 1000 }),
|
|
4839
4846
|
// The sign-in page's recovery line. It resets the file this portal reads, and names no path on this
|
|
4840
4847
|
// machine, because the page is read before anyone has signed in (signInResetCommand says how).
|
|
4841
|
-
resetCommand: signInResetCommand({ credentialPath, npxVersion:
|
|
4848
|
+
resetCommand: signInResetCommand({ credentialPath, npxVersion: npxVersionOf() }),
|
|
4842
4849
|
};
|
|
4843
4850
|
})();
|
|
4844
4851
|
|
package/driver/suite-census.json
CHANGED
|
@@ -141,6 +141,12 @@
|
|
|
141
141
|
"skips": 0,
|
|
142
142
|
"todos": 0
|
|
143
143
|
},
|
|
144
|
+
"a-command-printed-from-npx-outlives-the-cache.test.mjs": {
|
|
145
|
+
"tests": 3,
|
|
146
|
+
"asserts": 14,
|
|
147
|
+
"skips": 0,
|
|
148
|
+
"todos": 0
|
|
149
|
+
},
|
|
144
150
|
"a-company-can-be-given-a-framework-after-it-exists.test.mjs": {
|
|
145
151
|
"tests": 8,
|
|
146
152
|
"asserts": 19,
|
|
@@ -375,6 +381,12 @@
|
|
|
375
381
|
"skips": 0,
|
|
376
382
|
"todos": 0
|
|
377
383
|
},
|
|
384
|
+
"a-key-the-demo-prints-signs-for-that-demo.test.mjs": {
|
|
385
|
+
"tests": 5,
|
|
386
|
+
"asserts": 30,
|
|
387
|
+
"skips": 0,
|
|
388
|
+
"todos": 0
|
|
389
|
+
},
|
|
378
390
|
"a-killed-browser-takes-its-children.test.mjs": {
|
|
379
391
|
"tests": 2,
|
|
380
392
|
"asserts": 4,
|
|
@@ -543,6 +555,12 @@
|
|
|
543
555
|
"skips": 0,
|
|
544
556
|
"todos": 0
|
|
545
557
|
},
|
|
558
|
+
"a-read-the-engine-could-not-answer-names-the-check.test.mjs": {
|
|
559
|
+
"tests": 4,
|
|
560
|
+
"asserts": 14,
|
|
561
|
+
"skips": 0,
|
|
562
|
+
"todos": 0
|
|
563
|
+
},
|
|
546
564
|
"a-real-dispatch-records-the-tool-that-served-it.test.mjs": {
|
|
547
565
|
"tests": 1,
|
|
548
566
|
"asserts": 5,
|
|
@@ -970,9 +988,9 @@
|
|
|
970
988
|
"todos": 0
|
|
971
989
|
},
|
|
972
990
|
"an-install-from-npx-moves-out-of-the-cache.test.mjs": {
|
|
973
|
-
"tests":
|
|
974
|
-
"asserts":
|
|
975
|
-
"skips":
|
|
991
|
+
"tests": 13,
|
|
992
|
+
"asserts": 78,
|
|
993
|
+
"skips": 7,
|
|
976
994
|
"todos": 0
|
|
977
995
|
},
|
|
978
996
|
"an-mcp-route-says-which-challenge-it-answers-with.test.mjs": {
|
|
@@ -2747,7 +2765,7 @@
|
|
|
2747
2765
|
},
|
|
2748
2766
|
"no-internal-hostname-ships-in-the-package.test.mjs": {
|
|
2749
2767
|
"tests": 1,
|
|
2750
|
-
"asserts":
|
|
2768
|
+
"asserts": 4,
|
|
2751
2769
|
"skips": 0,
|
|
2752
2770
|
"todos": 0
|
|
2753
2771
|
},
|
|
@@ -3095,7 +3113,7 @@
|
|
|
3095
3113
|
},
|
|
3096
3114
|
"portal-service.test.mjs": {
|
|
3097
3115
|
"tests": 124,
|
|
3098
|
-
"asserts":
|
|
3116
|
+
"asserts": 673,
|
|
3099
3117
|
"skips": 1,
|
|
3100
3118
|
"todos": 0
|
|
3101
3119
|
},
|
|
@@ -4221,6 +4239,12 @@
|
|
|
4221
4239
|
"skips": 0,
|
|
4222
4240
|
"todos": 0
|
|
4223
4241
|
},
|
|
4242
|
+
"status-and-stop-see-a-foreground-start.test.mjs": {
|
|
4243
|
+
"tests": 6,
|
|
4244
|
+
"asserts": 25,
|
|
4245
|
+
"skips": 0,
|
|
4246
|
+
"todos": 0
|
|
4247
|
+
},
|
|
4224
4248
|
"status-says-when-a-service-predates-its-config.test.mjs": {
|
|
4225
4249
|
"tests": 7,
|
|
4226
4250
|
"asserts": 24,
|
|
@@ -4515,6 +4539,12 @@
|
|
|
4515
4539
|
"skips": 0,
|
|
4516
4540
|
"todos": 0
|
|
4517
4541
|
},
|
|
4542
|
+
"the-demo-offers-no-background-form.test.mjs": {
|
|
4543
|
+
"tests": 4,
|
|
4544
|
+
"asserts": 14,
|
|
4545
|
+
"skips": 0,
|
|
4546
|
+
"todos": 0
|
|
4547
|
+
},
|
|
4518
4548
|
"the-door-answers-the-address-it-advertises.test.mjs": {
|
|
4519
4549
|
"tests": 9,
|
|
4520
4550
|
"asserts": 37,
|
|
@@ -4587,6 +4617,12 @@
|
|
|
4587
4617
|
"skips": 0,
|
|
4588
4618
|
"todos": 0
|
|
4589
4619
|
},
|
|
4620
|
+
"the-first-hour-reads-true.test.mjs": {
|
|
4621
|
+
"tests": 8,
|
|
4622
|
+
"asserts": 41,
|
|
4623
|
+
"skips": 0,
|
|
4624
|
+
"todos": 0
|
|
4625
|
+
},
|
|
4590
4626
|
"the-fold-receipt-names-the-model.test.mjs": {
|
|
4591
4627
|
"tests": 5,
|
|
4592
4628
|
"asserts": 11,
|
|
@@ -112,19 +112,13 @@ export function unitsNeedingRender(root = ROOT) {
|
|
|
112
112
|
// through `envFrom`, so a name that later gains an old spelling in the alias table keeps resolving —
|
|
113
113
|
// a literal `process.env.X` here would be a spelling, and a spelling goes stale in silence.
|
|
114
114
|
//
|
|
115
|
-
// A `KEY=value` parser and nothing more
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let v = m[2].trim();
|
|
123
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
124
|
-
out[m[1]] = v;
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
115
|
+
// A `KEY=value` parser and nothing more, and it lives in `shared/env-file-merge.mjs` — a file that
|
|
116
|
+
// imports nothing. THIS module is a command too: its `--apply` path awaits at the top level and imports
|
|
117
|
+
// `bin/start.mjs` from inside that await, so anything that took the parser from here put a CLI in its
|
|
118
|
+
// import path, and `start.mjs` doing so closed a cycle that stopped 21 install arms at once. Re-exported
|
|
119
|
+
// so every reader here keeps one reader and one spelling.
|
|
120
|
+
export { parseEnvFile } from "../../shared/env-file-merge.mjs";
|
|
121
|
+
import { parseEnvFile } from "../../shared/env-file-merge.mjs";
|
|
128
122
|
|
|
129
123
|
export function resolveValues(names, { env = process.env, envFile = null } = {}) {
|
|
130
124
|
const fileVals = envFile && existsSync(envFile) ? parseEnvFile(readFileSync(envFile, "utf8")) : {};
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.9",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/package.json
CHANGED
|
@@ -10508,6 +10508,7 @@ var api = {
|
|
|
10508
10508
|
}),
|
|
10509
10509
|
accountOrgs: Object.fromEntries(Object.entries(asRecord(b["accountOrgs"])).filter(([, v]) => typeof v === "string" && v)),
|
|
10510
10510
|
genericOrgs: asArray(b["genericOrgs"]).filter((o) => typeof o === "string" && o.length > 0),
|
|
10511
|
+
organisationCommand: asString(b["organisationCommand"]) || "clearotron start --organisation \"<name>\"",
|
|
10511
10512
|
email: asString(b["email"]) ?? "",
|
|
10512
10513
|
accounts: asArray(b["accounts"]).filter((a) => typeof a === "string"),
|
|
10513
10514
|
allAccounts: b["accounts"] === "*",
|
|
@@ -19106,7 +19107,7 @@ function NewCompany({ ctx }) {
|
|
|
19106
19107
|
if (!raw.trim()) return [];
|
|
19107
19108
|
return parseLines(raw, spec.commaSeparated ?? false).filter((e) => !spec.item.ok(e));
|
|
19108
19109
|
});
|
|
19109
|
-
const unmet = !typedName ? "Needs a name." : !key ? "Needs a key — type one below." : !orgChosen ? orgsHeld.length === 0 ?
|
|
19110
|
+
const unmet = !typedName ? "Needs a name." : !key ? "Needs a key — type one below." : !orgChosen ? orgsHeld.length === 0 ? `No organisation is filed on this install yet. File one with: ${ctx.me.organisationCommand}` : "Choose the organisation it belongs to." : refusedEntries.length ? `${refusedEntries.join(", ")} cannot be searched — fix or remove ${refusedEntries.length === 1 ? "it" : "them"}.` : null;
|
|
19110
19111
|
async function create() {
|
|
19111
19112
|
if (unmet || busy) return;
|
|
19112
19113
|
setBusy(true);
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-qes7gLpM.js"></script>
|
|
53
53
|
<link rel="stylesheet" crossorigin href="/portal/assets/index-Cv-E_agg.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.9",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.9",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -135,13 +135,14 @@ export function readAudience({ location = "", status = null, error = null, wwwAu
|
|
|
135
135
|
if (error) return { kind: "unreachable", why: String(error?.message ?? error).slice(0, 200) };
|
|
136
136
|
// ── THREE DOORS, NOT ONE ────────────────────────────────────────────────────────────────────────
|
|
137
137
|
//
|
|
138
|
-
// This returned `not-fronted` for every response with no redirect, and measured against production
|
|
139
|
-
// four configured hostnames that one label covered three materially different states
|
|
138
|
+
// This returned `not-fronted` for every response with no redirect, and measured against a production
|
|
139
|
+
// deployment's four configured hostnames that one label covered three materially different states
|
|
140
|
+
// (the hosts are written here as placeholders; a deployment names its own):
|
|
140
141
|
//
|
|
141
|
-
//
|
|
142
|
-
// mcp.
|
|
143
|
-
// clients-mcp.
|
|
144
|
-
// agent-mcp.
|
|
142
|
+
// portal.example.com 302 redirect present → audience read, kid agrees
|
|
143
|
+
// mcp.example.com/mcp 401 no redirect → not-fronted ← FALSE, it IS fronted
|
|
144
|
+
// clients-mcp.example.com/mcp 401 no redirect → not-fronted ← FALSE, it IS fronted
|
|
145
|
+
// agent-mcp.example.com/mcp 502 no redirect → not-fronted ← an origin fault
|
|
145
146
|
//
|
|
146
147
|
// None of these was a false pass — every one returned ok:false, which is the property that matters
|
|
147
148
|
// most and is untouched. It was a WRONG DIAGNOSIS on a safe failure, and the cost is a reader's hour
|
package/shared/client-door.mjs
CHANGED
|
@@ -102,6 +102,45 @@ export function denylistFor({ paths, demo = false, env = {}, home }) {
|
|
|
102
102
|
return paths.denylist ?? denylistPathFor(env, home);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* THE DEMO'S SIGNING SECRET, KEPT IN THE DEMO'S OWN BASE.
|
|
107
|
+
*
|
|
108
|
+
* The demo generated its signing secret per run and held it in memory only, so the key command its
|
|
109
|
+
* terminal printed could never sign a key its door would accept: run from a second terminal, as printed,
|
|
110
|
+
* it read the install's settings and refused (measured on a published beta, 2026-09-11). The secret now
|
|
111
|
+
* lives beside the demo's revocation list, mode 600, so `key issue --base <demo base>` reads the one the
|
|
112
|
+
* door was given, and removing the demo is still removing one directory.
|
|
113
|
+
*/
|
|
114
|
+
export const demoTokenSecretPath = (base) => join(base, "token-secret");
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The secret a demo's door signs with: the one its base already holds, or a new one written there.
|
|
118
|
+
* `io` is `{ read, write, mint }`; a read that finds nothing is the first start.
|
|
119
|
+
*/
|
|
120
|
+
export function demoTokenSecret(base, io) {
|
|
121
|
+
const path = demoTokenSecretPath(base);
|
|
122
|
+
let held = "";
|
|
123
|
+
try { held = String(io.read(path) ?? "").trim(); } catch { /* not yet written */ }
|
|
124
|
+
if (held) return held;
|
|
125
|
+
const fresh = io.mint();
|
|
126
|
+
io.write(path, `${fresh}\n`);
|
|
127
|
+
return fresh;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The key command a start prints beside its client door — runnable exactly as printed.
|
|
132
|
+
*
|
|
133
|
+
* A demo's door admits the demo's own account, so the command names it rather than a placeholder a reader
|
|
134
|
+
* cannot type, and it names the demo's base so the verb reads that demo's secret and guest list. An
|
|
135
|
+
* install moved with `--base` is named too, for its guest list; the default install needs neither.
|
|
136
|
+
*/
|
|
137
|
+
export function keyIssueCommand({ prefix = "", demo = false, user = null, base = null, defaultBase = null } = {}) {
|
|
138
|
+
const q = (d) => (/\s/.test(d) ? `"${d}"` : d);
|
|
139
|
+
const who = demo && user ? user : "<email>";
|
|
140
|
+
const where = base && (demo || base !== defaultBase) ? ` --base ${q(base)}` : "";
|
|
141
|
+
return `${prefix}clearotron key issue ${who}${where}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* Create the denylist if it is absent, so the door is born consulting a file that exists.
|
|
107
146
|
*
|
|
@@ -34,6 +34,30 @@
|
|
|
34
34
|
* like an engine fault. Names here must be values this code MINTS; putting
|
|
35
35
|
* a credential a reader supplied in this list would delete their work.
|
|
36
36
|
*/
|
|
37
|
+
// ── THE ONE `KEY=value` READER, AND IT LIVES IN A LEAF ────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// This reads the same file systemd's EnvironmentFile= reads, and systemd does no shell expansion there
|
|
40
|
+
// either. Quotes are stripped because operators write them.
|
|
41
|
+
//
|
|
42
|
+
// IT USED TO LIVE IN `driver/systemd/render-units.mjs`, which is a COMMAND as well as a module, and its
|
|
43
|
+
// `--apply` path awaits at the top level and imports `bin/start.mjs` from inside that await. So a reader
|
|
44
|
+
// that took the parser from there put the command's own module in its import path: `start.mjs` importing
|
|
45
|
+
// it closed a cycle, the CLI's top-level await never settled, and 21 install arms died at once with
|
|
46
|
+
// "Detected unsettled top-level await" and nothing naming the cause (measured 2026-09-12). The parser has
|
|
47
|
+
// no business depending on any of that — it is eleven lines of string handling — so it sits here, in a
|
|
48
|
+
// file that imports nothing, and `render-units.mjs` re-exports it so its readers keep one reader.
|
|
49
|
+
export function parseEnvFile(text) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
52
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
let v = m[2].trim();
|
|
55
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
56
|
+
out[m[1]] = v;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
export function mergeEnvFile(text, additions, { by = "`npm start` (bin/start.mjs)", notes = {}, refresh = [] } = {}) {
|
|
38
62
|
const body = typeof text === "string" ? text : "";
|
|
39
63
|
const present = new Set();
|
package/shared/invocation.mjs
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
// below is not a resolver check: the shim is read and must NAME THIS INSTALL, and a `clearotron` found
|
|
37
37
|
// earlier on PATH demotes the bare form rather than confirming it. Identity, not availability. A later
|
|
38
38
|
// sweep tempted to simplify this into `command -v` would be reintroducing the defect, not tidying it.
|
|
39
|
-
import { existsSync } from "node:fs";
|
|
40
|
-
import { basename, sep } from "node:path";
|
|
39
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
+
import { basename, join, sep } from "node:path";
|
|
41
41
|
import { INSTALL_DIR, inspectShim, pathPosition, shimDir, shimPath } from "./verb-shim.mjs";
|
|
42
42
|
import { chdirPrefix } from "./os-advice.mjs";
|
|
43
43
|
|
|
@@ -205,6 +205,24 @@ export function invocationForm(env = process.env, io = FS, installDir = INSTALL_
|
|
|
205
205
|
// right there and naming it in full is true from any directory.
|
|
206
206
|
return { form: "shim-path", prefix: `${globalDir}${sep}`, shim: globalExe, dir: globalDir, onPath, shadowedBy, staleInterpreter: null, via: "global" };
|
|
207
207
|
}
|
|
208
|
+
// ── FROM NPX'S CACHE, THE PUBLISHED VERSION, NOT THE CACHE ──────────────────────────────────────────
|
|
209
|
+
//
|
|
210
|
+
// `in-place` below names the directory this install runs from, and under npx that is npm's cache:
|
|
211
|
+
// `cd <npm's cache> && npx clearotron …`. npm deletes that directory when it cleans its cache, so every
|
|
212
|
+
// command a verb printed there stopped working before its reader typed it (a packaged-install drive of
|
|
213
|
+
// `doctor`, 2026-09-11, found thirteen). `npx -p clearotron@<version>` fetches this same version from any
|
|
214
|
+
// directory. It is the decision `browserCommand` makes for a page, `npx clearotron@<version> <verb>`,
|
|
215
|
+
// spelled for a PREFIX, which the word `clearotron` follows. Asked after a shim of ours and a global
|
|
216
|
+
// install, which are stronger evidence than the cache path; a version that cannot be read falls through
|
|
217
|
+
// to `in-place` rather than naming a guess.
|
|
218
|
+
const npxVersion = npxVersionOf(installDir, io.read ?? readFileSync);
|
|
219
|
+
if (npxVersion) {
|
|
220
|
+
return {
|
|
221
|
+
form: "npx-pinned", prefix: `npx -p clearotron@${npxVersion} `, shim: path, dir,
|
|
222
|
+
onPath: false, shadowedBy: null, shimKind: shim.kind, otherInstall: shim.installDir,
|
|
223
|
+
staleInterpreter: shim.interpreterMissing === true ? shim.interpreter : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
208
226
|
// ✕ A BROKEN SHIM MUST NOT BE THE FORM WE HAND BACK, however well it identifies itself. Driven and
|
|
209
227
|
// caught: doctor reported the stale interpreter and then told the reader to run `clearotron install`
|
|
210
228
|
// to fix it — through the very shim it had just called broken. The advice for repairing a route
|
|
@@ -272,6 +290,25 @@ export function invocationPrefix(argv1 = process.argv[1] ?? "", env = process.en
|
|
|
272
290
|
export const invoke = (verb, argv1 = process.argv[1] ?? "", env = process.env, io = FS, installDir = INSTALL_DIR) =>
|
|
273
291
|
`${invocationPrefix(argv1, env, io, installDir)}clearotron ${verb}`;
|
|
274
292
|
|
|
293
|
+
/** This install's version when it runs from npx's cache, read off its own manifest; `null` anywhere else. */
|
|
294
|
+
export function npxVersionOf(installDir = INSTALL_DIR, read = readFileSync) {
|
|
295
|
+
if (!/[\\/]_npx[\\/]/.test(String(installDir ?? ""))) return null;
|
|
296
|
+
try { return JSON.parse(read(join(installDir, "package.json"), "utf8")).version ?? null; } catch { return null; }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A command the reader can type from any directory that still runs after npm cleans its cache.
|
|
301
|
+
*
|
|
302
|
+
* `invoke` answers how this install is reached NOW. From npx's cache that is `cd <the cache> && npx
|
|
303
|
+
* clearotron …`, which stops working once npm cleans the cache, and a remedy is typed later, often after
|
|
304
|
+
* that. So from npx's cache this names the published version, `npx clearotron@<version> …`, which npm
|
|
305
|
+
* fetches again wherever it is typed. Everywhere else it is `invoke`'s answer.
|
|
306
|
+
*/
|
|
307
|
+
export function reachableCommand(verb, { argv1 = process.argv[1] ?? "", env = process.env, io = FS, installDir = INSTALL_DIR, read = readFileSync } = {}) {
|
|
308
|
+
const version = npxVersionOf(installDir, read);
|
|
309
|
+
return version ? `npx clearotron@${version} ${verb}` : invoke(verb, argv1, env, io, installDir);
|
|
310
|
+
}
|
|
311
|
+
|
|
275
312
|
/**
|
|
276
313
|
* The verb by NAME ONLY — no prefix, no path, no `npx`.
|
|
277
314
|
*
|
|
@@ -292,6 +329,16 @@ export const invoke = (verb, argv1 = process.argv[1] ?? "", env = process.env, i
|
|
|
292
329
|
*/
|
|
293
330
|
export const bareInvocation = (verb) => `clearotron ${verb}`;
|
|
294
331
|
|
|
332
|
+
/**
|
|
333
|
+
* A command for a page rendered in a browser: `bareInvocation`'s rule, never a path on this machine, with
|
|
334
|
+
* one exception. Run from npx's cache the bare name is one the reader does not have, so the published
|
|
335
|
+
* version is named instead, `npx clearotron@<version> …`, which runs anywhere and names no path.
|
|
336
|
+
*/
|
|
337
|
+
export const browserCommand = (verb, installDir = INSTALL_DIR, read = readFileSync) => {
|
|
338
|
+
const version = npxVersionOf(installDir, read);
|
|
339
|
+
return version ? `npx clearotron@${version} ${verb}` : bareInvocation(verb);
|
|
340
|
+
};
|
|
341
|
+
|
|
295
342
|
/**
|
|
296
343
|
* Refuse a prompt when there is nobody to answer it.
|
|
297
344
|
*
|