@bli-cockpit/cli 0.2.97 → 0.2.99
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/cli.js +13 -0
- package/dist/commands/doctor.js +6 -2
- package/dist/commands/local-args-collector-status.js +27 -6
- package/dist/commands/local-args-tower-admin.js +17 -6
- package/dist/commands/local-args-tower-memory.js +151 -0
- package/dist/commands/local-args-tower-pages.js +6 -1
- package/dist/commands/local-args-tower-usage.js +8 -0
- package/dist/commands/local-args-tower.js +2 -1
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-command-shapes-memory.js +22 -0
- package/dist/commands/local-help-commands-tower.js +31 -5
- package/dist/commands/local-help-commands.js +25 -5
- package/dist/commands/local-help.js +7 -4
- package/dist/commands/local.js +18 -0
- package/dist/commands/memory-hook-counts.js +29 -8
- package/dist/commands/memory-store.js +331 -0
- package/dist/commands/notes-writes.js +37 -0
- package/dist/commands/notes.js +5 -3
- package/dist/commands/ops-render.js +5 -1
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/usage.js +23 -0
- package/dist/crash-guard.js +167 -0
- package/dist/process-runner.js +39 -1
- package/package.json +5 -5
package/dist/commands/local.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defaultIo, errorMessage, writeLine } from "./cli-io.js";
|
|
2
2
|
import { isLocalHelpRequest, localCommandHelp, rootCommandNames } from "./local-help.js";
|
|
3
|
+
import { beginStage, endStage } from "../crash-guard.js";
|
|
3
4
|
import { describeError } from "../health-detail.js";
|
|
4
5
|
import { reportInstallEventsBestEffort } from "./install-receipts.js";
|
|
5
6
|
import { persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
|
|
@@ -33,6 +34,7 @@ import { runAutostart } from "./autostart-command.js";
|
|
|
33
34
|
import { runAgentRules } from "./agent-rules-command.js";
|
|
34
35
|
import { runMemoryLog } from "./memory-log.js";
|
|
35
36
|
import { runMemoryInstall } from "./memory-install.js";
|
|
37
|
+
import { runMemoryStore, runMemoryVerbList } from "./memory-store.js";
|
|
36
38
|
import { runClean } from "./clean.js";
|
|
37
39
|
import { runDocs } from "./docs.js";
|
|
38
40
|
import { runMsg } from "./msg.js";
|
|
@@ -41,6 +43,7 @@ import { runCal } from "./cal.js";
|
|
|
41
43
|
import { runMail } from "./mail.js";
|
|
42
44
|
import { runProject } from "./project.js";
|
|
43
45
|
import { runSearch } from "./search.js";
|
|
46
|
+
import { runUsage } from "./usage.js";
|
|
44
47
|
import { parseLocalArgs } from "./local-args.js";
|
|
45
48
|
// `./local.js` is the published entry point for this command surface: the
|
|
46
49
|
// public CLI's generated root, commands/root.ts, doctor.ts and the test suite
|
|
@@ -76,6 +79,9 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
76
79
|
// including the scheduled one nobody types into (BLI-2362). Done here so it
|
|
77
80
|
// applies to whichever command carried the flag.
|
|
78
81
|
await rememberDiscoveryLimits(command);
|
|
82
|
+
// BLI-4110: the outermost stage name, so a crash that escapes every catch
|
|
83
|
+
// below still says which subcommand was running.
|
|
84
|
+
beginStage(`command:${command.kind}`);
|
|
79
85
|
try {
|
|
80
86
|
switch (command.kind) {
|
|
81
87
|
case "install":
|
|
@@ -148,7 +154,14 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
148
154
|
case "memory-log":
|
|
149
155
|
return await runMemoryLog(command, io);
|
|
150
156
|
case "memory":
|
|
157
|
+
// BLI-4047: a bare `cockpit memory` says what the noun does. It used
|
|
158
|
+
// to install; `install` is still one word away and every unattended
|
|
159
|
+
// caller asks for it by name in code.
|
|
160
|
+
if (command.action === "help")
|
|
161
|
+
return runMemoryVerbList(io, command.json);
|
|
151
162
|
return await runMemoryInstall(command, io);
|
|
163
|
+
case "memory-store":
|
|
164
|
+
return await runMemoryStore(command, io);
|
|
152
165
|
case "clean":
|
|
153
166
|
return await runClean(command, io);
|
|
154
167
|
case "docs":
|
|
@@ -167,6 +180,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
167
180
|
return await runSearch(command, io);
|
|
168
181
|
case "release":
|
|
169
182
|
return await runRelease(command, io);
|
|
183
|
+
case "usage":
|
|
184
|
+
return await runUsage(command, io);
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
catch (error) {
|
|
@@ -184,6 +199,9 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
184
199
|
writeLine(io.stderr, errorMessage(error));
|
|
185
200
|
return 1;
|
|
186
201
|
}
|
|
202
|
+
finally {
|
|
203
|
+
endStage(`command:${command.kind}`);
|
|
204
|
+
}
|
|
187
205
|
}
|
|
188
206
|
async function runDoctorLogin(command, io) {
|
|
189
207
|
return runLogin({
|
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
* rule are `@bli-cockpit/telemetry-core`'s `memory-hook-stats.ts`, which the
|
|
12
12
|
* writing package spends too. That is the whole point of putting them there.
|
|
13
13
|
*
|
|
14
|
-
* ## Which event, and why
|
|
14
|
+
* ## Which event, and why the detail is on one of them
|
|
15
15
|
*
|
|
16
|
-
* The PROMPT hook. It is the one a person waits on
|
|
17
|
-
* the one whose budget QA tick 18 caught it
|
|
18
|
-
* every turn — so it is the only one whose
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The PROMPT hook gets the full breakdown. It is the one a person waits on
|
|
17
|
+
* with their sentence typed, the one whose budget QA tick 18 caught it
|
|
18
|
+
* losing, and the one that runs on every turn — so it is the only one whose
|
|
19
|
+
* MISS RATE means anything as a daily number.
|
|
20
|
+
*
|
|
21
|
+
* BLI-4057 added the other two as a run count and a failure count each. Until
|
|
22
|
+
* then the fleet heard from one hook of three, so a SAVE hook that had
|
|
23
|
+
* stopped firing was invisible on every surface: `hooks ok` on the install
|
|
24
|
+
* receipt, 40 prompt recalls in the counts, and nothing anywhere saying that
|
|
25
|
+
* no memory had been written in ten days. Two numbers each is the smallest
|
|
26
|
+
* thing that can say "it ran" and "it ran and could not finish" apart.
|
|
21
27
|
*
|
|
22
28
|
* ## Absent is not zero
|
|
23
29
|
*
|
|
@@ -37,8 +43,14 @@ export function readMemoryHookCounts(options) {
|
|
|
37
43
|
const parsed = parseMemoryHookStats(raw);
|
|
38
44
|
if (!parsed.ok)
|
|
39
45
|
return { counts: null, reason: parsed.reason };
|
|
40
|
-
const
|
|
41
|
-
|
|
46
|
+
const at = options.now ?? new Date();
|
|
47
|
+
const window = summariseMemoryHookWindow(parsed.file, "prompt", { now: at, hours: 24 });
|
|
48
|
+
// BLI-4057. The same rows, the same function, the other two events — said
|
|
49
|
+
// out loud in the module header: the file always held all three and only one
|
|
50
|
+
// of them ever left the machine.
|
|
51
|
+
const stop = summariseMemoryHookWindow(parsed.file, "stop", { now: at, hours: 24 });
|
|
52
|
+
const sessionStart = summariseMemoryHookWindow(parsed.file, "session-start", {
|
|
53
|
+
now: at,
|
|
42
54
|
hours: 24,
|
|
43
55
|
});
|
|
44
56
|
return {
|
|
@@ -55,6 +67,15 @@ export function readMemoryHookCounts(options) {
|
|
|
55
67
|
: {}),
|
|
56
68
|
hook_skipped_trivial_24h: window.skippedTrivial,
|
|
57
69
|
hook_billing_exhausted_24h: window.billingExhausted,
|
|
70
|
+
// A lost deadline and a refusal are ONE number for these two, unlike the
|
|
71
|
+
// prompt hook above: for a recall the difference decides whether to
|
|
72
|
+
// re-fit a budget or pay a bill, and for a save both mean the same
|
|
73
|
+
// thing — nothing was written. The prompt hook keeps them apart because
|
|
74
|
+
// it is the only one whose miss rate is read as a rate.
|
|
75
|
+
hook_stop_runs_24h: stop.runs,
|
|
76
|
+
hook_stop_failed_24h: stop.failed + stop.timeouts,
|
|
77
|
+
hook_session_start_runs_24h: sessionStart.runs,
|
|
78
|
+
hook_session_start_failed_24h: sessionStart.failed + sessionStart.timeouts,
|
|
58
79
|
// Ticket 3934. Reported as the bin's LOWER bound — the smaller, true
|
|
59
80
|
// claim — and only when something was measured. A machine that recorded
|
|
60
81
|
// no histogram carries no key at all, which the board reads as
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory search|save|update|forget` — BLI Memory at a terminal
|
|
3
|
+
* (BLI-4047).
|
|
4
|
+
*
|
|
5
|
+
* Memory was the one Tower surface with no command-line door. The browser had
|
|
6
|
+
* one, the `bli-memory` MCP server had four tools, and a person or a script
|
|
7
|
+
* had nothing — so a machine without an agent host could not read or write the
|
|
8
|
+
* store at all, and `cockpit memory` typed on its own registered an MCP server
|
|
9
|
+
* instead of saying what the noun does. These four verbs are the twins of that
|
|
10
|
+
* server's four tools.
|
|
11
|
+
*
|
|
12
|
+
* **The same doors, never a second store.** `POST /api/memory/{search,save,
|
|
13
|
+
* update,forget}`, with this machine's collector device token, through
|
|
14
|
+
* `agent-door.ts` — the identical route the MCP server calls in
|
|
15
|
+
* `packages/cockpit-memory-mcp/src/server.ts` and the browser calls from
|
|
16
|
+
* `lib/memory/agent-memories/`. Nothing here talks to Postgres, embeds
|
|
17
|
+
* anything, or knows what a container alias is; every ranking, redaction and
|
|
18
|
+
* lifecycle decision stays server-side where its one implementation lives.
|
|
19
|
+
*
|
|
20
|
+
* **A refusal keeps the door's own reason label.** `needs_rls_client`,
|
|
21
|
+
* `unknown_memory`, `already_forgotten`, `already_superseded`,
|
|
22
|
+
* `needs_container_tag`, `no_match`, `custom_id_conflict`, `search_timed_out`
|
|
23
|
+
* — printed verbatim by `failAgentDoor`, never rephrased here. A CLI that
|
|
24
|
+
* invents its own prose for a server's refusal is a CLI whose error messages
|
|
25
|
+
* lie one release later.
|
|
26
|
+
*
|
|
27
|
+
* **The container tag is `@bli-cockpit/memory-mcp`'s, not a second copy.**
|
|
28
|
+
* `resolveContainerTag` is imported from that package rather than
|
|
29
|
+
* reimplemented, so a memory saved by `cockpit memory save` lands in exactly
|
|
30
|
+
* the space the MCP server and the three hooks read from. The scheme is the
|
|
31
|
+
* vendor plugin's byte for byte and two implementations of it would strand
|
|
32
|
+
* memories in a space nothing asks about — see that module's header.
|
|
33
|
+
*
|
|
34
|
+
* **A body never comes from the command line.** `save`, `update` and
|
|
35
|
+
* `forget --content-stdin` read stdin (or `--file`). Same discipline as
|
|
36
|
+
* `cockpit docs create` and `cockpit notes paste`, and for the same reasons:
|
|
37
|
+
* `ps` shows an argument list, shell history keeps it, and a memory with a
|
|
38
|
+
* newline in it cannot be quoted correctly anyway.
|
|
39
|
+
*/
|
|
40
|
+
import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.js";
|
|
41
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
42
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
43
|
+
import { readNoteFile } from "./notes-file.js";
|
|
44
|
+
const TAG = "[memory cli]";
|
|
45
|
+
const SEARCH_DEADLINE_MS = 60_000;
|
|
46
|
+
/** `--extract` is one model call, sometimes two; the door waits for it when asked to. */
|
|
47
|
+
const WRITE_DEADLINE_MS = 120_000;
|
|
48
|
+
/** The door's own ceiling on a memory (`saveMemoryInput`, 200k characters). */
|
|
49
|
+
const BODY_MAX_CHARS = 200_000;
|
|
50
|
+
/** The door's own ceiling on forget-by-text. */
|
|
51
|
+
const FORGET_MAX_CHARS = 10_000;
|
|
52
|
+
export async function runMemoryStore(command, io) {
|
|
53
|
+
const door = await openAgentDoor("memory", command, io);
|
|
54
|
+
switch (command.action) {
|
|
55
|
+
case "search":
|
|
56
|
+
return searchMemory(command, door);
|
|
57
|
+
case "save":
|
|
58
|
+
return saveMemory(command, door);
|
|
59
|
+
case "update":
|
|
60
|
+
return updateMemory(command, door);
|
|
61
|
+
case "forget":
|
|
62
|
+
return forgetMemory(command, door);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The verb list a bare `cockpit memory` prints (BLI-4047).
|
|
67
|
+
*
|
|
68
|
+
* It used to run the installer. Typing a noun to find out what it does is the
|
|
69
|
+
* single most common thing a person does with a new CLI, and this was the one
|
|
70
|
+
* command in the collector that answered that by writing five files in a home
|
|
71
|
+
* directory. Installing is still one word away, and every unattended caller
|
|
72
|
+
* asks for `install` by name in code.
|
|
73
|
+
*/
|
|
74
|
+
export function runMemoryVerbList(io, json) {
|
|
75
|
+
if (json) {
|
|
76
|
+
writeLine(io.stdout, JSON.stringify({
|
|
77
|
+
ok: true,
|
|
78
|
+
noun: "memory",
|
|
79
|
+
machine_verbs: ["install", "status"],
|
|
80
|
+
store_verbs: ["search", "save", "update", "forget"],
|
|
81
|
+
log_verbs: ["log"],
|
|
82
|
+
}));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
for (const line of [
|
|
86
|
+
"cockpit memory — BLI Memory: the store, and this machine's registration.",
|
|
87
|
+
"",
|
|
88
|
+
"The store (Tower, over /api/memory/*):",
|
|
89
|
+
" cockpit memory search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent] [--json]",
|
|
90
|
+
" cockpit memory save [--container <tag>] [--custom-id <id>] [--extract] [--file <path>] [--json]",
|
|
91
|
+
" the memory comes from stdin, or --file; never from the command line",
|
|
92
|
+
" cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
|
|
93
|
+
" cockpit memory forget <id> [--reason <label>] [--json]",
|
|
94
|
+
" cockpit memory forget --content-stdin --container <tag> [--reason <label>] [--json]",
|
|
95
|
+
"",
|
|
96
|
+
"This machine:",
|
|
97
|
+
" cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
|
|
98
|
+
" cockpit memory status [--json] read the stored config back",
|
|
99
|
+
"",
|
|
100
|
+
"The experience log:",
|
|
101
|
+
" cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
|
|
102
|
+
"",
|
|
103
|
+
"Omit --container and this repository's own memory space is used — the same",
|
|
104
|
+
"space the bli-memory MCP server and its hooks read and write.",
|
|
105
|
+
]) {
|
|
106
|
+
writeLine(io.stdout, line);
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* This repository's memory space, resolved by the MCP server's own resolver.
|
|
112
|
+
* `--container` wins; `--workspace` says which directory to derive it from.
|
|
113
|
+
*/
|
|
114
|
+
function containerTagsFor(command) {
|
|
115
|
+
if (command.containerTags.length > 0)
|
|
116
|
+
return command.containerTags;
|
|
117
|
+
return [resolveContainerTag({ cwd: command.workspace ?? process.cwd() }).containerTag];
|
|
118
|
+
}
|
|
119
|
+
async function searchMemory(command, door) {
|
|
120
|
+
const containerTags = containerTagsFor(command);
|
|
121
|
+
const answer = await askAgentDoor(door, {
|
|
122
|
+
path: "/api/memory/search",
|
|
123
|
+
method: "POST",
|
|
124
|
+
label: "memory search",
|
|
125
|
+
timeoutMs: SEARCH_DEADLINE_MS,
|
|
126
|
+
body: {
|
|
127
|
+
query: command.query,
|
|
128
|
+
containerTag: containerTags,
|
|
129
|
+
...(command.limit === undefined ? {} : { limit: command.limit }),
|
|
130
|
+
includeProfile: command.includeRecent !== false,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
if (!answer.ok)
|
|
134
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
135
|
+
const body = answer.body;
|
|
136
|
+
const results = body.results ?? [];
|
|
137
|
+
const recent = body.recent ?? [];
|
|
138
|
+
// Counts and labels only: a memory body never enters a log line.
|
|
139
|
+
writeLine(door.io.stderr, `${TAG} searched ${JSON.stringify({
|
|
140
|
+
requested_containers: containerTags.length,
|
|
141
|
+
searched_containers: body.containerTags?.length ?? containerTags.length,
|
|
142
|
+
results: results.length,
|
|
143
|
+
recent: recent.length,
|
|
144
|
+
dropped_below_floor: body.dropped_below_floor ?? 0,
|
|
145
|
+
degraded: body.degraded ?? null,
|
|
146
|
+
})}`);
|
|
147
|
+
if (door.json)
|
|
148
|
+
return emitAgentDoor(door, answer.body);
|
|
149
|
+
if (results.length === 0) {
|
|
150
|
+
// The distinction the whole store is built on: silence is not an outage.
|
|
151
|
+
// A failure took the `!answer.ok` branch above and named itself there.
|
|
152
|
+
writeLine(door.io.stdout, "No memories matched. The record is silent on this — it does not mean the store is down.");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
for (const row of results) {
|
|
156
|
+
writeLine(door.io.stdout, `${similarityLabel(row)}${oneLine(row["memory"])}`);
|
|
157
|
+
writeLine(door.io.stdout, ` ${String(row["id"] ?? "")} ${String(row["containerTag"] ?? "")}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (recent.length > 0) {
|
|
161
|
+
writeLine(door.io.stdout, "");
|
|
162
|
+
writeLine(door.io.stdout, "Recent context:");
|
|
163
|
+
for (const row of recent)
|
|
164
|
+
writeLine(door.io.stdout, ` - ${oneLine(row["preview"] ?? row["memory"])}`);
|
|
165
|
+
}
|
|
166
|
+
if (body.degraded) {
|
|
167
|
+
writeLine(door.io.stdout, "");
|
|
168
|
+
writeLine(door.io.stdout, `This answer is thinner than usual (${body.degraded}) — one search channel did not run.`);
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
async function saveMemory(command, door) {
|
|
173
|
+
const containerTag = containerTagsFor(command)[0];
|
|
174
|
+
const body = await readBody(command, door, BODY_MAX_CHARS, "A memory");
|
|
175
|
+
if (!body.ok)
|
|
176
|
+
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
177
|
+
if (!body.text.trim()) {
|
|
178
|
+
return failAgentDoor(door, TAG, "content_empty", "There is nothing to save. Pipe the memory in: `echo \"...\" | cockpit memory save`, or pass --file.");
|
|
179
|
+
}
|
|
180
|
+
const answer = await askAgentDoor(door, {
|
|
181
|
+
path: "/api/memory/save",
|
|
182
|
+
method: "POST",
|
|
183
|
+
label: "memory save",
|
|
184
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
185
|
+
body: {
|
|
186
|
+
content: body.text,
|
|
187
|
+
containerTag,
|
|
188
|
+
...(command.customId ? { customId: command.customId } : {}),
|
|
189
|
+
mode: command.extract ? "extract" : "verbatim",
|
|
190
|
+
// Same reason the MCP tool sets it: a caller that ASKED for distillation
|
|
191
|
+
// is waiting for the answer, so take the synchronous branch instead of
|
|
192
|
+
// the 202 the Stop hook wants.
|
|
193
|
+
...(command.extract ? { wait: true } : {}),
|
|
194
|
+
sourceRef: { kind: "cli" },
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
if (!answer.ok)
|
|
198
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
199
|
+
const saved = answer.body;
|
|
200
|
+
writeLine(door.io.stderr, `${TAG} saved ${JSON.stringify({
|
|
201
|
+
outcome: saved["outcome"] ?? null,
|
|
202
|
+
chars: body.text.length,
|
|
203
|
+
embedded: saved["embedded"] ?? null,
|
|
204
|
+
embed_skipped_reason: saved["embedSkippedReason"] ?? null,
|
|
205
|
+
extract: saved["extract"] ?? null,
|
|
206
|
+
masked_spans: saved["maskedSpans"] ?? 0,
|
|
207
|
+
})}`);
|
|
208
|
+
if (door.json)
|
|
209
|
+
return emitAgentDoor(door, answer.body);
|
|
210
|
+
// The container the SERVER used, which is the canonical one when this
|
|
211
|
+
// repository has more than one tag on the shelf; saying the tag we sent
|
|
212
|
+
// would name a space the row is not in.
|
|
213
|
+
const landedIn = String(saved["containerTag"] ?? containerTag);
|
|
214
|
+
writeLine(door.io.stdout, `Memory ${String(saved["outcome"] ?? "created")} in ${landedIn} (id: ${String(saved["id"] ?? "—")}).`);
|
|
215
|
+
if (saved["embedded"] === false && saved["embedSkippedReason"]) {
|
|
216
|
+
writeLine(door.io.stdout, `Stored without a search vector (${String(saved["embedSkippedReason"])}): findable by keyword, not yet by meaning.`);
|
|
217
|
+
}
|
|
218
|
+
if (saved["extract"] === "fell_back_verbatim") {
|
|
219
|
+
writeLine(door.io.stdout, `Saved as written; extraction failed (${String(saved["extractFailureReason"] ?? "unknown")}).`);
|
|
220
|
+
}
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
async function updateMemory(command, door) {
|
|
224
|
+
const body = await readBody(command, door, BODY_MAX_CHARS, "A memory");
|
|
225
|
+
if (!body.ok)
|
|
226
|
+
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
227
|
+
if (!body.text.trim()) {
|
|
228
|
+
return failAgentDoor(door, TAG, "content_empty", "An update needs replacement text on stdin or --file. To remove a memory, use `cockpit memory forget`.");
|
|
229
|
+
}
|
|
230
|
+
const answer = await askAgentDoor(door, {
|
|
231
|
+
path: "/api/memory/update",
|
|
232
|
+
method: "POST",
|
|
233
|
+
label: "memory update",
|
|
234
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
235
|
+
body: { id: command.memoryId, content: body.text },
|
|
236
|
+
});
|
|
237
|
+
if (!answer.ok)
|
|
238
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
239
|
+
const updated = answer.body;
|
|
240
|
+
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({
|
|
241
|
+
chars: body.text.length,
|
|
242
|
+
re_embedded: updated["reEmbedded"] ?? null,
|
|
243
|
+
embed_skipped_reason: updated["embedSkippedReason"] ?? null,
|
|
244
|
+
})}`);
|
|
245
|
+
if (door.json)
|
|
246
|
+
return emitAgentDoor(door, answer.body);
|
|
247
|
+
writeLine(door.io.stdout, `Updated. The new memory is ${String(updated["id"] ?? "")}, and ${String(updated["supersededId"] ?? command.memoryId ?? "")} is kept as its previous version.`);
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
async function forgetMemory(command, door) {
|
|
251
|
+
let content;
|
|
252
|
+
if (command.contentStdin) {
|
|
253
|
+
const body = await readBody(command, door, FORGET_MAX_CHARS, "Text to forget");
|
|
254
|
+
if (!body.ok)
|
|
255
|
+
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
256
|
+
if (!body.text.trim()) {
|
|
257
|
+
return failAgentDoor(door, TAG, "needs_id_or_content", "Nothing arrived on stdin to forget.");
|
|
258
|
+
}
|
|
259
|
+
content = body.text;
|
|
260
|
+
}
|
|
261
|
+
const answer = await askAgentDoor(door, {
|
|
262
|
+
path: "/api/memory/forget",
|
|
263
|
+
method: "POST",
|
|
264
|
+
label: "memory forget",
|
|
265
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
266
|
+
body: {
|
|
267
|
+
...(command.memoryId ? { id: command.memoryId } : {}),
|
|
268
|
+
...(content ? { content } : {}),
|
|
269
|
+
// Only defaulted for a text match, which is the case that would
|
|
270
|
+
// otherwise sweep every space this caller can read. An id needs none.
|
|
271
|
+
...(content ? { containerTag: containerTagsFor(command)[0] } : {}),
|
|
272
|
+
...(command.reason ? { reason: command.reason } : {}),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
if (!answer.ok)
|
|
276
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
277
|
+
const result = answer.body;
|
|
278
|
+
writeLine(door.io.stderr, `${TAG} forgot ${JSON.stringify({ count: result.count ?? 0, by: content ? "content" : "id" })}`);
|
|
279
|
+
if (door.json)
|
|
280
|
+
return emitAgentDoor(door, answer.body);
|
|
281
|
+
// The door answers with WHAT it removed, deliberately: a bare "done" is the
|
|
282
|
+
// only shape in which "forget X" quietly removing Y goes unnoticed.
|
|
283
|
+
writeLine(door.io.stdout, result.message ?? `Forgotten (${result.count ?? 0}).`);
|
|
284
|
+
for (const row of result.forgotten ?? []) {
|
|
285
|
+
const entry = row;
|
|
286
|
+
writeLine(door.io.stdout, ` ${String(entry["id"] ?? "")} ${oneLine(entry["textPreview"])}`);
|
|
287
|
+
}
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
/** A body from `--file`, else stdin. An interactive terminal yields nothing rather than hanging. */
|
|
291
|
+
async function readBody(command, door, maxChars, what) {
|
|
292
|
+
if (command.filePath) {
|
|
293
|
+
const read = await readNoteFile(command.filePath);
|
|
294
|
+
if (!read.ok) {
|
|
295
|
+
return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
|
|
296
|
+
}
|
|
297
|
+
return { ok: true, text: read.bytes.toString("utf8") };
|
|
298
|
+
}
|
|
299
|
+
if (isInteractiveStdin(door.io))
|
|
300
|
+
return { ok: true, text: "" };
|
|
301
|
+
try {
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
text: await readPipedText(door.io.stdin, {
|
|
305
|
+
maxChars,
|
|
306
|
+
overflowMessage: `${what} is limited to ${maxChars} characters.`,
|
|
307
|
+
}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: "content_too_long",
|
|
314
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** `[87%] ` when the row was found by meaning, empty when it was a keyword hit. */
|
|
319
|
+
function similarityLabel(row) {
|
|
320
|
+
const channels = row["channels"];
|
|
321
|
+
if (Array.isArray(channels) && !channels.includes("semantic"))
|
|
322
|
+
return "";
|
|
323
|
+
const similarity = Number(row["similarity"]);
|
|
324
|
+
return Number.isFinite(similarity) && similarity >= 0 && similarity <= 1
|
|
325
|
+
? `[${Math.round(similarity * 100)}%] `
|
|
326
|
+
: "";
|
|
327
|
+
}
|
|
328
|
+
/** One terminal line: newlines folded, so a long memory cannot break the listing. */
|
|
329
|
+
function oneLine(value) {
|
|
330
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
331
|
+
}
|
|
@@ -180,6 +180,43 @@ export async function shareNote(command, door) {
|
|
|
180
180
|
sayUpload(door, body);
|
|
181
181
|
return 0;
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* `cockpit notes place <note-id> [--apply]` — BLI-4058.
|
|
185
|
+
*
|
|
186
|
+
* Asks Tower where a note belongs and prints the reason. Without `--apply` it
|
|
187
|
+
* moves nothing: knowing where a note would go and filing it there are two
|
|
188
|
+
* different acts, and the read-only one is the default because it is the one
|
|
189
|
+
* somebody types while they are still deciding.
|
|
190
|
+
*/
|
|
191
|
+
export async function placeNote(command, door) {
|
|
192
|
+
const applying = command.apply === true;
|
|
193
|
+
const answer = await ask(door, {
|
|
194
|
+
path: "/api/notes/place",
|
|
195
|
+
method: "POST",
|
|
196
|
+
label: applying ? "notes place --apply" : "notes place",
|
|
197
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
198
|
+
body: { note_id: command.noteId, apply: applying },
|
|
199
|
+
});
|
|
200
|
+
if (!answer.ok)
|
|
201
|
+
return fail(door, answer.reason, answer.detail);
|
|
202
|
+
const body = answer.body;
|
|
203
|
+
writeLine(door.io.stderr, `${TAG} place answered ${JSON.stringify({
|
|
204
|
+
note_id: command.noteId ?? null,
|
|
205
|
+
ok: body.ok === true,
|
|
206
|
+
decided_by: body.decidedBy ?? null,
|
|
207
|
+
applied: body.applied === true,
|
|
208
|
+
reason: body.reason ?? null,
|
|
209
|
+
})}`);
|
|
210
|
+
if (door.json)
|
|
211
|
+
return emit(door, body, body.ok === true ? 0 : 1);
|
|
212
|
+
writeLine(door.io.stdout, body.headline ?? "Tower answered without a sentence.");
|
|
213
|
+
for (const line of body.lines ?? [])
|
|
214
|
+
writeLine(door.io.stdout, line);
|
|
215
|
+
if (body.ok === true && !applying) {
|
|
216
|
+
writeLine(door.io.stdout, "Nothing moved. Add --apply to file it there.");
|
|
217
|
+
}
|
|
218
|
+
return body.ok === true ? 0 : 1;
|
|
219
|
+
}
|
|
183
220
|
export async function moveNote(command, door) {
|
|
184
221
|
const answer = await ask(door, {
|
|
185
222
|
path: "/api/notes/move",
|
package/dist/commands/notes.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* table of contents). Every responsibility lives in a `notes-*.ts` sibling:
|
|
28
28
|
*
|
|
29
29
|
* - `notes-reads.ts` — the library, shelves list, one note, your own shelf.
|
|
30
|
-
* - `notes-writes.ts` — upload, paste, share/unshare, move.
|
|
30
|
+
* - `notes-writes.ts` — upload, paste, share/unshare, move, place.
|
|
31
31
|
* - `notes-door.ts` — the one HTTP request, and the shared answer / refusal /
|
|
32
32
|
* emit / say-* rendering both halves above call.
|
|
33
33
|
*
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { loadPairedSession } from "../tower-client.js";
|
|
37
37
|
import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
38
|
-
import { uploadNotes, pasteNote, shareNote, moveNote } from "./notes-writes.js";
|
|
38
|
+
import { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
39
39
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
40
|
-
export { uploadNotes, pasteNote, shareNote, moveNote } from "./notes-writes.js";
|
|
40
|
+
export { uploadNotes, pasteNote, shareNote, moveNote, placeNote } from "./notes-writes.js";
|
|
41
41
|
export { ask, emit, fail, sayUpload, sayScope, errorText, TAG, } from "./notes-door.js";
|
|
42
42
|
export async function runNotes(command, io) {
|
|
43
43
|
const session = await loadPairedSession("notes", command.homeDir);
|
|
@@ -65,5 +65,7 @@ export async function runNotes(command, io) {
|
|
|
65
65
|
return shareNote(command, door);
|
|
66
66
|
case "move":
|
|
67
67
|
return moveNote(command, door);
|
|
68
|
+
case "place":
|
|
69
|
+
return placeNote(command, door);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -101,7 +101,11 @@ options = {}) {
|
|
|
101
101
|
// `STALE collector-fleet 11h` was already misread once (BLI-3699) as "the
|
|
102
102
|
// fleet", not "one machine in it", and the name is otherwise buried in
|
|
103
103
|
// `detail` below.
|
|
104
|
-
|
|
104
|
+
// BLI-4057 generalised it from `stale` alone: `memory-hooks` names the
|
|
105
|
+
// machine whose hooks stopped and reads `failing`, because nothing about
|
|
106
|
+
// it is late. A row that took the trouble to name a device is naming it
|
|
107
|
+
// for the reader, whatever word it printed beside it.
|
|
108
|
+
const staleName = row.staleDeviceName ? ` ${dim(`(${row.staleDeviceName})`)}` : "";
|
|
105
109
|
lines.push(`${verdict} ${id} ${age}${staleName} ${dim(intervalWord(row))}`);
|
|
106
110
|
if (row.verdict !== "healthy") {
|
|
107
111
|
// The absence of an age is explained BEFORE the detail, because it is
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.99");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
export async function runUsage(command, io) {
|
|
4
|
+
const door = await openAgentDoor("usage", command, io);
|
|
5
|
+
const query = new URLSearchParams({ since: command.since });
|
|
6
|
+
if (command.until)
|
|
7
|
+
query.set("until", command.until);
|
|
8
|
+
if (command.includeAutomated)
|
|
9
|
+
query.set("include_automated", "1");
|
|
10
|
+
const answer = await askAgentDoor(door, { path: `/api/usage/people?${query}`, method: "GET", label: "usage people", timeoutMs: 30_000 });
|
|
11
|
+
if (!answer.ok)
|
|
12
|
+
return failAgentDoor(door, "[usage]", answer.reason, answer.detail);
|
|
13
|
+
const body = answer.body;
|
|
14
|
+
if (door.json)
|
|
15
|
+
return emitAgentDoor(door, body);
|
|
16
|
+
writeLine(io.stdout, "PERSON TOKENS OUTPUT LIST EQUIVALENT COVERAGE");
|
|
17
|
+
for (const row of body.people ?? [])
|
|
18
|
+
writeLine(io.stdout, `${(row.display_name ?? row.email ?? "Unknown").slice(0, 20).padEnd(20)} ${String(row.tokens_total).padStart(11)} ${String(row.output_tokens).padStart(11)} ${`$${row.api_list_price_equivalent_usd.toFixed(2)}`.padStart(15)} ${row.sessions_extracted}/${row.sessions_observed}`);
|
|
19
|
+
writeLine(io.stdout, "");
|
|
20
|
+
writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent (not actual spend)");
|
|
21
|
+
writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|