clearotron 0.3.0-beta.3 → 0.3.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +15 -6
- package/bin/connect.mjs +68 -25
- package/bin/disconnect.mjs +16 -11
- package/bin/onboard.mjs +42 -10
- package/bin/start.mjs +56 -16
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +35 -10
- package/driver/engine/openai-agent.mjs +52 -2
- package/driver/package.json +1 -1
- package/driver/portal-upstream.mjs +9 -0
- package/driver/suite-census.json +35 -11
- package/mcp-server/CHANGELOG.md +12 -2
- package/mcp-server/lib/options.mjs +4 -1
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CsCuPshD.css → index-Cv-E_agg.css} +199 -86
- package/portal-ui/dist/assets/{index-C1gpdkyH.js → index-DBUVdQT-.js} +506 -375
- package/portal-ui/dist/index.html +2 -2
- 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/scripts/engine-probe.mjs +2 -2
- package/shared/connect-clients.mjs +282 -321
- package/shared/names-in-force.mjs +1 -0
- package/shared/stdio-connect.mjs +63 -0
package/INSTALL.md
CHANGED
|
@@ -749,6 +749,8 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
|
|
|
749
749
|
|
|
750
750
|
- Generates `PORTAL_SECRET` and `TRADEMARK_MCP_TOKEN_SECRET` and **appends** them to `~/.config/clearotron/.env` at
|
|
751
751
|
mode 600. Append, never rewrite: that file also holds the credentials `npx clearotron install` collected.
|
|
752
|
+
It also records where saved searches are kept, `CLEAROTRON_RECIPES_DIR` and `RECIPE_REPO_ROOT`, so
|
|
753
|
+
`clearotron doctor` and a connected assistant read the same saved searches as the portal.
|
|
752
754
|
- Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
|
|
753
755
|
and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
|
|
754
756
|
of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
|
|
@@ -922,12 +924,19 @@ their own integration work.
|
|
|
922
924
|
**Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
|
|
923
925
|
2026-09-03, on the vendor's own documented behaviour:
|
|
924
926
|
|
|
925
|
-
1. **
|
|
926
|
-
Codex
|
|
927
|
-
network, no ingress. `npx clearotron start` prints the one line to
|
|
928
|
-
connect` hands it over per assistant.
|
|
929
|
-
2. **
|
|
930
|
-
|
|
927
|
+
1. **Clearotron is installed on the machine the assistant runs on** — Claude's desktop app, Claude Code,
|
|
928
|
+
Codex, the ChatGPT desktop app, an agent that runs commands. The assistant spawns the server over
|
|
929
|
+
stdio. No address, no key, no network, no ingress. `npx clearotron start` prints the one line to
|
|
930
|
+
paste, and `npx clearotron connect --where here` hands it over per assistant.
|
|
931
|
+
2. **Clearotron is running somewhere else** — a server, a cloud machine, anywhere the assistant is
|
|
932
|
+
not. These need **a publicly reachable HTTPS address**, plus a key made for the person connecting.
|
|
933
|
+
Always. Claude (app, web, Cowork and mobile), ChatGPT on the web, Perplexity and other agents only
|
|
934
|
+
ever connect this way; Claude Code and Codex can connect either way. `npx clearotron connect
|
|
935
|
+
--where elsewhere` makes the key and prints the steps for the assistant you name.
|
|
936
|
+
|
|
937
|
+
Without `--where`, `connect` asks when both answers are possible, and `--client <name>` on its own keeps
|
|
938
|
+
the answer that assistant had before: a key for Claude, the local line for Claude Code and Codex.
|
|
939
|
+
`npx clearotron disconnect` takes the same `--where`.
|
|
931
940
|
|
|
932
941
|
**A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
|
|
933
942
|
connector is reached **from the vendor's cloud**, never from the reader's device. Anthropic's own help
|
package/bin/connect.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import { homedir, userInfo } from "node:os";
|
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { execFileSync } from "node:child_process";
|
|
47
47
|
import { createServer } from "node:net";
|
|
48
|
-
import { CONNECT_CLIENTS, clientById, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
48
|
+
import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
49
49
|
import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
|
|
50
50
|
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
51
51
|
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
@@ -555,17 +555,12 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
555
555
|
return 1;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
if (offer.route === "disk"
|
|
559
|
-
//
|
|
560
|
-
// block is how a reader pastes four lines into a shell
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
? ` Add this to ${s.where}:`
|
|
564
|
-
: " Run this once, on this machine:");
|
|
558
|
+
if (offer.route === "disk") {
|
|
559
|
+
// THE ROW'S OWN STEPS, and nothing written here. A command and a settings block are not the same
|
|
560
|
+
// instruction — saying "run this" over a TOML block is how a reader pastes four lines into a shell —
|
|
561
|
+
// and the steps already say which each one is and where it goes.
|
|
562
|
+
printSteps(offer, null);
|
|
565
563
|
say("");
|
|
566
|
-
for (const line of String(offer.command).split("\n")) say(` ${line}`);
|
|
567
|
-
say("");
|
|
568
|
-
if (s?.after) { say(` ${s.after}`); say(""); }
|
|
569
564
|
say(` ${offer.note}`);
|
|
570
565
|
return 0;
|
|
571
566
|
}
|
|
@@ -621,22 +616,40 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
621
616
|
}
|
|
622
617
|
|
|
623
618
|
say(` Address: ${offer.address}`);
|
|
624
|
-
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
619
|
+
// Printed once, stored nowhere — and only here when no step hands it over. A step whose copy carries
|
|
620
|
+
// the key prints it inside the line the reader pastes, which is the one place it is needed.
|
|
621
|
+
const keyInSteps = (offer.steps ?? []).some((s) => s.copy?.kind === "secret");
|
|
622
|
+
if (key && !keyInSteps) say(` Key: ${key}`);
|
|
623
|
+
printSteps(offer, key);
|
|
624
|
+
say("");
|
|
625
|
+
say(` ${offer.note}`);
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The offer's own steps, numbered, with each copy printed under the step that hands it over.
|
|
631
|
+
*
|
|
632
|
+
* ── AND WHERE TO PUT THEM ( — F35) ───────────────────────────────────────
|
|
633
|
+
*
|
|
634
|
+
* The owner was left with two strings and no destination: *"I don't know how to connect it in Claude
|
|
635
|
+
* Cowork with those details."* The steps were DEFINED IN THE PRODUCT the whole time and this verb simply
|
|
636
|
+
* never printed them. Nothing is authored here; a second set of instructions written at the CLI would
|
|
637
|
+
* drift from the page's, which is the defect connect-clients-are-data exists against. A secret copy is
|
|
638
|
+
* printed with the key this press minted put in its slot — the page does the same substitution into the
|
|
639
|
+
* clipboard — and with no key (a dry run) the step prints and its copy does not.
|
|
640
|
+
*/
|
|
641
|
+
function printSteps(offer, key) {
|
|
632
642
|
if (offer.steps?.length) {
|
|
633
643
|
say("");
|
|
634
644
|
say(` In ${offer.client?.name ?? "your assistant"}:`);
|
|
635
|
-
offer.steps.forEach((step, n) =>
|
|
645
|
+
offer.steps.forEach((step, n) => {
|
|
646
|
+
say(` ${n + 1}. ${plainStep(step.text)}`);
|
|
647
|
+
const c = step.copy;
|
|
648
|
+
const text = !c ? null : c.kind === "secret" ? (key ? c.template.split(c.slot).join(key) : null) : c.text;
|
|
649
|
+
if (text) { say(""); for (const line of text.split("\n")) say(` ${line}`); say(""); }
|
|
650
|
+
if (step.hint) say(` ${plainStep(step.hint)}`);
|
|
651
|
+
});
|
|
636
652
|
}
|
|
637
|
-
say("");
|
|
638
|
-
say(` ${offer.note}`);
|
|
639
|
-
return 0;
|
|
640
653
|
}
|
|
641
654
|
|
|
642
655
|
async function main() {
|
|
@@ -654,6 +667,9 @@ async function main() {
|
|
|
654
667
|
say(" not about you.");
|
|
655
668
|
say("");
|
|
656
669
|
say(" --client <name> skip the question (see --list for the names)");
|
|
670
|
+
say(" --where here your assistant runs on this machine: it starts the software itself");
|
|
671
|
+
say(" --where elsewhere your assistant runs somewhere else and reaches this install over the");
|
|
672
|
+
say(" internet, with a key made for you now");
|
|
657
673
|
say(" --list the assistants this build knows");
|
|
658
674
|
say(" --dry-run say what would change, change nothing");
|
|
659
675
|
say(" --allow-checkout-move");
|
|
@@ -663,7 +679,7 @@ async function main() {
|
|
|
663
679
|
say("");
|
|
664
680
|
return 0;
|
|
665
681
|
}
|
|
666
|
-
const known = new Set(["--client", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
682
|
+
const known = new Set(["--client", "--where", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
667
683
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
668
684
|
if (unknown.length) {
|
|
669
685
|
console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -671,6 +687,12 @@ async function main() {
|
|
|
671
687
|
process.exit(2);
|
|
672
688
|
}
|
|
673
689
|
const dryRun = argv.includes("--dry-run");
|
|
690
|
+
const w = argv.indexOf("--where");
|
|
691
|
+
if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
|
|
692
|
+
console.error(`connect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
|
|
693
|
+
process.exit(2);
|
|
694
|
+
}
|
|
695
|
+
let route = w >= 0 ? WHERE_FLAG[argv[w + 1]] : null;
|
|
674
696
|
// F40 — reason about the RUNNING product from the units' own environment, not this CLI's env file.
|
|
675
697
|
const running = runningEnv();
|
|
676
698
|
const have = deploymentHas(running.env);
|
|
@@ -712,9 +734,30 @@ async function main() {
|
|
|
712
734
|
chosen = CONNECT_CLIENTS[Number(answer) - 1] ?? clientById(answer);
|
|
713
735
|
} finally { rl.close(); }
|
|
714
736
|
if (!chosen) { console.error("connect: not one of the listed assistants."); process.exit(2); }
|
|
737
|
+
// THE SECOND QUESTION, ASKED ONLY WHEN IT HAS TWO ANSWERS — the page's own, in the terminal's words.
|
|
738
|
+
// Where only one route is served here, asking would offer a choice whose other half cannot work.
|
|
739
|
+
const both = ["disk", "public-http"].filter((r) => whatItNeeds(chosen, have, r)?.served);
|
|
740
|
+
if (!route && both.length > 1) {
|
|
741
|
+
say("");
|
|
742
|
+
say(" Where does it run?");
|
|
743
|
+
say("");
|
|
744
|
+
say(" 1) On this machine — it starts the software itself, nothing to open up");
|
|
745
|
+
say(" 2) Somewhere else — it reaches this install over the internet, with a key made for you now");
|
|
746
|
+
say("");
|
|
747
|
+
const rl2 = createInterface({ input: stdin, output: stdout });
|
|
748
|
+
try {
|
|
749
|
+
const answer = (await rl2.question(" 1-2: ")).trim();
|
|
750
|
+
route = answer === "1" ? "disk" : answer === "2" ? "public-http" : null;
|
|
751
|
+
} finally { rl2.close(); }
|
|
752
|
+
if (!route) { console.error("connect: answer 1 or 2."); process.exit(2); }
|
|
753
|
+
}
|
|
754
|
+
route ??= both[0] ?? chosen.lead;
|
|
715
755
|
}
|
|
716
756
|
|
|
717
|
-
|
|
757
|
+
// NAMED WITHOUT --where: the route this id had before every row took both, so a scripted
|
|
758
|
+
// `--client cowork` still mints a key and `--client codex` still prints a settings block.
|
|
759
|
+
route ??= leadRouteFor(argv[i + 1]);
|
|
760
|
+
return await render(whatItNeeds(chosen, have, route), have, { dryRun, running, allowMove: argv.includes("--allow-checkout-move") });
|
|
718
761
|
}
|
|
719
762
|
|
|
720
763
|
// THE DISPATCH RUNS ONLY WHEN THIS FILE IS THE COMMAND. Without the guard, importing
|
package/bin/disconnect.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import { stdin, stdout } from "node:process";
|
|
|
57
57
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
58
58
|
import { join, dirname } from "node:path";
|
|
59
59
|
import { homedir, userInfo } from "node:os";
|
|
60
|
-
import { CONNECT_CLIENTS, clientById } from "../shared/connect-clients.mjs";
|
|
60
|
+
import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor } from "../shared/connect-clients.mjs";
|
|
61
61
|
import { defaultDenylistPath, disablePlan, revokeEveryonePlan, applyDisablePlan, describeClosure, recordedKeysFor, removeRecordedKeys } from "../shared/client-door.mjs";
|
|
62
62
|
import { loadGrants } from "../shared/scope.mjs";
|
|
63
63
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
@@ -83,6 +83,8 @@ async function main() {
|
|
|
83
83
|
say(" `clearotron grant` manages that.");
|
|
84
84
|
say("");
|
|
85
85
|
say(" --client <name> which assistant you connected (see --list for the names)");
|
|
86
|
+
say(" --where here|elsewhere");
|
|
87
|
+
say(" how you connected it: on this machine, or over the internet with a key");
|
|
86
88
|
say(" --list the assistants this build knows");
|
|
87
89
|
say(" --everyone the admin act: revoke EVERY issued key on this install. It says");
|
|
88
90
|
say(" how many keys and how many people that is before doing it.");
|
|
@@ -90,7 +92,7 @@ async function main() {
|
|
|
90
92
|
say("");
|
|
91
93
|
return 0;
|
|
92
94
|
}
|
|
93
|
-
const known = new Set(["--client", "--list", "--dry-run", "--everyone", "--help", "-h"]);
|
|
95
|
+
const known = new Set(["--client", "--where", "--list", "--dry-run", "--everyone", "--help", "-h"]);
|
|
94
96
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
95
97
|
if (unknown.length) {
|
|
96
98
|
console.error(`disconnect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -137,18 +139,21 @@ async function main() {
|
|
|
137
139
|
say(` ${chosen.name}`);
|
|
138
140
|
say("");
|
|
139
141
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// the
|
|
143
|
-
|
|
142
|
+
// THE ROUTE DECIDES THE SIDE, not the assistant: every row takes both now. Connected on this machine,
|
|
143
|
+
// it never touched this install; connected over the internet, it holds a key and the key is what goes.
|
|
144
|
+
// Unnamed, the route is the one `connect` gives the same id unnamed, so the two verbs pair up.
|
|
145
|
+
const w = argv.indexOf("--where");
|
|
146
|
+
if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
|
|
147
|
+
console.error(`disconnect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
|
|
148
|
+
process.exit(2);
|
|
149
|
+
}
|
|
150
|
+
const route = w >= 0 ? WHERE_FLAG[argv[w + 1]] : (i >= 0 ? leadRouteFor(argv[i + 1]) : chosen.lead);
|
|
151
|
+
if (route === "disk") {
|
|
144
152
|
say(" Connecting this assistant changed nothing on this install — it runs the software itself,");
|
|
145
153
|
say(" from the configuration you added on its side. To disconnect it, remove that entry in the");
|
|
146
154
|
say(" assistant's own settings.");
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
say(" If you connected it by address instead, the door and key are shared — disconnect the");
|
|
150
|
-
say(" assistant you named when the door was opened, and the closure covers this one too.");
|
|
151
|
-
}
|
|
155
|
+
say("");
|
|
156
|
+
say(" If you connected it over the internet instead, run this again with --where elsewhere.");
|
|
152
157
|
return 0;
|
|
153
158
|
}
|
|
154
159
|
|
package/bin/onboard.mjs
CHANGED
|
@@ -99,7 +99,15 @@ import { entrypointOf } from "../driver/systemd/install-census.mjs"; //
|
|
|
99
99
|
import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
|
|
100
100
|
import { whereSavesGo, storeCommitRefusal, storeInRepo, storeOutsideRepoMessage, resolveStoreRepoRoot } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed, and why saved searches are off
|
|
101
101
|
import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
|
|
102
|
-
import { probeEngineTurn, probeFailureText,
|
|
102
|
+
import { probeEngineTurn, probeFailureText, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
103
|
+
|
|
104
|
+
// THE PROVING SENTENCES NAME NO MODEL. They printed the driver's tier word, which is an Anthropic model's
|
|
105
|
+
// name, on both engines, so a codex user was told setup was about to spend on a model family they do not
|
|
106
|
+
// use. "Its cheapest model" is true of either engine and brands neither.
|
|
107
|
+
export const probingLine = (engineId) =>
|
|
108
|
+
`Probing ${engineId} with one turn on its cheapest model (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`;
|
|
109
|
+
export const proveQuestion = ({ engineId, lane }) =>
|
|
110
|
+
`Prove ${engineId} on the ${lane} lane now with one turn on its cheapest model (a few tokens, ${PROBE_TIMEOUT_SEC}s ceiling)?`;
|
|
103
111
|
import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
|
|
104
112
|
import { pinEnv, envFrom } from "../shared/env-aliases.mjs";
|
|
105
113
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
@@ -1206,7 +1214,7 @@ export async function runCheck() {
|
|
|
1206
1214
|
// deliberate — a second reader would drift from this one exactly as the composer and the checker did
|
|
1207
1215
|
// in F41, and the drift is invisible because both sides keep passing their own arms.
|
|
1208
1216
|
const unitDir = join(homedir(), ".config", "systemd", "user");
|
|
1209
|
-
const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
|
|
1217
|
+
const { BACKGROUND_UNITS, startPaths } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
|
|
1210
1218
|
const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
|
|
1211
1219
|
const unitEnv = hosted
|
|
1212
1220
|
? unitEnvironment({
|
|
@@ -1440,7 +1448,7 @@ export async function runCheck() {
|
|
|
1440
1448
|
} else if (!bin.executable || bin.relative) {
|
|
1441
1449
|
info("not probed — there is no usable binary to probe. Fix the line above first.");
|
|
1442
1450
|
} else {
|
|
1443
|
-
say(`\n
|
|
1451
|
+
say(`\n ${probingLine(engineId)}`);
|
|
1444
1452
|
// The engine as a RUN would see it: environment first, .env behind it. Only the engine-selection
|
|
1445
1453
|
// keys — the probe must bill exactly the way this box bills and moves no other variable.
|
|
1446
1454
|
const probeEnv = { ...process.env };
|
|
@@ -1628,6 +1636,13 @@ export async function runCheck() {
|
|
|
1628
1636
|
// because a reader debugging `clearotron run` by hand IS this process.
|
|
1629
1637
|
info(`this command's own process ${where} — a CLI is not started by the units' EnvironmentFile, so `
|
|
1630
1638
|
+ "that differs by design and is not what a run uses");
|
|
1639
|
+
} else if (!hosted && svcCustomers?.from === serviceEnvLabel) {
|
|
1640
|
+
// NAMED IN THE ENV FILE ALONE, ON AN INSTALL WITH NO UNITS. `clearotron start` loads that file and
|
|
1641
|
+
// its services inherit the store; this command does not load it, so its own process fell back to
|
|
1642
|
+
// the bundled roster and printed that as the answer, one line below a ✓ for the same variable.
|
|
1643
|
+
ok(`the services resolve profiles from ${svcCustomers.v} (${svcCustomers.from})`);
|
|
1644
|
+
info(`this command's own process ${where} — it does not load ${serviceEnvFile}, so that differs by `
|
|
1645
|
+
+ `design and is not what \`${invoke("start")}\` hands the services`);
|
|
1631
1646
|
} else {
|
|
1632
1647
|
if (r.situation === "overlay" && !r.findings.length) ok(`${where} — the configured store`);
|
|
1633
1648
|
else if (r.situation === "bundled-fallback") info(`${where} — THE BUNDLED DEMO ROSTER, because CLEAROTRON_CUSTOMERS_DIR is unset. Legitimate on a generic-defaults install; a fallback either way, and it is what a misconfigured deployment also looks like`);
|
|
@@ -1638,7 +1653,8 @@ export async function runCheck() {
|
|
|
1638
1653
|
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1639
1654
|
+ "what the services resolve is not judged here, and the line above is this process's own answer");
|
|
1640
1655
|
info("what a RUN used is its own `profile-store` journal line — this command reports what the "
|
|
1641
|
-
+ (hosted ? "units' file says, which the running services read at their own start"
|
|
1656
|
+
+ (hosted ? "units' file says, which the running services read at their own start"
|
|
1657
|
+
: `environment you are typing in and your environment file say, which \`${invoke("start")}\` reads at its own start`));
|
|
1642
1658
|
// ── AND WHO IS ACTUALLY IN IT ─────────────────────────────────────────
|
|
1643
1659
|
//
|
|
1644
1660
|
// The line above names the STORE. An operator who has just configured one wants to know their
|
|
@@ -1807,16 +1823,32 @@ export async function runCheck() {
|
|
|
1807
1823
|
// be read, the lines above already said so and nothing is judged here. A store that is configured but
|
|
1808
1824
|
// holds a file that cannot be read fails every company's saved searches, in the portal and the
|
|
1809
1825
|
// connector alike, so that is read here too.
|
|
1826
|
+
//
|
|
1827
|
+
// WITH NO UNITS, THE SERVICES ARE `clearotron start`'s CHILDREN, and it hands every one of them a store
|
|
1828
|
+
// whether or not the env file names it. So "off" is only ever true of a hosted box. This read the env
|
|
1829
|
+
// file alone, and on a local install started before `start` wrote the store there, it told a portal
|
|
1830
|
+
// that was listing saved searches that they were off.
|
|
1810
1831
|
if (!hosted || serviceKnown) {
|
|
1811
|
-
|
|
1832
|
+
// Layered as effectiveForService layers it — this command's environment over the services' file — so
|
|
1833
|
+
// the repository root is read from the same place the store directory is.
|
|
1834
|
+
let storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
|
|
1835
|
+
let recipesSet = effectiveForService("CLEAROTRON_RECIPES_DIR");
|
|
1836
|
+
let handedBy = "";
|
|
1837
|
+
if (!hosted && !recipesSet?.v) {
|
|
1838
|
+
const handed = startPaths({ env: storeEnv });
|
|
1839
|
+
recipesSet = { v: handed.recipes, from: "clearotron start", name: "CLEAROTRON_RECIPES_DIR" };
|
|
1840
|
+
storeEnv = { ...storeEnv, RECIPE_REPO_ROOT: handed.configStore };
|
|
1841
|
+
handedBy = ` — where \`${invoke("start")}\` puts them`;
|
|
1842
|
+
}
|
|
1812
1843
|
const recipesDir = recipesSet?.v || null;
|
|
1813
1844
|
if (!recipesDir) {
|
|
1814
1845
|
info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
|
|
1815
1846
|
+ "connector offer none. Name a directory inside a git repository to switch them on");
|
|
1847
|
+
} else if (handedBy && !existsSync(join(storeEnv.RECIPE_REPO_ROOT, ".git"))) {
|
|
1848
|
+
// NOT CREATED YET IS NOT UNREADABLE. The store does not exist until the first start makes it, and
|
|
1849
|
+
// reading it now would report a missing directory as a broken one.
|
|
1850
|
+
info(`saved searches switch on at the first \`${invoke("start")}\`, which creates their store in ${recipesDir}`);
|
|
1816
1851
|
} else {
|
|
1817
|
-
// Layered as effectiveForService layers it — this command's environment over the services' file — so
|
|
1818
|
-
// the repository root is read from the same place the store directory above was.
|
|
1819
|
-
const storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
|
|
1820
1852
|
const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
|
|
1821
1853
|
const reach = storeInRepo(recipesDir, resolved.root);
|
|
1822
1854
|
if (!reach.ok) {
|
|
@@ -1829,7 +1861,7 @@ export async function runCheck() {
|
|
|
1829
1861
|
if (unreadable) {
|
|
1830
1862
|
warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
|
|
1831
1863
|
+ "to load, in the portal and the connector, until that file is fixed");
|
|
1832
|
-
} else ok(`saved searches are read from ${recipesDir}, and saves are committed in ${reach.repo}`);
|
|
1864
|
+
} else ok(`saved searches are read from ${recipesDir}${handedBy}, and saves are committed in ${reach.repo}`);
|
|
1833
1865
|
}
|
|
1834
1866
|
}
|
|
1835
1867
|
}
|
|
@@ -3281,7 +3313,7 @@ try {
|
|
|
3281
3313
|
// skipping it costs an hour later.
|
|
3282
3314
|
info("A file that exists is not an engine that works — so setup tries one before writing anything.");
|
|
3283
3315
|
info("It takes a few seconds here. Skipped, a broken engine surfaces an hour into a real search.");
|
|
3284
|
-
if (!await confirm(
|
|
3316
|
+
if (!await confirm(proveQuestion({ engineId: pick.id, lane: authPick.id }), true)) {
|
|
3285
3317
|
info("Not proven, so not written. Pick again — the last row configures no engine at all.");
|
|
3286
3318
|
continue;
|
|
3287
3319
|
}
|
package/bin/start.mjs
CHANGED
|
@@ -355,6 +355,50 @@ export function defaultGrantsPath({ env = process.env, demo = false } = {}) {
|
|
|
355
355
|
return installPaths(join(homedir(), demo ? "trademark-demo" : "trademark")).grants;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* The paths `clearotron start` hands its services: the install's layout under `base`, with whatever the
|
|
360
|
+
* environment already names winning over the layout's default, as `start` has always applied it.
|
|
361
|
+
*
|
|
362
|
+
* EXPORTED FOR THE ONE OTHER READER THAT MUST AGREE WITH IT. On an install with no background units,
|
|
363
|
+
* `clearotron doctor` has no unit file to read the services' environment from: the services are this
|
|
364
|
+
* command's children and get the saved-search store from here. An install started before the store was
|
|
365
|
+
* written to the env file has no line there to read, so doctor asks this function for it, and the
|
|
366
|
+
* answer it prints and the one the services were given have one author.
|
|
367
|
+
*
|
|
368
|
+
* NOT IN A DEMO. Nothing the environment says about an install is the demo's: with the reader's settings
|
|
369
|
+
* in force, 0.3.0-beta.1's demo seeded its example reports into their real archive.
|
|
370
|
+
*/
|
|
371
|
+
export function startPaths({ env = process.env, base = join(homedir(), "trademark"), demo = false } = {}) {
|
|
372
|
+
const paths = installPaths(base);
|
|
373
|
+
if (demo) return paths;
|
|
374
|
+
for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
|
|
375
|
+
["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
|
|
376
|
+
["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (env[name]) paths[k] = env[name];
|
|
377
|
+
if (env.RECIPE_REPO_ROOT) paths.configStore = env.RECIPE_REPO_ROOT;
|
|
378
|
+
if (env.PORTAL_AUDIT) paths.audit = env.PORTAL_AUDIT;
|
|
379
|
+
return paths;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The saved-search store, as the env file must record it for every reader OUTSIDE this command's tree.
|
|
384
|
+
*
|
|
385
|
+
* The services are handed the store by `childEnv`. A connector an assistant launches, `clearotron doctor`
|
|
386
|
+
* and every other hand-run command are not this command's children: they read the env file, and the
|
|
387
|
+
* file never named the store. So on a local install the portal listed a company's saved searches, the
|
|
388
|
+
* connected assistant was told there were none, and doctor said saved searches were off. Written
|
|
389
|
+
* add-only beside the secrets, as `npx clearotron install` writes the reports and work directories: a
|
|
390
|
+
* line an operator wrote wins.
|
|
391
|
+
*/
|
|
392
|
+
export function storesForOtherReaders(paths) {
|
|
393
|
+
return { CLEAROTRON_RECIPES_DIR: paths.recipes, RECIPE_REPO_ROOT: paths.configStore };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Written above each line, so a reader who moves the install sees that these two move with it.
|
|
397
|
+
const STORE_NOTES = {
|
|
398
|
+
CLEAROTRON_RECIPES_DIR: "Where saved searches are kept. `clearotron start` created it; edit both lines if you move the install.",
|
|
399
|
+
RECIPE_REPO_ROOT: "The git repository saved searches are committed in, which holds the directory above.",
|
|
400
|
+
};
|
|
401
|
+
|
|
358
402
|
/** Everything this install keeps on disk, under one base directory. */
|
|
359
403
|
export function installPaths(base) {
|
|
360
404
|
return {
|
|
@@ -846,20 +890,11 @@ if (isMain) {
|
|
|
846
890
|
const DEMO = argv.includes("--demo");
|
|
847
891
|
// The same base `npm run setup` writes under, so whichever of the two a reader ran first, the other
|
|
848
892
|
// finds the same install rather than a second one beside it.
|
|
849
|
-
const paths = installPaths(flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")));
|
|
850
893
|
// Whatever the environment already says wins over the base-derived default, for every path — a reader
|
|
851
|
-
// who ran `npm run setup` has these in .env already and this must not move their data.
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
|
|
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;
|
|
862
|
-
}
|
|
894
|
+
// who ran `npm run setup` has these in .env already and this must not move their data. Not in a demo,
|
|
895
|
+
// which `startPaths` says why. One author, because `doctor` asks the same function what the services
|
|
896
|
+
// were handed.
|
|
897
|
+
const paths = startPaths({ env: process.env, base: flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")), demo: DEMO });
|
|
863
898
|
// ── THIS INSTALL'S FIRST START, read before this start writes either file that answers it ────────────
|
|
864
899
|
//
|
|
865
900
|
// The grants file and the config store's repository are both written further down, on every start
|
|
@@ -1117,14 +1152,19 @@ if (isMain) {
|
|
|
1117
1152
|
const portalSecret = secretFor("PORTAL_SECRET");
|
|
1118
1153
|
const tokenSecret = secretFor("TRADEMARK_MCP_TOKEN_SECRET");
|
|
1119
1154
|
if (!process.env.PORTAL_LOCAL_USER) generated.PORTAL_LOCAL_USER = user;
|
|
1155
|
+
const stores = DEMO ? {} : storesForOtherReaders(paths);
|
|
1120
1156
|
|
|
1121
1157
|
// A DEMO WRITES NO SECRETS AND NO ADDRESS. They are generated per run and live in memory only, which
|
|
1122
1158
|
// is the same posture the ops key already has here — and it is what makes "removing the demo is one
|
|
1123
1159
|
// directory" true rather than nearly true.
|
|
1124
|
-
|
|
1160
|
+
//
|
|
1161
|
+
// THE STORES ARE WRITTEN EVEN WHEN NO SECRET IS. An install whose secrets are already in the file is
|
|
1162
|
+
// exactly the install whose connector and doctor could not see its saved searches, and a gate on the
|
|
1163
|
+
// secrets alone would never reach it.
|
|
1164
|
+
if ((Object.keys(generated).length || Object.keys(stores).length) && !DEMO) {
|
|
1125
1165
|
let existing = "";
|
|
1126
1166
|
try { existing = readFileSync(ENV_PATH, "utf8"); } catch (e) { if (e.code !== "ENOENT") fatal(`${ENV_PATH} exists but could not be read (${e.code}).`); }
|
|
1127
|
-
const merged = mergeEnvFile(existing, generated);
|
|
1167
|
+
const merged = mergeEnvFile(existing, { ...generated, ...stores }, { notes: STORE_NOTES });
|
|
1128
1168
|
if (merged.added.length) {
|
|
1129
1169
|
// ONE WRITER FOR EVERY FILE THAT HOLDS CREDENTIALS, and it creates the directory. This site had its
|
|
1130
1170
|
// own copy of the write and did not learn what the wizard's copy learned when `.env` moved under
|
|
@@ -1135,7 +1175,7 @@ if (isMain) {
|
|
|
1135
1175
|
} catch (e) {
|
|
1136
1176
|
fatal(`could not write ${ENV_PATH} (${String(e?.message ?? e)}).`);
|
|
1137
1177
|
}
|
|
1138
|
-
// NAMES only.
|
|
1178
|
+
// NAMES only. Some of what this file holds is credentials.
|
|
1139
1179
|
say(` wrote ${merged.added.join(", ")} to ${ENV_PATH} (mode 600)`);
|
|
1140
1180
|
}
|
|
1141
1181
|
}
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5f7e295: Fixed: A Clearotron install signed in to OpenAI's `codex` with a subscription keeps working after codex refreshes its login. Before, every search after the first refresh failed within seconds until you signed in again.
|
|
8
|
+
- 2878809: Fixed: When the company store cannot record a new company, the New company page now says why and what fixes it, instead of "Try again shortly". Someone who does not run the installation is told to ask whoever does.
|
|
9
|
+
- e7cd9e1: Fixed: On an install run with `clearotron start`, a connected assistant now lists the same saved searches as the portal, including Generic's. `clearotron doctor` no longer says working saved searches are off, and it names one place profiles come from.
|
|
10
|
+
- e7cd9e1: Fixed: When setup offers to try your engine, it no longer names an Anthropic model to someone who chose OpenAI's `codex`.
|
|
11
|
+
|
|
12
|
+
## 0.3.0-beta.4
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- a9f5375: New: Use your own AI asks first where Clearotron is running, then shows the steps for your app beside the list. The same five apps are offered either way.
|
|
17
|
+
|
|
18
|
+
New: Claude Code and Codex can connect to an installation running elsewhere, and the ChatGPT desktop app to one on the same machine.
|
|
19
|
+
|
|
20
|
+
Fixed: Codex was told to paste a settings block into a terminal. Its steps now name the file the block goes in.
|
|
21
|
+
|
|
22
|
+
For operators: `clearotron connect` and `clearotron disconnect` take `--where here` or `--where elsewhere`. Assistant names used before, such as `cowork`, still work.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- a9f5375: New: the company switcher in the sidebar ends with `+ New company`. Making a company is now one click from every screen, whether or not a company is selected.
|
|
27
|
+
|
|
3
28
|
## 0.3.0-beta.3
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -7,9 +32,9 @@
|
|
|
7
32
|
- a782aad: Fixed: Creating a company is refused, with nothing left behind, when the configuration store cannot record it. The company used to be created anyway, with no record of who made it or when, and its organisation was given access to it.
|
|
8
33
|
|
|
9
34
|
A store with no git identity is the usual cause on a new machine, and the refusal names the command that fixes it. Setup and `clearotron start` now check a store they adopt for this straight away.
|
|
10
|
-
- d1ef225: Fixed: A
|
|
35
|
+
- d1ef225: Fixed: A search step stopped at its time limit now records the output it actually produced. It used to record a small fraction, so a step that was working read as one that had stalled.
|
|
11
36
|
|
|
12
|
-
The token totals `clearotron tokens` reports for runs with a stopped
|
|
37
|
+
The token totals `clearotron tokens` reports for runs with a stopped step now include that output.
|
|
13
38
|
- 0ff42d1: Fixed: `clearotron doctor` now says when saved searches are switched off and why, and when a saved search file cannot be read.
|
|
14
39
|
|
|
15
40
|
An assistant asking for saved searches is told when they could not be read, instead of being told there are none.
|
|
@@ -63,11 +88,11 @@
|
|
|
63
88
|
|
|
64
89
|
Fixed: A search now records any of your default territories that the engine cannot search, in the record of that search. A mistyped default no longer narrows a search silently.
|
|
65
90
|
- eacfce4: Fixed: An assistant connected to a local install now sees the saved searches the portal shows, and can plan a run from one of them. Before, outside the demo, only the portal was told where saved searches are kept, so an assistant was told the install had none.
|
|
66
|
-
- 34cc1c7: For operators: A health check that could not
|
|
91
|
+
- 34cc1c7: For operators: A deployment health check that could not finish now fails instead of reporting success. Two parts of the service check could skip themselves. One skipped when the installation did not say which installation it is; the other when the scan of the service files did not finish. Both used to note that they had not run and then pass.
|
|
67
92
|
|
|
68
93
|
For operators: Set `CLEAROTRON_BOX` to `prod` or `test`. Those are the only two values the check accepts; anything else, including any other name, reads as unnamed and fails. The failure names the setting and says what went unchecked.
|
|
69
94
|
|
|
70
|
-
For operators: The
|
|
95
|
+
For operators: The part that could skip itself is the one that notices a service that has stopped and stayed stopped. The other part lists what is running, so it cannot see something that is no longer there. While the first part is skipped, a service can disappear without the check saying anything.
|
|
71
96
|
- b45a5cf: Fixed: `clearotron doctor` now reports everything a search would be refused for, checked against the environment the search will run in. So an installation it passes is one that can run a search.
|
|
72
97
|
|
|
73
98
|
Fixed: A token set in the installation's own configuration file now reaches the engine check. A headless server set up the documented way proves its engine instead of reporting it signed out.
|
|
@@ -112,7 +137,7 @@
|
|
|
112
137
|
Fixed: When a feature is switched off on your installation, the screen says so and what to change. It used to suggest trying again shortly.
|
|
113
138
|
|
|
114
139
|
Fixed: The link to the risk-framework guide opens in a new tab and goes straight to the section on writing your own. It also appears on the Company profile screen.
|
|
115
|
-
- ad088d6: Fixed: The audit workbook's "What was searched" sheet is now in plain words. Its Result and Note columns carry the search log's own notes.
|
|
140
|
+
- ad088d6: Fixed: The audit workbook's "What was searched" sheet is now in plain words. Its Result and Note columns carry the search log's own notes. Internal terms could reach them: a status such as "deferred", a full web address, a bare HTTP code. Those words are now replaced as the workbook is built, a republished report included. A search recorded as not run still reads as not searched. Search terms and names stay exactly as written, because they record what was searched.
|
|
116
141
|
- ff16a3b: For operators: The deployment health check now reports whether the component that updates an installation is itself up to date. It was the one part of a deployment the check could not identify. An installation kept current by an out-of-date updater could report healthy while serving stale code.
|
|
117
142
|
|
|
118
143
|
For operators: An updater that cannot be identified is now reported as a failure rather than passed over. A copy old enough to predate this reporting writes nothing at all. That silence is the case worth knowing about, so it is treated as a finding.
|
|
@@ -142,7 +167,7 @@
|
|
|
142
167
|
|
|
143
168
|
The product now says company throughout. It used to say brand owner, account, client and customer for the same thing. The firm running the installation is named separately, in the top bar.
|
|
144
169
|
|
|
145
|
-
Companies created through the settings page were saved without a risk framework. Their matters were then rated under the
|
|
170
|
+
Companies created through the settings page were saved without a risk framework. Their matters were then rated under the default risk framework, with nothing on screen saying so. Every company created now carries one, and says which.
|
|
146
171
|
- f75266d: New: The setup wizard now asks for the organisation's name after the sign-in address, and the person who installs starts with access to everything.
|
|
147
172
|
|
|
148
173
|
`clearotron grant add` sets a person's two permissions with `--run` and `--manage`; with neither, the person can look and start nothing.
|
|
@@ -164,18 +189,18 @@
|
|
|
164
189
|
|
|
165
190
|
Getting the deck's shape wrong used to fail quietly. The profile screen showed the framework's title and your band colours, and silently omitted the box saying what the bands mean. The new command answers that question directly, using the screen's own read of the deck.
|
|
166
191
|
|
|
167
|
-
Fixed: a framework
|
|
192
|
+
Fixed: When you have a configuration store set and a risk framework comes from the product's own files instead, the product now says so.
|
|
168
193
|
|
|
169
194
|
Your store is looked in first, and the product's files answer when it is silent. The product ships decks under names you may also have chosen. So a deck that went missing from your store was replaced by ours rather than reported absent. Same band words, different rubric, nothing raised anywhere. The profile screen now writes one line naming what happened, and the new command reports it.
|
|
170
195
|
|
|
171
|
-
Fixed:
|
|
196
|
+
Fixed: The built-in triage framework's profile page explains its bands again.
|
|
172
197
|
|
|
173
198
|
Its band sections stated their meanings as plain paragraphs, which the screen does not read. Every company without a framework of its own saw band colours and no explanation. The wording is unchanged.
|
|
174
|
-
- cfb9a9f: Fixed: `clearotron doctor` now
|
|
199
|
+
- cfb9a9f: Fixed: `clearotron doctor` now lists the companies your portal's key may start runs for.
|
|
175
200
|
|
|
176
201
|
The key carries a list of the companies it may start runs for. A company added after the key was minted is outside it. Doctor reported the key's expiry and never its coverage. So the one command whose job is to tell you what a machine is configured for said nothing about it.
|
|
177
202
|
|
|
178
|
-
It reads the
|
|
203
|
+
It reads the company list the background services read, not the one a command run in a terminal would find, and it says which. The two can differ, and when they do, that difference is the answer.
|
|
179
204
|
|
|
180
205
|
The line is a note, not a failure. Your portal takes a fresh credential at the start of every call, so a company outside the key is not normally refused. It is refused when the portal cannot take a fresh one, and the line says so and gives you the command to widen the key.
|
|
181
206
|
- ba2899b: Fixed: On reports searched through Signa, each register finding now links to the office's own page for that record, where the office publishes one. Singapore publishes no such page, so its registrations are cited by number, and the report says why. A number that an office's page cannot take is cited the same way.
|