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.
Files changed (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. 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,6 @@
1
+ /**
2
+ * Type guard: checks if a value is a non-null, non-array object.
3
+ */
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
@@ -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
+ }