@aliou/pi-guardrails 0.11.2 → 0.12.1

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 (95) hide show
  1. package/README.md +72 -167
  2. package/extensions/guardrails/commands/examples/index.ts +520 -0
  3. package/extensions/guardrails/commands/onboarding/config.ts +54 -0
  4. package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
  5. package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
  6. package/extensions/guardrails/commands/settings/examples.ts +399 -0
  7. package/extensions/guardrails/commands/settings/index.ts +596 -0
  8. package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
  9. package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
  10. package/extensions/guardrails/commands/settings/utils.ts +108 -0
  11. package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
  12. package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
  13. package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
  14. package/extensions/guardrails/components/onboarding-types.ts +10 -0
  15. package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
  16. package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
  17. package/extensions/guardrails/index.ts +106 -0
  18. package/extensions/guardrails/rules.test.ts +107 -0
  19. package/extensions/guardrails/rules.ts +119 -0
  20. package/extensions/guardrails/targets.test.ts +44 -0
  21. package/extensions/guardrails/targets.ts +66 -0
  22. package/extensions/path-access/grants.test.ts +47 -0
  23. package/extensions/path-access/grants.ts +68 -0
  24. package/extensions/path-access/index.ts +143 -0
  25. package/extensions/path-access/prompt.ts +196 -0
  26. package/extensions/path-access/rules.test.ts +46 -0
  27. package/extensions/path-access/rules.ts +37 -0
  28. package/extensions/path-access/targets.test.ts +40 -0
  29. package/extensions/path-access/targets.ts +19 -0
  30. package/extensions/permission-gate/grants.ts +21 -0
  31. package/extensions/permission-gate/index.ts +122 -0
  32. package/extensions/permission-gate/prompt.ts +222 -0
  33. package/extensions/permission-gate/rules.test.ts +132 -0
  34. package/extensions/permission-gate/rules.ts +72 -0
  35. package/package.json +18 -20
  36. package/schema.json +286 -0
  37. package/src/core/check.test.ts +169 -0
  38. package/src/core/check.ts +38 -0
  39. package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
  40. package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
  41. package/src/core/commands/index.ts +15 -0
  42. package/src/core/index.ts +13 -0
  43. package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
  44. package/src/core/paths/index.ts +14 -0
  45. package/src/core/shell/command-args.test.ts +142 -0
  46. package/src/{utils → core/shell}/command-args.ts +71 -0
  47. package/src/core/shell/index.ts +2 -0
  48. package/src/core/types.ts +55 -0
  49. package/src/shared/config/defaults.ts +118 -0
  50. package/src/shared/config/index.ts +17 -0
  51. package/src/shared/config/loader.ts +64 -0
  52. package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
  53. package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
  54. package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
  55. package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
  56. package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
  57. package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
  58. package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
  59. package/src/shared/config/migration/index.ts +44 -0
  60. package/src/shared/config/migration/version.ts +7 -0
  61. package/src/shared/config/types.ts +141 -0
  62. package/src/shared/events.ts +100 -0
  63. package/src/shared/index.ts +6 -0
  64. package/src/shared/matching.test.ts +86 -0
  65. package/src/{utils → shared}/matching.ts +4 -4
  66. package/src/{utils → shared/paths}/bash-paths.test.ts +32 -2
  67. package/src/{utils → shared/paths}/bash-paths.ts +4 -4
  68. package/src/shared/paths/index.ts +1 -0
  69. package/src/shared/warnings.ts +17 -0
  70. package/docs/defaults.md +0 -140
  71. package/docs/examples.md +0 -170
  72. package/src/commands/onboarding.ts +0 -390
  73. package/src/commands/settings-command.ts +0 -1616
  74. package/src/config.ts +0 -392
  75. package/src/hooks/index.ts +0 -11
  76. package/src/hooks/path-access.ts +0 -395
  77. package/src/hooks/permission-gate/index.test.ts +0 -332
  78. package/src/hooks/permission-gate/index.ts +0 -595
  79. package/src/hooks/policies.ts +0 -322
  80. package/src/index.ts +0 -96
  81. package/src/lib/executor.ts +0 -280
  82. package/src/lib/index.ts +0 -16
  83. package/src/lib/model-resolver.ts +0 -47
  84. package/src/lib/timing.ts +0 -42
  85. package/src/lib/types.ts +0 -115
  86. package/src/utils/command-args.test.ts +0 -83
  87. package/src/utils/events.ts +0 -32
  88. package/src/utils/migration.test.ts +0 -58
  89. package/src/utils/migration.ts +0 -340
  90. package/src/utils/warnings.ts +0 -7
  91. /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
  92. /package/src/{utils → core/paths}/path.test.ts +0 -0
  93. /package/src/{utils → core/paths}/path.ts +0 -0
  94. /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
  95. /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
