@bli-cockpit/cli 0.2.255 → 0.2.257
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/dist/commands/agent-session-root-scope.js +19 -0
- package/dist/commands/backfill-outside-roots.js +7 -3
- package/dist/commands/codex-mcp-table.js +185 -0
- package/dist/commands/doctor-registration.js +5 -1
- package/dist/commands/local-args-tower-notes.js +18 -1
- package/dist/commands/local-command-shapes-notes.js +1 -0
- package/dist/commands/local-help-commands-tower.js +2 -46
- package/dist/commands/local-help-notes.js +50 -0
- package/dist/commands/memory-install-codex.js +21 -72
- package/dist/commands/memory-install-toml.js +17 -1
- package/dist/commands/notes-record-capture.js +152 -0
- package/dist/commands/notes-record-wav.js +244 -0
- package/dist/commands/notes-record.js +214 -0
- package/dist/commands/notes.js +10 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +10 -5
- package/dist/commands/toml-duplicate-keys.js +192 -0
- package/dist/commands/tower-mcp-codex.js +19 -68
- package/package.json +4 -4
|
@@ -47,7 +47,7 @@ import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js"
|
|
|
47
47
|
import { describeError } from "../health-detail.js";
|
|
48
48
|
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
49
49
|
import { isMissingFileError, writeJsonFileAtomic, writeJsonFile } from "../local-state-files.js";
|
|
50
|
-
import { rootScopeForReason } from "./agent-session-root-scope.js";
|
|
50
|
+
import { NO_WORKSPACE_REASONS, needsTransportWorkspace, rootScopeForReason } from "./agent-session-root-scope.js";
|
|
51
51
|
import { ensureOutsideRootsWorkspace, isOutsideRootsWorkspace, outsideRootsWorkspace, } from "./backfill-outside-roots.js";
|
|
52
52
|
/** Most outside-root sessions that upload bytes in one tick. 4 ticks an hour, so up to 3,840 a day. */
|
|
53
53
|
export const OUTSIDE_ROOT_SESSIONS_PER_TICK = 40;
|
|
@@ -112,7 +112,7 @@ export function widenForCatchUp(sinceMinutes, catchUpPending, normalMinutes) {
|
|
|
112
112
|
* than the horizon (that is backfill's).
|
|
113
113
|
*/
|
|
114
114
|
export function outsideRootEntryOwesUpload(entry, now = new Date()) {
|
|
115
|
-
if (rootScopeForReason(entry.reason) !== "outside")
|
|
115
|
+
if (rootScopeForReason(entry.reason) !== "outside" && !NO_WORKSPACE_REASONS.has(entry.reason))
|
|
116
116
|
return false;
|
|
117
117
|
if (entry.uploaded_object_key && !isRedactionStubObjectKey(entry.uploaded_object_key))
|
|
118
118
|
return false;
|
|
@@ -120,9 +120,13 @@ export function outsideRootEntryOwesUpload(entry, now = new Date()) {
|
|
|
120
120
|
return false;
|
|
121
121
|
return now.getTime() - entry.file_mtime_ms <= OUTSIDE_ROOT_HORIZON_MINUTES * 60_000;
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Is this scan result a session with no worktree of its own that attribution
|
|
125
|
+
* left unattributed (outside every approved root, no repo signals, or any other
|
|
126
|
+
* terminal state)? All of them ride the synthetic workspace: a label is never a gate.
|
|
127
|
+
*/
|
|
124
128
|
function isUnclaimedOutsideRootResult(result) {
|
|
125
|
-
return
|
|
129
|
+
return needsTransportWorkspace(result);
|
|
126
130
|
}
|
|
127
131
|
/** Already durable remotely with the bytes the scan just read: it needs no upload. */
|
|
128
132
|
function isDurableAndUnchanged(result, entry) {
|
|
@@ -224,7 +228,8 @@ export async function admitOutsideRootSessions(options) {
|
|
|
224
228
|
},
|
|
225
229
|
outsideRoots: { workspace, holds, counts },
|
|
226
230
|
};
|
|
227
|
-
|
|
231
|
+
const noRepoSignals = admitted.filter((sighting) => NO_WORKSPACE_REASONS.has(sighting.result.reason)).length;
|
|
232
|
+
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "outside_root_sessions_admitted", no_repo_signals_riding: noRepoSignals, ...counts, ...budget }));
|
|
228
233
|
return labelled;
|
|
229
234
|
}
|
|
230
235
|
/** Session ids and queue turns only; no source paths or transcript content. */
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the first key a TOML file defines twice (BLI-22766).
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: `codex` refuses to start when `~/.codex/config.toml` defines
|
|
5
|
+
* one key twice — for example `[mcp_servers.bli-memory]` holding an inline
|
|
6
|
+
* `env = { … }` AND a separate `[mcp_servers.bli-memory.env]` table. It prints
|
|
7
|
+
* `config.toml:180:25: duplicate key` and does nothing else, so every Codex
|
|
8
|
+
* worker on that machine dies silently. `cockpit doctor` needs to say so, by
|
|
9
|
+
* file and line.
|
|
10
|
+
*
|
|
11
|
+
* This is NOT a TOML parser, for the same reason `memory-install-toml.ts` is
|
|
12
|
+
* not one: the public CLI ships no TOML dependency. It models only what makes a
|
|
13
|
+
* key "defined": table headers, array-of-table headers, `key = value` lines
|
|
14
|
+
* (dotted keys included) and inline tables. It skips over multi-line strings
|
|
15
|
+
* and multi-line arrays so their contents are never mistaken for keys. The
|
|
16
|
+
* tests check its answer against a real TOML parser on the same fixtures.
|
|
17
|
+
*
|
|
18
|
+
* What it does NOT catch: syntax errors that are not duplicate keys (a missing
|
|
19
|
+
* quote, a bad date). Those still break Codex; this check reports only the
|
|
20
|
+
* class of breakage our own writer could cause.
|
|
21
|
+
*/
|
|
22
|
+
export function findTomlDuplicateKey(text) {
|
|
23
|
+
const values = new Set();
|
|
24
|
+
const tables = new Set();
|
|
25
|
+
const dottedTables = new Set();
|
|
26
|
+
const arrayTables = new Map();
|
|
27
|
+
let current = [];
|
|
28
|
+
let pending = null;
|
|
29
|
+
const lines = text.split(/\r?\n/u);
|
|
30
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
31
|
+
const line = lines[index] ?? "";
|
|
32
|
+
if (pending) {
|
|
33
|
+
pending = continuePending(line, pending);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const trimmed = stripComment(line).trim();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
continue;
|
|
39
|
+
if (trimmed.startsWith("[[")) {
|
|
40
|
+
const raw = trimmed.slice(2, trimmed.lastIndexOf("]]"));
|
|
41
|
+
const resolved = resolveArrayPrefixes(splitKeyPath(raw), arrayTables, false);
|
|
42
|
+
const id = encode(resolved);
|
|
43
|
+
const count = (arrayTables.get(id) ?? 0) + 1;
|
|
44
|
+
arrayTables.set(id, count);
|
|
45
|
+
current = [...resolved, `#${count}`];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (trimmed.startsWith("[")) {
|
|
49
|
+
const resolved = resolveArrayPrefixes(splitKeyPath(trimmed.slice(1, trimmed.lastIndexOf("]"))), arrayTables, true);
|
|
50
|
+
const id = encode(resolved);
|
|
51
|
+
if (tables.has(id) || dottedTables.has(id) || hasValuePrefix(values, resolved)) {
|
|
52
|
+
return duplicate(index, resolved);
|
|
53
|
+
}
|
|
54
|
+
tables.add(id);
|
|
55
|
+
current = resolved;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const equals = indexOutsideQuotes(trimmed, "=");
|
|
59
|
+
if (equals <= 0)
|
|
60
|
+
continue;
|
|
61
|
+
const full = [...current, ...splitKeyPath(trimmed.slice(0, equals))];
|
|
62
|
+
const id = encode(full);
|
|
63
|
+
if (hasValuePrefix(values, full) || tables.has(id) || dottedTables.has(id)) {
|
|
64
|
+
return duplicate(index, full);
|
|
65
|
+
}
|
|
66
|
+
if (hasTableUnder(tables, full))
|
|
67
|
+
return duplicate(index, full);
|
|
68
|
+
values.add(id);
|
|
69
|
+
for (let length = current.length + 1; length < full.length; length += 1) {
|
|
70
|
+
dottedTables.add(encode(full.slice(0, length)));
|
|
71
|
+
}
|
|
72
|
+
pending = openPending(trimmed.slice(equals + 1).trim());
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function duplicate(index, path) {
|
|
77
|
+
return { line: index + 1, key: path.filter((part) => !part.startsWith("#")).join(".") };
|
|
78
|
+
}
|
|
79
|
+
function encode(path) {
|
|
80
|
+
return JSON.stringify(path);
|
|
81
|
+
}
|
|
82
|
+
function hasValuePrefix(values, path) {
|
|
83
|
+
for (let length = 1; length <= path.length; length += 1) {
|
|
84
|
+
if (values.has(encode(path.slice(0, length))))
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
function hasTableUnder(tables, path) {
|
|
90
|
+
const prefix = encode(path).slice(0, -1);
|
|
91
|
+
for (const table of tables) {
|
|
92
|
+
if (table.startsWith(`${prefix},`))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/** `[a.b]` after `[[a]]` means "b inside the LAST a" — give it that element's path. */
|
|
98
|
+
function resolveArrayPrefixes(path, arrayTables, includeLast) {
|
|
99
|
+
const resolved = [];
|
|
100
|
+
path.forEach((part, index) => {
|
|
101
|
+
resolved.push(part);
|
|
102
|
+
if (!includeLast && index === path.length - 1)
|
|
103
|
+
return;
|
|
104
|
+
const count = arrayTables.get(encode(resolved));
|
|
105
|
+
if (count !== undefined)
|
|
106
|
+
resolved.push(`#${count}`);
|
|
107
|
+
});
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
/** Decides whether a value continues onto later lines. */
|
|
111
|
+
function openPending(value) {
|
|
112
|
+
for (const closer of ['"""', "'''"]) {
|
|
113
|
+
if (value.startsWith(closer)) {
|
|
114
|
+
return value.indexOf(closer, closer.length) >= 0 ? null : { kind: "string", closer };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (value.startsWith("[") || value.startsWith("{")) {
|
|
118
|
+
const depth = bracketDepth(value, 0);
|
|
119
|
+
return depth > 0 ? { kind: "brackets", depth } : null;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function continuePending(line, pending) {
|
|
124
|
+
if (pending.kind === "string")
|
|
125
|
+
return line.includes(pending.closer) ? null : pending;
|
|
126
|
+
const depth = bracketDepth(line, pending.depth);
|
|
127
|
+
return depth > 0 ? { kind: "brackets", depth } : null;
|
|
128
|
+
}
|
|
129
|
+
function bracketDepth(raw, start) {
|
|
130
|
+
let depth = start;
|
|
131
|
+
let quote = null;
|
|
132
|
+
for (const character of raw) {
|
|
133
|
+
if (quote) {
|
|
134
|
+
if (character === quote)
|
|
135
|
+
quote = null;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (character === '"' || character === "'")
|
|
139
|
+
quote = character;
|
|
140
|
+
else if (character === "#")
|
|
141
|
+
break;
|
|
142
|
+
else if (character === "[" || character === "{")
|
|
143
|
+
depth += 1;
|
|
144
|
+
else if (character === "]" || character === "}")
|
|
145
|
+
depth -= 1;
|
|
146
|
+
}
|
|
147
|
+
return depth;
|
|
148
|
+
}
|
|
149
|
+
function indexOutsideQuotes(raw, target) {
|
|
150
|
+
let quote = null;
|
|
151
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
152
|
+
const character = raw[index];
|
|
153
|
+
if (quote) {
|
|
154
|
+
if (character === quote)
|
|
155
|
+
quote = null;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (character === '"' || character === "'")
|
|
159
|
+
quote = character;
|
|
160
|
+
else if (character === target)
|
|
161
|
+
return index;
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
}
|
|
165
|
+
function stripComment(raw) {
|
|
166
|
+
const index = indexOutsideQuotes(raw, "#");
|
|
167
|
+
return index < 0 ? raw : raw.slice(0, index);
|
|
168
|
+
}
|
|
169
|
+
function splitKeyPath(raw) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
let current = "";
|
|
172
|
+
let quote = null;
|
|
173
|
+
for (const character of raw) {
|
|
174
|
+
if (quote) {
|
|
175
|
+
if (character === quote)
|
|
176
|
+
quote = null;
|
|
177
|
+
else
|
|
178
|
+
current += character;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (character === '"' || character === "'")
|
|
182
|
+
quote = character;
|
|
183
|
+
else if (character === ".") {
|
|
184
|
+
parts.push(current.trim());
|
|
185
|
+
current = "";
|
|
186
|
+
}
|
|
187
|
+
else
|
|
188
|
+
current += character;
|
|
189
|
+
}
|
|
190
|
+
parts.push(current.trim());
|
|
191
|
+
return parts;
|
|
192
|
+
}
|
|
@@ -12,46 +12,25 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { TOWER_MCP_SERVER_ID } from "./tower-mcp-contract.js";
|
|
15
|
-
import {
|
|
15
|
+
import { MEMORY_MCP_SERVER_ID, } from "./memory-install-contract.js";
|
|
16
|
+
import { findTomlTableSpan } from "./memory-install-toml.js";
|
|
17
|
+
import { applyCodexServerTable, codexConfigUnparseable, codexServerTableMatches, } from "./codex-mcp-table.js";
|
|
16
18
|
const TOWER_TOML_PATH = ["mcp_servers", TOWER_MCP_SERVER_ID];
|
|
19
|
+
/** BLI Memory's writer runs first on the same command and repairs its own table. */
|
|
20
|
+
const SIBLING_TOML_PATH = ["mcp_servers", MEMORY_MCP_SERVER_ID];
|
|
17
21
|
export function codexConfigFile(homeDir) {
|
|
18
22
|
return path.join(homeDir, ".codex", "config.toml");
|
|
19
23
|
}
|
|
20
24
|
export async function installCodexTowerIntegration(options) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
await options.io.writeText(file, next);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
return {
|
|
35
|
-
target: "tower_codex_mcp",
|
|
36
|
-
status: "failed",
|
|
37
|
-
reason: "write_failed",
|
|
38
|
-
path: file,
|
|
39
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
// BLI-2541: parse the table back out of the stored bytes, same discipline
|
|
43
|
-
// as every other target this installer writes.
|
|
44
|
-
const stored = await options.io.readText(file);
|
|
45
|
-
if (stored === null || !tableMatches(stored, options.config)) {
|
|
46
|
-
return {
|
|
47
|
-
target: "tower_codex_mcp",
|
|
48
|
-
status: "failed",
|
|
49
|
-
reason: "read_back_mismatch",
|
|
50
|
-
path: file,
|
|
51
|
-
detail: "the table on disk does not parse back to the entry that was written",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return { target: "tower_codex_mcp", status: "installed", reason: "wrote_entry", path: file };
|
|
25
|
+
return applyCodexServerTable({
|
|
26
|
+
file: codexConfigFile(options.homeDir),
|
|
27
|
+
target: "tower_codex_mcp",
|
|
28
|
+
tablePath: TOWER_TOML_PATH,
|
|
29
|
+
siblingPath: SIBLING_TOML_PATH,
|
|
30
|
+
config: options.config,
|
|
31
|
+
dryRun: options.dryRun,
|
|
32
|
+
io: options.io,
|
|
33
|
+
});
|
|
55
34
|
}
|
|
56
35
|
export async function inspectCodexTowerIntegration(options) {
|
|
57
36
|
const file = codexConfigFile(options.homeDir);
|
|
@@ -59,42 +38,14 @@ export async function inspectCodexTowerIntegration(options) {
|
|
|
59
38
|
if (raw === null) {
|
|
60
39
|
return { target: "tower_codex_mcp", status: "missing", reason: "file_absent", path: file };
|
|
61
40
|
}
|
|
62
|
-
|
|
41
|
+
// BLI-22766: a file Codex cannot load is named first, whatever our table says.
|
|
42
|
+
const broken = codexConfigUnparseable(raw, file, "tower_codex_mcp");
|
|
43
|
+
if (broken)
|
|
44
|
+
return broken;
|
|
45
|
+
if (codexServerTableMatches(raw, TOWER_TOML_PATH, options.config)) {
|
|
63
46
|
return { target: "tower_codex_mcp", status: "installed", reason: "already_current", path: file };
|
|
64
47
|
}
|
|
65
48
|
return findTomlTableSpan(raw, TOWER_TOML_PATH)
|
|
66
49
|
? { target: "tower_codex_mcp", status: "mismatch", reason: "entry_differs", path: file }
|
|
67
50
|
: { target: "tower_codex_mcp", status: "missing", reason: "entry_absent", path: file };
|
|
68
|
-
}
|
|
69
|
-
function renderTowerTable(config) {
|
|
70
|
-
const entries = [
|
|
71
|
-
["command", config.mcp_server.command],
|
|
72
|
-
["args", config.mcp_server.args],
|
|
73
|
-
];
|
|
74
|
-
if (Object.keys(config.mcp_server.env).length > 0) {
|
|
75
|
-
entries.push(["env", config.mcp_server.env]);
|
|
76
|
-
}
|
|
77
|
-
return renderTomlTable(TOWER_TOML_PATH, entries);
|
|
78
|
-
}
|
|
79
|
-
function tableMatches(raw, config) {
|
|
80
|
-
const table = readTomlTable(raw, TOWER_TOML_PATH);
|
|
81
|
-
if (!table)
|
|
82
|
-
return false;
|
|
83
|
-
if (table["command"] !== config.mcp_server.command)
|
|
84
|
-
return false;
|
|
85
|
-
const args = table["args"];
|
|
86
|
-
if (!Array.isArray(args))
|
|
87
|
-
return config.mcp_server.args.length === 0;
|
|
88
|
-
if (args.length !== config.mcp_server.args.length)
|
|
89
|
-
return false;
|
|
90
|
-
if (!args.every((entry, index) => entry === config.mcp_server.args[index]))
|
|
91
|
-
return false;
|
|
92
|
-
const env = table["env"];
|
|
93
|
-
for (const [key, value] of Object.entries(config.mcp_server.env)) {
|
|
94
|
-
if (!env || Array.isArray(env) || typeof env === "string")
|
|
95
|
-
return false;
|
|
96
|
-
if (env[key] !== value)
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return true;
|
|
100
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.257",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.76",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.157",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.83"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|