pi-blackhole 0.4.2 → 0.4.3
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 +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field value validation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Each `validateFieldValue` call checks a single value against the
|
|
5
|
+
* field's type-specific constraints (enum membership, boolean type,
|
|
6
|
+
* string no-newlines, number range / integer / values / step) and
|
|
7
|
+
* returns either `undefined` (valid) or a warning message string.
|
|
8
|
+
*
|
|
9
|
+
* The primary consumer is the settings modal (body.ts), which shows
|
|
10
|
+
* per-field warnings in the focused-row description area. Callers
|
|
11
|
+
* can also use this in ConfigManager to skip invalid values during
|
|
12
|
+
* save, or to repair loaded configs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Field } from "./types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate a field value against the field's type-specific constraints.
|
|
19
|
+
*
|
|
20
|
+
* @param field - The field definition
|
|
21
|
+
* @param value - The value to validate
|
|
22
|
+
* @returns A warning message if invalid, `undefined` if valid
|
|
23
|
+
*/
|
|
24
|
+
export function validateFieldValue(
|
|
25
|
+
field: Field,
|
|
26
|
+
value: unknown,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (value === undefined || value === null) return undefined;
|
|
29
|
+
|
|
30
|
+
switch (field.type) {
|
|
31
|
+
case "enum": {
|
|
32
|
+
if (typeof value !== "string")
|
|
33
|
+
return `Must be a string, got ${typeof value}`;
|
|
34
|
+
if (!field.options.includes(value as never))
|
|
35
|
+
return `Must be one of: ${field.options.join(", ")}`;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "boolean": {
|
|
40
|
+
if (typeof value !== "boolean")
|
|
41
|
+
return `Must be a boolean, got ${typeof value}`;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "string": {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return `Must be a string, got ${typeof value}`;
|
|
48
|
+
if (value.includes("\n") || value.includes("\r"))
|
|
49
|
+
return "Must be a single-line string (no newlines)";
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "text": {
|
|
54
|
+
if (typeof value !== "string")
|
|
55
|
+
return `Must be a string, got ${typeof value}`;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "number": {
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
61
|
+
return `Must be a finite number, got ${typeof value === "number" ? String(value) : typeof value}`;
|
|
62
|
+
if (field.integer && !Number.isInteger(value))
|
|
63
|
+
return "Must be an integer";
|
|
64
|
+
if (field.values !== undefined && field.values.length > 0) {
|
|
65
|
+
if (!field.values.includes(value))
|
|
66
|
+
return `Must be one of: ${field.values.join(", ")}`;
|
|
67
|
+
}
|
|
68
|
+
if (typeof field.min === "number" && value < field.min)
|
|
69
|
+
return `Must be at least ${field.min}`;
|
|
70
|
+
if (typeof field.max === "number" && value > field.max)
|
|
71
|
+
return `Must be at most ${field.max}`;
|
|
72
|
+
if (field.step !== undefined && field.step > 0) {
|
|
73
|
+
// Check alignment to step within floating-point tolerance
|
|
74
|
+
const min = field.min ?? 0;
|
|
75
|
+
const offset = value - min;
|
|
76
|
+
const rem = offset % field.step;
|
|
77
|
+
if (rem > 1e-9 && field.step - rem > 1e-9) {
|
|
78
|
+
return `Must be aligned to step ${field.step}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "secret": {
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
return `Must be a string, got ${typeof value}`;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "path": {
|
|
91
|
+
if (typeof value !== "string")
|
|
92
|
+
return `Must be a string, got ${typeof value}`;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "model": {
|
|
97
|
+
if (typeof value !== "object" || value === null)
|
|
98
|
+
return `Must be an object with id, got ${typeof value}`;
|
|
99
|
+
const v = value as { id?: unknown };
|
|
100
|
+
if (typeof v.id !== "string")
|
|
101
|
+
return `Must have a string id, got ${typeof v.id}`;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "action":
|
|
106
|
+
case "custom":
|
|
107
|
+
// Custom types can't be validated generically
|
|
108
|
+
return undefined;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Split a shell command string into tokens, preserving quoted strings. */
|
|
2
|
+
export function tokenizeCommand(command: string): string[] {
|
|
3
|
+
const tokens: string[] = [];
|
|
4
|
+
let current = "";
|
|
5
|
+
let inSingle = false;
|
|
6
|
+
let inDouble = false;
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < command.length; i++) {
|
|
9
|
+
const ch = command[i];
|
|
10
|
+
if (inSingle) {
|
|
11
|
+
current += ch;
|
|
12
|
+
if (ch === "'") inSingle = false;
|
|
13
|
+
} else if (inDouble) {
|
|
14
|
+
current += ch;
|
|
15
|
+
if (ch === '"') inDouble = false;
|
|
16
|
+
else if (ch === "\\" && i + 1 < command.length) current += command[++i];
|
|
17
|
+
} else if (ch === "'") {
|
|
18
|
+
current += ch;
|
|
19
|
+
inSingle = true;
|
|
20
|
+
} else if (ch === '"') {
|
|
21
|
+
current += ch;
|
|
22
|
+
inDouble = true;
|
|
23
|
+
} else if (ch === " " || ch === "\t") {
|
|
24
|
+
if (current) {
|
|
25
|
+
tokens.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current) tokens.push(current);
|
|
33
|
+
return tokens;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Split a command string at the first unquoted pipe or redirect boundary.
|
|
38
|
+
*
|
|
39
|
+
* Shell operators: | 2> >> 1>&2 < && || ;
|
|
40
|
+
* We split on any of these that appear outside quotes.
|
|
41
|
+
*
|
|
42
|
+
* Returns [before, atAndAfter] or null if none found.
|
|
43
|
+
*/
|
|
44
|
+
export function splitShellBoundary(str: string): [string, string] | null {
|
|
45
|
+
// Match shell operators: | || && ; > >> 2> 1>&2 < etc.
|
|
46
|
+
// Anchored with ^ so match is O(1) per character, not O(N).
|
|
47
|
+
const pattern = /^(?:&&|\|\||[|&;<>]|(?:[12]?>>?[&]?[12]?))/;
|
|
48
|
+
let inSingle = false;
|
|
49
|
+
let inDouble = false;
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < str.length; i++) {
|
|
52
|
+
const ch = str[i];
|
|
53
|
+
if (inSingle) {
|
|
54
|
+
if (ch === "'") inSingle = false;
|
|
55
|
+
} else if (inDouble) {
|
|
56
|
+
if (ch === '"') inDouble = false;
|
|
57
|
+
else if (ch === "\\" && i + 1 < str.length) i++;
|
|
58
|
+
} else if (ch === "'") {
|
|
59
|
+
inSingle = true;
|
|
60
|
+
} else if (ch === '"') {
|
|
61
|
+
inDouble = true;
|
|
62
|
+
} else if (ch === "\\" && i + 1 < str.length) {
|
|
63
|
+
// Outside quotes: backslash escapes the next character
|
|
64
|
+
i++;
|
|
65
|
+
} else {
|
|
66
|
+
const rest = str.slice(i);
|
|
67
|
+
const m = rest.match(pattern);
|
|
68
|
+
if (m) {
|
|
69
|
+
return [str.slice(0, i).trimEnd(), str.slice(i)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wrap a string in single quotes for safe shell passthrough.
|
|
78
|
+
* Already-quoted strings (single or double) are left as-is.
|
|
79
|
+
* Values without shell metacharacters are returned unchanged.
|
|
80
|
+
*/
|
|
81
|
+
const SHELL_META = /[\s{}()$`!'"\\|&;<>#*?[\]~]/;
|
|
82
|
+
export function shellQuote(value: string): string {
|
|
83
|
+
// Already-quoted single: pass through only if no internal quotes break it
|
|
84
|
+
if (
|
|
85
|
+
value.startsWith("'") &&
|
|
86
|
+
value.endsWith("'") &&
|
|
87
|
+
!value.slice(1, -1).includes("'")
|
|
88
|
+
) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
// Already-quoted double: pass through only if no internal quotes or expansion chars
|
|
92
|
+
if (
|
|
93
|
+
value.startsWith('"') &&
|
|
94
|
+
value.endsWith('"') &&
|
|
95
|
+
!value.slice(1, -1).includes('"') &&
|
|
96
|
+
!/[\\$`]/.test(value.slice(1, -1))
|
|
97
|
+
) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
// No shell metacharacters - safe to pass through unchanged
|
|
101
|
+
if (!SHELL_META.test(value)) {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Strip surrounding shell quotes from a string. */
|
|
108
|
+
export function stripQuotes(s: string): string {
|
|
109
|
+
if (
|
|
110
|
+
s.length >= 2 &&
|
|
111
|
+
((s.startsWith("'") && s.endsWith("'")) ||
|
|
112
|
+
(s.startsWith('"') && s.endsWith('"')))
|
|
113
|
+
) {
|
|
114
|
+
return s.slice(1, -1);
|
|
115
|
+
}
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common theme overlays for TUI components.
|
|
3
|
+
*/
|
|
4
|
+
export function overlaySelectListTheme(theme: {
|
|
5
|
+
fg: (color: string, text: string) => string;
|
|
6
|
+
}) {
|
|
7
|
+
return {
|
|
8
|
+
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
9
|
+
selectedText: (text: string) => theme.fg("accent", text),
|
|
10
|
+
description: (text: string) => theme.fg("muted", text),
|
|
11
|
+
scrollInfo: (text: string) => theme.fg("dim", text),
|
|
12
|
+
noMatch: (text: string) => theme.fg("warning", text),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ANSI escape code constants.
|
|
18
|
+
*/
|
|
19
|
+
export const ESC = "\x1b";
|
|
20
|
+
export const BEL = "\x07";
|
|
21
|
+
export const ST = "\x1b\\"; // String Terminator for OSC 99 and tmux DCS
|
|
22
|
+
export const RESET = "\x1b[0m";
|
|
23
|
+
|
|
24
|
+
export type RGB = readonly [number, number, number];
|
|
25
|
+
|
|
26
|
+
export function fgRGB(c: RGB): string {
|
|
27
|
+
return `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function bgRGB(c: RGB): string {
|
|
31
|
+
return `\x1b[48;2;${c[0]};${c[1]};${c[2]}m`;
|
|
32
|
+
}
|