gsd-pi 2.10.2 → 2.10.5

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 (136) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +7 -0
  3. package/dist/loader.js +1 -0
  4. package/dist/onboarding.js +104 -59
  5. package/dist/update-cmd.d.ts +1 -0
  6. package/dist/update-cmd.js +40 -0
  7. package/node_modules/@gsd/native/dist/hasher/index.d.ts +32 -0
  8. package/node_modules/@gsd/native/dist/hasher/index.js +37 -0
  9. package/node_modules/@gsd/native/dist/native.d.ts +4 -1
  10. package/node_modules/@gsd/native/dist/native.js +39 -9
  11. package/node_modules/@gsd/native/dist/xxhash/index.d.ts +14 -0
  12. package/node_modules/@gsd/native/dist/xxhash/index.js +17 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js +58 -9
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js +254 -43
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js +6 -4
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js +12 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +18 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +1 -0
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  49. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -2
  50. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  51. package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/src/core/agent-session.ts +65 -9
  57. package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
  58. package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.ts +283 -53
  59. package/node_modules/@gsd/pi-coding-agent/src/core/model-registry.ts +6 -4
  60. package/node_modules/@gsd/pi-coding-agent/src/core/settings-manager.ts +29 -0
  61. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
  62. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
  63. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +29 -0
  64. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +8 -0
  65. package/node_modules/@gsd/pi-coding-agent/src/index.ts +6 -0
  66. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  67. package/package.json +8 -2
  68. package/packages/native/dist/hasher/index.d.ts +32 -0
  69. package/packages/native/dist/hasher/index.js +37 -0
  70. package/packages/native/dist/native.d.ts +4 -1
  71. package/packages/native/dist/native.js +39 -9
  72. package/packages/native/dist/xxhash/index.d.ts +14 -0
  73. package/packages/native/dist/xxhash/index.js +17 -0
  74. package/packages/native/src/native.ts +39 -9
  75. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
  76. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/agent-session.js +58 -9
  78. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
  80. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/auth-storage.js +254 -43
  82. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
  84. package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
  86. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
  88. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/model-registry.js +6 -4
  90. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
  92. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
  94. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
  96. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
  98. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
  102. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
  104. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/tools/bash.js +18 -0
  106. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  108. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
  110. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/index.d.ts +2 -2
  112. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/index.js +1 -1
  114. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  118. package/packages/pi-coding-agent/src/core/agent-session.ts +65 -9
  119. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
  120. package/packages/pi-coding-agent/src/core/auth-storage.ts +283 -53
  121. package/packages/pi-coding-agent/src/core/model-registry.ts +6 -4
  122. package/packages/pi-coding-agent/src/core/settings-manager.ts +29 -0
  123. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
  124. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
  125. package/packages/pi-coding-agent/src/core/tools/bash.ts +29 -0
  126. package/packages/pi-coding-agent/src/core/tools/index.ts +8 -0
  127. package/packages/pi-coding-agent/src/index.ts +6 -0
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  129. package/src/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
  130. package/src/resources/extensions/async-jobs/await-tool.ts +101 -0
  131. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
  132. package/src/resources/extensions/async-jobs/index.ts +133 -0
  133. package/src/resources/extensions/async-jobs/job-manager.ts +250 -0
  134. package/src/resources/extensions/gsd/git-service.ts +13 -3
  135. package/src/resources/extensions/gsd/prompts/system.md +5 -2
  136. package/src/resources/extensions/gsd/tests/git-service.test.ts +36 -0
