clearotron 0.3.0-beta.3 → 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 CHANGED
@@ -922,12 +922,19 @@ their own integration work.
922
922
  **Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
923
923
  2026-09-03, on the vendor's own documented behaviour:
924
924
 
925
- 1. **An assistant that can launch a local process** — Claude Code, Claude Desktop's local config,
926
- Codex CLI, an agent that runs commands. It spawns the server over stdio. No address, no key, no
927
- network, no ingress. `npx clearotron start` prints the one line to paste, and `npx clearotron
928
- connect` hands it over per assistant.
929
- 2. **Everything else, wherever it appears to run** — Cowork, ChatGPT, claude.ai, Perplexity, the mobile
930
- apps. These need **a publicly reachable HTTPS address**, plus a key. Always.
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`.
931
938
 
932
939
  **A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
933
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" || (offer.route === "either" && offer.command)) {
559
- // A COMMAND AND A CONFIG BLOCK ARE NOT THE SAME INSTRUCTION, and saying "run this" over a TOML
560
- // block is how a reader pastes four lines into a shell. The shape says which it is.
561
- const s = offer.stdio;
562
- say(s?.kind === "config"
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
- if (key) say(` Key: ${key}`); // printed once, stored nowhere
625
- // ── AND WHERE TO PUT THEM ( F35) ───────────────────────────────────────
626
- //
627
- // The owner was left with two strings and no destination: *"I don't know how to connect it in Claude
628
- // Cowork with those details."* The steps were DEFINED IN THE PRODUCT the whole time — `withSteps`
629
- // computes them for every offer, interpolating this install's own address and operator — and this
630
- // verb simply never printed them. Nothing new is authored here; a second set of instructions written
631
- // at the CLI would drift from the page's, which is the defect connect-clients-are-data exists against.
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) => say(` ${n + 1}. ${step}`));
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
- return await render(whatItNeeds(chosen, have), have, { dryRun, running, allowMove: argv.includes("--allow-checkout-move") });
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
@@ -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
- // The row's own property decides the side. `accepts: "stdio"` never touched this install; "either"
141
- // only opens the door when its stdio route was missing, and on THIS box (the one disconnect runs on)
142
- // the stdio route resolves, so its connect handed over a command too.
143
- if (chosen.accepts === "stdio" || chosen.accepts === "either") {
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
- if (chosen.accepts === "either") {
148
- say("");
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/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "5c5342ab541f8240732467dfc8bbf3c065ef13f6",
3
- "version": "0.3.0-beta.3"
2
+ "commit": "0fb023f5822c9458f9c63bf993876c3a3a8053e3",
3
+ "version": "0.3.0-beta.4"
4
4
  }
@@ -1,5 +1,21 @@
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
+
3
19
  ## 0.3.0-beta.3
4
20
 
5
21
  ### Patch Changes
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.0-beta.3",
5
+ "version": "0.3.0-beta.4",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -1366,8 +1366,8 @@
1366
1366
  "todos": 0
1367
1367
  },
1368
1368
  "connect-clients-are-data.test.mjs": {
1369
- "tests": 12,
1370
- "asserts": 46,
1369
+ "tests": 15,
1370
+ "asserts": 75,
1371
1371
  "skips": 0,
1372
1372
  "todos": 0
1373
1373
  },
@@ -1397,7 +1397,7 @@
1397
1397
  },
1398
1398
  "connector-annotations-and-pack.test.mjs": {
1399
1399
  "tests": 7,
1400
- "asserts": 11,
1400
+ "asserts": 12,
1401
1401
  "skips": 0,
1402
1402
  "todos": 0
1403
1403
  },
@@ -5774,8 +5774,8 @@
5774
5774
  "todos": 0
5775
5775
  },
5776
5776
  "newCompanyReachableFromProfile.test.ts": {
5777
- "tests": 3,
5778
- "asserts": 7,
5777
+ "tests": 5,
5778
+ "asserts": 13,
5779
5779
  "skips": 0,
5780
5780
  "todos": 0
5781
5781
  },
@@ -5913,7 +5913,7 @@
5913
5913
  },
5914
5914
  "useYourAI.test.ts": {
5915
5915
  "tests": 13,
5916
- "asserts": 46,
5916
+ "asserts": 41,
5917
5917
  "skips": 0,
5918
5918
  "todos": 0
5919
5919
  },
@@ -1,5 +1,9 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.0-beta.4
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.0-beta.3
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.0-beta.3",
3
+ "version": "0.3.0-beta.4",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.0-beta.3",
4
+ "version": "0.3.0-beta.4",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",