clearotron 0.3.2-beta.13 → 0.3.2-beta.14
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/clearotron.mjs +7 -1
- package/bin/example.mjs +49 -22
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +8 -0
- package/driver/engine/mcp/codex-config.mjs +3 -11
- package/driver/package.json +1 -1
- package/driver/publish/render-knockout.mjs +31 -6
- package/driver/suite-census.json +13 -1
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/shared/demo-start-args.mjs +10 -0
- package/shared/stdio-connect.mjs +41 -10
- package/shared/toml-string.mjs +25 -0
package/bin/clearotron.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
|
25
25
|
import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
|
|
26
26
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
|
|
27
27
|
import { watchParent } from "../shared/parent-watch.mjs";
|
|
28
|
+
import { EXPERIMENTAL_WARNING_OFF } from "../shared/demo-start-args.mjs";
|
|
28
29
|
|
|
29
30
|
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
31
|
|
|
@@ -193,7 +194,12 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
193
194
|
// — tell the child how the READER reached us, so its own advice names a command they can type.
|
|
194
195
|
// Without this every spawned verb sees argv[1] = its own implementation file and would print `npx`
|
|
195
196
|
// even for somebody who typed a bare `clearotron`.
|
|
196
|
-
|
|
197
|
+
// THE DEMO RUNS WITHOUT NODE'S EXPERIMENTAL-FEATURE WARNING. Node prints one the first time anything loads
|
|
198
|
+
// its built-in SQLite, and on the demo it landed on the first screen, above the sentence saying what the
|
|
199
|
+
// demo is. A flag on the demo's own process; `bin/example.mjs` hands it on to the services it starts
|
|
200
|
+
// (EXPERIMENTAL_WARNING_OFF). Every other warning still prints.
|
|
201
|
+
const quiet = verb === "demo" ? [EXPERIMENTAL_WARNING_OFF] : [];
|
|
202
|
+
const child = spawn(process.execPath, [...quiet, target, ...builtin, ...rest], {
|
|
197
203
|
stdio: "inherit",
|
|
198
204
|
env: { ...process.env, CLEAROTRON_INVOKED_AS: process.argv[1] ?? "" },
|
|
199
205
|
});
|
package/bin/example.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import "../shared/env-local.mjs"; // step 4 / — FIRST: this program read a
|
|
|
38
38
|
// handed it nothing and the read fell through to a default. Proven both ways from one
|
|
39
39
|
// environment: without this import the value is invisible, with it the retired spelling is
|
|
40
40
|
// back-filled. Placed above every other import because a side-effecting import runs in order.
|
|
41
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
41
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
42
42
|
import { homedir, tmpdir } from "node:os";
|
|
43
43
|
import { removeDirectory } from "../shared/os-advice.mjs";
|
|
44
44
|
import { invoke } from "../shared/invocation.mjs"; // — the printed command is resolved once, for the reader who is actually standing there
|
|
@@ -53,7 +53,7 @@ import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.m
|
|
|
53
53
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
54
54
|
|
|
55
55
|
import { usageBlock } from "../shared/usage-block.mjs";
|
|
56
|
-
import { demoStartArgs } from "../shared/demo-start-args.mjs";
|
|
56
|
+
import { demoStartArgs, withWarningOff } from "../shared/demo-start-args.mjs";
|
|
57
57
|
import { bundleVerdict } from "../shared/bundle-freshness.mjs";
|
|
58
58
|
const argv = process.argv.slice(2);
|
|
59
59
|
const flag = (n, d = null) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; };
|
|
@@ -258,6 +258,14 @@ if (existsSync(poolRoot) && !statSync(poolRoot).isDirectory()) die(`demo: ${pool
|
|
|
258
258
|
|
|
259
259
|
// ── 3. replay ────────────────────────────────────────────────────────────────────────────────────────
|
|
260
260
|
console.log(`\n ${BRAND.name} ${BRAND.product.toLowerCase()} — demo\n`);
|
|
261
|
+
// THE LABEL, FIRST. The reader is about to look at a document that reads like advice about a real mark. It
|
|
262
|
+
// is not, and the demo says so before anything else rather than in a footnote nobody reaches. It printed
|
|
263
|
+
// after the replay, below the engine's own diagnostics and a Node warning, so the first thing a newcomer
|
|
264
|
+
// read was internal tallies (measured on the published beta, 2026-09-19).
|
|
265
|
+
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
266
|
+
console.log(" Replaying it needs no account, no key and no network.");
|
|
267
|
+
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
268
|
+
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
261
269
|
console.log(samples.length === 1
|
|
262
270
|
? ` sample: ${samples[0].dir}`
|
|
263
271
|
: ` samples: ${samples.length}${failures.length ? ` of ${shipped}` : ""} — ${samples.map((x) => x.name).join(", ")}`);
|
|
@@ -276,14 +284,33 @@ const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publis
|
|
|
276
284
|
// The failures are collected and reported together at the end, and the process exits non-zero, because a
|
|
277
285
|
// demo that came up missing a quarter of itself is not a success however good the three look.
|
|
278
286
|
const results = [];
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
// THE PUBLISHER'S OWN DIAGNOSTICS GO TO A FILE. It prints its tallies as it works (`[record-links] …`,
|
|
288
|
+
// `knockout receipts: …`), which is right for an operator watching a delivery and wrong as the first
|
|
289
|
+
// screen a newcomer reads. The replay's output is written to `replay.log` in the folder this demo made,
|
|
290
|
+
// and restored when the replay ends, so everything below prints as before. A failure is not lost: it is
|
|
291
|
+
// collected here and reported after the list.
|
|
292
|
+
const replayLog = join(flag("--pool") ? poolRoot : demoBase, "replay.log");
|
|
293
|
+
const toLog = (chunk, encoding, cb) => {
|
|
294
|
+
try { appendFileSync(replayLog, chunk, typeof encoding === "string" ? encoding : undefined); } catch { /* a log that cannot be written must not stop the replay */ }
|
|
295
|
+
if (typeof encoding === "function") encoding(); else if (typeof cb === "function") cb();
|
|
296
|
+
return true;
|
|
297
|
+
};
|
|
298
|
+
const screen = { out: process.stdout.write, err: process.stderr.write };
|
|
299
|
+
process.stdout.write = toLog;
|
|
300
|
+
process.stderr.write = toLog;
|
|
301
|
+
try {
|
|
302
|
+
for (const s0 of samples) {
|
|
303
|
+
try {
|
|
304
|
+
// poolUrl "" on purpose: the report's own link block is for a deployment that serves the pool at a
|
|
305
|
+
// public URL. This one is served from this process, at a port picked below.
|
|
306
|
+
results.push({ ...s0, published: await republishRun({ runId: s0.meta.runId, meta: s0.meta, pool: poolRoot, poolUrl: "", runDir: join(s0.publishFrom, "run") }) });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
failures.push({ name: s0.name, why: String(e?.message ?? e) });
|
|
309
|
+
}
|
|
286
310
|
}
|
|
311
|
+
} finally {
|
|
312
|
+
process.stdout.write = screen.out;
|
|
313
|
+
process.stderr.write = screen.err;
|
|
287
314
|
}
|
|
288
315
|
// PUBLISHED, SO THE COPIES THEY WERE PUBLISHED FROM GO NOW, whatever comes next.
|
|
289
316
|
releaseDemoCopies();
|
|
@@ -293,12 +320,6 @@ if (!results.length) {
|
|
|
293
320
|
}
|
|
294
321
|
const published = results[0].published;
|
|
295
322
|
|
|
296
|
-
// THE LABEL. The reader is about to look at a document that reads like advice about a real mark. It is
|
|
297
|
-
// not, and the demo says so before the browser opens rather than in a footnote nobody reaches.
|
|
298
|
-
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
299
|
-
console.log(" Replaying it needs no account, no key and no network.");
|
|
300
|
-
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
301
|
-
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
302
323
|
// NAMES THE POPULATION. This printed "13 finding(s)" beside a report showing
|
|
303
324
|
// twelve, in the first sentence a reader meets. The number is not wrong — it comes from the audit
|
|
304
325
|
// spine (`driver/publish/audit-from-spine.mjs`, `counts.findings`), which counts every finding the run
|
|
@@ -307,10 +328,8 @@ console.log(" re-rendered from its artifacts. It is an example, not advice.\n")
|
|
|
307
328
|
// EACH LANE'S PUBLISHER REPORTS ITS OWN NUMBER, AND THEY ARE NOT THE SAME NUMBER.
|
|
308
329
|
//
|
|
309
330
|
// The clearance branch returns `counts.findings` — every finding in the run's audit spine. The knockout
|
|
310
|
-
// branch returns no `counts` at all; it returns
|
|
311
|
-
//
|
|
312
|
-
// is printed in its own words rather than mapped onto the clearance sentence. They happen to agree on
|
|
313
|
-
// the demo in this tree, which is one member of a class and proves nothing about the metric.
|
|
331
|
+
// branch returns no `counts` at all; it returns the names it screened and the rating each was given, so
|
|
332
|
+
// its line states those rather than a count mapped onto the clearance sentence.
|
|
314
333
|
//
|
|
315
334
|
// The third branch is the point: a lane whose publisher reports no count says so. This line printed a
|
|
316
335
|
// bare "?" to the knockout — a could-not-look wearing the costume of a number.
|
|
@@ -322,9 +341,13 @@ const spineOf = (pub) =>
|
|
|
322
341
|
Number.isFinite(pub.counts?.findings)
|
|
323
342
|
? `${pub.counts.findings} finding(s) recorded in the run's audit trail; the report shows
|
|
324
343
|
the ones it retains`
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
// A KNOCKOUT STATES WHAT IT SCREENED AND HOW EACH NAME WAS RATED, in the words its own report opens
|
|
345
|
+
// with ("One name screened: VENQORI, rated Low."). It used to count citations traced to held evidence,
|
|
346
|
+
// which a knockout's register-only findings never carry, so the line read "0 finding(s)" and a stranger
|
|
347
|
+
// took it as "the demo found nothing". Chosen 2026-09-19.
|
|
348
|
+
: Array.isArray(pub.reports) && pub.reports.length
|
|
349
|
+
? `${pub.reports.length} name${pub.reports.length === 1 ? "" : "s"} screened: ${pub.reports
|
|
350
|
+
.map((r) => (r.band ? `${r.mark}, rated ${r.band}` : r.mark)).join("; ")}; the report shows why`
|
|
328
351
|
: `this lane's publisher reported no finding count — the report itself is the record`;
|
|
329
352
|
for (const r of results) console.log(` published: ${r.published.runId}\n ${r.name} — ${spineOf(r.published)}`);
|
|
330
353
|
// "ONE PER PRODUCT" ONLY WHEN IT IS TRUE: with a sample missing, the count below says how many of how many.
|
|
@@ -418,6 +441,10 @@ console.log("");
|
|
|
418
441
|
// made, it starts from here as before.
|
|
419
442
|
const programRoot = ensureDemoProgram({ base: demoBase, say: (line) => console.log(line) });
|
|
420
443
|
const startFrom = programRoot ?? REPO;
|
|
444
|
+
// The services inherit the demo's Node flag through NODE_OPTIONS, so none of them prints the SQLite warning
|
|
445
|
+
// either. Set on this process's own environment, which both branches below hand on; the reader's own
|
|
446
|
+
// NODE_OPTIONS is kept.
|
|
447
|
+
process.env.NODE_OPTIONS = withWarningOff(process.env.NODE_OPTIONS);
|
|
421
448
|
const child = spawn(process.execPath, [join(startFrom, "bin", "start.mjs"), ...startArgs], {
|
|
422
449
|
cwd: startFrom, stdio: ["ignore", "inherit", "inherit"],
|
|
423
450
|
env: programRoot ? demoProgramEnv(process.env) : process.env,
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.2-beta.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed: The lines for connecting an AI assistant now work when a folder name contains spaces, and the Codex settings block now works on Windows.
|
|
8
|
+
- Fixed: The demo now opens by saying what it shows, without internal diagnostics or a Node warning, and its knockout sample states the rating it reached.
|
|
9
|
+
- Fixed: A knockout report ordered for named countries now lists those countries under Registers counted, instead of a count of the provider's registers.
|
|
10
|
+
|
|
3
11
|
## 0.3.2-beta.13
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// exact server/tool wiring. Two inputs, both produced today by the gateway gather block:
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { tomlString } from "../../../shared/toml-string.mjs";
|
|
8
9
|
|
|
9
10
|
// • mcpConfig — the claude-shaped JSON string `{mcpServers:{name:{command,args,env,connectionTimeoutMs?}}}`
|
|
10
11
|
// (buildGatherMcpConfig → JSON.stringify)
|
|
@@ -36,17 +37,8 @@ export const CRED_ENV_FORWARD = [
|
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
// ── TOML value escaping (basic strings) ──────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// TOML basic-string escapes: backslash, double-quote, and the C0 controls TOML names (\b \t \n \f \r).
|
|
42
|
-
const esc = str
|
|
43
|
-
.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
44
|
-
.replace(/\x08/g, "\\b").replace(/\t/g, "\\t").replace(/\n/g, "\\n")
|
|
45
|
-
.replace(/\f/g, "\\f").replace(/\r/g, "\\r")
|
|
46
|
-
// any remaining control char → \uXXXX (TOML-legal)
|
|
47
|
-
.replace(/[\x00-\x1f]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
48
|
-
return `"${esc}"`;
|
|
49
|
-
}
|
|
40
|
+
// One encoder for every TOML block the product writes; re-exported so this module's callers keep their import.
|
|
41
|
+
export { tomlString };
|
|
50
42
|
function tomlStringArray(arr) {
|
|
51
43
|
return "[" + (arr || []).map(tomlString).join(", ") + "]";
|
|
52
44
|
}
|
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.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.14",
|
|
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": {
|
|
@@ -63,6 +63,7 @@ import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mj
|
|
|
63
63
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
64
64
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
65
65
|
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
66
|
+
import OFFERED from '../../shared/offered-territories.json' with { type: 'json' }; // the order form's own territory names
|
|
66
67
|
|
|
67
68
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
68
69
|
|
|
@@ -481,6 +482,14 @@ function territoryName(code, { bare = false } = {}) {
|
|
|
481
482
|
} catch { return ''; }
|
|
482
483
|
}
|
|
483
484
|
|
|
485
|
+
/** A territory the order names, in the order form's own words, or '' when that list has no such code. */
|
|
486
|
+
const ORDER_FORM_NAMES = new Map([...OFFERED.regions, ...OFFERED.countries].map((t) => [t.code, t.name]));
|
|
487
|
+
const orderedTerritoryName = (code) => {
|
|
488
|
+
const c = String(code ?? '').trim().toUpperCase();
|
|
489
|
+
// EM is the European Union register's own code, and some records carry it for an EU order.
|
|
490
|
+
return ORDER_FORM_NAMES.get(c === 'EM' ? 'EU' : c) ?? '';
|
|
491
|
+
};
|
|
492
|
+
|
|
484
493
|
/** "a, b and c" — the reader's list, not a join on commas. */
|
|
485
494
|
function listWords(items) {
|
|
486
495
|
const xs = (items ?? []).filter(Boolean);
|
|
@@ -1244,12 +1253,28 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1244
1253
|
// the middle of the panel is the thing a reader skips; the count and its source are what they read.
|
|
1245
1254
|
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1246
1255
|
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1247
|
-
// THE
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1256
|
+
// THE ORDER'S TERRITORIES, NOT THE PROVIDER'S REGISTERS (owner, 2026-09-19). The row counted the
|
|
1257
|
+
// registers the provider searched, and for a one-country order that is not one: a United States order is
|
|
1258
|
+
// counted over the United States and the international register, because an international filing can
|
|
1259
|
+
// designate the United States. A client who ordered one country read "2 registers" as a mistake. So an
|
|
1260
|
+
// order that names its territories is stated as it named them, in the order form's own names; the
|
|
1261
|
+
// registers actually counted stay in the line under the counts table. A worldwide order keeps the board's
|
|
1262
|
+
// row, "186 registers, on Clarivate Compumark.", because there the count is the scope (the record says
|
|
1263
|
+
// worldwide by carrying no jurisdictions). A territory the provider does not cover was not counted, so it
|
|
1264
|
+
// is not listed here; it is disclosed where deferrals are. A territory the form's list cannot name, or an
|
|
1265
|
+
// office this install could not reach, falls back to the count rather than printing a code or claiming
|
|
1266
|
+
// a territory nothing was counted in.
|
|
1267
|
+
const on = provider ? `, on ${provider}` : '';
|
|
1268
|
+
const sc = registerCounts?.scope ?? {};
|
|
1269
|
+
const deferred = new Set((sc.deferredJurisdictions ?? []).map((c) => String(c ?? '').trim().toUpperCase()));
|
|
1270
|
+
const ordered = [...new Set((sc.jurisdictions ?? [])
|
|
1271
|
+
.filter((c) => !deferred.has(String(c ?? '').trim().toUpperCase())).map(orderedTerritoryName))];
|
|
1272
|
+
const namesTheOrder = sc.worldwide !== true && !(sc.unreachableOffices ?? []).length
|
|
1273
|
+
&& ordered.length > 0 && ordered.every(Boolean);
|
|
1274
|
+
const counted = namesTheOrder ? `${listWords(ordered)}${on}.`
|
|
1275
|
+
: regions.length
|
|
1276
|
+
? `${regions.length} ${regions.length === 1 ? 'register' : 'registers'}${on}.`
|
|
1277
|
+
: (registerCounts ? `Counted worldwide${on}` : '');
|
|
1253
1278
|
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1254
1279
|
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1255
1280
|
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
package/driver/suite-census.json
CHANGED
|
@@ -201,6 +201,12 @@
|
|
|
201
201
|
"skips": 0,
|
|
202
202
|
"todos": 0
|
|
203
203
|
},
|
|
204
|
+
"a-connect-line-keeps-every-path-whole.test.mjs": {
|
|
205
|
+
"tests": 8,
|
|
206
|
+
"asserts": 20,
|
|
207
|
+
"skips": 0,
|
|
208
|
+
"todos": 0
|
|
209
|
+
},
|
|
204
210
|
"a-connect-line-says-where-it-runs.test.mjs": {
|
|
205
211
|
"tests": 11,
|
|
206
212
|
"asserts": 56,
|
|
@@ -4869,6 +4875,12 @@
|
|
|
4869
4875
|
"skips": 0,
|
|
4870
4876
|
"todos": 0
|
|
4871
4877
|
},
|
|
4878
|
+
"the-demo-says-what-it-is-first.test.mjs": {
|
|
4879
|
+
"tests": 3,
|
|
4880
|
+
"asserts": 16,
|
|
4881
|
+
"skips": 0,
|
|
4882
|
+
"todos": 0
|
|
4883
|
+
},
|
|
4872
4884
|
"the-demo-takes-its-folder-with-it.test.mjs": {
|
|
4873
4885
|
"tests": 6,
|
|
4874
4886
|
"asserts": 12,
|
|
@@ -5051,7 +5063,7 @@
|
|
|
5051
5063
|
},
|
|
5052
5064
|
"the-knockout-page-leads-with-the-read.test.mjs": {
|
|
5053
5065
|
"tests": 39,
|
|
5054
|
-
"asserts":
|
|
5066
|
+
"asserts": 125,
|
|
5055
5067
|
"skips": 0,
|
|
5056
5068
|
"todos": 0
|
|
5057
5069
|
},
|
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.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.14",
|
|
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
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.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.14",
|
|
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.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.14",
|
|
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).",
|
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
* `--demo-own-base` says it is the demo's default, which is what lets the supervisor remove the folder
|
|
13
13
|
* when the window closes. `--keep` is the reader's, and it travels as given.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The Node flag the demo runs under, so Node's one-time warning that its built-in SQLite is experimental
|
|
17
|
+
* does not land on the demo's first screen. Named once: the launcher puts it on the demo's own process,
|
|
18
|
+
* and `bin/example.mjs` hands it to the services it starts through NODE_OPTIONS.
|
|
19
|
+
*/
|
|
20
|
+
export const EXPERIMENTAL_WARNING_OFF = "--disable-warning=ExperimentalWarning";
|
|
21
|
+
|
|
22
|
+
/** The same flag added to whatever NODE_OPTIONS the reader already has, which is kept. PURE. */
|
|
23
|
+
export const withWarningOff = (nodeOptions) => [nodeOptions, EXPERIMENTAL_WARNING_OFF].filter(Boolean).join(" ");
|
|
24
|
+
|
|
15
25
|
export function demoStartArgs({ demoBase, readerBase = false, keep = false, port = null, noOpen = false } = {}) {
|
|
16
26
|
const args = ["--demo", "--base", demoBase];
|
|
17
27
|
if (!readerBase) args.push("--demo-own-base");
|
package/shared/stdio-connect.mjs
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { stableInstallRoot } from "./permanent-install.mjs";
|
|
35
|
+
import { tomlString } from "./toml-string.mjs";
|
|
35
36
|
|
|
36
37
|
/** The install root — the directory holding `mcp-server/`, resolved from this module rather than cwd. */
|
|
37
38
|
export const INSTALL_ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
@@ -103,15 +104,38 @@ export function stdioConnectOffer(opts = {}) {
|
|
|
103
104
|
function envOf({ workDir = null, reportsDir = null } = {}) {
|
|
104
105
|
return { ...(workDir ? { CLEAROTRON_WORK_DIR: workDir } : {}), ...(reportsDir ? { CLEAROTRON_REPORTS_DIR: reportsDir } : {}) };
|
|
105
106
|
}
|
|
106
|
-
const envFlags = (o) => Object.entries(envOf(o)).map(([k, v]) => ` -e ${k}=${v}`).join("");
|
|
107
107
|
const envBlock = (o) => (Object.keys(envOf(o)).length ? { env: envOf(o) } : {});
|
|
108
108
|
|
|
109
|
+
// ── ONE WORD PER ARGUMENT, IN THE SHELL THAT READS THE LINE ───────────────────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// The command was the arguments joined with spaces, so a path with a space in it became two arguments:
|
|
112
|
+
// `/tmp/Example User/node/bin/node` reached the assistant as `/tmp/Example` and `User/node/bin/node`
|
|
113
|
+
// (measured on the published beta, 2026-09-19). Each argument is now written as one word for the shell it
|
|
114
|
+
// is pasted into. A word made only of characters no shell treats specially is left bare, so the line for
|
|
115
|
+
// an ordinary install reads exactly as it did.
|
|
116
|
+
//
|
|
117
|
+
// POSIX shells: single quotes, the one quoting in which no character is special. A single quote inside is
|
|
118
|
+
// closed, escaped and reopened.
|
|
119
|
+
const POSIX_BARE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
120
|
+
export const posixWord = (s) => (POSIX_BARE.test(String(s)) ? String(s) : `'${String(s).replace(/'/g, "'\\''")}'`);
|
|
121
|
+
// Windows: cmd and PowerShell both honour double quotes, and the program then splits its command line by
|
|
122
|
+
// the C runtime's rules, under which backslashes are literal except where they run up to a quote; there
|
|
123
|
+
// they are doubled, and a quote inside the word is escaped. A comma or a parenthesis is quoted too, because
|
|
124
|
+
// PowerShell reads a bare one as an array or an expression. `$`, a backtick and `%` cannot be quoted the
|
|
125
|
+
// same way for both shells, and a path holding one is left to the reader.
|
|
126
|
+
const WINDOWS_BARE = /^[A-Za-z0-9_+=:./\\-]+$/;
|
|
127
|
+
export const windowsWord = (s) => {
|
|
128
|
+
const w = String(s);
|
|
129
|
+
if (WINDOWS_BARE.test(w)) return w;
|
|
130
|
+
return `"${w.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"`;
|
|
131
|
+
};
|
|
132
|
+
|
|
109
133
|
/**
|
|
110
134
|
* The argument that ends Claude Code's own options. Windows PowerShell 5.1 drops a bare `--` before it
|
|
111
135
|
* reaches a native program, so the line failed with "missing required argument"; quoted, every shell passes
|
|
112
136
|
* it through (bash, zsh, PowerShell and cmd alike), so a Windows install is handed the quoted form.
|
|
113
137
|
*/
|
|
114
|
-
const separator = (
|
|
138
|
+
const separator = (windowsShell) => (windowsShell ? '"--"' : "--");
|
|
115
139
|
|
|
116
140
|
/**
|
|
117
141
|
* WHAT THE HOST ACTUALLY RUNS, and on WSL that is not `node`.
|
|
@@ -180,11 +204,17 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
180
204
|
where: null,
|
|
181
205
|
render: ({ server, workDir, reportsDir, platform, wsl, node }) => {
|
|
182
206
|
const l = stdioLauncher({ server, workDir, reportsDir, wsl, node });
|
|
207
|
+
// WHICH SHELL READS IT. A native Windows install's line is pasted on Windows, and so is the row
|
|
208
|
+
// that crosses into WSL: it exists for an assistant on the Windows side. Everything else is pasted
|
|
209
|
+
// into a POSIX shell.
|
|
210
|
+
const windowsShell = platform === "win32" || l.crossesIntoWsl;
|
|
211
|
+
const word = windowsShell ? windowsWord : posixWord;
|
|
183
212
|
// The host's own `-e` flags set variables for the process IT starts. Off WSL that is the server;
|
|
184
213
|
// through the wrapper it is `wsl.exe`, and they stop at the boundary — so on WSL they ride inside
|
|
185
214
|
// the command instead and this line carries none.
|
|
186
|
-
const flags = l.crossesIntoWsl ? ""
|
|
187
|
-
|
|
215
|
+
const flags = l.crossesIntoWsl ? ""
|
|
216
|
+
: Object.entries(envOf({ workDir, reportsDir })).map(([k, v]) => ` -e ${windowsShell ? word(`${k}=${v}`) : `${k}=${word(v)}`}`).join("");
|
|
217
|
+
return `claude mcp add ${STDIO_SERVER_NAME} --scope user${flags} ${separator(windowsShell)} ${[l.command, ...l.args].map(word).join(" ")}`;
|
|
188
218
|
},
|
|
189
219
|
after: null,
|
|
190
220
|
},
|
|
@@ -224,7 +254,8 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
224
254
|
kind: "config",
|
|
225
255
|
where: "~/.codex/config.toml",
|
|
226
256
|
// Written out rather than produced by a TOML library: this is four lines with no user-supplied
|
|
227
|
-
// strings in key positions, and adding a dependency to emit them would be the larger risk.
|
|
257
|
+
// strings in key positions, and adding a dependency to emit them would be the larger risk. Every
|
|
258
|
+
// value goes through `tomlString`, so a Windows path's backslashes are escaped as TOML requires.
|
|
228
259
|
//
|
|
229
260
|
// `env` and not `env_vars`, deliberately, and CONNECT.md explains why the distinction matters:
|
|
230
261
|
// Codex does not forward the shell environment, and a credential would have to be forwarded BY NAME
|
|
@@ -234,10 +265,10 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
234
265
|
const l = stdioLauncher({ server, workDir, reportsDir, wsl, node });
|
|
235
266
|
return [
|
|
236
267
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
237
|
-
`command =
|
|
238
|
-
`args = [${l.args.map(
|
|
268
|
+
`command = ${tomlString(l.command)}`,
|
|
269
|
+
`args = [${l.args.map(tomlString).join(", ")}]`,
|
|
239
270
|
...(Object.keys(l.env).length
|
|
240
|
-
? [`env = { ${Object.entries(l.env).map(([k, v]) => `${k} =
|
|
271
|
+
? [`env = { ${Object.entries(l.env).map(([k, v]) => `${k} = ${tomlString(v)}`).join(", ")} }`] : []),
|
|
241
272
|
].join("\n");
|
|
242
273
|
},
|
|
243
274
|
after: null,
|
|
@@ -297,7 +328,7 @@ export const REMOTE_SHAPES = Object.freeze({
|
|
|
297
328
|
label: "Copy block",
|
|
298
329
|
render: ({ address }) => [
|
|
299
330
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
300
|
-
`url =
|
|
331
|
+
`url = ${tomlString(address)}`,
|
|
301
332
|
].join("\n"),
|
|
302
333
|
},
|
|
303
334
|
"claude-cli-http": {
|
|
@@ -309,7 +340,7 @@ export const REMOTE_SHAPES = Object.freeze({
|
|
|
309
340
|
label: null,
|
|
310
341
|
render: ({ address }) => [
|
|
311
342
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
312
|
-
`url =
|
|
343
|
+
`url = ${tomlString(address)}`,
|
|
313
344
|
`bearer_token_env_var = "${KEY_ENV_VAR}"`,
|
|
314
345
|
].join("\n"),
|
|
315
346
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// ONE TOML STRING ENCODER, for every block this product writes.
|
|
5
|
+
//
|
|
6
|
+
// Two composers write TOML: the engine's per-run Codex configuration and the connection block a reader
|
|
7
|
+
// pastes into `~/.codex/config.toml`. The second wrapped raw values in double quotes, so a Windows path
|
|
8
|
+
// such as `C:\Program Files\nodejs\node.exe` produced a file TOML parsers refuse ("Unescaped '\' in a
|
|
9
|
+
// string"). A value is a TOML string only once it has been through here.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A TOML basic string holding exactly `s`. PURE.
|
|
13
|
+
*
|
|
14
|
+
* TOML requires the quotation mark, the backslash and every control character other than tab to be
|
|
15
|
+
* escaped (U+0000 to U+0008, U+000A to U+001F, and U+007F). The named escapes are used where TOML has one,
|
|
16
|
+
* and `\uXXXX` for the rest.
|
|
17
|
+
*/
|
|
18
|
+
export function tomlString(s) {
|
|
19
|
+
const esc = String(s ?? "")
|
|
20
|
+
.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
21
|
+
.replace(/\x08/g, "\\b").replace(/\t/g, "\\t").replace(/\n/g, "\\n")
|
|
22
|
+
.replace(/\f/g, "\\f").replace(/\r/g, "\\r")
|
|
23
|
+
.replace(/[\x00-\x1f\x7f]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
24
|
+
return `"${esc}"`;
|
|
25
|
+
}
|