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
|
@@ -82,6 +82,7 @@ export const NAMES_IN_FORCE = Object.freeze([
|
|
|
82
82
|
"CLEAROTRON_JX_SERP_DEADLINE_MS",
|
|
83
83
|
"CLEAROTRON_JX_SERP_GRID",
|
|
84
84
|
"CLEAROTRON_JX_SUBCLASS_DB",
|
|
85
|
+
"CLEAROTRON_KEY",
|
|
85
86
|
"CLEAROTRON_KIFF_PROBE",
|
|
86
87
|
"CLEAROTRON_KILL_ESCALATE_MS",
|
|
87
88
|
"CLEAROTRON_KNOCKOUT_COUNT_FIXTURES",
|
package/shared/stdio-connect.mjs
CHANGED
|
@@ -144,6 +144,69 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
144
144
|
},
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
// ── THE SAME SERVER, REACHED OVER THE WEB ──────────────────────────────────────────────────────────
|
|
148
|
+
//
|
|
149
|
+
// An install running elsewhere is reached at its public address with a key, and three hosts take that in
|
|
150
|
+
// three spellings: Claude Code registers it with `--transport http` and a header, Codex reads a `url` and
|
|
151
|
+
// the NAME of a variable holding the key, and everything else takes the two lines as they are. They are
|
|
152
|
+
// composed here for the reason the stdio shapes are: a surface that spelled `claude mcp add` for itself
|
|
153
|
+
// would be a second author of it, and the browser does not know the address anyway.
|
|
154
|
+
//
|
|
155
|
+
// A KEY IS NEVER IN THESE STRINGS. It is minted when a person presses, for that person, and the page
|
|
156
|
+
// holds it only for the length of the press. So a shape that needs one carries KEY_SLOT where the key
|
|
157
|
+
// goes, and the only thing a surface does with the string is put the key it was just handed there.
|
|
158
|
+
|
|
159
|
+
/** Where a minted key goes in a composed string. Nothing else in a copy looks like it. */
|
|
160
|
+
export const KEY_SLOT = "{key}";
|
|
161
|
+
|
|
162
|
+
/** The variable Codex reads the key from, so it never sits in the settings file itself. */
|
|
163
|
+
export const KEY_ENV_VAR = "CLEAROTRON_KEY";
|
|
164
|
+
|
|
165
|
+
/** How each host takes the public address. A step of CONNECT_CLIENTS names one of these by key. */
|
|
166
|
+
export const REMOTE_SHAPES = Object.freeze({
|
|
167
|
+
"address-and-key": {
|
|
168
|
+
label: "Copy address and key",
|
|
169
|
+
render: ({ address }) => `${address}\n${KEY_SLOT}`,
|
|
170
|
+
},
|
|
171
|
+
// A host that signs its reader in through the browser needs the address and nothing else.
|
|
172
|
+
"address": {
|
|
173
|
+
label: null,
|
|
174
|
+
render: ({ address }) => address,
|
|
175
|
+
},
|
|
176
|
+
"claude-cli-http": {
|
|
177
|
+
label: "Copy command",
|
|
178
|
+
render: ({ address }) =>
|
|
179
|
+
`claude mcp add --transport http ${STDIO_SERVER_NAME} ${address} --header "Authorization: Bearer ${KEY_SLOT}"`,
|
|
180
|
+
},
|
|
181
|
+
"codex-toml-http": {
|
|
182
|
+
label: null,
|
|
183
|
+
render: ({ address }) => [
|
|
184
|
+
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
185
|
+
`url = "${address}"`,
|
|
186
|
+
`bearer_token_env_var = "${KEY_ENV_VAR}"`,
|
|
187
|
+
].join("\n"),
|
|
188
|
+
},
|
|
189
|
+
"codex-key-line": {
|
|
190
|
+
label: "Copy key line",
|
|
191
|
+
render: () => `export ${KEY_ENV_VAR}=${KEY_SLOT}`,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* One remote copy, resolved against this install's public address. PURE.
|
|
197
|
+
*
|
|
198
|
+
* `secret` is read off the composed string rather than declared beside it, so a shape cannot say it
|
|
199
|
+
* needs no key while carrying the slot, or the reverse. Null for an unknown shape or for an install with
|
|
200
|
+
* no public address: there is nothing true to hand over in either case.
|
|
201
|
+
*/
|
|
202
|
+
export function remoteConnectFor(shape, { address = null } = {}) {
|
|
203
|
+
const spec = Object.hasOwn(REMOTE_SHAPES, String(shape ?? "")) ? REMOTE_SHAPES[shape] : null;
|
|
204
|
+
if (!spec || !address) return null;
|
|
205
|
+
const text = spec.render({ address });
|
|
206
|
+
const secret = text.includes(KEY_SLOT);
|
|
207
|
+
return { shape, secret, label: secret ? spec.label : null, text, name: STDIO_SERVER_NAME };
|
|
208
|
+
}
|
|
209
|
+
|
|
147
210
|
/**
|
|
148
211
|
* The stdio route for ONE host, in that host's own shape. PURE.
|
|
149
212
|
*
|
package/shared/store-in-repo.mjs
CHANGED
|
@@ -142,7 +142,7 @@ export function storeOutsideRepoMessage({ storeVar, storeDir, repoVar, repoRoot
|
|
|
142
142
|
// knows both. So the appender returns a path only when that path is committable, and the core commits
|
|
143
143
|
// what it is handed.
|
|
144
144
|
|
|
145
|
-
import { appendFileSync } from "node:fs";
|
|
145
|
+
import { appendFileSync, rmSync } from "node:fs";
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* A repo root is a PATH, and every helper below interpolates it into a git invocation. Anything else
|
|
@@ -289,6 +289,39 @@ import { execFileSync } from "node:child_process";
|
|
|
289
289
|
export const isTransientGitFault = (detail) =>
|
|
290
290
|
/index\.lock|another git process seems to be running|Unable to create/i.test(String(detail ?? ""));
|
|
291
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Why a commit into `repoRoot` would be refused, asked BEFORE anything is written; null when it would not
|
|
294
|
+
* be. Read-only: `rev-parse` and `git var` change nothing. `{ code, detail, message }`, where `message` names
|
|
295
|
+
* the store and the command that clears it, in terms an operator acts on rather than git's own words:
|
|
296
|
+
*
|
|
297
|
+
* not-a-repository — the store is not inside a git repository this process can use;
|
|
298
|
+
* no-identity — git has no committer identity here. That is the default state of any machine where
|
|
299
|
+
* nobody ran `git config user.email`, a fresh Windows install among them. A save names
|
|
300
|
+
* its author; the COMMITTER is the machine's, and git refuses a commit it cannot name
|
|
301
|
+
* one for. `git -c user.email=…` on a one-off seed commit does not help: `-c` configures
|
|
302
|
+
* that invocation, not the repository, so every save after it fails the same way.
|
|
303
|
+
*/
|
|
304
|
+
export function storeCommitRefusal(repoRoot, { env = process.env } = {}) {
|
|
305
|
+
requireRepoRootPath(repoRoot, "storeCommitRefusal");
|
|
306
|
+
const ask = (...args) => execFileSync("git", ["-C", repoRoot, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env });
|
|
307
|
+
const said = (e) => String(e?.stderr || e?.message || e).trim().split("\n").filter(Boolean);
|
|
308
|
+
try { ask("rev-parse", "--git-dir"); }
|
|
309
|
+
catch (e) {
|
|
310
|
+
const detail = said(e)[0]?.slice(0, 200) ?? "";
|
|
311
|
+
const fix = /dubious ownership/i.test(detail)
|
|
312
|
+
? `run \`git config --global --add safe.directory ${repoRoot}\` as the account the service runs as`
|
|
313
|
+
: `run \`git init\` in ${repoRoot}, or point PROFILE_REPO_ROOT at the repository that holds the store`;
|
|
314
|
+
return { code: "not-a-repository", detail, message: `the store at ${repoRoot} is not a git repository this install can record into (${detail}) — ${fix}` };
|
|
315
|
+
}
|
|
316
|
+
try { ask("var", "GIT_COMMITTER_IDENT"); }
|
|
317
|
+
catch (e) {
|
|
318
|
+
return { code: "no-identity", detail: said(e).pop()?.slice(0, 200) ?? "",
|
|
319
|
+
message: `the store at ${repoRoot} has no git identity, so nothing saved to it can be recorded — run `
|
|
320
|
+
+ `\`git -C ${repoRoot} config user.email "you@example.com"\` and \`git -C ${repoRoot} config user.name "Your Name"\`` };
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
292
325
|
export function makeStoreCommit({ repoRoot, log = () => {}, what = "store", retries = 3, waitMs = 50 }) {
|
|
293
326
|
requireRepoRootPath(repoRoot, "makeStoreCommit");
|
|
294
327
|
const git = (...args) => execFileSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" }).toString().trim();
|
|
@@ -309,7 +342,7 @@ export function makeStoreCommit({ repoRoot, log = () => {}, what = "store", retr
|
|
|
309
342
|
const napping = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* best effort */ } };
|
|
310
343
|
const detailOf = (e) => String(e?.stderr ?? e?.message ?? e);
|
|
311
344
|
|
|
312
|
-
|
|
345
|
+
const commit = ({ files, message, author }) => {
|
|
313
346
|
// Asked BEFORE anything composes a diff, so the caller gets the refusal rather than a fallback-mode
|
|
314
347
|
// parse error. Throwing here also means the write is never followed by a silent half-save: the
|
|
315
348
|
// caller's own catch is what turns this into a failed save.
|
|
@@ -350,6 +383,21 @@ export function makeStoreCommit({ repoRoot, log = () => {}, what = "store", retr
|
|
|
350
383
|
}
|
|
351
384
|
}
|
|
352
385
|
};
|
|
386
|
+
// ── TWO MORE ANSWERS, FOR A CREATE, WHICH IS REFUSED RATHER THAN LEFT HALF-MADE ──────────────────────
|
|
387
|
+
//
|
|
388
|
+
// A save of something that already exists stays as above: live, staged, completed by the next save.
|
|
389
|
+
// A CREATE is different, because "before" exists: the paths it wrote were absent. So a create asks
|
|
390
|
+
// `refusal()` first and writes nothing when the store cannot record it, and when the commit fails anyway
|
|
391
|
+
// (a hook, a full disk), `withdraw(files)` returns those paths to absent: out of the index, so the next
|
|
392
|
+
// save's completion step cannot commit a company that does not exist, and off the disk. Nothing else is
|
|
393
|
+
// touched, and the audit row the create staged stays staged, because it records what happened.
|
|
394
|
+
commit.refusal = () => storeCommitRefusal(repoRoot);
|
|
395
|
+
commit.withdraw = (files) => {
|
|
396
|
+
const paths = (files ?? []).map((f) => resolve(repoRoot, f));
|
|
397
|
+
if (paths.length) git("rm", "--cached", "--quiet", "--ignore-unmatch", "--", ...paths);
|
|
398
|
+
for (const p of paths) rmSync(p, { force: true });
|
|
399
|
+
};
|
|
400
|
+
return commit;
|
|
353
401
|
}
|
|
354
402
|
|
|
355
403
|
export function resolveStoreRepoRoot({ names, fallback = null, env = process.env } = {}) {
|