@@ -0,0 +1,222 @@
1
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ Container,
4
+ Key,
5
+ matchesKey,
6
+ Spacer,
7
+ Text,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ wrapTextWithAnsi,
11
+ } from "@earendil-works/pi-tui";
12
+
13
+ interface MinimalTheme {
14
+ fg(color: string, text: string): string;
15
+ bg(color: string, text: string): string;
16
+ bold(text: string): string;
17
+ }
18
+
19
+ interface NumberedWrappedRow {
20
+ logicalLineNumber: number;
21
+ rendered: string;
22
+ }
23
+
24
+ interface CommandViewportState {
25
+ maxScrollOffset: number;
26
+ pinnedRows: NumberedWrappedRow[];
27
+ scrollWindowLines: number;
28
+ scrollableRows: NumberedWrappedRow[];
29
+ }
30
+
31
+ const COMMAND_VIEWPORT_LINES = 12;
32
+
33
+ function buildNumberedWrappedLines(
34
+ command: string,
35
+ contentWidth: number,
36
+ theme: Pick<MinimalTheme, "fg">,
37
+ ): NumberedWrappedRow[] {
38
+ const logicalLines = command.split("\n");
39
+ const lineNumberWidth = Math.max(2, String(logicalLines.length).length);
40
+ const prefixSpacing = 1;
41
+ const textWidth = Math.max(1, contentWidth - lineNumberWidth - prefixSpacing);
42
+ const rows: Array<{ logicalLineNumber: number; rendered: string }> = [];
43
+
44
+ for (const [index, logicalLine] of logicalLines.entries()) {
45
+ const lineNumber = index + 1;
46
+ const wrapped = wrapTextWithAnsi(theme.fg("text", logicalLine), textWidth);
47
+ const wrappedLines = wrapped.length > 0 ? wrapped : [""];
48
+ const prefix = theme.fg(
49
+ "dim",
50
+ String(lineNumber).padStart(lineNumberWidth),
51
+ );
52
+
53
+ for (const line of wrappedLines) {
54
+ rows.push({
55
+ logicalLineNumber: lineNumber,
56
+ rendered: `${prefix} ${line}`,
57
+ });
58
+ }
59
+ }
60
+
61
+ return rows;
62
+ }
63
+
64
+ function getCommandViewportState(
65
+ command: string,
66
+ contentWidth: number,
67
+ theme: Pick<MinimalTheme, "fg">,
68
+ ): CommandViewportState {
69
+ const numberedRows = buildNumberedWrappedLines(command, contentWidth, theme);
70
+ const pinnedRows = numberedRows.filter((row) => row.logicalLineNumber === 1);
71
+ const scrollableRows = numberedRows.filter(
72
+ (row) => row.logicalLineNumber !== 1,
73
+ );
74
+ const scrollWindowLines = Math.max(
75
+ 0,
76
+ COMMAND_VIEWPORT_LINES - pinnedRows.length,
77
+ );
78
+
79
+ return {
80
+ maxScrollOffset: Math.max(0, scrollableRows.length - scrollWindowLines),
81
+ pinnedRows,
82
+ scrollWindowLines,
83
+ scrollableRows,
84
+ };
85
+ }
86
+
87
+ function buildRightAlignedBorder(
88
+ width: number,
89
+ themeLine: (s: string) => string,
90
+ label: string,
91
+ ): string {
92
+ const safeWidth = Math.max(1, width);
93
+ const truncatedLabel = truncateToWidth(label, safeWidth);
94
+ const remaining = safeWidth - visibleWidth(truncatedLabel);
95
+ return themeLine("─".repeat(Math.max(0, remaining)) + truncatedLabel);
96
+ }
97
+
98
+ export function createPermissionGateConfirmComponent(
99
+ command: string,
100
+ description: string,
101
+ ) {
102
+ return (
103
+ tui: { terminal: { rows: number; columns: number }; requestRender(): void },
104
+ theme: MinimalTheme,
105
+ _kb: unknown,
106
+ done: (result: "allow" | "allow-session" | "deny") => void,
107
+ ) => {
108
+ const container = new Container();
109
+ const redBorder = (s: string) => theme.fg("error", s);
110
+ const dimBorder = (s: string) => theme.fg("dim", s);
111
+ let scrollOffset = 0;
112
+
113
+ container.addChild(new DynamicBorder(redBorder));
114
+ container.addChild(
115
+ new Text(
116
+ theme.fg("error", theme.bold("Dangerous Command Detected")),
117
+ 1,
118
+ 0,
119
+ ),
120
+ );
121
+ container.addChild(new Spacer(1));
122
+ container.addChild(
123
+ new Text(
124
+ theme.fg("warning", `This command contains ${description}:`),
125
+ 1,
126
+ 0,
127
+ ),
128
+ );
129
+ container.addChild(new Spacer(1));
130
+ const commandTopBorder = new Text("", 0, 0);
131
+ container.addChild(commandTopBorder);
132
+ const commandText = new Text("", 1, 0);
133
+ container.addChild(commandText);
134
+ const commandBottomBorder = new Text("", 0, 0);
135
+ container.addChild(commandBottomBorder);
136
+ container.addChild(new Spacer(1));
137
+ container.addChild(new Text(theme.fg("text", "Allow execution?"), 1, 0));
138
+ container.addChild(new Spacer(1));
139
+ container.addChild(
140
+ new Text(
141
+ theme.fg(
142
+ "dim",
143
+ "↑/↓ or j/k: scroll • y/enter: allow • a: session • n/esc: deny",
144
+ ),
145
+ 1,
146
+ 0,
147
+ ),
148
+ );
149
+ container.addChild(new DynamicBorder(redBorder));
150
+
151
+ return {
152
+ render: (width: number) => {
153
+ const contentWidth = Math.max(1, width - 4);
154
+ const {
155
+ maxScrollOffset,
156
+ pinnedRows,
157
+ scrollWindowLines,
158
+ scrollableRows,
159
+ } = getCommandViewportState(command, contentWidth, theme);
160
+ scrollOffset = Math.max(0, Math.min(scrollOffset, maxScrollOffset));
161
+
162
+ const visibleScrollableRows = scrollableRows.slice(
163
+ scrollOffset,
164
+ scrollOffset + scrollWindowLines,
165
+ );
166
+ const visibleRows = [...pinnedRows, ...visibleScrollableRows];
167
+ const linesBelow = Math.max(
168
+ 0,
169
+ scrollableRows.length - (scrollOffset + visibleScrollableRows.length),
170
+ );
171
+
172
+ commandTopBorder.setText(
173
+ buildRightAlignedBorder(
174
+ width,
175
+ dimBorder,
176
+ scrollOffset > 0 ? `↑ ${scrollOffset} more` : "",
177
+ ),
178
+ );
179
+ commandText.setText(visibleRows.map((row) => row.rendered).join("\n"));
180
+ commandBottomBorder.setText(
181
+ buildRightAlignedBorder(
182
+ width,
183
+ dimBorder,
184
+ linesBelow > 0 ? `↓ ${linesBelow} more` : "",
185
+ ),
186
+ );
187
+ return container.render(width);
188
+ },
189
+ invalidate: () => container.invalidate(),
190
+ handleInput: (data: string) => {
191
+ const contentWidth = Math.max(1, tui.terminal.columns - 4);
192
+ const { maxScrollOffset } = getCommandViewportState(
193
+ command,
194
+ contentWidth,
195
+ theme,
196
+ );
197
+
198
+ if (matchesKey(data, Key.up) || data === "k") {
199
+ scrollOffset = Math.max(0, scrollOffset - 1);
200
+ tui.requestRender();
201
+ } else if (matchesKey(data, Key.down) || data === "j") {
202
+ scrollOffset = Math.min(maxScrollOffset, scrollOffset + 1);
203
+ tui.requestRender();
204
+ } else if (
205
+ matchesKey(data, Key.enter) ||
206
+ data === "y" ||
207
+ data === "Y"
208
+ ) {
209
+ done("allow");
210
+ } else if (data === "a" || data === "A") {
211
+ done("allow-session");
212
+ } else if (
213
+ matchesKey(data, Key.escape) ||
214
+ data === "n" ||
215
+ data === "N"
216
+ ) {
217
+ done("deny");
218
+ }
219
+ },
220
+ };
221
+ };
222
+ }
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createPermissionGateRule,
4
+ formatAutoDenyReason,
5
+ matchCommandPattern,
6
+ matchesAnyCommandPattern,
7
+ } from "./rules";
8
+
9
+ describe("createPermissionGateRule", () => {
10
+ it("passes file actions", async () => {
11
+ const rule = createPermissionGateRule({
12
+ patterns: [{ pattern: "rm -rf", description: "recursive delete" }],
13
+ useBuiltinMatchers: false,
14
+ });
15
+ expect(rule.check({ kind: "file", path: "package.json" })).toEqual({
16
+ kind: "pass",
17
+ });
18
+ });
19
+
20
+ it("matches configured dangerous command patterns", async () => {
21
+ const rule = createPermissionGateRule({
22
+ patterns: [
23
+ { pattern: "terraform destroy", description: "Destroy infra" },
24
+ ],
25
+ useBuiltinMatchers: false,
26
+ });
27
+
28
+ expect(
29
+ rule.check({
30
+ kind: "command",
31
+ command: "terraform destroy -auto-approve",
32
+ }),
33
+ ).toEqual({
34
+ kind: "match",
35
+ reason: "Destroy infra",
36
+ metadata: {
37
+ command: "terraform destroy -auto-approve",
38
+ description: "Destroy infra",
39
+ pattern: "terraform destroy",
40
+ },
41
+ });
42
+ });
43
+
44
+ it("can use builtin dangerous command matchers", async () => {
45
+ const rule = createPermissionGateRule({
46
+ patterns: [],
47
+ useBuiltinMatchers: true,
48
+ });
49
+ expect(
50
+ rule.check({ kind: "command", command: "rm -rf dist" }),
51
+ ).toMatchObject({ kind: "match" });
52
+ });
53
+ });
54
+
55
+ describe("matchesAnyCommandPattern", () => {
56
+ it("matches substring and regex command patterns", () => {
57
+ expect(
58
+ matchesAnyCommandPattern("npm publish --dry-run", [
59
+ { pattern: "npm publish" },
60
+ ]),
61
+ ).toBe(true);
62
+ expect(
63
+ matchesAnyCommandPattern("DROP TABLE users", [
64
+ { pattern: "^DROP TABLE", regex: true },
65
+ ]),
66
+ ).toBe(true);
67
+ expect(
68
+ matchesAnyCommandPattern("npm test", [{ pattern: "npm publish" }]),
69
+ ).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("matchCommandPattern", () => {
74
+ it("returns the matched PatternConfig", () => {
75
+ const patterns = [{ pattern: "npm publish" }, { pattern: "rm -rf" }];
76
+ expect(matchCommandPattern("npm publish --dry-run", patterns)).toBe(
77
+ patterns[0],
78
+ );
79
+ });
80
+
81
+ it("returns the matching regex pattern", () => {
82
+ const patterns = [{ pattern: "^DROP TABLE", regex: true }];
83
+ expect(matchCommandPattern("DROP TABLE users", patterns)).toBe(patterns[0]);
84
+ });
85
+
86
+ it("returns null when no pattern matches", () => {
87
+ expect(
88
+ matchCommandPattern("npm test", [{ pattern: "npm publish" }]),
89
+ ).toBeNull();
90
+ });
91
+
92
+ it("preserves description on the returned pattern", () => {
93
+ const patterns = [
94
+ {
95
+ pattern: "python -m venv",
96
+ description: "Use the project .venv instead",
97
+ },
98
+ ];
99
+ const result = matchCommandPattern("python -m venv .venv", patterns);
100
+ expect(result).not.toBeNull();
101
+ expect(result?.description).toBe("Use the project .venv instead");
102
+ });
103
+ });
104
+
105
+ describe("formatAutoDenyReason", () => {
106
+ it("uses description when present", () => {
107
+ expect(
108
+ formatAutoDenyReason({
109
+ pattern: "python -m venv",
110
+ description: "Use the project .venv instead",
111
+ }),
112
+ ).toBe("Command auto-denied: Use the project .venv instead");
113
+ });
114
+
115
+ it("falls back to generic reason when description is missing", () => {
116
+ expect(formatAutoDenyReason({ pattern: "python -m venv" })).toBe(
117
+ "Command matched auto-deny pattern and was blocked automatically.",
118
+ );
119
+ });
120
+
121
+ it("falls back when description is empty string", () => {
122
+ expect(
123
+ formatAutoDenyReason({ pattern: "python -m venv", description: "" }),
124
+ ).toBe("Command matched auto-deny pattern and was blocked automatically.");
125
+ });
126
+
127
+ it("falls back when description is whitespace-only", () => {
128
+ expect(
129
+ formatAutoDenyReason({ pattern: "python -m venv", description: " " }),
130
+ ).toBe("Command matched auto-deny pattern and was blocked automatically.");
131
+ });
132
+ });
@@ -0,0 +1,72 @@
1
+ import type { Action, Rule } from "../../src/core";
2
+ import { checkDangerousCommand } from "../../src/core/commands";
3
+ import type { DangerousPattern, PatternConfig } from "../../src/shared/config";
4
+ import { compileCommandPatterns } from "../../src/shared/matching";
5
+
6
+ export type PermissionGateMeta = {
7
+ command: string;
8
+ description: string;
9
+ pattern: string;
10
+ };
11
+
12
+ export type PermissionGateRuleOptions = {
13
+ patterns: DangerousPattern[];
14
+ useBuiltinMatchers: boolean;
15
+ };
16
+
17
+ export function createPermissionGateRule({
18
+ patterns,
19
+ useBuiltinMatchers,
20
+ }: PermissionGateRuleOptions): Rule<PermissionGateMeta> {
21
+ const compiledPatterns = compileCommandPatterns(patterns);
22
+
23
+ return {
24
+ key: "permission-gate.dangerous-command",
25
+ check(action: Action) {
26
+ if (action.kind !== "command") return { kind: "pass" };
27
+
28
+ const match = checkDangerousCommand({
29
+ command: action.command,
30
+ patterns: compiledPatterns,
31
+ useBuiltinMatchers,
32
+ fallbackPatterns: patterns,
33
+ });
34
+ if (!match) return { kind: "pass" };
35
+
36
+ return {
37
+ kind: "match",
38
+ reason: match.description,
39
+ metadata: {
40
+ command: action.command,
41
+ description: match.description,
42
+ pattern: match.pattern,
43
+ },
44
+ };
45
+ },
46
+ };
47
+ }
48
+
49
+ export function matchCommandPattern(
50
+ command: string,
51
+ patterns: PatternConfig[],
52
+ ): PatternConfig | null {
53
+ const compiled = compileCommandPatterns(patterns);
54
+ for (let i = 0; i < compiled.length; i++) {
55
+ if (compiled[i].test(command)) return patterns[i];
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export function matchesAnyCommandPattern(
61
+ command: string,
62
+ patterns: PatternConfig[],
63
+ ): boolean {
64
+ return matchCommandPattern(command, patterns) !== null;
65
+ }
66
+
67
+ export function formatAutoDenyReason(pattern: PatternConfig): string {
68
+ const description = pattern.description?.trim();
69
+ return description
70
+ ? `Command auto-denied: ${description}`
71
+ : "Command matched auto-deny pattern and was blocked automatically.";
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-guardrails",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -17,7 +17,9 @@
17
17
  },
18
18
  "pi": {
19
19
  "extensions": [
20
- "./src/index.ts"
20
+ "./extensions/path-access/index.ts",
21
+ "./extensions/guardrails/index.ts",
22
+ "./extensions/permission-gate/index.ts"
21
23
  ],
22
24
  "video": "https://assets.aliou.me/pi-extensions/demos/pi-guardrails.mp4"
23
25
  },
@@ -26,48 +28,44 @@
26
28
  },
27
29
  "files": [
28
30
  "src",
29
- "docs",
30
- "README.md"
31
+ "extensions",
32
+ "README.md",
33
+ "schema.json"
31
34
  ],
32
35
  "dependencies": {
33
- "@aliou/pi-utils-settings": "^0.11.2",
36
+ "@aliou/pi-utils-settings": "^0.15.1",
34
37
  "@aliou/sh": "^0.1.0"
35
38
  },
36
39
  "peerDependencies": {
37
- "@mariozechner/pi-agent-core": "0.61.0",
38
- "@mariozechner/pi-ai": "0.61.0",
39
- "@mariozechner/pi-coding-agent": "0.61.0",
40
- "@mariozechner/pi-tui": "0.61.0"
40
+ "@earendil-works/pi-coding-agent": "0.74.0",
41
+ "@earendil-works/pi-tui": "0.74.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@aliou/biome-plugins": "^0.3.2",
44
45
  "@biomejs/biome": "^2.3.13",
45
46
  "@changesets/cli": "^2.27.11",
46
- "@mariozechner/pi-agent-core": "0.61.0",
47
- "@mariozechner/pi-ai": "0.61.0",
48
- "@mariozechner/pi-coding-agent": "0.61.0",
47
+ "@earendil-works/pi-coding-agent": "0.74.0",
48
+ "@earendil-works/pi-tui": "0.74.0",
49
49
  "@sinclair/typebox": "^0.34.48",
50
50
  "@types/node": "^25.0.10",
51
51
  "husky": "^9.1.7",
52
+ "memfs": "^4.57.2",
53
+ "ts-json-schema-generator": "^2.9.0",
52
54
  "typescript": "^5.9.3",
53
55
  "vitest": "^4.1.4"
54
56
  },
55
57
  "peerDependenciesMeta": {
56
- "@mariozechner/pi-agent-core": {
58
+ "@earendil-works/pi-coding-agent": {
57
59
  "optional": true
58
60
  },
59
- "@mariozechner/pi-ai": {
60
- "optional": true
61
- },
62
- "@mariozechner/pi-coding-agent": {
63
- "optional": true
64
- },
65
- "@mariozechner/pi-tui": {
61
+ "@earendil-works/pi-tui": {
66
62
  "optional": true
67
63
  }
68
64
  },
69
65
  "scripts": {
70
66
  "typecheck": "tsc --noEmit",
67
+ "gen:schema": "ts-json-schema-generator --path src/shared/config/types.ts --type GuardrailsConfig --no-type-check -o schema.json",
68
+ "check:schema": "ts-json-schema-generator --path src/shared/config/types.ts --type GuardrailsConfig --no-type-check -o /tmp/pi-guardrails-schema-check.json && diff -q schema.json /tmp/pi-guardrails-schema-check.json",
71
69
  "test": "vitest run",
72
70
  "test:watch": "vitest",
73
71
  "lint": "biome check",