@bli-cockpit/cli 0.2.56 → 0.2.58
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-door.js +85 -0
- package/dist/commands/docs.js +227 -0
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/local-args-tower-docs-msg.js +126 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +10 -2
- package/dist/commands/local-help.js +70 -0
- package/dist/commands/local.js +12 -0
- package/dist/commands/mcp-bin-resolve.js +102 -0
- package/dist/commands/memory-install-claude.js +13 -5
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-report.js +89 -0
- package/dist/commands/memory-install.js +51 -362
- package/dist/commands/msg.js +188 -0
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/ops-render.js +18 -2
- package/dist/commands/ops.js +9 -2
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/tower-mcp-claude.js +30 -0
- package/dist/commands/tower-mcp-codex.js +100 -0
- package/dist/commands/tower-mcp-contract.js +39 -0
- package/dist/commands/tower-mcp-install.js +75 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- package/dist/upload-envelope-build.js +240 -0
- package/dist/upload-envelope-event.js +198 -0
- package/dist/upload-envelope.js +16 -427
- package/dist/upload-ingest-receipt.js +121 -0
- package/dist/upload-session-reports-queue.js +156 -0
- package/dist/upload-session-reports-wire.js +275 -0
- package/dist/upload-session-reports.js +14 -425
- package/dist/upload-sync.js +291 -0
- package/dist/upload.js +24 -396
- package/package.json +6 -5
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an MCP server's bin lives on THIS machine, for any server shipped as a
|
|
3
|
+
* dependency of `@bli-cockpit/cli` (BLI-3580's `resolveMemoryMcpBin`,
|
|
4
|
+
* generalised for BLI-3706's `bli-tower` registration so the second server
|
|
5
|
+
* does not carry a second copy of this walk).
|
|
6
|
+
*
|
|
7
|
+
* Two steps, in this order — see `memory-install.ts`'s `resolveMemoryMcpBin`
|
|
8
|
+
* doc comment for the full BLI-3580 story of why the walk starts at THIS
|
|
9
|
+
* module's own file (never a shim) and realpath-resolves every anchor before
|
|
10
|
+
* walking:
|
|
11
|
+
*
|
|
12
|
+
* 1. Beside the CLI that is running — the ONLY lookup that cannot find
|
|
13
|
+
* somebody else's copy of the bin.
|
|
14
|
+
* 2. PATH, as a fallback for a linked checkout or a hand-installed server.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
export async function resolveMcpBin(options) {
|
|
20
|
+
const exists = options.fileExists ?? defaultFileExists;
|
|
21
|
+
const beside = await resolveBesideCli(options, exists);
|
|
22
|
+
if (beside)
|
|
23
|
+
return { path: beside, source: "cli_dependency" };
|
|
24
|
+
const onPath = await resolveOnPath(options, exists);
|
|
25
|
+
return onPath ? { path: onPath, source: "path" } : null;
|
|
26
|
+
}
|
|
27
|
+
function besideAnchors(options) {
|
|
28
|
+
const anchors = [];
|
|
29
|
+
const own = currentModulePath();
|
|
30
|
+
if (own)
|
|
31
|
+
anchors.push(own);
|
|
32
|
+
const entry = options.cliEntryPoint ?? process.argv[1];
|
|
33
|
+
if (entry)
|
|
34
|
+
anchors.push(entry);
|
|
35
|
+
return anchors;
|
|
36
|
+
}
|
|
37
|
+
function currentModulePath() {
|
|
38
|
+
try {
|
|
39
|
+
return fileURLToPath(import.meta.url);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function resolveBesideCli(options, exists) {
|
|
46
|
+
const platformPath = options.platform === "win32" ? path.win32 : path.posix;
|
|
47
|
+
const extensions = binExtensions(options.platform);
|
|
48
|
+
const realpath = options.realpath ?? defaultRealpath;
|
|
49
|
+
for (const anchor of besideAnchors(options)) {
|
|
50
|
+
let directory = platformPath.dirname(realpath(platformPath.resolve(anchor)));
|
|
51
|
+
// Bounded walk: deep enough for `…/node_modules/@scope/pkg/dist/cli.js`
|
|
52
|
+
// plus a hoisted root above it, and it stops at the filesystem root anyway.
|
|
53
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
54
|
+
for (const extension of extensions) {
|
|
55
|
+
const candidate = platformPath.join(directory, "node_modules", ".bin", `${options.binName}${extension}`);
|
|
56
|
+
if (await exists(candidate))
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
const parent = platformPath.dirname(directory);
|
|
60
|
+
if (parent === directory)
|
|
61
|
+
break;
|
|
62
|
+
directory = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function defaultRealpath(value) {
|
|
68
|
+
try {
|
|
69
|
+
return fs.realpathSync.native(value);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function resolveOnPath(options, exists) {
|
|
76
|
+
const platformPath = options.platform === "win32" ? path.win32 : path.posix;
|
|
77
|
+
const entries = (options.env["PATH"] ?? options.env["Path"] ?? "")
|
|
78
|
+
.split(platformPath.delimiter)
|
|
79
|
+
.map((entry) => entry.trim())
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
for (const extension of binExtensions(options.platform)) {
|
|
83
|
+
const candidate = platformPath.join(entry, `${options.binName}${extension}`);
|
|
84
|
+
if (await exists(candidate))
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** npm writes `.cmd` (and `.ps1`) shims on Windows; POSIX gets the bare name. */
|
|
91
|
+
function binExtensions(platform) {
|
|
92
|
+
return platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
93
|
+
}
|
|
94
|
+
async function defaultFileExists(file) {
|
|
95
|
+
const { stat } = await import("node:fs/promises");
|
|
96
|
+
try {
|
|
97
|
+
return (await stat(file)).isFile();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -77,7 +77,13 @@ export async function inspectClaudeMemoryIntegration(options) {
|
|
|
77
77
|
}),
|
|
78
78
|
];
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Exported for `tower-mcp-claude.ts` (BLI-3706): the read/refuse-if-unparseable
|
|
82
|
+
* /apply/dry-run/write/read-back-verify sequence is identical for a SECOND
|
|
83
|
+
* server's `mcpServers` entry — only which target id and which `apply`/
|
|
84
|
+
* `matches` pair runs differs, both already parameters here.
|
|
85
|
+
*/
|
|
86
|
+
export async function applyJsonTarget(input) {
|
|
81
87
|
const { file, options } = input;
|
|
82
88
|
const raw = await input.options.io.readText(file);
|
|
83
89
|
let root;
|
|
@@ -120,7 +126,8 @@ async function applyJsonTarget(input) {
|
|
|
120
126
|
}
|
|
121
127
|
return { target: input.target, status: "installed", reason: "wrote_entry", path: file };
|
|
122
128
|
}
|
|
123
|
-
|
|
129
|
+
/** Exported for `tower-mcp-claude.ts` (BLI-3706) — see `applyJsonTarget`'s note. */
|
|
130
|
+
export async function inspectJsonTarget(input) {
|
|
124
131
|
const raw = await input.io.readText(input.file);
|
|
125
132
|
if (raw === null || !raw.trim()) {
|
|
126
133
|
return { target: input.target, status: "missing", reason: "file_absent", path: input.file };
|
|
@@ -140,7 +147,8 @@ async function inspectJsonTarget(input) {
|
|
|
140
147
|
path: input.file,
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
|
-
|
|
150
|
+
/** Exported for `tower-mcp-claude.ts` (BLI-3706) — the `mcpServers` shape has nothing memory-specific about it. */
|
|
151
|
+
export function applyMcpServer(root, config) {
|
|
144
152
|
const servers = asRecord(root["mcpServers"]) ?? {};
|
|
145
153
|
servers[config.server_id] = {
|
|
146
154
|
command: config.mcp_server.command,
|
|
@@ -152,13 +160,13 @@ function applyMcpServer(root, config) {
|
|
|
152
160
|
root["mcpServers"] = servers;
|
|
153
161
|
return root;
|
|
154
162
|
}
|
|
155
|
-
function readMcpServer(root, serverId) {
|
|
163
|
+
export function readMcpServer(root, serverId) {
|
|
156
164
|
const servers = asRecord(root["mcpServers"]);
|
|
157
165
|
if (!servers)
|
|
158
166
|
return null;
|
|
159
167
|
return asRecord(servers[serverId]);
|
|
160
168
|
}
|
|
161
|
-
function mcpServerMatches(root, config) {
|
|
169
|
+
export function mcpServerMatches(root, config) {
|
|
162
170
|
const entry = readMcpServer(root, config.server_id);
|
|
163
171
|
if (!entry)
|
|
164
172
|
return false;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { builtinMemoryInstallConfig, isUnsafeBinPath, MEMORY_MCP_BIN, parsePrintedMemoryInstallConfig, withResolvedBinPath, } from "./memory-install-contract.js";
|
|
2
|
+
import { resolveMcpBin } from "./mcp-bin-resolve.js";
|
|
3
|
+
import { resolveDashboardUrl } from "./memory-install-report.js";
|
|
4
|
+
import { envWithNodeRuntimeOnPath } from "../scheduled-self-update.js";
|
|
5
|
+
/**
|
|
6
|
+
* `bli-memory-mcp` ships as a DEPENDENCY of `@bli-cockpit/cli` — see
|
|
7
|
+
* `mcp-bin-resolve.ts` (BLI-3706's generalisation of this BLI-3580 lookup,
|
|
8
|
+
* shared with `bli-tower`'s registration) for the full two-step walk and the
|
|
9
|
+
* BLI-3580 story of why it starts at that module's own file rather than
|
|
10
|
+
* `process.argv[1]`.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveMemoryMcpBin(options) {
|
|
13
|
+
return resolveMcpBin({ ...options, binName: MEMORY_MCP_BIN });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* **`no_bin_no_write`.** When `bli-memory-mcp` cannot be resolved, this command
|
|
17
|
+
* writes NOTHING — not the MCP entry, not the hooks, not the Codex table, not
|
|
18
|
+
* the skills.
|
|
19
|
+
*
|
|
20
|
+
* An earlier revision wrote the registration anyway, on the reasoning that a
|
|
21
|
+
* correct shape waiting for the package is better than nothing. It is not:
|
|
22
|
+
* Claude Code RUNS a registered hook. Three hooks pointing at a binary that
|
|
23
|
+
* does not exist would print a hook failure on every SessionStart, every
|
|
24
|
+
* prompt and every Stop, on every intern machine, until the package shipped —
|
|
25
|
+
* a self-inflicted outage in the one surface people look at all day.
|
|
26
|
+
*
|
|
27
|
+
* So an absent bin is `skipped bin_missing`: a receipt, not a write. The daily
|
|
28
|
+
* self-heal retries tomorrow, and the first tick after the package lands does
|
|
29
|
+
* the whole registration at once.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveMemoryConfig(command, io, platform, deps) {
|
|
32
|
+
const dashboardUrl = await resolveDashboardUrl(command, deps);
|
|
33
|
+
const found = await resolveMemoryMcpBin({
|
|
34
|
+
env: envWithNodeRuntimeOnPath(io.env ?? process.env),
|
|
35
|
+
platform,
|
|
36
|
+
fileExists: deps.fileExists,
|
|
37
|
+
cliEntryPoint: deps.cliEntryPoint,
|
|
38
|
+
realpath: deps.realpath,
|
|
39
|
+
});
|
|
40
|
+
if (!found) {
|
|
41
|
+
return {
|
|
42
|
+
config: null,
|
|
43
|
+
source: "none",
|
|
44
|
+
binTarget: {
|
|
45
|
+
target: "bin",
|
|
46
|
+
status: "skipped",
|
|
47
|
+
reason: "bin_missing",
|
|
48
|
+
detail: `${MEMORY_MCP_BIN} is not installed beside this CLI or on PATH; nothing was written, and the next daily run will try again`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (isUnsafeBinPath(found.path)) {
|
|
53
|
+
// A hook command is a shell string by the platform's design. A path that
|
|
54
|
+
// cannot be quoted safely is not escaped cleverly, and it is not swapped
|
|
55
|
+
// for a bare name that may resolve to something else either — the install
|
|
56
|
+
// refuses and says why.
|
|
57
|
+
return {
|
|
58
|
+
config: null,
|
|
59
|
+
source: "none",
|
|
60
|
+
binTarget: {
|
|
61
|
+
target: "bin",
|
|
62
|
+
status: "failed",
|
|
63
|
+
reason: "bin_path_unsafe",
|
|
64
|
+
detail: "the resolved bin path contains characters that cannot appear in a hook command; nothing was written",
|
|
65
|
+
},
|
|
66
|
+
bin_source: found.source,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const printed = await printedMemoryConfig(io, found.path);
|
|
70
|
+
if (printed) {
|
|
71
|
+
// The bin prints a BARE command name — it cannot know where it was
|
|
72
|
+
// installed, and it is nested inside the CLI's node_modules rather than on
|
|
73
|
+
// PATH. Path-qualifying it here is what makes the registration runnable at
|
|
74
|
+
// all; see `withResolvedBinPath`.
|
|
75
|
+
const qualified = withResolvedBinPath(printed, {
|
|
76
|
+
binPath: found.path,
|
|
77
|
+
platform,
|
|
78
|
+
dashboardUrl,
|
|
79
|
+
});
|
|
80
|
+
if (qualified) {
|
|
81
|
+
return {
|
|
82
|
+
config: qualified,
|
|
83
|
+
source: "bin",
|
|
84
|
+
binTarget: { target: "bin", status: "already", reason: "bin_printed_config" },
|
|
85
|
+
bin_source: found.source,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// A printed hook command this installer cannot re-point. The template is
|
|
89
|
+
// always path-qualified, so it is the safe answer — and the reason says
|
|
90
|
+
// which of the two fallbacks happened.
|
|
91
|
+
return {
|
|
92
|
+
config: builtinMemoryInstallConfig({ binPath: found.path, platform, dashboardUrl }),
|
|
93
|
+
source: "template",
|
|
94
|
+
binTarget: {
|
|
95
|
+
target: "bin",
|
|
96
|
+
status: "already",
|
|
97
|
+
reason: "bin_printed_config_unqualifiable",
|
|
98
|
+
detail: "the bin printed a hook command this installer could not re-point at the resolved path; the built-in shape was used",
|
|
99
|
+
},
|
|
100
|
+
bin_source: found.source,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
config: builtinMemoryInstallConfig({
|
|
105
|
+
binPath: found.path,
|
|
106
|
+
platform,
|
|
107
|
+
dashboardUrl,
|
|
108
|
+
}),
|
|
109
|
+
source: "template",
|
|
110
|
+
binTarget: {
|
|
111
|
+
target: "bin",
|
|
112
|
+
status: "already",
|
|
113
|
+
reason: "bin_present_template_used",
|
|
114
|
+
detail: "the bin did not print a usable --print-config; the built-in shape was used",
|
|
115
|
+
},
|
|
116
|
+
bin_source: found.source,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Asks the server for its own shape. Arguments go as an ARRAY — nothing is
|
|
121
|
+
* interpolated into a command line — and on Windows the captured runner routes
|
|
122
|
+
* a `.cmd` shim through ComSpec with its own escaping (process-runner.ts).
|
|
123
|
+
*/
|
|
124
|
+
async function printedMemoryConfig(io, binPath) {
|
|
125
|
+
const exec = io.exec;
|
|
126
|
+
if (!exec)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const result = await exec(binPath, ["--print-config", "--claude"]);
|
|
130
|
+
if (result.code !== 0)
|
|
131
|
+
return null;
|
|
132
|
+
return parsePrintedMemoryInstallConfig(result.stdout);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Deliberately silent here: the caller reports
|
|
136
|
+
// `bin_present_template_used`, which is the same information with a name
|
|
137
|
+
// on it, and this path is reached on every machine that has an older bin.
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
2
|
+
export async function resolveDashboardUrl(command, deps) {
|
|
3
|
+
if (command.dashboardUrl)
|
|
4
|
+
return command.dashboardUrl;
|
|
5
|
+
const paths = getCollectorRuntimePaths(deps.homeDir ?? command.homeDir);
|
|
6
|
+
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
7
|
+
return config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
|
|
8
|
+
}
|
|
9
|
+
export function aggregate(targets) {
|
|
10
|
+
const failed = targets.filter((target) => target.status === "failed");
|
|
11
|
+
if (failed.length > 0) {
|
|
12
|
+
return {
|
|
13
|
+
status: "failed",
|
|
14
|
+
// The first named reason, not a count: an operator needs the reason, and
|
|
15
|
+
// the per-target list beside it carries the rest.
|
|
16
|
+
reason: failed[0]?.reason ?? "unknown_failure",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (targets.some((target) => target.status === "would_install")) {
|
|
20
|
+
return { status: "would_install", reason: "dry_run" };
|
|
21
|
+
}
|
|
22
|
+
if (targets.some((target) => target.status === "installed")) {
|
|
23
|
+
return { status: "installed", reason: "wrote_entry" };
|
|
24
|
+
}
|
|
25
|
+
if (targets.some((target) => target.status === "mismatch" || target.status === "missing")) {
|
|
26
|
+
return { status: "missing", reason: "entry_absent" };
|
|
27
|
+
}
|
|
28
|
+
// BLI-3706: checked BEFORE `skipped`, not after. `bli-tower` registering
|
|
29
|
+
// beside `bli-memory` on this same command means a machine can be fully
|
|
30
|
+
// "already" registered for one server while the other's bin is not here
|
|
31
|
+
// yet — that machine's OWN state must not read as "skipped" (nothing is
|
|
32
|
+
// happening) when something plainly already is. `skipped` only wins the
|
|
33
|
+
// whole outcome when NOTHING on this machine has ever reached "already"
|
|
34
|
+
// either — the original one-server case (a fresh machine, bin missing,
|
|
35
|
+
// nothing written at all) still returns "skipped" via the fallback below.
|
|
36
|
+
if (targets.some((target) => target.status === "already")) {
|
|
37
|
+
return { status: "already", reason: "already_current" };
|
|
38
|
+
}
|
|
39
|
+
const skipped = targets.find((target) => target.status === "skipped");
|
|
40
|
+
if (skipped) {
|
|
41
|
+
// Not a failure and not a success: nothing was written, on purpose, and
|
|
42
|
+
// the next daily run will try again. `bin_missing` is the only one today.
|
|
43
|
+
return { status: "skipped", reason: skipped.reason };
|
|
44
|
+
}
|
|
45
|
+
return { status: "already", reason: "already_current" };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Both branches log, and neither carries a path — a home directory names a
|
|
49
|
+
* person, and this line ends up in `sync.err.log` on every machine.
|
|
50
|
+
*/
|
|
51
|
+
export function logMemoryOutcome(outcome, platform) {
|
|
52
|
+
const fields = {
|
|
53
|
+
status: outcome.status,
|
|
54
|
+
reason: outcome.reason,
|
|
55
|
+
config_source: outcome.config_source,
|
|
56
|
+
bin_found: outcome.bin_found,
|
|
57
|
+
platform,
|
|
58
|
+
installed_count: outcome.targets.filter((target) => target.status === "installed").length,
|
|
59
|
+
already_count: outcome.targets.filter((target) => target.status === "already").length,
|
|
60
|
+
failed: outcome.targets
|
|
61
|
+
.filter((target) => target.status === "failed")
|
|
62
|
+
.map((target) => `${target.target}:${target.reason}`),
|
|
63
|
+
};
|
|
64
|
+
// stderr on both branches: launchd captures it to sync.err.log, and stdout is
|
|
65
|
+
// reserved for `--json`.
|
|
66
|
+
console.error(outcome.status === "failed"
|
|
67
|
+
? "[memory-install] BLI Memory is not fully registered on this machine"
|
|
68
|
+
: "[memory-install] BLI Memory registration converged", JSON.stringify(fields));
|
|
69
|
+
}
|
|
70
|
+
export function memoryOutcomeLines(outcome) {
|
|
71
|
+
const headline = outcome.status === "installed"
|
|
72
|
+
? "BLI Memory registered on this machine."
|
|
73
|
+
: outcome.status === "already"
|
|
74
|
+
? "BLI Memory is already registered on this machine."
|
|
75
|
+
: outcome.status === "would_install"
|
|
76
|
+
? "BLI Memory would be registered (dry run; nothing was written)."
|
|
77
|
+
: outcome.status === "missing"
|
|
78
|
+
? "BLI Memory is not registered on this machine."
|
|
79
|
+
: outcome.status === "skipped"
|
|
80
|
+
? "BLI Memory was not registered and nothing was written: the bli-memory-mcp server is not on this machine yet."
|
|
81
|
+
: `BLI Memory is not fully registered: ${outcome.reason}.`;
|
|
82
|
+
const lines = [headline];
|
|
83
|
+
for (const target of outcome.targets) {
|
|
84
|
+
const where = target.path ? ` ${target.path}` : "";
|
|
85
|
+
const detail = target.detail ? ` — ${target.detail}` : "";
|
|
86
|
+
lines.push(` ${target.target}: ${target.status} (${target.reason})${where}${detail}`);
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|