membot 0.8.0 → 0.10.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/README.md +3 -0
- package/package.json +1 -1
- package/src/config/schemas.ts +19 -0
- package/src/constants.ts +15 -0
- package/src/ingest/concurrency.ts +60 -0
- package/src/ingest/describer.ts +49 -3
- package/src/ingest/ingest.ts +277 -67
- package/src/operations/add.ts +49 -17
- package/src/operations/refresh.ts +43 -24
- package/src/output/formatter.ts +21 -0
- package/src/output/logger.ts +36 -0
- package/src/output/progress.ts +408 -46
|
@@ -3,9 +3,29 @@ import { resolveEmbeddingWorkers } from "../context.ts";
|
|
|
3
3
|
import { listDueRefreshes } from "../db/files.ts";
|
|
4
4
|
import { withEmbedderPool } from "../ingest/embedder-pool.ts";
|
|
5
5
|
import { colors } from "../output/formatter.ts";
|
|
6
|
+
import { isInteractive } from "../output/tty.ts";
|
|
6
7
|
import { refreshOne } from "../refresh/runner.ts";
|
|
7
8
|
import { defineOperation } from "./types.ts";
|
|
8
9
|
|
|
10
|
+
interface RefreshEntry {
|
|
11
|
+
logical_path: string;
|
|
12
|
+
status: "ok" | "unchanged" | "failed";
|
|
13
|
+
new_version_id?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Render one refresh result as a persistent stderr / final-summary line. */
|
|
18
|
+
function formatEntryLine(p: RefreshEntry): string {
|
|
19
|
+
if (p.status === "ok") {
|
|
20
|
+
const ver = p.new_version_id ? colors.dim(`→ ${p.new_version_id}`) : "";
|
|
21
|
+
return `${colors.green("✓")} ${colors.cyan(p.logical_path)} ${ver}`;
|
|
22
|
+
}
|
|
23
|
+
if (p.status === "unchanged") {
|
|
24
|
+
return `${colors.dim("·")} ${colors.dim(p.logical_path)} ${colors.dim("(unchanged)")}`;
|
|
25
|
+
}
|
|
26
|
+
return `${colors.red("✗")} ${p.logical_path} ${colors.dim(p.error ?? "")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
export const refreshOperation = defineOperation({
|
|
10
30
|
name: "membot_refresh",
|
|
11
31
|
cliName: "refresh",
|
|
@@ -31,22 +51,23 @@ export const refreshOperation = defineOperation({
|
|
|
31
51
|
let updated = 0;
|
|
32
52
|
let unchanged = 0;
|
|
33
53
|
let failed = 0;
|
|
34
|
-
const
|
|
35
|
-
if (p.status === "ok")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
if (p.status === "unchanged") {
|
|
41
|
-
unchanged++;
|
|
42
|
-
return `${colors.dim("·")} ${colors.dim(p.logical_path)} ${colors.dim("(unchanged)")}`;
|
|
43
|
-
}
|
|
44
|
-
failed++;
|
|
45
|
-
return `${colors.red("✗")} ${p.logical_path} ${colors.dim(p.error ?? "")}`;
|
|
46
|
-
});
|
|
54
|
+
for (const p of result.processed) {
|
|
55
|
+
if (p.status === "ok") updated++;
|
|
56
|
+
else if (p.status === "unchanged") unchanged++;
|
|
57
|
+
else failed++;
|
|
58
|
+
}
|
|
47
59
|
const parts = [colors.green(`updated ${updated}`), colors.dim(`unchanged ${unchanged}`)];
|
|
48
60
|
if (failed) parts.push(colors.red(`failed ${failed}`));
|
|
49
|
-
|
|
61
|
+
const summary = parts.join(", ");
|
|
62
|
+
|
|
63
|
+
// In interactive mode the per-entry results were already streamed to
|
|
64
|
+
// stderr via progress.entry() during the run; printing the same list
|
|
65
|
+
// to stdout would just duplicate the scrollback. Non-interactive
|
|
66
|
+
// callers (JSON, piped, CI) still get the full list.
|
|
67
|
+
if (isInteractive()) return summary;
|
|
68
|
+
|
|
69
|
+
const lines = result.processed.map(formatEntryLine);
|
|
70
|
+
return `${lines.join("\n")}\n${summary}`;
|
|
50
71
|
},
|
|
51
72
|
handler: async (input, ctx) => {
|
|
52
73
|
// Per-command embedder pool: workers come up at the start of the
|
|
@@ -57,21 +78,19 @@ export const refreshOperation = defineOperation({
|
|
|
57
78
|
const targets = input.logical_path
|
|
58
79
|
? [input.logical_path]
|
|
59
80
|
: (await listDueRefreshes(ctx.db)).map((r) => r.logical_path);
|
|
60
|
-
const out:
|
|
61
|
-
logical_path: string;
|
|
62
|
-
status: "ok" | "unchanged" | "failed";
|
|
63
|
-
new_version_id?: string;
|
|
64
|
-
error?: string;
|
|
65
|
-
}> = [];
|
|
81
|
+
const out: RefreshEntry[] = [];
|
|
66
82
|
ctx.progress.start(targets.length, "refresh");
|
|
67
83
|
for (const path of targets) {
|
|
68
|
-
ctx.progress.
|
|
84
|
+
ctx.progress.setLabel(path);
|
|
85
|
+
let entry: RefreshEntry;
|
|
69
86
|
try {
|
|
70
|
-
|
|
71
|
-
out.push(r);
|
|
87
|
+
entry = await refreshOne(ctx, path, input.force, (sublabel) => ctx.progress.update(sublabel));
|
|
72
88
|
} catch (err) {
|
|
73
|
-
|
|
89
|
+
entry = { logical_path: path, status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
74
90
|
}
|
|
91
|
+
out.push(entry);
|
|
92
|
+
ctx.progress.tick(path);
|
|
93
|
+
ctx.progress.entry(formatEntryLine(entry));
|
|
75
94
|
}
|
|
76
95
|
ctx.progress.done(`refresh: ${out.filter((r) => r.status === "ok").length}/${out.length} updated`);
|
|
77
96
|
return { processed: out, count: out.length };
|
package/src/output/formatter.ts
CHANGED
|
@@ -18,6 +18,27 @@ export function renderResult<T>(result: T, opts: { console_formatter?: (result:
|
|
|
18
18
|
return JSON.stringify(result, null, 2);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Format a byte count as a short human-readable string: 5654 → `5.5 KB`,
|
|
23
|
+
* 14_859 → `14.5 KB`, 2_345_678 → `2.2 MB`. Uses 1024-based units (binary
|
|
24
|
+
* prefixes) since file sizes on disk are typically reported that way.
|
|
25
|
+
* Negative or non-finite inputs render as `0 B`.
|
|
26
|
+
*/
|
|
27
|
+
export function formatBytes(n: number): string {
|
|
28
|
+
if (!Number.isFinite(n) || n < 0) return "0 B";
|
|
29
|
+
if (n < 1024) return `${n} B`;
|
|
30
|
+
const units = ["KB", "MB", "GB", "TB"] as const;
|
|
31
|
+
let value = n / 1024;
|
|
32
|
+
let unit: string = units[0];
|
|
33
|
+
for (let i = 1; i < units.length && value >= 1024; i++) {
|
|
34
|
+
value /= 1024;
|
|
35
|
+
unit = units[i] as string;
|
|
36
|
+
}
|
|
37
|
+
// One decimal until 100, then round to integer (so the column stays narrow).
|
|
38
|
+
const formatted = value < 100 ? value.toFixed(1) : `${Math.round(value)}`;
|
|
39
|
+
return `${formatted} ${unit}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
/**
|
|
22
43
|
* Pretty-print a 2D array of cells as an aligned table. Column widths are
|
|
23
44
|
* computed from the visible (escape-stripped) length of each cell so coloured
|
package/src/output/logger.ts
CHANGED
|
@@ -9,6 +9,17 @@ export interface Spinner {
|
|
|
9
9
|
stop(): void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Anything occupying a fixed area of stderr that needs to be torn down before
|
|
14
|
+
* the logger writes a stray line, then redrawn afterward. nanospinner's
|
|
15
|
+
* single-line spinner and progress.ts's multi-line worker view both implement
|
|
16
|
+
* this so log/info/warn lines don't shred the live display.
|
|
17
|
+
*/
|
|
18
|
+
export interface LiveArea {
|
|
19
|
+
clear(): void;
|
|
20
|
+
render(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const NOOP_SPINNER: Spinner = { update() {}, success() {}, error() {}, stop() {} };
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -20,6 +31,7 @@ const NOOP_SPINNER: Spinner = { update() {}, success() {}, error() {}, stop() {}
|
|
|
20
31
|
class Logger {
|
|
21
32
|
private static instance: Logger;
|
|
22
33
|
private activeSpinner: ReturnType<typeof createSpinner> | null = null;
|
|
34
|
+
private activeLiveArea: LiveArea | null = null;
|
|
23
35
|
|
|
24
36
|
/** Singleton accessor. Use the exported `logger` const instead in normal code. */
|
|
25
37
|
static getInstance(): Logger {
|
|
@@ -31,7 +43,24 @@ class Logger {
|
|
|
31
43
|
return useColor() ? fn(msg) : msg;
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Register a multi-line live display. Logger will `clear()` it before any
|
|
48
|
+
* stderr write and `render()` it after, so log lines don't punch through
|
|
49
|
+
* the live area. Pass null to deregister. Mutually exclusive with the
|
|
50
|
+
* nanospinner path (only one live thing on stderr at a time).
|
|
51
|
+
*/
|
|
52
|
+
setActiveLiveArea(area: LiveArea | null): void {
|
|
53
|
+
this.activeLiveArea = area;
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
private writeStderr(msg: string): void {
|
|
57
|
+
const area = this.activeLiveArea;
|
|
58
|
+
if (area) {
|
|
59
|
+
area.clear();
|
|
60
|
+
process.stderr.write(`${msg}\n`);
|
|
61
|
+
area.render();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
35
64
|
if (this.activeSpinner) {
|
|
36
65
|
this.activeSpinner.clear();
|
|
37
66
|
process.stderr.write(`${msg}\n`);
|
|
@@ -66,6 +95,13 @@ class Logger {
|
|
|
66
95
|
|
|
67
96
|
/** Raw stderr write, no formatting added. Spinner-aware. */
|
|
68
97
|
writeRaw(msg: string): void {
|
|
98
|
+
const area = this.activeLiveArea;
|
|
99
|
+
if (area) {
|
|
100
|
+
area.clear();
|
|
101
|
+
process.stderr.write(msg);
|
|
102
|
+
area.render();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
69
105
|
if (this.activeSpinner) {
|
|
70
106
|
this.activeSpinner.clear();
|
|
71
107
|
process.stderr.write(msg);
|