@@ -0,0 +1,198 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ checkBashInterception,
5
+ compileInterceptor,
6
+ DEFAULT_BASH_INTERCEPTOR_RULES,
7
+ type BashInterceptorRule,
8
+ } from "./bash-interceptor.js";
9
+
10
+ const ALL_TOOLS = ["read", "grep", "find", "edit", "write"];
11
+ const NO_TOOLS: string[] = [];
12
+
13
+ describe("checkBashInterception", () => {
14
+ describe("read rule (cat/head/tail/less/more)", () => {
15
+ it("blocks cat with a file argument", () => {
16
+ const r = checkBashInterception("cat README.md", ALL_TOOLS);
17
+ assert.equal(r.block, true);
18
+ assert.equal(r.suggestedTool, "read");
19
+ });
20
+
21
+ it("blocks head and tail", () => {
22
+ assert.equal(checkBashInterception("head -n 20 file.ts", ALL_TOOLS).block, true);
23
+ assert.equal(checkBashInterception("tail -f app.log", ALL_TOOLS).block, true);
24
+ });
25
+
26
+ it("does NOT block cat used as heredoc (cat <<EOF)", () => {
27
+ const r = checkBashInterception("cat <<EOF > file.txt", ALL_TOOLS);
28
+ assert.notEqual(r.suggestedTool, "read");
29
+ });
30
+
31
+ it("does NOT block when read tool is absent", () => {
32
+ assert.equal(checkBashInterception("cat README.md", NO_TOOLS).block, false);
33
+ assert.equal(checkBashInterception("cat README.md", ["grep"]).block, false);
34
+ });
35
+ });
36
+
37
+ describe("grep rule", () => {
38
+ it("blocks grep and rg", () => {
39
+ assert.equal(checkBashInterception("grep foo bar.ts", ALL_TOOLS).block, true);
40
+ assert.equal(checkBashInterception("rg -r pattern .", ALL_TOOLS).block, true);
41
+ });
42
+
43
+ it("blocks grep with leading whitespace", () => {
44
+ assert.equal(checkBashInterception(" grep -r foo .", ALL_TOOLS).block, true);
45
+ });
46
+
47
+ it("does NOT block when grep tool is absent", () => {
48
+ assert.equal(checkBashInterception("grep foo bar", ["read", "edit"]).block, false);
49
+ });
50
+ });
51
+
52
+ describe("find rule", () => {
53
+ it("blocks find with -name flag", () => {
54
+ assert.equal(checkBashInterception('find . -name "*.ts"', ALL_TOOLS).block, true);
55
+ });
56
+
57
+ it("blocks find with -type flag", () => {
58
+ assert.equal(checkBashInterception("find /tmp -maxdepth 1 -type f", ALL_TOOLS).block, true);
59
+ });
60
+
61
+ it("does NOT block find without name/type flags", () => {
62
+ assert.equal(checkBashInterception("find /tmp -maxdepth 1", ALL_TOOLS).block, false);
63
+ });
64
+
65
+ it("does NOT block when find tool is absent", () => {
66
+ assert.equal(checkBashInterception('find . -name "*.ts"', ["read", "grep"]).block, false);
67
+ });
68
+ });
69
+
70
+ describe("edit rule (sed/perl/awk)", () => {
71
+ it("blocks sed -i", () => {
72
+ assert.equal(checkBashInterception("sed -i 's/foo/bar/' file.ts", ALL_TOOLS).block, true);
73
+ assert.equal(checkBashInterception("sed --in-place 's/x/y/' f", ALL_TOOLS).block, true);
74
+ });
75
+
76
+ it("does NOT block sed without -i (read-only)", () => {
77
+ assert.equal(checkBashInterception("sed 's/foo/bar/' file.ts", ALL_TOOLS).block, false);
78
+ });
79
+
80
+ it("blocks perl -pi and perl -p -i", () => {
81
+ assert.equal(checkBashInterception("perl -pi -e 's/foo/bar/' file", ALL_TOOLS).block, true);
82
+ assert.equal(checkBashInterception("perl -p -i -e 's/x/y/' f", ALL_TOOLS).block, true);
83
+ });
84
+
85
+ it("blocks awk -i inplace", () => {
86
+ assert.equal(checkBashInterception("awk -i inplace '{print}' file", ALL_TOOLS).block, true);
87
+ });
88
+
89
+ it("does NOT block when edit tool is absent", () => {
90
+ assert.equal(checkBashInterception("sed -i 's/a/b/' f", ["read", "grep"]).block, false);
91
+ });
92
+ });
93
+
94
+ describe("write rule (echo/printf/heredoc redirect)", () => {
95
+ it("blocks echo with > redirect", () => {
96
+ assert.equal(checkBashInterception("echo hello > file.txt", ALL_TOOLS).block, true);
97
+ });
98
+
99
+ it("blocks printf with > redirect", () => {
100
+ assert.equal(checkBashInterception('printf "%s" content > out.txt', ALL_TOOLS).block, true);
101
+ });
102
+
103
+ it("does NOT block echo without redirect", () => {
104
+ assert.equal(checkBashInterception("echo hello", ALL_TOOLS).block, false);
105
+ });
106
+
107
+ it("does NOT block >> append redirect (write tool does not support appending)", () => {
108
+ assert.equal(checkBashInterception("echo hello >> file.txt", ALL_TOOLS).block, false);
109
+ });
110
+
111
+ it("does NOT block stderr redirect (2>)", () => {
112
+ assert.equal(checkBashInterception("echo test 2> /dev/null", ALL_TOOLS).block, false);
113
+ });
114
+
115
+ it("does NOT block pipe (echo foo | grep bar)", () => {
116
+ assert.equal(checkBashInterception("echo foo | grep bar", ALL_TOOLS).block, false);
117
+ });
118
+
119
+ it("does NOT block when write tool is absent", () => {
120
+ assert.equal(checkBashInterception("echo hello > file.txt", ["read", "grep"]).block, false);
121
+ });
122
+ });
123
+
124
+ describe("pass-through commands", () => {
125
+ it("passes npm install", () => {
126
+ assert.equal(checkBashInterception("npm install", ALL_TOOLS).block, false);
127
+ });
128
+
129
+ it("passes ls > output.txt (not an echo/printf/cat)", () => {
130
+ assert.equal(checkBashInterception("ls > output.txt", ALL_TOOLS).block, false);
131
+ });
132
+
133
+ it("passes tee file.txt", () => {
134
+ assert.equal(checkBashInterception("tee file.txt", ALL_TOOLS).block, false);
135
+ });
136
+
137
+ it("passes git log", () => {
138
+ assert.equal(checkBashInterception("git log --oneline", ALL_TOOLS).block, false);
139
+ });
140
+ });
141
+
142
+ describe("block message content", () => {
143
+ it("includes the original command in the block message", () => {
144
+ const r = checkBashInterception("cat README.md", ALL_TOOLS);
145
+ assert.ok(r.message?.includes("cat README.md"), "message should contain original command");
146
+ });
147
+
148
+ it("returns block:false with no message when not blocked", () => {
149
+ const r = checkBashInterception("npm install", ALL_TOOLS);
150
+ assert.equal(r.block, false);
151
+ assert.equal(r.message, undefined);
152
+ });
153
+ });
154
+ });
155
+
156
+ describe("compileInterceptor", () => {
157
+ it("produces same results as checkBashInterception", () => {
158
+ const interceptor = compileInterceptor(DEFAULT_BASH_INTERCEPTOR_RULES);
159
+ const cases: [string, string[], boolean][] = [
160
+ ["cat README.md", ALL_TOOLS, true],
161
+ ["npm install", ALL_TOOLS, false],
162
+ ["grep foo bar", ALL_TOOLS, true],
163
+ ["echo hello >> file", ALL_TOOLS, false],
164
+ ["echo test 2> /dev/null", ALL_TOOLS, false],
165
+ ];
166
+ for (const [cmd, tools, expected] of cases) {
167
+ assert.equal(
168
+ interceptor.check(cmd, tools).block,
169
+ expected,
170
+ `pre-compiled: "${cmd}" expected block=${expected}`,
171
+ );
172
+ }
173
+ });
174
+
175
+ it("silently skips rules with invalid regex patterns", () => {
176
+ const rules: BashInterceptorRule[] = [
177
+ { pattern: "[invalid(", tool: "read", message: "broken" },
178
+ { pattern: "^\\s*cat\\s+", tool: "read", message: "valid" },
179
+ ];
180
+ const interceptor = compileInterceptor(rules);
181
+ assert.equal(interceptor.check("cat file.txt", ["read"]).block, true);
182
+ });
183
+
184
+ it("returns block:false when available tools list is empty", () => {
185
+ const interceptor = compileInterceptor(DEFAULT_BASH_INTERCEPTOR_RULES);
186
+ assert.equal(interceptor.check("cat README.md", []).block, false);
187
+ });
188
+
189
+ it("allows custom rule override", () => {
190
+ const customRules: BashInterceptorRule[] = [
191
+ { pattern: "^\\s*curl\\s+", tool: "fetch", message: "Use fetch tool instead." },
192
+ ];
193
+ const interceptor = compileInterceptor(customRules);
194
+ assert.equal(interceptor.check("curl https://example.com", ["fetch"]).block, true);
195
+ // default rules not active
196
+ assert.equal(interceptor.check("cat file.txt", ["read"]).block, false);
197
+ });
198
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Bash command interceptor — blocks shell commands that duplicate dedicated tools.
3
+ *
4
+ * Each rule defines a regex pattern, a suggested replacement tool, and a message.
5
+ * A command is only blocked when the suggested tool exists in the session's active tool list.
6
+ */
7
+
8
+ export interface BashInterceptorRule {
9
+ pattern: string;
10
+ flags?: string;
11
+ tool: string;
12
+ message: string;
13
+ }
14
+
15
+ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
16
+ {
17
+ // cat/head/tail for file viewing — excludes heredoc syntax (cat <<)
18
+ pattern: "^\\s*(cat(?!\\s*<<)|head|tail|less|more)\\s+",
19
+ tool: "read",
20
+ message: "Use the read tool to view file contents instead of shell commands.",
21
+ },
22
+ {
23
+ pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
24
+ tool: "grep",
25
+ message: "Use the grep tool for searching file contents instead of shell commands.",
26
+ },
27
+ {
28
+ pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
29
+ tool: "find",
30
+ message: "Use the find tool for locating files by name/type instead of shell commands.",
31
+ },
32
+ {
33
+ pattern: "^\\s*sed\\s+(-i|--in-place)",
34
+ tool: "edit",
35
+ message: "Use the edit tool for in-place file modifications instead of sed.",
36
+ },
37
+ {
38
+ pattern: "^\\s*perl\\s+.*-[pn]?i",
39
+ tool: "edit",
40
+ message: "Use the edit tool for in-place file modifications instead of perl.",
41
+ },
42
+ {
43
+ pattern: "^\\s*awk\\s+.*-i\\s+inplace",
44
+ tool: "edit",
45
+ message: "Use the edit tool for in-place file modifications instead of awk.",
46
+ },
47
+ {
48
+ // echo/printf/heredoc writing to a file via > (not >> append, not 2> stderr redirect)
49
+ // Matches a single > not preceded by |, >, or a digit (fd redirect like 2>)
50
+ pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*(?<![|>\\d])>(?!>)\\s*\\S",
51
+ tool: "write",
52
+ message: "Use the write tool to create/overwrite files instead of shell redirects.",
53
+ },
54
+ ];
55
+
56
+ export interface InterceptionResult {
57
+ block: boolean;
58
+ message?: string;
59
+ suggestedTool?: string;
60
+ }
61
+
62
+ export interface CompiledInterceptor {
63
+ check: (command: string, availableTools: string[]) => InterceptionResult;
64
+ }
65
+
66
+ /**
67
+ * Compile rules into an interceptor with pre-built regex objects.
68
+ * Silently skips rules with invalid patterns.
69
+ *
70
+ * Pre-compiling at construction time avoids repeated `new RegExp()` calls
71
+ * on every bash command invocation.
72
+ */
73
+ export function compileInterceptor(rules: BashInterceptorRule[]): CompiledInterceptor {
74
+ const compiled = rules.flatMap((rule) => {
75
+ try {
76
+ return [{ regex: new RegExp(rule.pattern, rule.flags), rule }];
77
+ } catch {
78
+ return []; // skip invalid regex
79
+ }
80
+ });
81
+
82
+ return {
83
+ check(command: string, availableTools: string[]): InterceptionResult {
84
+ const trimmed = command.trim();
85
+ for (const { regex, rule } of compiled) {
86
+ if (regex.test(trimmed) && availableTools.includes(rule.tool)) {
87
+ return {
88
+ block: true,
89
+ message: `Blocked: ${rule.message}\n\nOriginal command: ${command}`,
90
+ suggestedTool: rule.tool,
91
+ };
92
+ }
93
+ }
94
+ return { block: false };
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Check whether a bash command should be intercepted.
101
+ *
102
+ * Compiles rules on each call — prefer `compileInterceptor()` for repeated use.
103
+ *
104
+ * @param command - The shell command to check
105
+ * @param availableTools - Tool names present in the current session
106
+ * @param rules - Override the default rule set (optional)
107
+ */
108
+ export function checkBashInterception(
109
+ command: string,
110
+ availableTools: string[],
111
+ rules?: BashInterceptorRule[],
112
+ ): InterceptionResult {
113
+ const effectiveRules = rules ?? DEFAULT_BASH_INTERCEPTOR_RULES;
114
+ return compileInterceptor(effectiveRules).check(command, availableTools);
115
+ }
@@ -7,6 +7,7 @@ import type { AgentTool } from "@gsd/pi-agent-core";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import { spawn } from "child_process";
9
9
  import { getShellConfig, getShellEnv, killProcessTree, sanitizeCommand } from "../../utils/shell.js";
10
+ import { type BashInterceptorRule, compileInterceptor, DEFAULT_BASH_INTERCEPTOR_RULES } from "./bash-interceptor.js";
10
11
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
11
12
  import type { ArtifactManager } from "../artifact-manager.js";
12
13
 
@@ -191,6 +192,13 @@ export interface BashToolOptions {
191
192
  spawnHook?: BashSpawnHook;
192
193
  /** Session-scoped artifact storage. When provided, spills to artifact files instead of temp files. */
193
194
  artifactManager?: ArtifactManager;
195
+ /** Bash interceptor configuration — blocks commands that duplicate dedicated tools */
196
+ interceptor?: {
197
+ enabled: boolean;
198
+ rules?: BashInterceptorRule[];
199
+ };
200
+ /** Tool names available in the session, used by the interceptor to check if replacement tools exist */
201
+ availableToolNames?: string[] | (() => string[]);
194
202
  }
195
203
 
196
204
  export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
@@ -199,6 +207,12 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
199
207
  const spawnHook = options?.spawnHook;
200
208
  const artifactManager = options?.artifactManager;
201
209
 
210
+ // Pre-compile interceptor rules once at construction time
211
+ const interceptorInstance =
212
+ options?.interceptor?.enabled
213
+ ? compileInterceptor(options.interceptor.rules ?? DEFAULT_BASH_INTERCEPTOR_RULES)
214
+ : null;
215
+
202
216
  return {
203
217
  name: "bash",
204
218
  label: "bash",
@@ -210,6 +224,21 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
210
224
  signal?: AbortSignal,
211
225
  onUpdate?,
212
226
  ) => {
227
+ // Check bash interceptor — block commands that duplicate dedicated tools
228
+ if (interceptorInstance) {
229
+ const toolNames =
230
+ typeof options!.availableToolNames === "function"
231
+ ? options!.availableToolNames()
232
+ : options!.availableToolNames ?? [];
233
+ const interception = interceptorInstance.check(command, toolNames);
234
+ if (interception.block) {
235
+ return {
236
+ content: [{ type: "text" as const, text: interception.message ?? "Command blocked by interceptor" }],
237
+ details: undefined,
238
+ };
239
+ }
240
+ }
241
+
213
242
  // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
214
243
  const resolvedCommand = sanitizeCommand(commandPrefix ? `${commandPrefix}\n${command}` : command);
215
244
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
@@ -8,6 +8,14 @@ export {
8
8
  bashTool,
9
9
  createBashTool,
10
10
  } from "./bash.js";
11
+ export {
12
+ type BashInterceptorRule,
13
+ checkBashInterception,
14
+ type CompiledInterceptor,
15
+ compileInterceptor,
16
+ DEFAULT_BASH_INTERCEPTOR_RULES,
17
+ type InterceptionResult,
18
+ } from "./bash-interceptor.js";
11
19
  export {
12
20
  createEditTool,
13
21
  type EditOperations,
@@ -202,6 +202,7 @@ export {
202
202
  export { BlobStore, isBlobRef, parseBlobRef, externalizeImageData, resolveImageData } from "./core/blob-store.js";
203
203
  export { ArtifactManager } from "./core/artifact-manager.js";
204
204
  export {
205
+ type AsyncSettings,
205
206
  type CompactionSettings,
206
207
  type ImageSettings,
207
208
  type PackageSource,
@@ -220,6 +221,7 @@ export {
220
221
  } from "./core/skills.js";
221
222
  // Tools
222
223
  export {
224
+ type BashInterceptorRule,
223
225
  type BashOperations,
224
226
  type BashSpawnContext,
225
227
  type BashSpawnHook,
@@ -227,6 +229,10 @@ export {
227
229
  type BashToolInput,
228
230
  type BashToolOptions,
229
231
  bashTool,
232
+ checkBashInterception,
233
+ type CompiledInterceptor,
234
+ compileInterceptor,
235
+ DEFAULT_BASH_INTERCEPTOR_RULES,
230
236
  codingTools,
231
237
  DEFAULT_MAX_BYTES,
232
238
  DEFAULT_MAX_LINES,
@@ -175,7 +175,7 @@ export class InteractiveMode {
175
175
  private pendingTools = new Map<string, ToolExecutionComponent>();
176
176
 
177
177
  // Tool output expansion state
178
- private toolOutputExpanded = true;
178
+ private toolOutputExpanded = false;
179
179
 
180
180
  // Thinking block visibility state
181
181
  private hideThinkingBlock = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.10.2",
3
+ "version": "2.10.5",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,7 +54,8 @@
54
54
  "pi:install-global": "node scripts/install-pi-global.js",
55
55
  "pi:uninstall-global": "node scripts/uninstall-pi-global.js",
56
56
  "sync-pkg-version": "node scripts/sync-pkg-version.cjs",
57
- "prepublishOnly": "npm run sync-pkg-version && npm run build"
57
+ "sync-platform-versions": "node native/scripts/sync-platform-versions.cjs",
58
+ "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && npm run build"
58
59
  },
59
60
  "dependencies": {
60
61
  "@clack/prompts": "^1.1.0",
@@ -81,6 +82,11 @@
81
82
  "typescript": "^5.4.0"
82
83
  },
83
84
  "optionalDependencies": {
85
+ "@gsd-build/engine-darwin-arm64": "2.10.5",
86
+ "@gsd-build/engine-darwin-x64": "2.10.5",
87
+ "@gsd-build/engine-linux-x64-gnu": "2.10.5",
88
+ "@gsd-build/engine-linux-arm64-gnu": "2.10.5",
89
+ "@gsd-build/engine-win32-x64-msvc": "2.10.5",
84
90
  "fsevents": "~2.3.3"
85
91
  },
86
92
  "overrides": {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Native BLAKE3 content hashing — Rust implementation via napi-rs.
3
+ *
4
+ * Provides ultra-fast content hashing (~GB/s) using BLAKE3 with automatic
5
+ * SIMD acceleration (AVX2, SSE4.1, NEON). All hashes are lowercase hex,
6
+ * 64 characters (256-bit).
7
+ */
8
+ export interface HashDirectoryOptions {
9
+ /** Glob pattern to filter files (e.g. "**\/*.ts"). Defaults to all files. */
10
+ glob?: string;
11
+ /** Whether to respect .gitignore rules. Defaults to true. */
12
+ gitignore?: boolean;
13
+ }
14
+ /** BLAKE3 hash of a UTF-8 string, returned as lowercase 64-char hex. */
15
+ export declare function hashString(text: string): string;
16
+ /** BLAKE3 hash of a file's contents, returned as lowercase 64-char hex. */
17
+ export declare function hashFile(path: string): string;
18
+ /**
19
+ * BLAKE3 hash of multiple files in parallel.
20
+ * Returns a map of path -> hex hash. Silently skips unreadable files.
21
+ */
22
+ export declare function hashFiles(paths: string[]): Record<string, string>;
23
+ /**
24
+ * BLAKE3 hash all files in a directory, optionally filtered by glob.
25
+ * Returns a map of relative path -> hex hash.
26
+ */
27
+ export declare function hashDirectory(dirPath: string, options?: HashDirectoryOptions): Record<string, string>;
28
+ /**
29
+ * Given previous hashes (path -> hex), re-hash each file and return
30
+ * paths whose content changed or no longer exist.
31
+ */
32
+ export declare function didFilesChange(hashes: Record<string, string>): string[];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Native BLAKE3 content hashing — Rust implementation via napi-rs.
3
+ *
4
+ * Provides ultra-fast content hashing (~GB/s) using BLAKE3 with automatic
5
+ * SIMD acceleration (AVX2, SSE4.1, NEON). All hashes are lowercase hex,
6
+ * 64 characters (256-bit).
7
+ */
8
+ import { native } from "../native.js";
9
+ /** BLAKE3 hash of a UTF-8 string, returned as lowercase 64-char hex. */
10
+ export function hashString(text) {
11
+ return native.hashString(text);
12
+ }
13
+ /** BLAKE3 hash of a file's contents, returned as lowercase 64-char hex. */
14
+ export function hashFile(path) {
15
+ return native.hashFile(path);
16
+ }
17
+ /**
18
+ * BLAKE3 hash of multiple files in parallel.
19
+ * Returns a map of path -> hex hash. Silently skips unreadable files.
20
+ */
21
+ export function hashFiles(paths) {
22
+ return native.hashFiles(paths);
23
+ }
24
+ /**
25
+ * BLAKE3 hash all files in a directory, optionally filtered by glob.
26
+ * Returns a map of relative path -> hex hash.
27
+ */
28
+ export function hashDirectory(dirPath, options) {
29
+ return native.hashDirectory(dirPath, options);
30
+ }
31
+ /**
32
+ * Given previous hashes (path -> hex), re-hash each file and return
33
+ * paths whose content changed or no longer exist.
34
+ */
35
+ export function didFilesChange(hashes) {
36
+ return native.didFilesChange(hashes);
37
+ }
@@ -2,7 +2,10 @@
2
2
  * Native addon loader.
3
3
  *
4
4
  * Locates and loads the compiled Rust N-API addon (`.node` file).
5
- * Tries platform-tagged release builds first, then falls back to dev builds.
5
+ * Resolution order:
6
+ * 1. @gsd-build/engine-{platform} npm optional dependency (production install)
7
+ * 2. native/addon/gsd_engine.{platform}.node (local release build)
8
+ * 3. native/addon/gsd_engine.dev.node (local debug build)
6
9
  */
7
10
  export declare const native: {
8
11
  search: (content: Buffer | Uint8Array, options: unknown) => unknown;
@@ -2,7 +2,10 @@
2
2
  * Native addon loader.
3
3
  *
4
4
  * Locates and loads the compiled Rust N-API addon (`.node` file).
5
- * Tries platform-tagged release builds first, then falls back to dev builds.
5
+ * Resolution order:
6
+ * 1. @gsd-build/engine-{platform} npm optional dependency (production install)
7
+ * 2. native/addon/gsd_engine.{platform}.node (local release build)
8
+ * 3. native/addon/gsd_engine.dev.node (local debug build)
6
9
  */
7
10
  import { createRequire } from "node:module";
8
11
  import * as path from "node:path";
@@ -11,24 +14,51 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
14
  const require = createRequire(import.meta.url);
12
15
  const addonDir = path.resolve(__dirname, "..", "..", "..", "native", "addon");
13
16
  const platformTag = `${process.platform}-${process.arch}`;
14
- const candidates = [
15
- path.join(addonDir, `gsd_engine.${platformTag}.node`),
16
- path.join(addonDir, "gsd_engine.dev.node"),
17
- ];
17
+ /** Map Node.js platform/arch to the npm package suffix */
18
+ const platformPackageMap = {
19
+ "darwin-arm64": "darwin-arm64",
20
+ "darwin-x64": "darwin-x64",
21
+ "linux-x64": "linux-x64-gnu",
22
+ "linux-arm64": "linux-arm64-gnu",
23
+ "win32-x64": "win32-x64-msvc",
24
+ };
18
25
  function loadNative() {
19
26
  const errors = [];
20
- for (const candidate of candidates) {
27
+ // 1. Try the platform-specific npm optional dependency
28
+ const packageSuffix = platformPackageMap[platformTag];
29
+ if (packageSuffix) {
21
30
  try {
22
- return require(candidate);
31
+ return require(`@gsd-build/engine-${packageSuffix}`);
23
32
  }
24
33
  catch (err) {
25
34
  const message = err instanceof Error ? err.message : String(err);
26
- errors.push(`${candidate}: ${message}`);
35
+ errors.push(`@gsd-build/engine-${packageSuffix}: ${message}`);
27
36
  }
28
37
  }
38
+ // 2. Try local release build (native/addon/gsd_engine.{platform}.node)
39
+ const releasePath = path.join(addonDir, `gsd_engine.${platformTag}.node`);
40
+ try {
41
+ return require(releasePath);
42
+ }
43
+ catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ errors.push(`${releasePath}: ${message}`);
46
+ }
47
+ // 3. Try local dev build (native/addon/gsd_engine.dev.node)
48
+ const devPath = path.join(addonDir, "gsd_engine.dev.node");
49
+ try {
50
+ return require(devPath);
51
+ }
52
+ catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ errors.push(`${devPath}: ${message}`);
55
+ }
29
56
  const details = errors.map((e) => ` - ${e}`).join("\n");
57
+ const supportedPlatforms = Object.keys(platformPackageMap);
30
58
  throw new Error(`Failed to load gsd_engine native addon for ${platformTag}.\n\n` +
31
59
  `Tried:\n${details}\n\n` +
32
- `Build with: npm run build:native -w @gsd/native`);
60
+ `Supported platforms: ${supportedPlatforms.join(", ")}\n` +
61
+ `If your platform is listed, try reinstalling: npm i -g gsd-pi\n` +
62
+ `Otherwise, please open an issue: https://github.com/gsd-build/gsd-2/issues`);
33
63
  }
34
64
  export const native = loadNative();
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Native xxHash32 — Rust implementation via napi-rs.
3
+ *
4
+ * Drop-in replacement for the pure-JS xxHash32 in hashline.ts.
5
+ * Hashes the UTF-8 representation of the input string with the given seed.
6
+ */
7
+ /**
8
+ * Compute xxHash32 of a UTF-8 string.
9
+ *
10
+ * @param input The string to hash (encoded as UTF-8 internally).
11
+ * @param seed 32-bit seed value.
12
+ * @returns 32-bit unsigned hash.
13
+ */
14
+ export declare function xxHash32(input: string, seed: number): number;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Native xxHash32 — Rust implementation via napi-rs.
3
+ *
4
+ * Drop-in replacement for the pure-JS xxHash32 in hashline.ts.
5
+ * Hashes the UTF-8 representation of the input string with the given seed.
6
+ */
7
+ import { native } from "../native.js";
8
+ /**
9
+ * Compute xxHash32 of a UTF-8 string.
10
+ *
11
+ * @param input The string to hash (encoded as UTF-8 internally).
12
+ * @param seed 32-bit seed value.
13
+ * @returns 32-bit unsigned hash.
14
+ */
15
+ export function xxHash32(input, seed) {
16
+ return native.xxHash32(input, seed);
17
+ }