clearotron 0.3.2-beta.13 → 0.3.2-beta.15
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/bin/clearotron.mjs +7 -1
- package/bin/example.mjs +49 -22
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +22 -0
- package/driver/contract-vocabulary.mjs +5 -5
- package/driver/engine/mcp/codex-config.mjs +3 -11
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +3 -1
- package/driver/pipeline.mjs +30 -7
- package/driver/publish/index.mjs +10 -1
- package/driver/publish/render-knockout.mjs +31 -6
- package/driver/publish/render.mjs +54 -2
- package/driver/publish/templates/report.css +16 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +229 -55
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-variants/SKILL.md +10 -4
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +45 -3
- package/driver/variant-manifest-model.mjs +39 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/audit-view.mjs +5 -3
- package/mcp-server/lib/brief.mjs +10 -4
- package/mcp-server/lib/runs.mjs +35 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +6 -3
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-7Lq-dXDV.css → index-5CCwiJG7.css} +10 -0
- package/portal-ui/dist/assets/{index-w8GFZftk.js → index-DMthc7PQ.js} +8 -1
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/execute-plan.mjs +11 -1
- package/providers/_shared/term-shape.mjs +76 -0
- package/providers/clarivate/src/capabilities.js +36 -0
- package/providers/clarivate/src/core.js +119 -0
- package/providers/corsearch/src/capabilities.js +24 -0
- package/providers/corsearch/src/core.js +7 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +29 -0
- package/providers/signa/src/core.js +17 -0
- package/shared/demo-start-args.mjs +10 -0
- package/shared/stdio-connect.mjs +41 -10
- package/shared/toml-string.mjs +25 -0
|
@@ -125,6 +125,35 @@ export const CAPABILITIES = Object.freeze({
|
|
|
125
125
|
maxOrWidth: 1,
|
|
126
126
|
// filters.nice_classes[] is a top-level OR filter — one call, no fan-out.
|
|
127
127
|
classFilter: "native",
|
|
128
|
+
// ── THE GOODS-AND-SERVICES TEXT FILTER ──────────────────────────────────────────────────────────
|
|
129
|
+
// `filters.goods_services_text` — a free-text filter over the goods and services descriptions,
|
|
130
|
+
// documented by the vendor and composing with `nice_classes` and the query in the same request. It
|
|
131
|
+
// is what lets a crowded contains sweep be narrowed to what a filing actually covers rather than
|
|
132
|
+
// the bucket it was filed in.
|
|
133
|
+
//
|
|
134
|
+
// The key is accepted, it NARROWS, and it takes FREE TEXT — not the whole-word-plus-OR shape
|
|
135
|
+
// Clarivate's equivalent field demands. So the same word list goes to both providers and each
|
|
136
|
+
// connector writes it the way its own field takes it.
|
|
137
|
+
//
|
|
138
|
+
// This API rejects an unknown filter key outright (`HTTP 400 Unrecognized key: …` — the defect that
|
|
139
|
+
// hid a bogus `status` key for two months), which is why one call could settle acceptance.
|
|
140
|
+
goodsTextSearch: true,
|
|
141
|
+
// A multi-word goods term: a bare space here is an implicit AND and quoting is IGNORED, so a phrase
|
|
142
|
+
// cannot be matched AS a phrase — the words simply intersect. That intersection is the nearest
|
|
143
|
+
// honest form of what a phrase asks for, and it is what this connector sends. Nothing pretends it
|
|
144
|
+
// is a phrase.
|
|
145
|
+
// A multi-word term here INTERSECTS its words — it is not a phrase match, and the name says so.
|
|
146
|
+
// See clarivate/capabilities.js for the vocabulary.
|
|
147
|
+
goodsTextMultiWord: "word-intersection",
|
|
148
|
+
// A LIST of alternatives cannot be expressed here AT ALL. Every form was tried and none answers the
|
|
149
|
+
// union: `OR`, lower-case `or` and a quoted OR all return a population SMALLER than either word
|
|
150
|
+
// alone (the OR is matched as a literal third word), a pipe and a comma return the two words
|
|
151
|
+
// INTERSECTED, and an array is refused outright. A long list would therefore intersect to nothing
|
|
152
|
+
// while still answering 200 — the quiet false clean this contract exists to refuse.
|
|
153
|
+
//
|
|
154
|
+
// So the compiler does not build a multi-term goods entry for this provider, and it must never fall
|
|
155
|
+
// back to a space-joined string. No coverage is lost: the broad class-wide sweep still runs.
|
|
156
|
+
goodsTextListOr: false,
|
|
128
157
|
// Search rows already carry status / nice_classes / owner_name → screening is inline, zero extra calls.
|
|
129
158
|
screenSource: "search-row",
|
|
130
159
|
// No hard result ceiling, and no total to compare one against.
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { readFileSync, existsSync } from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { goodsTermsList } from "../../_shared/term-shape.mjs"; // the shared reader for the goods words
|
|
26
27
|
|
|
27
28
|
import { makeLedger } from "../../_shared/ledger.mjs";
|
|
28
29
|
import { nonAnswerBodyError, parseJsonBody, unparsedBodyError } from "../../_shared/http-body.mjs";
|
|
@@ -132,6 +133,22 @@ function buildFilters(p) {
|
|
|
132
133
|
// with a text query in ONE request, and the intersection is a real narrowing rather than one clause
|
|
133
134
|
// being silently ignored — the three populations (term alone, owner alone, both) differ from each other.
|
|
134
135
|
if (typeof p.owner === "string" && p.owner.trim()) f.owner_name = p.owner.trim();
|
|
136
|
+
// The goods-and-services narrowing. ONE term only: this filter is a single string whose words are
|
|
137
|
+
// ANDed, and no OR form exists (OR is matched as a literal third word; a pipe and a comma intersect;
|
|
138
|
+
// an array is refused). So a list of alternatives cannot be expressed, and joining one with spaces
|
|
139
|
+
// would ask for filings covering EVERY word — a population that shrinks as the list grows while
|
|
140
|
+
// still answering 200. The compiler does not build a multi-term entry for this provider; this
|
|
141
|
+
// refusal is the backstop for any path that does not go through it.
|
|
142
|
+
const goods = goodsTermsList(p);
|
|
143
|
+
if (goods.length > 1) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`goods_text carries ${goods.length} terms and this register has no OR on its goods filter: the `
|
|
146
|
+
+ `words would be intersected, not offered as alternatives, and the answer would narrow as the `
|
|
147
|
+
+ `list grew with no error. Send one term.`);
|
|
148
|
+
}
|
|
149
|
+
// A single term's own words ARE intersected, and that is the nearest honest form of the phrase the
|
|
150
|
+
// caller asked for — it is sent as written, and nothing here calls it a phrase match.
|
|
151
|
+
if (goods.length) f.goods_services_text = goods[0];
|
|
135
152
|
return f;
|
|
136
153
|
}
|
|
137
154
|
|
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
* `--demo-own-base` says it is the demo's default, which is what lets the supervisor remove the folder
|
|
13
13
|
* when the window closes. `--keep` is the reader's, and it travels as given.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The Node flag the demo runs under, so Node's one-time warning that its built-in SQLite is experimental
|
|
17
|
+
* does not land on the demo's first screen. Named once: the launcher puts it on the demo's own process,
|
|
18
|
+
* and `bin/example.mjs` hands it to the services it starts through NODE_OPTIONS.
|
|
19
|
+
*/
|
|
20
|
+
export const EXPERIMENTAL_WARNING_OFF = "--disable-warning=ExperimentalWarning";
|
|
21
|
+
|
|
22
|
+
/** The same flag added to whatever NODE_OPTIONS the reader already has, which is kept. PURE. */
|
|
23
|
+
export const withWarningOff = (nodeOptions) => [nodeOptions, EXPERIMENTAL_WARNING_OFF].filter(Boolean).join(" ");
|
|
24
|
+
|
|
15
25
|
export function demoStartArgs({ demoBase, readerBase = false, keep = false, port = null, noOpen = false } = {}) {
|
|
16
26
|
const args = ["--demo", "--base", demoBase];
|
|
17
27
|
if (!readerBase) args.push("--demo-own-base");
|
package/shared/stdio-connect.mjs
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { stableInstallRoot } from "./permanent-install.mjs";
|
|
35
|
+
import { tomlString } from "./toml-string.mjs";
|
|
35
36
|
|
|
36
37
|
/** The install root — the directory holding `mcp-server/`, resolved from this module rather than cwd. */
|
|
37
38
|
export const INSTALL_ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
@@ -103,15 +104,38 @@ export function stdioConnectOffer(opts = {}) {
|
|
|
103
104
|
function envOf({ workDir = null, reportsDir = null } = {}) {
|
|
104
105
|
return { ...(workDir ? { CLEAROTRON_WORK_DIR: workDir } : {}), ...(reportsDir ? { CLEAROTRON_REPORTS_DIR: reportsDir } : {}) };
|
|
105
106
|
}
|
|
106
|
-
const envFlags = (o) => Object.entries(envOf(o)).map(([k, v]) => ` -e ${k}=${v}`).join("");
|
|
107
107
|
const envBlock = (o) => (Object.keys(envOf(o)).length ? { env: envOf(o) } : {});
|
|
108
108
|
|
|
109
|
+
// ── ONE WORD PER ARGUMENT, IN THE SHELL THAT READS THE LINE ───────────────────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// The command was the arguments joined with spaces, so a path with a space in it became two arguments:
|
|
112
|
+
// `/tmp/Example User/node/bin/node` reached the assistant as `/tmp/Example` and `User/node/bin/node`
|
|
113
|
+
// (measured on the published beta, 2026-09-19). Each argument is now written as one word for the shell it
|
|
114
|
+
// is pasted into. A word made only of characters no shell treats specially is left bare, so the line for
|
|
115
|
+
// an ordinary install reads exactly as it did.
|
|
116
|
+
//
|
|
117
|
+
// POSIX shells: single quotes, the one quoting in which no character is special. A single quote inside is
|
|
118
|
+
// closed, escaped and reopened.
|
|
119
|
+
const POSIX_BARE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
120
|
+
export const posixWord = (s) => (POSIX_BARE.test(String(s)) ? String(s) : `'${String(s).replace(/'/g, "'\\''")}'`);
|
|
121
|
+
// Windows: cmd and PowerShell both honour double quotes, and the program then splits its command line by
|
|
122
|
+
// the C runtime's rules, under which backslashes are literal except where they run up to a quote; there
|
|
123
|
+
// they are doubled, and a quote inside the word is escaped. A comma or a parenthesis is quoted too, because
|
|
124
|
+
// PowerShell reads a bare one as an array or an expression. `$`, a backtick and `%` cannot be quoted the
|
|
125
|
+
// same way for both shells, and a path holding one is left to the reader.
|
|
126
|
+
const WINDOWS_BARE = /^[A-Za-z0-9_+=:./\\-]+$/;
|
|
127
|
+
export const windowsWord = (s) => {
|
|
128
|
+
const w = String(s);
|
|
129
|
+
if (WINDOWS_BARE.test(w)) return w;
|
|
130
|
+
return `"${w.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"`;
|
|
131
|
+
};
|
|
132
|
+
|
|
109
133
|
/**
|
|
110
134
|
* The argument that ends Claude Code's own options. Windows PowerShell 5.1 drops a bare `--` before it
|
|
111
135
|
* reaches a native program, so the line failed with "missing required argument"; quoted, every shell passes
|
|
112
136
|
* it through (bash, zsh, PowerShell and cmd alike), so a Windows install is handed the quoted form.
|
|
113
137
|
*/
|
|
114
|
-
const separator = (
|
|
138
|
+
const separator = (windowsShell) => (windowsShell ? '"--"' : "--");
|
|
115
139
|
|
|
116
140
|
/**
|
|
117
141
|
* WHAT THE HOST ACTUALLY RUNS, and on WSL that is not `node`.
|
|
@@ -180,11 +204,17 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
180
204
|
where: null,
|
|
181
205
|
render: ({ server, workDir, reportsDir, platform, wsl, node }) => {
|
|
182
206
|
const l = stdioLauncher({ server, workDir, reportsDir, wsl, node });
|
|
207
|
+
// WHICH SHELL READS IT. A native Windows install's line is pasted on Windows, and so is the row
|
|
208
|
+
// that crosses into WSL: it exists for an assistant on the Windows side. Everything else is pasted
|
|
209
|
+
// into a POSIX shell.
|
|
210
|
+
const windowsShell = platform === "win32" || l.crossesIntoWsl;
|
|
211
|
+
const word = windowsShell ? windowsWord : posixWord;
|
|
183
212
|
// The host's own `-e` flags set variables for the process IT starts. Off WSL that is the server;
|
|
184
213
|
// through the wrapper it is `wsl.exe`, and they stop at the boundary — so on WSL they ride inside
|
|
185
214
|
// the command instead and this line carries none.
|
|
186
|
-
const flags = l.crossesIntoWsl ? ""
|
|
187
|
-
|
|
215
|
+
const flags = l.crossesIntoWsl ? ""
|
|
216
|
+
: Object.entries(envOf({ workDir, reportsDir })).map(([k, v]) => ` -e ${windowsShell ? word(`${k}=${v}`) : `${k}=${word(v)}`}`).join("");
|
|
217
|
+
return `claude mcp add ${STDIO_SERVER_NAME} --scope user${flags} ${separator(windowsShell)} ${[l.command, ...l.args].map(word).join(" ")}`;
|
|
188
218
|
},
|
|
189
219
|
after: null,
|
|
190
220
|
},
|
|
@@ -224,7 +254,8 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
224
254
|
kind: "config",
|
|
225
255
|
where: "~/.codex/config.toml",
|
|
226
256
|
// Written out rather than produced by a TOML library: this is four lines with no user-supplied
|
|
227
|
-
// strings in key positions, and adding a dependency to emit them would be the larger risk.
|
|
257
|
+
// strings in key positions, and adding a dependency to emit them would be the larger risk. Every
|
|
258
|
+
// value goes through `tomlString`, so a Windows path's backslashes are escaped as TOML requires.
|
|
228
259
|
//
|
|
229
260
|
// `env` and not `env_vars`, deliberately, and CONNECT.md explains why the distinction matters:
|
|
230
261
|
// Codex does not forward the shell environment, and a credential would have to be forwarded BY NAME
|
|
@@ -234,10 +265,10 @@ export const STDIO_SHAPES = Object.freeze({
|
|
|
234
265
|
const l = stdioLauncher({ server, workDir, reportsDir, wsl, node });
|
|
235
266
|
return [
|
|
236
267
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
237
|
-
`command =
|
|
238
|
-
`args = [${l.args.map(
|
|
268
|
+
`command = ${tomlString(l.command)}`,
|
|
269
|
+
`args = [${l.args.map(tomlString).join(", ")}]`,
|
|
239
270
|
...(Object.keys(l.env).length
|
|
240
|
-
? [`env = { ${Object.entries(l.env).map(([k, v]) => `${k} =
|
|
271
|
+
? [`env = { ${Object.entries(l.env).map(([k, v]) => `${k} = ${tomlString(v)}`).join(", ")} }`] : []),
|
|
241
272
|
].join("\n");
|
|
242
273
|
},
|
|
243
274
|
after: null,
|
|
@@ -297,7 +328,7 @@ export const REMOTE_SHAPES = Object.freeze({
|
|
|
297
328
|
label: "Copy block",
|
|
298
329
|
render: ({ address }) => [
|
|
299
330
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
300
|
-
`url =
|
|
331
|
+
`url = ${tomlString(address)}`,
|
|
301
332
|
].join("\n"),
|
|
302
333
|
},
|
|
303
334
|
"claude-cli-http": {
|
|
@@ -309,7 +340,7 @@ export const REMOTE_SHAPES = Object.freeze({
|
|
|
309
340
|
label: null,
|
|
310
341
|
render: ({ address }) => [
|
|
311
342
|
`[mcp_servers.${STDIO_SERVER_NAME}]`,
|
|
312
|
-
`url =
|
|
343
|
+
`url = ${tomlString(address)}`,
|
|
313
344
|
`bearer_token_env_var = "${KEY_ENV_VAR}"`,
|
|
314
345
|
].join("\n"),
|
|
315
346
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// ONE TOML STRING ENCODER, for every block this product writes.
|
|
5
|
+
//
|
|
6
|
+
// Two composers write TOML: the engine's per-run Codex configuration and the connection block a reader
|
|
7
|
+
// pastes into `~/.codex/config.toml`. The second wrapped raw values in double quotes, so a Windows path
|
|
8
|
+
// such as `C:\Program Files\nodejs\node.exe` produced a file TOML parsers refuse ("Unescaped '\' in a
|
|
9
|
+
// string"). A value is a TOML string only once it has been through here.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A TOML basic string holding exactly `s`. PURE.
|
|
13
|
+
*
|
|
14
|
+
* TOML requires the quotation mark, the backslash and every control character other than tab to be
|
|
15
|
+
* escaped (U+0000 to U+0008, U+000A to U+001F, and U+007F). The named escapes are used where TOML has one,
|
|
16
|
+
* and `\uXXXX` for the rest.
|
|
17
|
+
*/
|
|
18
|
+
export function tomlString(s) {
|
|
19
|
+
const esc = String(s ?? "")
|
|
20
|
+
.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
21
|
+
.replace(/\x08/g, "\\b").replace(/\t/g, "\\t").replace(/\n/g, "\\n")
|
|
22
|
+
.replace(/\f/g, "\\f").replace(/\r/g, "\\r")
|
|
23
|
+
.replace(/[\x00-\x1f\x7f]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
24
|
+
return `"${esc}"`;
|
|
25
|
+
}
|