clearotron 0.3.0-beta.10 → 0.3.0-beta.2
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/.env.example +3 -13
- package/INSTALL.md +6 -110
- package/README.md +21 -31
- package/bin/connect.mjs +26 -71
- package/bin/disconnect.mjs +11 -16
- package/bin/example.mjs +2 -14
- package/bin/key.mjs +5 -23
- package/bin/onboard.mjs +44 -191
- package/bin/start.mjs +33 -197
- package/bin/status.mjs +3 -23
- package/bin/stop.mjs +8 -23
- package/bin/update.mjs +6 -38
- package/build-info.json +2 -2
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/email-body.md +1 -1
- package/demo/knockout-search/run/status.json +2 -2
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/architecture/04-configuration-reference.md +1 -2
- package/driver/CHANGELOG.md +8 -107
- package/driver/compose-read.mjs +2 -21
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +2 -65
- package/driver/driver.config.mjs +0 -5
- package/driver/engine/anthropic-agent.mjs +17 -34
- package/driver/engine/jx-turn.mjs +1 -4
- package/driver/engine/openai-agent.mjs +2 -60
- package/driver/enqueue-schema.mjs +1 -3
- package/driver/gateway.mjs +15 -88
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +3 -35
- package/driver/portal-service.mjs +22 -91
- package/driver/portal-upstream.mjs +1 -20
- package/driver/profile-service.mjs +3 -31
- package/driver/publish/index.mjs +9 -12
- package/driver/publish/knockout.mjs +2 -4
- package/driver/publish/office-record-links.mjs +21 -56
- package/driver/recipe-service.mjs +5 -14
- package/driver/record-origins.mjs +0 -14
- package/driver/search-policy.mjs +2 -7
- package/driver/suite-census.json +46 -154
- package/driver/systemd/render-units.mjs +13 -7
- package/mcp-server/CHANGELOG.md +2 -36
- package/mcp-server/CONNECT.md +2 -3
- package/mcp-server/lib/driver.mjs +0 -2
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +5 -20
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CwPAS0we.js → index-CcFjgM78.js} +383 -525
- package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-CsCuPshD.css} +86 -199
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +0 -5
- package/providers/oauth-mcp-bridge/CHANGELOG.md +0 -32
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +20 -197
- package/scripts/engine-probe.mjs +2 -2
- package/scripts/env-classify.mjs +0 -3
- package/scripts/revisit-render-check.mjs +4 -22
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/access-audience.mjs +6 -7
- package/shared/brand.mjs +3 -7
- package/shared/client-door.mjs +0 -39
- package/shared/connect-clients.mjs +321 -290
- package/shared/env-file-merge.mjs +0 -24
- package/shared/invocation.mjs +2 -49
- package/shared/names-in-force.mjs +0 -2
- package/shared/stdio-connect.mjs +14 -95
- package/shared/store-in-repo.mjs +2 -50
- package/shared/verb-shim.mjs +1 -10
- package/shared/permanent-install.mjs +0 -237
- package/shared/running-start.mjs +0 -73
- package/shared/wsl.mjs +0 -23
package/bin/start.mjs
CHANGED
|
@@ -109,25 +109,23 @@ async function runTables() {
|
|
|
109
109
|
return { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: RUN_DEFAULT_ENGINE };
|
|
110
110
|
}
|
|
111
111
|
import { spawn, spawnSync, execFileSync } from "node:child_process";
|
|
112
|
-
import { storeInRepo, storeOutsideRepoMessage
|
|
112
|
+
import { storeInRepo, storeOutsideRepoMessage } from "../shared/store-in-repo.mjs"; //
|
|
113
113
|
import { stdioConnectOffer } from "../shared/stdio-connect.mjs";
|
|
114
|
-
import { ensureDemoProgram } from "../shared/permanent-install.mjs"; // — a demo from npx keeps its own copy
|
|
115
114
|
import { mergeEnvFile } from "../shared/env-file-merge.mjs";
|
|
116
115
|
import { mcpOriginFor } from "../shared/lane-address.mjs"; // — one author for the origin
|
|
117
116
|
import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "../shared/server-units.mjs"; // — one authority, two callers
|
|
118
117
|
// — the door --background now INSTALLS, and the one authority for the settings
|
|
119
118
|
// it refuses to start without. (Until 2026-09-03 this import read "the one unit --background may
|
|
120
119
|
// tolerate and never manage"; settled point 2 superseded that.)
|
|
121
|
-
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort
|
|
120
|
+
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
|
|
122
121
|
import { createServer } from "node:net";
|
|
123
122
|
import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
|
|
124
123
|
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
125
124
|
import { randomBytes } from "node:crypto";
|
|
126
|
-
import { invocationPrefix, invoke
|
|
127
|
-
import { unitEnvPath
|
|
128
|
-
import { parseEnvFile } from "../shared/env-file-merge.mjs"; // ONE KEY=value reader, in a leaf: render-units is a COMMAND, and importing it from here closed a cycle
|
|
125
|
+
import { invocationPrefix, invoke } from "../shared/invocation.mjs"; // — the banner names the verb
|
|
126
|
+
import { unitEnvPath } from "../shared/env-local.mjs"; // — the file the units read, named once
|
|
129
127
|
import { homedir, userInfo } from "node:os";
|
|
130
|
-
import { dirname, join
|
|
128
|
+
import { dirname, join } from "node:path";
|
|
131
129
|
import { fileURLToPath } from "node:url";
|
|
132
130
|
import { usageBlock } from "../shared/usage-block.mjs";
|
|
133
131
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
@@ -143,7 +141,6 @@ import { addressRefusal } from "../shared/staff-domain.mjs";
|
|
|
143
141
|
import { withPerson, withOrganisation, withCompany } from "../shared/grants-edit.mjs";
|
|
144
142
|
import { assertGrantsShape, resolvePerson } from "../shared/scope.mjs";
|
|
145
143
|
import { backgroundManager } from "../shared/os-advice.mjs";
|
|
146
|
-
import { recordRunning } from "../shared/running-start.mjs";
|
|
147
144
|
import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
|
|
148
145
|
|
|
149
146
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -303,22 +300,6 @@ export function resolvePorts(env = {}) {
|
|
|
303
300
|
client: one("CLIENT_MCP_HTTP_PORT", clientDoorPort({})) };
|
|
304
301
|
}
|
|
305
302
|
|
|
306
|
-
/**
|
|
307
|
-
* WHAT TO DO WHEN THE PAGE THAT OPENS IS NOT OURS. Printed under every "Open" line.
|
|
308
|
-
*
|
|
309
|
-
* A port can be free where this runs and taken where the browser runs: on WSL, a Windows-side listener
|
|
310
|
-
* (VS Code's Remote-SSH forwarding is the one measured, 2026-09-11) answers 127.0.0.1 before WSL does. The
|
|
311
|
-
* doors bind cleanly, the in-use detection has nothing to see, and the browser shows somebody else's page
|
|
312
|
-
* with nothing on this screen saying so. `--port` already moves all three doors; the reader has to be told
|
|
313
|
-
* about it at the moment the address is handed over, which is here.
|
|
314
|
-
*/
|
|
315
|
-
export function foreignPageHint(verb) {
|
|
316
|
-
return [
|
|
317
|
-
"If the page that opens is not this install's sign-in, another program on this machine holds that",
|
|
318
|
-
`port from outside this environment. Run \`${invoke(verb)} --port 28802\` (or any free number) instead.`,
|
|
319
|
-
];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
303
|
/**
|
|
323
304
|
* Apply `--port <n>` to the three doors.
|
|
324
305
|
*
|
|
@@ -374,50 +355,6 @@ export function defaultGrantsPath({ env = process.env, demo = false } = {}) {
|
|
|
374
355
|
return installPaths(join(homedir(), demo ? "trademark-demo" : "trademark")).grants;
|
|
375
356
|
}
|
|
376
357
|
|
|
377
|
-
/**
|
|
378
|
-
* The paths `clearotron start` hands its services: the install's layout under `base`, with whatever the
|
|
379
|
-
* environment already names winning over the layout's default, as `start` has always applied it.
|
|
380
|
-
*
|
|
381
|
-
* EXPORTED FOR THE ONE OTHER READER THAT MUST AGREE WITH IT. On an install with no background units,
|
|
382
|
-
* `clearotron doctor` has no unit file to read the services' environment from: the services are this
|
|
383
|
-
* command's children and get the saved-search store from here. An install started before the store was
|
|
384
|
-
* written to the env file has no line there to read, so doctor asks this function for it, and the
|
|
385
|
-
* answer it prints and the one the services were given have one author.
|
|
386
|
-
*
|
|
387
|
-
* NOT IN A DEMO. Nothing the environment says about an install is the demo's: with the reader's settings
|
|
388
|
-
* in force, 0.3.0-beta.1's demo seeded its example reports into their real archive.
|
|
389
|
-
*/
|
|
390
|
-
export function startPaths({ env = process.env, base = join(homedir(), "trademark"), demo = false } = {}) {
|
|
391
|
-
const paths = installPaths(base);
|
|
392
|
-
if (demo) return paths;
|
|
393
|
-
for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
|
|
394
|
-
["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
|
|
395
|
-
["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (env[name]) paths[k] = env[name];
|
|
396
|
-
if (env.RECIPE_REPO_ROOT) paths.configStore = env.RECIPE_REPO_ROOT;
|
|
397
|
-
if (env.PORTAL_AUDIT) paths.audit = env.PORTAL_AUDIT;
|
|
398
|
-
return paths;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* The saved-search store, as the env file must record it for every reader OUTSIDE this command's tree.
|
|
403
|
-
*
|
|
404
|
-
* The services are handed the store by `childEnv`. A connector an assistant launches, `clearotron doctor`
|
|
405
|
-
* and every other hand-run command are not this command's children: they read the env file, and the
|
|
406
|
-
* file never named the store. So on a local install the portal listed a company's saved searches, the
|
|
407
|
-
* connected assistant was told there were none, and doctor said saved searches were off. Written
|
|
408
|
-
* add-only beside the secrets, as `npx clearotron install` writes the reports and work directories: a
|
|
409
|
-
* line an operator wrote wins.
|
|
410
|
-
*/
|
|
411
|
-
export function storesForOtherReaders(paths) {
|
|
412
|
-
return { CLEAROTRON_RECIPES_DIR: paths.recipes, RECIPE_REPO_ROOT: paths.configStore };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Written above each line, so a reader who moves the install sees that these two move with it.
|
|
416
|
-
const STORE_NOTES = {
|
|
417
|
-
CLEAROTRON_RECIPES_DIR: "Where saved searches are kept. `clearotron start` created it; edit both lines if you move the install.",
|
|
418
|
-
RECIPE_REPO_ROOT: "The git repository saved searches are committed in, which holds the directory above.",
|
|
419
|
-
};
|
|
420
|
-
|
|
421
358
|
/** Everything this install keeps on disk, under one base directory. */
|
|
422
359
|
export function installPaths(base) {
|
|
423
360
|
return {
|
|
@@ -759,10 +696,6 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
|
|
|
759
696
|
// already wrong for a process that is not the portal.
|
|
760
697
|
...(demo ? {
|
|
761
698
|
CLEAROTRON_DEMO: "1",
|
|
762
|
-
// A DEMO NAMES NO ORGANISATION. The portal reads this to name the one running the install, on its
|
|
763
|
-
// top bar and its sign-in refusal page, and an exported value is the reader's real organisation.
|
|
764
|
-
// The portal's alone: no door names an organisation, and the doors stay as a live install's.
|
|
765
|
-
CLEAROTRON_ORGANISATION_NAME: "",
|
|
766
699
|
// AND THE SIGN-IN CREDENTIAL LIVES IN THE DEMO'S OWN BASE. Without this it defaults to
|
|
767
700
|
// ~/.cordillera/portal-local-credential.json — shared with every install on the box — and the
|
|
768
701
|
// demo then inherits a digest minted for somebody else's address: the portal prints "the
|
|
@@ -911,46 +844,21 @@ if (isMain) {
|
|
|
911
844
|
// Decided before any path is, because in a demo every path below is the demo's own. The posture
|
|
912
845
|
// itself is described at the DEMO block further down.
|
|
913
846
|
const DEMO = argv.includes("--demo");
|
|
914
|
-
// A DEMO HAS NO BACKGROUND FORM, and asking for one is refused before anything is written. The
|
|
915
|
-
// background path installs units that run the reader's own install, so `--demo --background` would set
|
|
916
|
-
// up an empty install in their home and call it the demo.
|
|
917
|
-
if (DEMO && wantBackground)
|
|
918
|
-
fatal("the demo has no background form — it runs as long as its terminal does. `--background` starts your own install as services, not the demo.");
|
|
919
847
|
// The same base `npm run setup` writes under, so whichever of the two a reader ran first, the other
|
|
920
848
|
// finds the same install rather than a second one beside it.
|
|
849
|
+
const paths = installPaths(flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")));
|
|
921
850
|
// Whatever the environment already says wins over the base-derived default, for every path — a reader
|
|
922
|
-
// who ran `npm run setup` has these in .env already and this must not move their data.
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (DEMO) {
|
|
934
|
-
const base = resolve(paths.base);
|
|
935
|
-
const inside = (p) => { const v = String(p ?? "").trim(); if (!v) return false; const r = resolve(v); return r === base || r.startsWith(base + sep); };
|
|
936
|
-
let settings = {};
|
|
937
|
-
try { settings = parseEnvFile(readFileSync(activeEnvPath(), "utf8")); } catch { /* no settings in force: nothing of an install to collide with */ }
|
|
938
|
-
const found = [];
|
|
939
|
-
if (inside(settings.CLEAROTRON_REPORTS_DIR)) found.push(`an install keeps its reports in ${settings.CLEAROTRON_REPORTS_DIR}`);
|
|
940
|
-
if (inside(settings.RECIPE_REPO_ROOT)) found.push(`an install keeps its saved searches in ${settings.RECIPE_REPO_ROOT}`);
|
|
941
|
-
if (inside(settings.CLEAROTRON_WORK_DIR)) found.push(`an install works in ${settings.CLEAROTRON_WORK_DIR}`);
|
|
942
|
-
if (existsSync(join(paths.base, ".env"))) found.push(`it holds a settings file, ${join(paths.base, ".env")}`);
|
|
943
|
-
if (!found.length && base === resolve(join(homedir(), "trademark"))) found.push("it is the directory an install is set up in by default");
|
|
944
|
-
// AN INSTALL NOBODY CONFIGURED still answers. `start --base <dir>` writes a guest list on every start
|
|
945
|
-
// and no settings at all, so an install somewhere of its own passes every check above. A guest list
|
|
946
|
-
// with no demo secret beside it is somebody's install; a demo's own base has both.
|
|
947
|
-
if (!found.length && existsSync(paths.grants) && !existsSync(demoTokenSecretPath(paths.base)))
|
|
948
|
-
found.push(`it holds a guest list, ${paths.grants}, and no demo of its own`);
|
|
949
|
-
if (found.length)
|
|
950
|
-
fatal(`--demo cannot run in ${paths.base}: ${found.join("; ")}.\n`
|
|
951
|
-
+ " The demo keeps its own data, and its own signing secret, in its base. Leaving those in an\n"
|
|
952
|
-
+ " install's directory would make keys issued for that install refuse at its door.\n"
|
|
953
|
-
+ " Run the demo without --base, or give it a directory of its own.");
|
|
851
|
+
// who ran `npm run setup` has these in .env already and this must not move their data.
|
|
852
|
+
//
|
|
853
|
+
// NOT IN A DEMO. Nothing the environment says about an install is the demo's: with the reader's
|
|
854
|
+
// settings in force, 0.3.0-beta.1's demo seeded its example reports into their real archive. Not read,
|
|
855
|
+
// rather than deleted from the environment, so a real start cannot be reached by this branch at all.
|
|
856
|
+
if (!DEMO) {
|
|
857
|
+
for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
|
|
858
|
+
["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
|
|
859
|
+
["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (process.env[name]) paths[k] = process.env[name];
|
|
860
|
+
if (process.env.RECIPE_REPO_ROOT) paths.configStore = process.env.RECIPE_REPO_ROOT;
|
|
861
|
+
if (process.env.PORTAL_AUDIT) paths.audit = process.env.PORTAL_AUDIT;
|
|
954
862
|
}
|
|
955
863
|
// ── THIS INSTALL'S FIRST START, read before this start writes either file that answers it ────────────
|
|
956
864
|
//
|
|
@@ -1207,29 +1115,16 @@ if (isMain) {
|
|
|
1207
1115
|
return v;
|
|
1208
1116
|
};
|
|
1209
1117
|
const portalSecret = secretFor("PORTAL_SECRET");
|
|
1210
|
-
|
|
1211
|
-
// `demoTokenSecret`. Every other secret a demo uses stays in memory.
|
|
1212
|
-
const tokenSecret = DEMO
|
|
1213
|
-
? demoTokenSecret(paths.base, {
|
|
1214
|
-
read: (f) => readFileSync(f, "utf8"),
|
|
1215
|
-
write: (f, text) => { mkdirSync(dirname(f), { recursive: true }); writeSecretFile(f, text); },
|
|
1216
|
-
mint: () => randomBytes(32).toString("base64url"),
|
|
1217
|
-
})
|
|
1218
|
-
: secretFor("TRADEMARK_MCP_TOKEN_SECRET");
|
|
1118
|
+
const tokenSecret = secretFor("TRADEMARK_MCP_TOKEN_SECRET");
|
|
1219
1119
|
if (!process.env.PORTAL_LOCAL_USER) generated.PORTAL_LOCAL_USER = user;
|
|
1220
|
-
const stores = DEMO ? {} : storesForOtherReaders(paths);
|
|
1221
1120
|
|
|
1222
1121
|
// A DEMO WRITES NO SECRETS AND NO ADDRESS. They are generated per run and live in memory only, which
|
|
1223
1122
|
// is the same posture the ops key already has here — and it is what makes "removing the demo is one
|
|
1224
1123
|
// directory" true rather than nearly true.
|
|
1225
|
-
|
|
1226
|
-
// THE STORES ARE WRITTEN EVEN WHEN NO SECRET IS. An install whose secrets are already in the file is
|
|
1227
|
-
// exactly the install whose connector and doctor could not see its saved searches, and a gate on the
|
|
1228
|
-
// secrets alone would never reach it.
|
|
1229
|
-
if ((Object.keys(generated).length || Object.keys(stores).length) && !DEMO) {
|
|
1124
|
+
if (Object.keys(generated).length && !DEMO) {
|
|
1230
1125
|
let existing = "";
|
|
1231
1126
|
try { existing = readFileSync(ENV_PATH, "utf8"); } catch (e) { if (e.code !== "ENOENT") fatal(`${ENV_PATH} exists but could not be read (${e.code}).`); }
|
|
1232
|
-
const merged = mergeEnvFile(existing,
|
|
1127
|
+
const merged = mergeEnvFile(existing, generated);
|
|
1233
1128
|
if (merged.added.length) {
|
|
1234
1129
|
// ONE WRITER FOR EVERY FILE THAT HOLDS CREDENTIALS, and it creates the directory. This site had its
|
|
1235
1130
|
// own copy of the write and did not learn what the wizard's copy learned when `.env` moved under
|
|
@@ -1240,7 +1135,7 @@ if (isMain) {
|
|
|
1240
1135
|
} catch (e) {
|
|
1241
1136
|
fatal(`could not write ${ENV_PATH} (${String(e?.message ?? e)}).`);
|
|
1242
1137
|
}
|
|
1243
|
-
// NAMES only.
|
|
1138
|
+
// NAMES only. This file is where the credentials are.
|
|
1244
1139
|
say(` wrote ${merged.added.join(", ")} to ${ENV_PATH} (mode 600)`);
|
|
1245
1140
|
}
|
|
1246
1141
|
}
|
|
@@ -1297,7 +1192,7 @@ if (isMain) {
|
|
|
1297
1192
|
say(` grants ${paths.grants} named nobody, so ${user} was added: Run, Manage, access to everything`);
|
|
1298
1193
|
if (seeded.changed.includes("organisation")) say(` organisation "${organisation}", filed there — rename it there too`);
|
|
1299
1194
|
else if (seeded.changed.includes("person") && !Object.keys(seeded.grants.tenants ?? {}).length)
|
|
1300
|
-
say(` no organisation named yet — \`${
|
|
1195
|
+
say(` no organisation named yet — \`${invocationPrefix()}clearotron start --organisation "<name>"\` files the first one`);
|
|
1301
1196
|
// REPORTED, NOT REPAIRED. A file that names other people is somebody's decision, and adding this
|
|
1302
1197
|
// address to it would give access to everything to an address nobody enrolled.
|
|
1303
1198
|
if (seeded.unadmitted)
|
|
@@ -1357,12 +1252,6 @@ if (isMain) {
|
|
|
1357
1252
|
// search is what will fail.
|
|
1358
1253
|
err(` WARNING: could not initialise the saved-search store at ${paths.configStore} (${String(e?.message ?? e)}). Searches will list and run; SAVING one will fail until this is a git repository.`);
|
|
1359
1254
|
}
|
|
1360
|
-
} else {
|
|
1361
|
-
// AN ADOPTED STORE IS ASKED WHETHER IT CAN RECORD A SAVE, HERE, not at the first save. A repository
|
|
1362
|
-
// made by hand has no identity unless somebody gave it one, and on a machine with no global identity
|
|
1363
|
-
// the first company created in the portal is then refused. The one created above sets its own.
|
|
1364
|
-
const cannot = storeCommitRefusal(paths.configStore);
|
|
1365
|
-
if (cannot) err(` WARNING: ${cannot.message}. Until then, creating a company or saving a search is refused.`);
|
|
1366
1255
|
}
|
|
1367
1256
|
|
|
1368
1257
|
// ── THE DEMO'S OWN STORE, WITH ITS COMPANY IN IT ────────────────────────────────────────────────
|
|
@@ -1435,13 +1324,8 @@ if (isMain) {
|
|
|
1435
1324
|
// SEEDED FROM A COPY, for the reason the player publishes from one: republishing writes a receipt
|
|
1436
1325
|
// into the run directory it reads, and `demo/` is tracked. This is the path a reader actually takes
|
|
1437
1326
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1438
|
-
const { publishSource
|
|
1327
|
+
const { publishSource } = await import("../driver/demo-container.mjs");
|
|
1439
1328
|
const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
|
|
1440
|
-
// AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
|
|
1441
|
-
// the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
|
|
1442
|
-
// links are stamped with this portal's address, the one the Open line prints.
|
|
1443
|
-
const runs = seedDemoRuns({ workspace: paths.workspace, examplesDir: join(REPO, "demo"), portalOrigin: `http://${HOST}:${ports.portal}` });
|
|
1444
|
-
if (runs.seeded.length) say(` runs ${runs.seeded.length} sample run(s) your assistant can list, brief and open`);
|
|
1445
1329
|
// WHAT WAS ALREADY THERE IS SAID TOO. This branch used to run only when the pool
|
|
1446
1330
|
// was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
|
|
1447
1331
|
// about what was MISSING and says nothing on its own about how many are now listed.
|
|
@@ -1501,17 +1385,6 @@ if (isMain) {
|
|
|
1501
1385
|
// the file the verb resets is the file the portal reads. A demo keeps its own, by layout (childEnv).
|
|
1502
1386
|
const { installCredential } = await import("../driver/portal-local-auth.mjs");
|
|
1503
1387
|
const signIn = DEMO ? null : installCredential({ base: paths.base, env: process.env, firstStart: firstStartOfThisInstall });
|
|
1504
|
-
// ── A DEMO RUN FROM NPX KEEPS ITS OWN COPY OF THE PROGRAM ───────────────────────────────────────────
|
|
1505
|
-
//
|
|
1506
|
-
// Its connect line launched the connector from npm's cache, which npm deletes when it cleans up, so an
|
|
1507
|
-
// assistant registered with it lost the demo without a word (measured on a published beta, 2026-09-11).
|
|
1508
|
-
// The same version goes into `<base>/program` (shared/permanent-install.mjs), BEFORE the services start
|
|
1509
|
-
// so the portal's connect rows name it too, and only when it is not there already. It never stops the
|
|
1510
|
-
// demo: if npm fails, the demo runs as before and says the line will not survive a cache clean.
|
|
1511
|
-
// `clearotron demo` makes the copy before it starts this file, and starts this file FROM the copy, so
|
|
1512
|
-
// there this finds nothing to do; it acts for a `start --demo` typed at npx directly.
|
|
1513
|
-
const demoProgramRoot = DEMO ? ensureDemoProgram({ base: paths.base, say }) : null;
|
|
1514
|
-
|
|
1515
1388
|
const envs = childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken,
|
|
1516
1389
|
localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null,
|
|
1517
1390
|
credential: signIn && signIn.source !== "shared" ? signIn.path : null });
|
|
@@ -1960,7 +1833,6 @@ if (isMain) {
|
|
|
1960
1833
|
}
|
|
1961
1834
|
say("");
|
|
1962
1835
|
say(` Open: ${envs.url}`);
|
|
1963
|
-
for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
|
|
1964
1836
|
say(" This SURVIVES the terminal — close the window, the product keeps running.");
|
|
1965
1837
|
say(` Stop it: ${invoke("stop")} (stops and removes the units; issued connect keys survive — \`${invoke("disconnect")}\` revokes those)`);
|
|
1966
1838
|
say(` Is it up? ${invoke("status")}`);
|
|
@@ -2194,7 +2066,6 @@ if (isMain) {
|
|
|
2194
2066
|
|
|
2195
2067
|
say("");
|
|
2196
2068
|
say(` Open ${envs.url}`);
|
|
2197
|
-
for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
|
|
2198
2069
|
say("");
|
|
2199
2070
|
// ── TWO DOORS, TWO AUDIENCES, BOTH NAMED ( — F26) ─────────────────────────
|
|
2200
2071
|
//
|
|
@@ -2220,26 +2091,12 @@ if (isMain) {
|
|
|
2220
2091
|
if (adoptedClientDoor)
|
|
2221
2092
|
say(` Already running as ${CLIENT_DOOR_UNIT}; this start kept it, so existing keys still work.`);
|
|
2222
2093
|
else
|
|
2223
|
-
say(` It refuses every caller until a key is issued: ${
|
|
2094
|
+
say(` It refuses every caller until a key is issued: ${invocationPrefix()}clearotron key issue <email>`);
|
|
2224
2095
|
} else {
|
|
2225
2096
|
say(` Client door NOT RUNNING on ${HOST}:${ports.client} — its output above says why. The portal and`);
|
|
2226
2097
|
say(" the engine door are unaffected; a client assistant cannot connect until it is up.");
|
|
2227
2098
|
}
|
|
2228
2099
|
say("");
|
|
2229
|
-
// ── WHAT `status` AND `stop` READ ABOUT THIS START ───────────────────────────────────────────────
|
|
2230
|
-
//
|
|
2231
|
-
// Both verbs knew only about background units, so with this start serving, `status` described units
|
|
2232
|
-
// nobody installed and `stop` said nothing was running (measured on a published beta, 2026-09-11). The
|
|
2233
|
-
// record is the address this banner just printed. It goes on every exit — Ctrl-C, a fatal refusal, a
|
|
2234
|
-
// crash — and a start killed outright leaves a record whose process is gone, which the readers ignore.
|
|
2235
|
-
try {
|
|
2236
|
-
const forget = recordRunning({ pid: process.pid, demo: DEMO, base: paths.base, url: envs.url, host: HOST,
|
|
2237
|
-
ports: { portal: ports.portal, mcp: ports.mcp, client: doorRunning ? ports.client : null },
|
|
2238
|
-
startedAt: new Date().toISOString() });
|
|
2239
|
-
process.on("exit", forget);
|
|
2240
|
-
} catch (e) {
|
|
2241
|
-
say(` (\`status\` will not see this start: its record could not be written — ${String(e?.message ?? e)})`);
|
|
2242
|
-
}
|
|
2243
2100
|
// — THE BANNER TOLD THE SAME STORY ON EVERY START, and it was only true of the first.
|
|
2244
2101
|
//
|
|
2245
2102
|
// "printed once, above" describes what a FIRST start does. On every start after it the passphrase was
|
|
@@ -2306,10 +2163,7 @@ if (isMain) {
|
|
|
2306
2163
|
//
|
|
2307
2164
|
// The string comes from the ONE composer, not from a literal here: three surfaces state this route
|
|
2308
2165
|
// and a line of instruction with more than one author drifts silently.
|
|
2309
|
-
|
|
2310
|
-
// file, so its own line named no workspace and the connector fell back to the real install's.
|
|
2311
|
-
// A demo run from npx names its own copy of the program, which a cache clean does not remove.
|
|
2312
|
-
const connect = stdioConnectOffer({ workDir: paths.workspace, reportsDir: paths.pool, ...(demoProgramRoot ? { installRoot: demoProgramRoot } : {}) });
|
|
2166
|
+
const connect = stdioConnectOffer({ workDir: process.env.CLEAROTRON_WORK_DIR || null });
|
|
2313
2167
|
say(" Connect your assistant to this install — one line, no address and no sign-in:");
|
|
2314
2168
|
say("");
|
|
2315
2169
|
say(` ${connect.command}`);
|
|
@@ -2331,35 +2185,17 @@ if (isMain) {
|
|
|
2331
2185
|
// cannot succeed and given a service manager that is not on the machine and cannot be put there.
|
|
2332
2186
|
// Reported from a real run. Same rule as the engine refusal above: do not name a route this platform
|
|
2333
2187
|
// does not have.
|
|
2334
|
-
|
|
2188
|
+
const manager = backgroundManager();
|
|
2189
|
+
if (manager) {
|
|
2190
|
+
say(` To get your prompt back instead, stop this and run ${invoke("start")} --background`);
|
|
2191
|
+
say(` — same product, managed by ${manager}, and it survives logout.`);
|
|
2192
|
+
} else {
|
|
2193
|
+
say(" There is no background form on this platform: the product runs as long as this window does.");
|
|
2194
|
+
say(" Leave it open and use a second terminal for the commands above.");
|
|
2195
|
+
}
|
|
2335
2196
|
say("");
|
|
2336
2197
|
}
|
|
2337
2198
|
|
|
2338
|
-
/**
|
|
2339
|
-
* The closing offer of a foreground start: how to get the prompt back, where there is a way.
|
|
2340
|
-
*
|
|
2341
|
-
* NOT IN A DEMO. The offer ran `start --background`, which set up a new, empty install in the reader's
|
|
2342
|
-
* home — not the demo, its samples or its sign-in — and then failed where the user's systemd was not
|
|
2343
|
-
* reachable (measured on a published beta, 2026-09-11). A demo has no background form, so it is not
|
|
2344
|
-
* offered one. Where the offer stands, it names what it needs BEFORE the reader stops what is running.
|
|
2345
|
-
* PURE.
|
|
2346
|
-
*/
|
|
2347
|
-
export function backgroundOfferLines({ demo = false, manager = null, start = "clearotron start" } = {}) {
|
|
2348
|
-
if (demo) return [
|
|
2349
|
-
" The demo has no background form: it runs as long as this window does. Leave it open and use a",
|
|
2350
|
-
" second terminal for the commands above.",
|
|
2351
|
-
];
|
|
2352
|
-
if (!manager) return [
|
|
2353
|
-
" There is no background form on this platform: the product runs as long as this window does.",
|
|
2354
|
-
" Leave it open and use a second terminal for the commands above.",
|
|
2355
|
-
];
|
|
2356
|
-
return [
|
|
2357
|
-
` To get your prompt back instead, stop this and run ${start} --background`,
|
|
2358
|
-
` — same product, managed by ${manager}, and it survives logout. It needs ${manager}'s user manager`,
|
|
2359
|
-
" reachable from this session; where it is not, that command says so and changes nothing you use.",
|
|
2360
|
-
];
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
2199
|
/**
|
|
2364
2200
|
* What a reader is told when a child this install cannot run without exits.
|
|
2365
2201
|
*
|
package/bin/status.mjs
CHANGED
|
@@ -18,7 +18,6 @@ import { BACKGROUND_UNITS, resolvePorts } from "./start.mjs";
|
|
|
18
18
|
import { parseEnvFile } from "../driver/systemd/render-units.mjs"; // ONE KEY=value reader — what systemd actually reads
|
|
19
19
|
import { CLIENT_DOOR_UNIT, clientDoorPort } from "../shared/client-door.mjs";
|
|
20
20
|
import { invoke } from "../shared/invocation.mjs";
|
|
21
|
-
import { readRunning, probe } from "../shared/running-start.mjs";
|
|
22
21
|
import { configStaleness, stalenessWarning, parseSystemdTimestamp } from "../driver/config-staleness.mjs"; // — F48
|
|
23
22
|
|
|
24
23
|
const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
|
|
@@ -58,34 +57,15 @@ const startedEpochMs = (u) => {
|
|
|
58
57
|
return parseSystemdTimestamp(line?.slice("ActiveEnterTimestamp=".length));
|
|
59
58
|
} catch { return null; }
|
|
60
59
|
};say("");
|
|
61
|
-
// ── A FOREGROUND START ANSWERS HERE TOO ─────────────────────────────────────────────────────────────
|
|
62
|
-
//
|
|
63
|
-
// The README starts the product in a terminal, and this verb answered only for background units: with
|
|
64
|
-
// everything up it printed a sentence about units the reader never installed (measured on a published
|
|
65
|
-
// beta, 2026-09-11). The record gives the address `start` printed; the portal itself says whether it is up.
|
|
66
|
-
const running = readRunning();
|
|
67
|
-
for (const r of running) {
|
|
68
|
-
const what = r.demo ? "The demo" : "The product";
|
|
69
|
-
// The portal's liveness route, which answers without a session: the address itself asks for a sign-in
|
|
70
|
-
// and read as "not answering" over a portal that was up (driven 2026-09-11).
|
|
71
|
-
if (await probe(new URL("/portal/health", r.url).href)) say(` ${what} is up, in the foreground — a terminal holds \`start\` (pid ${r.pid}); Ctrl-C there stops it.`);
|
|
72
|
-
else say(` ${what} was started in the foreground (pid ${r.pid}), but its portal at ${r.url} is not answering.`);
|
|
73
|
-
say(` Open ${r.url}`);
|
|
74
|
-
say(` Engine door http://${r.host}:${r.ports.mcp}/mcp`);
|
|
75
|
-
if (r.ports.client) say(` Client door http://${r.host}:${r.ports.client}/mcp`);
|
|
76
|
-
say("");
|
|
77
|
-
}
|
|
78
60
|
let installed = 0;
|
|
79
61
|
for (const u of BACKGROUND_UNITS) {
|
|
80
62
|
if (!existsSync(join(UNIT_DIR, u))) continue;
|
|
81
63
|
installed++;
|
|
82
64
|
say(` ${u.padEnd(34)} ${state(u)}`);
|
|
83
65
|
}
|
|
84
|
-
if (!installed
|
|
85
|
-
say(" No background units are installed
|
|
86
|
-
|
|
87
|
-
say(" The product is not running: no terminal holds `start`, and no background units are installed.");
|
|
88
|
-
say(` \`${invoke("start")}\` runs it in a terminal; \`${invoke("start")} --background\` runs it as services that survive logout.`);
|
|
66
|
+
if (!installed) {
|
|
67
|
+
say(" No background units are installed — this box runs the product only while a terminal holds");
|
|
68
|
+
say(` \`clearotron start\`. \`clearotron start --background\` is the form that survives the terminal.`);
|
|
89
69
|
} else {
|
|
90
70
|
// The ports come from the same resolver start uses, RESOLVED OVER THE UNITS' OWN FILE: the running
|
|
91
71
|
// services read %h/.env via systemd, and this CLI's shell env may know nothing of it — the first
|
package/bin/stop.mjs
CHANGED
|
@@ -23,7 +23,6 @@ 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";
|
|
27
26
|
|
|
28
27
|
const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
|
|
29
28
|
const say = (s = "") => console.log(s);
|
|
@@ -92,31 +91,17 @@ for (const u of BACKGROUND_UNITS) {
|
|
|
92
91
|
}
|
|
93
92
|
// THE COMMENT HERE ALREADY NAMED THE CAUSE AND SHRUGGED AT IT. If there is no user bus, the disables
|
|
94
93
|
// above did not happen either — so this is where that is said.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
94
|
+
try {
|
|
95
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], CAPTURE_STDERR);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const said = systemdSaid(e);
|
|
98
|
+
err(` could not ask systemd to reload its units — ${said}`);
|
|
99
|
+
if (looksLikeBusFailure(said)) err(`\n${busRemedy()}\n`);
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
if (!found) {
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
103
|
+
say(" Nothing was running in the background — no pinned unit is installed on this box.");
|
|
104
|
+
say(" Nothing to do, and nothing was changed.");
|
|
120
105
|
} else if (failures.length) {
|
|
121
106
|
// THE SENTENCE THAT WAS WRONG. "The background product is stopped and the box runs nothing again" was
|
|
122
107
|
// printed unconditionally — including on the run where four services stayed up. A reader who is told
|
package/bin/update.mjs
CHANGED
|
@@ -59,8 +59,6 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-p
|
|
|
59
59
|
import { readEnvFile } from "./onboard.mjs";
|
|
60
60
|
import { invoke, invocationPrefix } from "../shared/invocation.mjs"; // — name a command the reader can actually type
|
|
61
61
|
import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // a pull cannot update an untracked bundle
|
|
62
|
-
import { packagedUpdate } from "../shared/permanent-install.mjs"; // — a packaged install updates at its own prefix
|
|
63
|
-
import { installShim, inspectShim, shimPath } from "../shared/verb-shim.mjs"; // — npm's link replaces the launcher
|
|
64
62
|
|
|
65
63
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
66
64
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
@@ -248,22 +246,13 @@ export async function update(argv = process.argv.slice(2)) {
|
|
|
248
246
|
// not succeed` — true, useless, and pointing at the wrong thing entirely. `update` is the verb a
|
|
249
247
|
// stranger reaches for, so the one install we expect most people to have must not be answered with
|
|
250
248
|
// a git error about a directory that was never a repository.
|
|
251
|
-
//
|
|
252
|
-
// A PACKAGED INSTALL NOW UPDATES ITSELF, the way it was installed: npm, at the prefix it lives under, on
|
|
253
|
-
// the channel it came from (shared/permanent-install.mjs). It runs AFTER the live-run refusal below,
|
|
254
|
-
// because npm replaces the program's files as surely as `npm ci` does. Only a layout that cannot be
|
|
255
|
-
// named is still refused.
|
|
256
|
-
let packaged = null;
|
|
257
249
|
if (!isGitCheckout()) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
console.error(" Nothing was touched.");
|
|
265
|
-
return 4;
|
|
266
|
-
}
|
|
250
|
+
console.error("\n This install is not a git checkout, so there is nothing to pull.");
|
|
251
|
+
console.error(" It was installed from a package rather than cloned, which is the ordinary way.");
|
|
252
|
+
console.error("\n Update it the way it was installed:\n");
|
|
253
|
+
console.error(" npm install -g clearotron@latest\n");
|
|
254
|
+
console.error(" Nothing was touched.");
|
|
255
|
+
return 4;
|
|
267
256
|
}
|
|
268
257
|
|
|
269
258
|
// ── AND THE SECOND REFUSAL: NOT OVER A LIVE RUN ──────────────────────────────────────────────
|
|
@@ -300,27 +289,6 @@ export async function update(argv = process.argv.slice(2)) {
|
|
|
300
289
|
return 4;
|
|
301
290
|
}
|
|
302
291
|
|
|
303
|
-
if (packaged) {
|
|
304
|
-
if (packaged.current) {
|
|
305
|
-
say(`\n This install is ${packaged.installed}, and nothing newer is published (${packaged.tag}: ${packaged.version}). Nothing was touched.\n`);
|
|
306
|
-
return 0;
|
|
307
|
-
}
|
|
308
|
-
if (packaged.unread) say(`\n npm did not say which versions are published, so this follows the ${packaged.tag} channel.`);
|
|
309
|
-
say(`\n Updating this install at ${packaged.prefix} from ${packaged.installed ?? "an unreadable version"} to clearotron@${packaged.spec}.`);
|
|
310
|
-
const rc = runInCheckout("npm", packaged.npmArgs);
|
|
311
|
-
if (rc !== 0) return rc;
|
|
312
|
-
// npm puts its own link back at `<prefix>/bin/clearotron` on every install, over the launcher the
|
|
313
|
-
// install wrote, and that link runs whichever `node` is first on PATH. Put the launcher back, but only
|
|
314
|
-
// over npm's link or our own: anything else there was not ours before this update either.
|
|
315
|
-
const kind = inspectShim(shimPath()).kind;
|
|
316
|
-
if (kind === "npm-link" || kind === "ours" || kind === "ours-other-install") {
|
|
317
|
-
const shim = installShim();
|
|
318
|
-
if (!shim.ok) console.error(`\n The update worked, but the launcher at ${shim.path ?? "~/.local/bin/clearotron"} could not be written back: ${shim.detail}.`);
|
|
319
|
-
}
|
|
320
|
-
say("\n Updated. An assistant starts the new version the next time it launches Clearotron; restart the services for the portal.\n");
|
|
321
|
-
return 0;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
292
|
say("\n Configuration store is outside the checkout. Updating the product.");
|
|
325
293
|
for (const e of entries) say(` ${e.name}=${e.value} (${e.from})`);
|
|
326
294
|
if (!entries.length) {
|
package/build-info.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "/
|
|
30
|
+
"url": "https://test-trademark.cordillera.ch/tmpdemo2014fullcountrysearch-venqori-2026-09-03-sample-capture/report.html",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-03T18:06:17.281Z",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "/
|
|
30
|
+
"url": "https://test-trademark.cordillera.ch/tmpdemo2014globalpreliminarysearch-venqori-2026-09-02-sample-capture/report.html",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-02T17:33:15.625Z",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<p style="font-family:Calibri,sans-serif;font-size:11pt">Knockout trademark review — DEMO-2014-knockout-search: <b>1 mark</b>, worst band <b style="color:#2f55a4">Low</b>. <a href="/
|
|
1
|
+
<p style="font-family:Calibri,sans-serif;font-size:11pt">Knockout trademark review — DEMO-2014-knockout-search: <b>1 mark</b>, worst band <b style="color:#2f55a4">Low</b>. <a href="https://test-trademark.cordillera.ch/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/report.html">Open the full report</a> · <a href="https://test-trademark.cordillera.ch/portal/report/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/audit.xlsx">audit workbook</a>.</p>
|
|
2
2
|
<ul style="font-family:Calibri,sans-serif;font-size:11pt"><li><b>VENQORI</b>: <b style="color:#2f55a4">Low</b> — Classes 9, 42, 41 — Register filings (classes 9, 42, 41): 0 identical, 0 containing, 0 on close variations.</li></ul>
|
|
3
3
|
<p style="font-family:Calibri,sans-serif;font-size:11pt"><i>Ratings reflect our common law assessment. Register analysis may adjust ratings in either direction.</i></p>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"stepN": 6,
|
|
34
34
|
"stepTotal": 6,
|
|
35
35
|
"verdict": "Low",
|
|
36
|
-
"url": "/
|
|
36
|
+
"url": "https://test-trademark.cordillera.ch/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/report.html",
|
|
37
37
|
"failedStage": null,
|
|
38
38
|
"reason": null,
|
|
39
39
|
"deliveredAt": "2026-09-02T21:46:53.238Z",
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
"reports": [
|
|
255
255
|
{
|
|
256
256
|
"mark": "VENQORI",
|
|
257
|
-
"url": "/
|
|
257
|
+
"url": "https://test-trademark.cordillera.ch/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/report.html"
|
|
258
258
|
}
|
|
259
259
|
],
|
|
260
260
|
"sendPending": true,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "/
|
|
30
|
+
"url": "https://test-trademark.cordillera.ch/tmpdemo2014multicountryfocussearch-venqori-2026-09-02-sample-capture/report.html",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-02T19:51:48.801Z",
|