clearotron 0.3.0-beta.2 → 0.3.0-beta.4
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 +32 -6
- package/bin/connect.mjs +68 -25
- package/bin/disconnect.mjs +16 -11
- package/bin/onboard.mjs +65 -6
- package/bin/start.mjs +7 -1
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +38 -0
- package/driver/driver.config.mjs +5 -0
- package/driver/engine/anthropic-agent.mjs +34 -17
- package/driver/gateway.mjs +76 -15
- package/driver/package.json +1 -1
- package/driver/portal-service.mjs +1 -1
- package/driver/profile-service.mjs +31 -3
- package/driver/publish/index.mjs +12 -9
- package/driver/publish/knockout.mjs +4 -2
- package/driver/publish/office-record-links.mjs +56 -21
- package/driver/recipe-service.mjs +14 -5
- package/driver/record-origins.mjs +14 -0
- package/driver/suite-census.json +34 -28
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/driver.mjs +2 -0
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +9 -2
- 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-CcFjgM78.js → index-DWYCsOCJ.js} +514 -380
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +5 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +114 -9
- package/scripts/revisit-render-check.mjs +22 -4
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/connect-clients.mjs +282 -321
- package/shared/names-in-force.mjs +1 -0
- package/shared/stdio-connect.mjs +63 -0
- package/shared/store-in-repo.mjs +50 -2
package/INSTALL.md
CHANGED
|
@@ -491,6 +491,25 @@ matches, the neutral Generic default applies.
|
|
|
491
491
|
- **Your real customers live outside the repo.** Point `CLEAROTRON_CUSTOMERS_DIR` at your own private
|
|
492
492
|
config store and the engine loads *those* accounts instead. **Same engine, different config path** —
|
|
493
493
|
the code carries no customer identities.
|
|
494
|
+
|
|
495
|
+
Two things go with it, and both are refusals rather than preferences:
|
|
496
|
+
|
|
497
|
+
- **`PROFILE_REPO_ROOT` moves too.** The customer directory has to sit inside the repository that
|
|
498
|
+
variable names, because editing a profile is a commit. Point one somewhere new and leave the other
|
|
499
|
+
behind and the portal and the profile service both refuse to start, naming both variables.
|
|
500
|
+
- **That repository needs a `user.name` and a `user.email` of its own.** Saves are committed under
|
|
501
|
+
the name of whoever asked for them, but git also records who *made* the commit, and it will not
|
|
502
|
+
commit at all without one. A service account usually has no global git identity, so a store created
|
|
503
|
+
by hand needs its own:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
git init -b main /srv/clearotron-store
|
|
507
|
+
git -C /srv/clearotron-store config user.name "clearotron local install"
|
|
508
|
+
git -C /srv/clearotron-store config user.email "clearotron@example.com"
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`clearotron start` does this for the store it creates. A store you make yourself does not get it,
|
|
512
|
+
and the symptom is the first save failing at a commit rather than anything about profiles.
|
|
494
513
|
- **Run data is external too.** Published reports, audits, and per-run state go to the archive pool at
|
|
495
514
|
`CLEAROTRON_REPORTS_DIR`. Nothing customer-specific is committed to the repository.
|
|
496
515
|
|
|
@@ -903,12 +922,19 @@ their own integration work.
|
|
|
903
922
|
**Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
|
|
904
923
|
2026-09-03, on the vendor's own documented behaviour:
|
|
905
924
|
|
|
906
|
-
1. **
|
|
907
|
-
Codex
|
|
908
|
-
network, no ingress. `npx clearotron start` prints the one line to
|
|
909
|
-
connect` hands it over per assistant.
|
|
910
|
-
2. **
|
|
911
|
-
|
|
925
|
+
1. **Clearotron is installed on the machine the assistant runs on** — Claude's desktop app, Claude Code,
|
|
926
|
+
Codex, the ChatGPT desktop app, an agent that runs commands. The assistant spawns the server over
|
|
927
|
+
stdio. No address, no key, no network, no ingress. `npx clearotron start` prints the one line to
|
|
928
|
+
paste, and `npx clearotron connect --where here` hands it over per assistant.
|
|
929
|
+
2. **Clearotron is running somewhere else** — a server, a cloud machine, anywhere the assistant is
|
|
930
|
+
not. These need **a publicly reachable HTTPS address**, plus a key made for the person connecting.
|
|
931
|
+
Always. Claude (app, web, Cowork and mobile), ChatGPT on the web, Perplexity and other agents only
|
|
932
|
+
ever connect this way; Claude Code and Codex can connect either way. `npx clearotron connect
|
|
933
|
+
--where elsewhere` makes the key and prints the steps for the assistant you name.
|
|
934
|
+
|
|
935
|
+
Without `--where`, `connect` asks when both answers are possible, and `--client <name>` on its own keeps
|
|
936
|
+
the answer that assistant had before: a key for Claude, the local line for Claude Code and Codex.
|
|
937
|
+
`npx clearotron disconnect` takes the same `--where`.
|
|
912
938
|
|
|
913
939
|
**A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
|
|
914
940
|
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
|
@@ -97,7 +97,7 @@ import { processTable } from "../shared/process-table.mjs"; // — /proc is no
|
|
|
97
97
|
import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
|
|
98
98
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // one ExecStart parser
|
|
99
99
|
import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
|
|
100
|
-
import { whereSavesGo } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed
|
|
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
102
|
import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
103
103
|
import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
|
|
@@ -1799,6 +1799,41 @@ export async function runCheck() {
|
|
|
1799
1799
|
}
|
|
1800
1800
|
}
|
|
1801
1801
|
|
|
1802
|
+
// — SAVED SEARCHES, JUDGED BY THE RULE THE PORTAL APPLIES WHEN IT STARTS. The portal switches them off,
|
|
1803
|
+
// and answers every saved-search route "not found", when the store is unset or sits outside the
|
|
1804
|
+
// repository its saves are committed in. Its boot log said so and nothing an operator runs did, so an
|
|
1805
|
+
// install whose Custom searches could never load passed this command. Same resolver, same containment
|
|
1806
|
+
// check, read from the services' environment for the reason the roster lines give; when that could not
|
|
1807
|
+
// be read, the lines above already said so and nothing is judged here. A store that is configured but
|
|
1808
|
+
// holds a file that cannot be read fails every company's saved searches, in the portal and the
|
|
1809
|
+
// connector alike, so that is read here too.
|
|
1810
|
+
if (!hosted || serviceKnown) {
|
|
1811
|
+
const recipesSet = hosted ? effectiveForService("CLEAROTRON_RECIPES_DIR") : effective("CLEAROTRON_RECIPES_DIR");
|
|
1812
|
+
const recipesDir = recipesSet?.v || null;
|
|
1813
|
+
if (!recipesDir) {
|
|
1814
|
+
info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
|
|
1815
|
+
+ "connector offer none. Name a directory inside a git repository to switch them on");
|
|
1816
|
+
} 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
|
+
const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
|
|
1821
|
+
const reach = storeInRepo(recipesDir, resolved.root);
|
|
1822
|
+
if (!reach.ok) {
|
|
1823
|
+
warn(`saved searches are OFF: ${storeOutsideRepoMessage({ storeVar: "CLEAROTRON_RECIPES_DIR", storeDir: reach.store, repoVar: "RECIPE_REPO_ROOT", repoRoot: reach.repo })} `
|
|
1824
|
+
+ `The repository came from ${resolved.from}. Until this is fixed the portal answers every saved-search request as not found`);
|
|
1825
|
+
} else {
|
|
1826
|
+
const { loadRecipes } = await import("../driver/search-policy.mjs");
|
|
1827
|
+
let unreadable = null;
|
|
1828
|
+
try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
|
|
1829
|
+
if (unreadable) {
|
|
1830
|
+
warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
|
|
1831
|
+
+ "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}`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1802
1837
|
// — WHICH DOCTRINE FILES THIS INSTALL OVERRIDES, AND WHETHER OURS HAVE MOVED UNDER THEM.
|
|
1803
1838
|
//
|
|
1804
1839
|
// The report itself shipped in and worked, reachable only as `npm run doctrine-report` — a name
|
|
@@ -1846,7 +1881,21 @@ export async function runCheck() {
|
|
|
1846
1881
|
}
|
|
1847
1882
|
|
|
1848
1883
|
say("\n Register provider");
|
|
1849
|
-
|
|
1884
|
+
// WHAT THE SERVICES WILL SEARCH, READ AS THE ORDER-TIME CHECK BELOW READS IT. This section asked the
|
|
1885
|
+
// shell and this command's .env, so in a fresh terminal on a hosted box it told a working install that
|
|
1886
|
+
// no register was selected, one screen above "nothing a search is refused for is missing from the units'
|
|
1887
|
+
// environment": one install, two answers, and the fix it named was already set. effectiveForService is
|
|
1888
|
+
// the reader that check asks, so the two lines cannot disagree; where it cannot read the units, this
|
|
1889
|
+
// says it could not look rather than reporting the register absent.
|
|
1890
|
+
// Only a HOSTED box has a second environment to read; with no units, the services are started from
|
|
1891
|
+
// this command's own file, and `effective` has always read and labelled exactly that.
|
|
1892
|
+
const serviceValue = hosted ? effectiveForService : effective;
|
|
1893
|
+
const prov = serviceKnown ? serviceValue("CLEAROTRON_DATABASE") : null;
|
|
1894
|
+
if (!serviceKnown) {
|
|
1895
|
+
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1896
|
+
+ "which register the services search, and whether its keys are set, is not judged here: a failure to look, not a finding");
|
|
1897
|
+
}
|
|
1898
|
+
else
|
|
1850
1899
|
// `blocking`, not `warn` and not `problem`. The exit status is a CONTRACT — an absence reports and
|
|
1851
1900
|
// exits 0, a misconfiguration exits 1, and onboard-wizard.test.mjs holds it — so this cannot become a
|
|
1852
1901
|
// `problem` however much it stops the reader: an install that has not chosen a register yet is
|
|
@@ -1865,7 +1914,7 @@ export async function runCheck() {
|
|
|
1865
1914
|
else {
|
|
1866
1915
|
ok(`${spec.id} — ${spec.label} (${prov.from})`);
|
|
1867
1916
|
for (const k of spec.credentials) {
|
|
1868
|
-
const c =
|
|
1917
|
+
const c = serviceValue(k);
|
|
1869
1918
|
// issue 1871 — SET, not WORKING, and the line now says which. An operator reads a tick as "this
|
|
1870
1919
|
// works"; this one is equally true of a valid key, an expired key, a key scoped to the wrong
|
|
1871
1920
|
// account and forty characters of nonsense. --probe-providers is what settles it.
|
|
@@ -1876,7 +1925,7 @@ export async function runCheck() {
|
|
|
1876
1925
|
// and never as a problem, but never silently either: the reader has to know which offices this
|
|
1877
1926
|
// box will not reach before they read a report that says nothing was found there.
|
|
1878
1927
|
for (const k of spec.optionalCredentials ?? []) {
|
|
1879
|
-
const c =
|
|
1928
|
+
const c = serviceValue(k);
|
|
1880
1929
|
if (c) ok(`${k} present (${c.from}) — presence only; add --probe-providers to prove it retrieves`);
|
|
1881
1930
|
else info(`${k} is NOT set — ${spec.id} will run without it and DISCLOSE the offices it cannot reach as deferred coverage. Set it to search them.`);
|
|
1882
1931
|
}
|
|
@@ -1884,8 +1933,10 @@ export async function runCheck() {
|
|
|
1884
1933
|
}
|
|
1885
1934
|
|
|
1886
1935
|
say("\n Research provider");
|
|
1887
|
-
|
|
1888
|
-
|
|
1936
|
+
// The same reader as the register, for the same reason: this is a key the services use.
|
|
1937
|
+
const px = serviceKnown ? serviceValue("PERPLEXITY_API_KEY") : null;
|
|
1938
|
+
if (!serviceKnown) info("the units' environment could not be read, so whether the services hold PERPLEXITY_API_KEY is not judged here — a failure to look, not a finding");
|
|
1939
|
+
else if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
|
|
1889
1940
|
else info("PERPLEXITY_API_KEY is not set — the three clearance searches carry the common-law grid and cannot switch it off, so a clearance stops before it starts, and names the missing key; a Knockout search still runs and discloses the half it skipped");
|
|
1890
1941
|
|
|
1891
1942
|
// ── — THE LANES A PRODUCT DECLARES IT NEEDS, BEFORE A REPORT NAMES THEM ──
|
|
@@ -3644,6 +3695,14 @@ try {
|
|
|
3644
3695
|
candidate["CLEAROTRON_CUSTOMERS_DIR"] = join(cfg, "profiles");
|
|
3645
3696
|
candidate.PROFILE_REPO_ROOT = cfg; // no alias row — this name is current
|
|
3646
3697
|
for (const k of ["CLEAROTRON_CUSTOMERS_DIR", "PROFILE_REPO_ROOT"]) ok(`${k}=${candidate[k]}`);
|
|
3698
|
+
// A REPOSITORY ALREADY THERE IS ASKED NOW, while the operator is still here, whether it can record a
|
|
3699
|
+
// save. `clearotron start` gives a store it creates an identity of its own; one made by hand has none
|
|
3700
|
+
// unless somebody set it, and on a machine with no global identity the first company created in the
|
|
3701
|
+
// portal is refused. Said here, with the command, rather than discovered on that first company.
|
|
3702
|
+
if (existsSync(join(cfgAbs, ".git"))) {
|
|
3703
|
+
const cannot = storeCommitRefusal(cfgAbs);
|
|
3704
|
+
if (cannot) warn(`${cannot.message}. Until then, creating a company in the portal is refused.`);
|
|
3705
|
+
}
|
|
3647
3706
|
|
|
3648
3707
|
// CLEAROTRON_INSTRUCTIONS_DIR IS DELIBERATELY NOT WRITTEN (found in review).
|
|
3649
3708
|
//
|
package/bin/start.mjs
CHANGED
|
@@ -109,7 +109,7 @@ 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 } from "../shared/store-in-repo.mjs"; //
|
|
112
|
+
import { storeInRepo, storeOutsideRepoMessage, storeCommitRefusal } from "../shared/store-in-repo.mjs"; //
|
|
113
113
|
import { stdioConnectOffer } from "../shared/stdio-connect.mjs";
|
|
114
114
|
import { mergeEnvFile } from "../shared/env-file-merge.mjs";
|
|
115
115
|
import { mcpOriginFor } from "../shared/lane-address.mjs"; // — one author for the origin
|
|
@@ -1252,6 +1252,12 @@ if (isMain) {
|
|
|
1252
1252
|
// search is what will fail.
|
|
1253
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.`);
|
|
1254
1254
|
}
|
|
1255
|
+
} else {
|
|
1256
|
+
// AN ADOPTED STORE IS ASKED WHETHER IT CAN RECORD A SAVE, HERE, not at the first save. A repository
|
|
1257
|
+
// made by hand has no identity unless somebody gave it one, and on a machine with no global identity
|
|
1258
|
+
// the first company created in the portal is then refused. The one created above sets its own.
|
|
1259
|
+
const cannot = storeCommitRefusal(paths.configStore);
|
|
1260
|
+
if (cannot) err(` WARNING: ${cannot.message}. Until then, creating a company or saving a search is refused.`);
|
|
1255
1261
|
}
|
|
1256
1262
|
|
|
1257
1263
|
// ── THE DEMO'S OWN STORE, WITH ITS COMPANY IN IT ────────────────────────────────────────────────
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.4
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
New: Claude Code and Codex can connect to an installation running elsewhere, and the ChatGPT desktop app to one on the same machine.
|
|
10
|
+
|
|
11
|
+
Fixed: Codex was told to paste a settings block into a terminal. Its steps now name the file the block goes in.
|
|
12
|
+
|
|
13
|
+
For operators: `clearotron connect` and `clearotron disconnect` take `--where here` or `--where elsewhere`. Assistant names used before, such as `cowork`, still work.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
## 0.3.0-beta.3
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
- d1ef225: Fixed: A stage stopped at its time limit now records the output it actually produced. It used to record a small fraction, so a stage that was working read as one that had stalled.
|
|
27
|
+
|
|
28
|
+
The token totals `clearotron tokens` reports for runs with a stopped stage now include that output.
|
|
29
|
+
- 0ff42d1: Fixed: `clearotron doctor` now says when saved searches are switched off and why, and when a saved search file cannot be read.
|
|
30
|
+
|
|
31
|
+
An assistant asking for saved searches is told when they could not be read, instead of being told there are none.
|
|
32
|
+
- a782aad: Fixed: `clearotron doctor` reports the register and the research key the background services will use, read from the file they read. Run from a new terminal, it used to say no register was selected on an install whose searches were running.
|
|
33
|
+
- 0ff42d1: Fixed: Global config appears in the account menu only for people who can open it. Someone managing one organisation used to be offered it, and the page then said it was not available.
|
|
34
|
+
|
|
35
|
+
Clearances and People no longer ask for installation-wide data that a manager of one organisation cannot see.
|
|
36
|
+
- a782aad: Fixed: A clearance or knockout searched through Compumark now links each register record to the trade mark office's own page for it. Those records used to show an internal reference nobody could open.
|
|
37
|
+
|
|
38
|
+
The offices linked are the United States, the European Union, the United Kingdom, Canada, Australia, Switzerland, France, Norway, Sweden and WIPO. A record from any other office is cited by its number, and the report says why once, under the findings.
|
|
39
|
+
- a782aad: Fixed: Saved searches work on a fresh install and in the demo, for every company, Generic included. They used to fail to load for every company, with a message saying to try again shortly.
|
|
40
|
+
|
|
3
41
|
## 0.3.0-beta.2
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
package/driver/driver.config.mjs
CHANGED
|
@@ -1110,6 +1110,11 @@ export const PROVIDERS = {
|
|
|
1110
1110
|
office: row?.office ?? null,
|
|
1111
1111
|
application_date: row?.application_date ?? null,
|
|
1112
1112
|
registration_date: row?.registration_date ?? null,
|
|
1113
|
+
// The office's own numbers: publish addresses the filing at the office's page from these, the
|
|
1114
|
+
// way it does for this register's clearance findings. Compumark states no filing route or IR
|
|
1115
|
+
// number of its own, so a Madrid designation is addressed as the national filing it is listed as.
|
|
1116
|
+
application_number: row?.application_number ?? null,
|
|
1117
|
+
registration_number: row?.registration_number ?? null,
|
|
1113
1118
|
// hasPublicRecordUrl:false on this provider — there is no per-record page to link, and a
|
|
1114
1119
|
// fabricated one would be worse than none.
|
|
1115
1120
|
record_url: null,
|
|
@@ -467,19 +467,32 @@ export const anthropicAgentEngine = {
|
|
|
467
467
|
let rateLimitEvent = null; // a 429 session-cap rejection rides a `rate_limit_event` (status:"rejected" + resetsAt)
|
|
468
468
|
// Streamed-usage accumulator: what the turn PROVABLY moved, observed from the stream itself, so a
|
|
469
469
|
// killed turn is never journalled as usage:null when millions of tokens moved (the "137 + usage:null
|
|
470
|
-
// ⇒ mislabelled transient/lane-wedge" class).
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
// ⇒ mislabelled transient/lane-wedge" class).
|
|
471
|
+
//
|
|
472
|
+
// KEYED BY MESSAGE, AND EACH FIELD IS THE LARGEST READING SEEN FOR THAT MESSAGE. One API call's usage
|
|
473
|
+
// reaches the stream several times: on `message_start`, on every `assistant` event (one per content
|
|
474
|
+
// block), and on its `message_delta`, whose output count is the call's own running total. Summing the
|
|
475
|
+
// assistant events counted a call's input and cache once per block and its output at whatever it
|
|
476
|
+
// was when the block went out, and dropping the delta lost the rest: a turn killed after 75 calls
|
|
477
|
+
// journalled 810 output tokens. Which event carries the final count need not be known here, and the
|
|
478
|
+
// order is inferred rather than recorded, so every reading is kept and the largest wins. A delta names
|
|
479
|
+
// no message, so it belongs to the one its last `message_start` opened; an assistant event with no id
|
|
480
|
+
// and no open message is a call of its own, as it always was.
|
|
481
|
+
const perMessage = new Map();
|
|
482
|
+
let openMessage = null, anonMessages = 0;
|
|
483
|
+
const foldUsage = (key, u) => {
|
|
484
|
+
if (!u) return;
|
|
485
|
+
const was = perMessage.get(key);
|
|
486
|
+
perMessage.set(key, was
|
|
487
|
+
? { input: Math.max(was.input, u.input), output: Math.max(was.output, u.output),
|
|
488
|
+
cacheRead: Math.max(was.cacheRead, u.cacheRead), cacheWrite: Math.max(was.cacheWrite, u.cacheWrite) }
|
|
489
|
+
: { input: u.input, output: u.output, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite });
|
|
490
|
+
};
|
|
475
491
|
const streamedUsage = () => {
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
input
|
|
479
|
-
|
|
480
|
-
cacheRead: streamTotals.cacheRead + p.cacheRead,
|
|
481
|
-
cacheWrite: streamTotals.cacheWrite + p.cacheWrite,
|
|
482
|
-
};
|
|
492
|
+
const u = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
493
|
+
for (const m of perMessage.values()) {
|
|
494
|
+
u.input += m.input; u.output += m.output; u.cacheRead += m.cacheRead; u.cacheWrite += m.cacheWrite;
|
|
495
|
+
}
|
|
483
496
|
u.total = u.input + u.output + u.cacheRead + u.cacheWrite;
|
|
484
497
|
return u.total > 0 ? u : null; // zero observed movement stays null — a never-admitted turn must keep classifying as a lane wedge
|
|
485
498
|
};
|
|
@@ -703,9 +716,9 @@ export const anthropicAgentEngine = {
|
|
|
703
716
|
periodStart = now; periodChunk = chunkSeq;
|
|
704
717
|
}
|
|
705
718
|
syncOpenAsk();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
719
|
+
// This block's usage, folded into its own call: the largest reading wins, so a block's early count
|
|
720
|
+
// never replaces the delta's running total, and a second block of the same call adds nothing twice.
|
|
721
|
+
foldUsage(ev.message?.id ?? openMessage ?? `__assistant_${++anonMessages}`, mapUsage(ev.message?.usage));
|
|
709
722
|
progress();
|
|
710
723
|
}
|
|
711
724
|
else if (ev.type === "user") {
|
|
@@ -737,8 +750,12 @@ export const anthropicAgentEngine = {
|
|
|
737
750
|
}
|
|
738
751
|
else if (ev.type === "stream_event") {
|
|
739
752
|
const t = ev.event?.type;
|
|
740
|
-
if (t === "message_start") {
|
|
741
|
-
|
|
753
|
+
if (t === "message_start") {
|
|
754
|
+
openMessage = ev.event.message?.id ?? `__start_${++anonMessages}`;
|
|
755
|
+
foldUsage(openMessage, mapUsage(ev.event.message?.usage));
|
|
756
|
+
progress();
|
|
757
|
+
}
|
|
758
|
+
else if (t === "message_delta") { foldUsage(openMessage ?? "__delta_without_start", mapUsage(ev.event?.usage)); progress(); }
|
|
742
759
|
else if (t === "content_block_delta" || t === "content_block_start") progress();
|
|
743
760
|
// THINKING GAUGE (partials): the earliest tells. Any one is sufficient; belt-and-braces so a
|
|
744
761
|
// display-mode or CLI-version change cannot silently blind the gauge.
|