@mercury-fw/core 0.25.1 → 0.27.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @mercury-fw/core
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @mercury-fw/plugin-types@0.27.0
8
+ - @mercury-fw/channel-types@0.27.0
9
+ - @mercury-fw/cli-engine@0.27.0
10
+ - @mercury-fw/confirm-engine@0.27.0
11
+
12
+ ## 0.26.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 8b92abb: - `mfw` operates an app from inside its folder: `start`, `stop` and `restart` (with `--no-cache`), `logs`, `repl`, `shell`, all as `docker compose` calls.
17
+ - `mfw vault` maintains the wiki vault (`list`, `read`, `grep`, `write-curated`, `write-raw`) in a one-off container.
18
+ - `mfw memory list` and `mfw memory read <collection>` read the memory on Qdrant, newest first where the collection has a timestamp index.
19
+ - `mfw reset memory` and `mfw reset wiki` delete a memory volume after you type the app's name, then bring the service back up empty; after a memory reset a running app is restarted, so it sets up its collections again.
20
+ - The command line is declared with commander: `--help` at every level, a suggestion for a mistyped command, and every argument checked before anything runs.
21
+ - A scaffolded app lists `@mercury-fw/cli` among its devDependencies, at the framework's version, and its README runs it through `bunx mfw`.
22
+ - A scaffolded app's `@types/bun` and `typescript` use caret ranges instead of exact versions.
23
+ - The core ships a read-only memory CLI (`src/memory/memory-cli.ts`) next to the vault one, which is what `mfw memory` runs.
24
+ - The vault CLI says so when asked to read a note that doesn't exist, instead of printing a stack trace.
25
+
26
+ ### Patch Changes
27
+
28
+ - @mercury-fw/plugin-types@0.26.0
29
+ - @mercury-fw/channel-types@0.26.0
30
+ - @mercury-fw/cli-engine@0.26.0
31
+ - @mercury-fw/confirm-engine@0.26.0
32
+
3
33
  ## 0.25.1
4
34
 
5
35
  ### Patch Changes
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bun
2
+ type ScrollOffset = string | number | Record<string, unknown> | null;
3
+ /** The slice of the Qdrant client this CLI uses. */
4
+ export type MemoryCliClient = {
5
+ getCollections(): Promise<{
6
+ collections: Array<{
7
+ name: string;
8
+ }>;
9
+ }>;
10
+ count(collection: string, params: {
11
+ exact: boolean;
12
+ }): Promise<{
13
+ count: number;
14
+ }>;
15
+ scroll(collection: string, params: {
16
+ limit: number;
17
+ offset?: ScrollOffset;
18
+ with_payload: boolean;
19
+ order_by?: {
20
+ key: string;
21
+ direction: "asc" | "desc";
22
+ };
23
+ }): Promise<{
24
+ points: Array<{
25
+ id: string | number;
26
+ payload?: Record<string, unknown> | null;
27
+ }>;
28
+ next_page_offset?: ScrollOffset;
29
+ }>;
30
+ };
31
+ type Io = {
32
+ client: MemoryCliClient;
33
+ out: (line: string) => void;
34
+ err: (line: string) => void;
35
+ };
36
+ /** Runs the CLI on `argv` against `io.client`; returns the exit code. */
37
+ export declare function runMemoryCli(argv: string[], { client, out, err }: Io): Promise<number>;
38
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/core",
3
- "version": "0.25.1",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@mercury-fw/channel-types": "0.25.1",
35
- "@mercury-fw/cli-engine": "0.25.1",
36
- "@mercury-fw/confirm-engine": "0.25.1",
37
- "@mercury-fw/plugin-types": "0.25.1",
34
+ "@mercury-fw/channel-types": "0.27.0",
35
+ "@mercury-fw/cli-engine": "0.27.0",
36
+ "@mercury-fw/confirm-engine": "0.27.0",
37
+ "@mercury-fw/plugin-types": "0.27.0",
38
38
  "@qdrant/js-client-rest": "^1.19.0",
39
39
  "ai": "^7.0.77",
40
40
  "ai-sdk-ollama": "^4.2.0",
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Read-only CLI for Layer-3 memory on Qdrant — runs INSIDE the Mercury
4
+ * container (where `QDRANT_URL` reaches Qdrant): `mfw memory` runs it in a
5
+ * one-off container, by this path.
6
+ * `list` shows the collections and their sizes, `read` a collection's points:
7
+ * newest first where the collection has a `timestamp` payload index (episodic
8
+ * memory, the verbatim archive), in Qdrant's own order otherwise, and it says
9
+ * which. Nothing here writes.
10
+ */
11
+ import { parseArgs } from "node:util";
12
+ import { QdrantClient } from "@qdrant/js-client-rest";
13
+ import { scrollCollection } from "../admin/qdrant-scroll.ts";
14
+
15
+ type ScrollOffset = string | number | Record<string, unknown> | null;
16
+
17
+ /** The slice of the Qdrant client this CLI uses. */
18
+ export type MemoryCliClient = {
19
+ getCollections(): Promise<{ collections: Array<{ name: string }> }>;
20
+ count(collection: string, params: { exact: boolean }): Promise<{ count: number }>;
21
+ scroll(
22
+ collection: string,
23
+ params: {
24
+ limit: number;
25
+ offset?: ScrollOffset;
26
+ with_payload: boolean;
27
+ order_by?: { key: string; direction: "asc" | "desc" };
28
+ },
29
+ ): Promise<{
30
+ points: Array<{ id: string | number; payload?: Record<string, unknown> | null }>;
31
+ next_page_offset?: ScrollOffset;
32
+ }>;
33
+ };
34
+
35
+ type Io = { client: MemoryCliClient; out: (line: string) => void; err: (line: string) => void };
36
+
37
+ const USAGE = [
38
+ "Usage: mfw memory <command> [args]",
39
+ "",
40
+ "Commands:",
41
+ " list the collections and their points",
42
+ " read <collection> [--limit N] a collection's points, newest first where it can (default 20)",
43
+ ];
44
+
45
+ /** A point as the lines `read` prints: its id, then one indented line per payload field. */
46
+ function pointLines(point: { id: string | number; payload?: Record<string, unknown> | null }): string[] {
47
+ const fields = Object.entries(point.payload ?? {}).map(
48
+ ([key, value]) => ` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
49
+ );
50
+ return [String(point.id), ...fields, ""];
51
+ }
52
+
53
+ /** Whether `err` is Qdrant refusing to order by a field it has no index on
54
+ * (HTTP 400, "No range index for `order_by` key"). */
55
+ function isMissingOrderIndex(err: unknown): boolean {
56
+ const e = err as { status?: number; message?: string; data?: unknown };
57
+ const text = `${e?.message ?? ""} ${JSON.stringify(e?.data ?? "")}`;
58
+ return (e?.status === 400 || /\b400\b/.test(text)) && /index/i.test(text);
59
+ }
60
+
61
+ /** Runs the CLI on `argv` against `io.client`; returns the exit code. */
62
+ export async function runMemoryCli(argv: string[], { client, out, err }: Io): Promise<number> {
63
+ const usage = () => {
64
+ USAGE.forEach((line) => err(line));
65
+ return 1;
66
+ };
67
+ let parsed;
68
+ try {
69
+ parsed = parseArgs({ args: argv, options: { limit: { type: "string" } }, allowPositionals: true });
70
+ } catch {
71
+ return usage();
72
+ }
73
+ const [command, collection, ...extra] = parsed.positionals;
74
+ if (extra.length > 0 || (command === "list" && (collection !== undefined || parsed.values.limit !== undefined))) {
75
+ return usage();
76
+ }
77
+ if (command !== "list" && !(command === "read" && collection !== undefined)) return usage();
78
+
79
+ const limitText = parsed.values.limit ?? "20";
80
+ const limit = Number(limitText);
81
+ if (!/^\d+$/.test(limitText) || limit < 1) {
82
+ err(`--limit takes a positive whole number (got "${limitText}").`);
83
+ return 1;
84
+ }
85
+
86
+ let names: string[];
87
+ try {
88
+ names = (await client.getCollections()).collections.map((c) => c.name).sort();
89
+ } catch (e) {
90
+ err(`Can't reach Qdrant: ${String(e)}`);
91
+ return 1;
92
+ }
93
+
94
+ try {
95
+ if (command === "list") {
96
+ if (names.length === 0) {
97
+ out("No collections yet.");
98
+ return 0;
99
+ }
100
+ for (const name of names) {
101
+ out(`${name} ${(await client.count(name, { exact: true })).count} points`);
102
+ }
103
+ return 0;
104
+ }
105
+
106
+ if (!names.includes(collection as string)) {
107
+ err(`No collection "${collection}". There are: ${names.join(", ") || "none"}.`);
108
+ return 1;
109
+ }
110
+ let points: Array<{ id: string | number; payload?: Record<string, unknown> | null }>;
111
+ try {
112
+ // Qdrant orders by a payload field only when it has an index on it.
113
+ ({ points } = await client.scroll(collection as string, {
114
+ limit,
115
+ with_payload: true,
116
+ order_by: { key: "timestamp", direction: "desc" },
117
+ }));
118
+ } catch (e) {
119
+ if (!isMissingOrderIndex(e)) throw e;
120
+ err(`${collection} has no timestamp index: points in Qdrant's own order.`);
121
+ ({ points } = await scrollCollection(client, collection as string, { limit }));
122
+ }
123
+ points.flatMap(pointLines).forEach((line) => out(line));
124
+ return 0;
125
+ } catch (e) {
126
+ err(`Qdrant error: ${String(e)}`);
127
+ return 1;
128
+ }
129
+ }
130
+
131
+ if (import.meta.main) {
132
+ const client = new QdrantClient({ url: process.env.QDRANT_URL ?? "http://qdrant:6333" });
133
+ process.exit(
134
+ await runMemoryCli(process.argv.slice(2), {
135
+ client,
136
+ out: (line) => console.log(line),
137
+ err: (line) => console.error(line),
138
+ }),
139
+ );
140
+ }
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * Maintenance CLI for the wiki vault — runs INSIDE the Mercury container
4
- * (the vault is a Docker named volume, not a host path, see `scripts/vault.sh`
5
- * and CLAUDE.md § "Manutenzione della vault wiki"). Thin argv wrapper around
4
+ * (the vault is a Docker named volume, not a host path): `mfw vault` runs it
5
+ * in a one-off container, by this path. Thin argv wrapper around
6
6
  * functions that already exist and are already tested (`wiki-note.ts`,
7
7
  * `vault-init.ts`) — no new write/read logic here, only routing.
8
8
  *
@@ -29,7 +29,7 @@ import { writeCuratedNote, writeRawEntry } from "./wiki-note.ts";
29
29
  function usage(): never {
30
30
  console.error(
31
31
  [
32
- "Usage: vault-cli <command> [args]",
32
+ "Usage: mfw vault <command> [args]",
33
33
  "",
34
34
  "Commands:",
35
35
  " write-curated <curated/...path.md> [--author NAME] body read from stdin",
@@ -107,7 +107,12 @@ async function main(): Promise<void> {
107
107
  case "read": {
108
108
  const relativePath = args[0];
109
109
  if (!relativePath) usage();
110
- console.log(await Bun.file(`${vaultPath}/${relativePath}`).text());
110
+ const note = Bun.file(`${vaultPath}/${relativePath}`);
111
+ if (!(await note.exists())) {
112
+ console.error(`no note at ${relativePath} (mfw vault list shows them)`);
113
+ process.exit(1);
114
+ }
115
+ console.log(await note.text());
111
116
  break;
112
117
  }
113
118