@diegopetrucci/pi-extensions 0.1.74 → 0.1.76

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 (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +50 -12
  54. package/extensions/fast/index.ts +513 -317
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,8 +29,7 @@
29
29
  ]
30
30
  },
31
31
  "peerDependencies": {
32
- "@earendil-works/pi-coding-agent": "*",
33
- "@earendil-works/pi-tui": "*"
32
+ "@earendil-works/pi-coding-agent": "*"
34
33
  },
35
34
  "type": "module",
36
35
  "engines": {
@@ -1,159 +1,209 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import {
3
- DEFAULT_MAX_BYTES,
4
- DEFAULT_MAX_LINES,
5
- formatSize,
6
- truncateTail,
7
- type ExtensionAPI,
8
- type ToolExecutionMode,
3
+ DEFAULT_MAX_BYTES,
4
+ DEFAULT_MAX_LINES,
5
+ formatSize,
6
+ truncateTail,
7
+ type ExtensionAPI,
8
+ type ToolExecutionMode,
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { Type, type Static } from "typebox";
11
11
 
12
12
  const GNOSIS_TIMEOUT_MS = 30_000;
13
13
 
14
14
  const GnosisParams = Type.Object({
15
- action: StringEnum(["plan", "review", "search", "latest", "show", "topics", "write", "reindex"] as const, {
16
- description: "The gnosis operation to run.",
17
- }),
18
- query: Type.Optional(Type.String({ minLength: 1, description: "Search query for action=search. Use uppercase OR/NOT for FTS operators." })),
19
- target: Type.Optional(Type.String({ minLength: 1, description: "Entry ID prefix or topic for action=show." })),
20
- topics: Type.Optional(
21
- Type.Array(Type.String({ minLength: 1 }), {
22
- minItems: 1,
23
- description: "Topics for action=write. Each normalized topic must be at least 7 chars.",
24
- }),
25
- ),
26
- text: Type.Optional(Type.String({ minLength: 1, description: "Knowledge text for action=write." })),
27
- related: Type.Optional(
28
- Type.Array(Type.String({ minLength: 1 }), { description: "Related entry IDs or unique prefixes for action=write." }),
29
- ),
30
- limit: Type.Optional(Type.Integer({ minimum: 1, description: "Positive result limit for action=search or action=latest." })),
15
+ action: StringEnum(
16
+ ["plan", "review", "search", "latest", "show", "topics", "write", "reindex"] as const,
17
+ {
18
+ description: "The gnosis operation to run.",
19
+ },
20
+ ),
21
+ query: Type.Optional(
22
+ Type.String({
23
+ minLength: 1,
24
+ description: "Search query for action=search. Use uppercase OR/NOT for FTS operators.",
25
+ }),
26
+ ),
27
+ target: Type.Optional(
28
+ Type.String({ minLength: 1, description: "Entry ID prefix or topic for action=show." }),
29
+ ),
30
+ topics: Type.Optional(
31
+ Type.Array(Type.String({ minLength: 1 }), {
32
+ minItems: 1,
33
+ description: "Topics for action=write. Each normalized topic must be at least 7 chars.",
34
+ }),
35
+ ),
36
+ text: Type.Optional(
37
+ Type.String({ minLength: 1, description: "Knowledge text for action=write." }),
38
+ ),
39
+ related: Type.Optional(
40
+ Type.Array(Type.String({ minLength: 1 }), {
41
+ description: "Related entry IDs or unique prefixes for action=write.",
42
+ }),
43
+ ),
44
+ limit: Type.Optional(
45
+ Type.Integer({
46
+ minimum: 1,
47
+ description: "Positive result limit for action=search or action=latest.",
48
+ }),
49
+ ),
31
50
  });
32
51
 
33
52
  type GnosisParams = Static<typeof GnosisParams>;
34
53
 
35
54
  function positiveInteger(value: number | undefined, name: string): string | undefined {
36
- if (value === undefined) return undefined;
37
- if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer.`);
38
- return String(value);
55
+ if (value === undefined) return undefined;
56
+ if (!Number.isInteger(value) || value <= 0)
57
+ throw new Error(`${name} must be a positive integer.`);
58
+ return String(value);
39
59
  }
40
60
 
41
61
  function requireString(value: string | undefined, name: string): string {
42
- const trimmed = value?.trim();
43
- if (!trimmed) throw new Error(`${name} is required.`);
44
- return trimmed;
62
+ const trimmed = value?.trim();
63
+ if (!trimmed) throw new Error(`${name} is required.`);
64
+ return trimmed;
45
65
  }
46
66
 
47
67
  function buildGnosisArgs(params: GnosisParams): string[] {
48
- switch (params.action) {
49
- case "plan":
50
- return ["help", "plan"];
51
- case "review":
52
- return ["help", "review"];
53
- case "search": {
54
- const args = ["search", requireString(params.query, "query")];
55
- const limit = positiveInteger(params.limit, "limit");
56
- if (limit) args.push("--limit", limit);
57
- return args;
58
- }
59
- case "latest": {
60
- const args = ["latest"];
61
- const limit = positiveInteger(params.limit, "limit");
62
- if (limit) args.push("--limit", limit);
63
- return args;
64
- }
65
- case "show":
66
- return ["show", requireString(params.target, "target")];
67
- case "topics":
68
- return ["topics"];
69
- case "write": {
70
- const topics = params.topics?.map((topic) => topic.trim()).filter(Boolean) ?? [];
71
- if (topics.length === 0) throw new Error("topics is required for action=write.");
72
- const args = ["write", topics.join(","), requireString(params.text, "text")];
73
- const related = params.related?.map((id) => id.trim()).filter(Boolean) ?? [];
74
- if (related.length > 0) args.push("--related", related.join(","));
75
- return args;
76
- }
77
- case "reindex":
78
- return ["reindex"];
79
- default: {
80
- const exhaustive: never = params.action;
81
- throw new Error(`Unsupported gnosis action: ${exhaustive}`);
82
- }
83
- }
68
+ switch (params.action) {
69
+ case "plan":
70
+ return ["help", "plan"];
71
+ case "review":
72
+ return ["help", "review"];
73
+ case "search": {
74
+ const args = ["search", requireString(params.query, "query")];
75
+ const limit = positiveInteger(params.limit, "limit");
76
+ if (limit) args.push("--limit", limit);
77
+ return args;
78
+ }
79
+ case "latest": {
80
+ const args = ["latest"];
81
+ const limit = positiveInteger(params.limit, "limit");
82
+ if (limit) args.push("--limit", limit);
83
+ return args;
84
+ }
85
+ case "show":
86
+ return ["show", requireString(params.target, "target")];
87
+ case "topics":
88
+ return ["topics"];
89
+ case "write": {
90
+ const topics = params.topics?.map((topic) => topic.trim()).filter(Boolean) ?? [];
91
+ if (topics.length === 0) throw new Error("topics is required for action=write.");
92
+ const args = ["write", topics.join(","), requireString(params.text, "text")];
93
+ const related = params.related?.map((id) => id.trim()).filter(Boolean) ?? [];
94
+ if (related.length > 0) args.push("--related", related.join(","));
95
+ return args;
96
+ }
97
+ case "reindex":
98
+ return ["reindex"];
99
+ default: {
100
+ const exhaustive: never = params.action;
101
+ throw new Error(`Unsupported gnosis action: ${exhaustive}`);
102
+ }
103
+ }
84
104
  }
85
105
 
86
106
  function installHint(): string {
87
- return [
88
- "The `gn` CLI is required for the gnosis extension.",
89
- "Install it with one of:",
90
- " brew install --cask skorokithakis/tap/gnosis",
91
- " go install github.com/skorokithakis/gnosis/cmd/gn@latest",
92
- ].join("\n");
107
+ return [
108
+ "The `gn` CLI is required for the gnosis extension.",
109
+ "Install it with one of:",
110
+ " brew install --cask skorokithakis/tap/gnosis",
111
+ " go install github.com/skorokithakis/gnosis/cmd/gn@latest",
112
+ ].join("\n");
113
+ }
114
+
115
+ function isEnoent(error: unknown): boolean {
116
+ return !!error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT";
117
+ }
118
+
119
+ function isMissingBinaryFailure(
120
+ error: unknown,
121
+ outputText = "",
122
+ exitCode?: number | null,
123
+ ): boolean {
124
+ if (exitCode === 127 || isEnoent(error)) return true;
125
+ const message = error instanceof Error ? error.message : error == null ? "" : String(error);
126
+ const text = `${message}\n${outputText}`;
127
+ return (
128
+ /\bENOENT\b/.test(text) ||
129
+ /command not found/i.test(text) ||
130
+ /not recognized as an internal or external command/i.test(text) ||
131
+ /(?:^|[\s:`])gn(?:\.exe)?:\s*not found\b/im.test(text)
132
+ );
93
133
  }
94
134
 
95
135
  function formatOutput(stdout: string, stderr: string): { text: string; truncated: boolean } {
96
- const raw = [stdout, stderr].filter(Boolean).join(stderr && stdout ? "\n" : "").trimEnd();
97
- if (!raw) return { text: "(no output)", truncated: false };
98
-
99
- const truncation = truncateTail(raw, {
100
- maxBytes: DEFAULT_MAX_BYTES,
101
- maxLines: DEFAULT_MAX_LINES,
102
- });
103
-
104
- let text = truncation.content;
105
- if (truncation.truncated) {
106
- text += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
107
- text += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).]`;
108
- }
109
- return { text, truncated: truncation.truncated };
136
+ const raw = [stdout, stderr]
137
+ .filter(Boolean)
138
+ .join(stderr && stdout ? "\n" : "")
139
+ .trimEnd();
140
+ if (!raw) return { text: "(no output)", truncated: false };
141
+
142
+ const truncation = truncateTail(raw, {
143
+ maxBytes: DEFAULT_MAX_BYTES,
144
+ maxLines: DEFAULT_MAX_LINES,
145
+ });
146
+
147
+ let text = truncation.content;
148
+ if (truncation.truncated) {
149
+ text += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
150
+ text += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).]`;
151
+ }
152
+ return { text, truncated: truncation.truncated };
110
153
  }
111
154
 
112
155
  export default function gnosisExtension(pi: ExtensionAPI) {
113
- pi.registerTool({
114
- name: "gnosis",
115
- label: "Gnosis",
116
- description:
117
- "Search and record repo-local project knowledge using the `gn` CLI. Supports plan/review doctrine, search, latest, show, topics, write, and reindex. Does not support edit/rm; use shell only with explicit user intent for destructive gnosis maintenance.",
118
- executionMode: "sequential" as ToolExecutionMode,
119
- promptSnippet: "Search and record repo-local project knowledge through gnosis (`gn`).",
120
- promptGuidelines: [
121
- "Use the gnosis tool before implementing changes that may touch prior architecture, project decisions, rejected alternatives, or human constraints.",
122
- "Use gnosis with action=\"search\" and uppercase OR between likely topic keywords, for example `auth OR token OR session`.",
123
- "Use gnosis with action=\"write\" only for durable, non-obvious project knowledge that is not already captured in code, comments, docs, or the commit message.",
124
- "Prefer code comments over gnosis with action=\"write\" when the knowledge has an obvious specific code anchor.",
125
- ],
126
- parameters: GnosisParams,
127
-
128
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
129
- const args = buildGnosisArgs(params);
130
-
131
- let result: Awaited<ReturnType<typeof pi.exec>>;
132
- try {
133
- result = await pi.exec("gn", args, {
134
- cwd: ctx.cwd,
135
- signal,
136
- timeout: GNOSIS_TIMEOUT_MS,
137
- });
138
- } catch (error) {
139
- const message = error instanceof Error ? error.message : String(error);
140
- throw new Error(`${installHint()}\n\nExecution error: ${message}`);
141
- }
142
-
143
- const output = formatOutput(result.stdout ?? "", result.stderr ?? "");
144
- if (result.killed) throw new Error(`gnosis ${params.action} timed out or was cancelled.\n\n${output.text}`);
145
- if (result.code !== 0) {
146
- throw new Error(`gnosis ${params.action} failed with exit code ${result.code}.\n\n${output.text}\n\n${installHint()}`);
147
- }
148
-
149
- return {
150
- content: [{ type: "text", text: output.text }],
151
- details: {
152
- action: params.action,
153
- args,
154
- truncated: output.truncated,
155
- },
156
- };
157
- },
158
- });
156
+ pi.registerTool({
157
+ name: "gnosis",
158
+ label: "Gnosis",
159
+ description:
160
+ "Search and record repo-local project knowledge using the `gn` CLI. Supports plan/review doctrine, search, latest, show, topics, write, and reindex. Does not support edit/rm; use shell only with explicit user intent for destructive gnosis maintenance.",
161
+ executionMode: "sequential" as ToolExecutionMode,
162
+ promptSnippet: "Search and record repo-local project knowledge through gnosis (`gn`).",
163
+ promptGuidelines: [
164
+ "Use the gnosis tool before implementing changes that may touch prior architecture, project decisions, rejected alternatives, or human constraints.",
165
+ 'Use gnosis with action="search" and uppercase OR between likely topic keywords, for example `auth OR token OR session`.',
166
+ 'Use gnosis with action="write" only for durable, non-obvious project knowledge that is not already captured in code, comments, docs, or the commit message.',
167
+ 'Prefer code comments over gnosis with action="write" when the knowledge has an obvious specific code anchor.',
168
+ ],
169
+ parameters: GnosisParams,
170
+
171
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
172
+ const args = buildGnosisArgs(params);
173
+
174
+ let result: Awaited<ReturnType<typeof pi.exec>>;
175
+ try {
176
+ result = await pi.exec("gn", args, {
177
+ cwd: ctx.cwd,
178
+ signal,
179
+ timeout: GNOSIS_TIMEOUT_MS,
180
+ });
181
+ } catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ const hint = isMissingBinaryFailure(error) ? `${installHint()}\n\n` : "";
184
+ throw new Error(`${hint}Execution error: ${message}`);
185
+ }
186
+
187
+ const output = formatOutput(result.stdout ?? "", result.stderr ?? "");
188
+ if (result.killed)
189
+ throw new Error(`gnosis ${params.action} timed out or was cancelled.\n\n${output.text}`);
190
+ if (result.code !== 0) {
191
+ const hint = isMissingBinaryFailure(undefined, output.text, result.code)
192
+ ? `\n\n${installHint()}`
193
+ : "";
194
+ throw new Error(
195
+ `gnosis ${params.action} failed with exit code ${result.code}.\n\n${output.text}${hint}`,
196
+ );
197
+ }
198
+
199
+ return {
200
+ content: [{ type: "text", text: output.text }],
201
+ details: {
202
+ action: params.action,
203
+ args,
204
+ truncated: output.truncated,
205
+ },
206
+ };
207
+ },
208
+ });
159
209
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-gnosis",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "A pi extension that exposes the gnosis repo-local knowledge base CLI as an agent tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -2,7 +2,7 @@
2
2
  * Inline Bash Extension - expands inline bash commands in user prompts.
3
3
  *
4
4
  * Start pi with this extension:
5
- * pi -e ./examples/extensions/inline-bash.ts
5
+ * pi -e ./extensions/inline-bash/index.ts
6
6
  *
7
7
  * Then type prompts with inline bash:
8
8
  * What's in !{pwd}?
@@ -17,91 +17,91 @@
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
 
19
19
  export default function (pi: ExtensionAPI) {
20
- const PATTERN = /!\{([^}]+)\}/g;
21
- const TIMEOUT_MS = 30000;
22
- const MAX_OUTPUT_CHARS = 50000;
23
-
24
- const trimAndLimit = (output: string) => {
25
- const trimmed = output.trim();
26
- if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed;
27
- return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}\n[inline-bash output truncated after ${MAX_OUTPUT_CHARS} characters]`;
28
- };
29
-
30
- pi.on("input", async (event, ctx) => {
31
- const text = event.text;
32
-
33
- // Only expand prompts supplied by the user/client. Extension-injected follow-ups
34
- // should not get an implicit path to local shell execution.
35
- if (event.source === "extension") {
36
- return { action: "continue" };
37
- }
38
-
39
- // Don't process if it's a whole-line bash command (starts with !)
40
- // This preserves the existing !command behavior
41
- if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) {
42
- return { action: "continue" };
43
- }
44
-
45
- // Check if there are any inline bash patterns
46
- if (!PATTERN.test(text)) {
47
- return { action: "continue" };
48
- }
49
-
50
- // Reset regex state after test()
51
- PATTERN.lastIndex = 0;
52
-
53
- let result = text;
54
- const expansions: Array<{ command: string; output: string; error?: string }> = [];
55
-
56
- // Find all matches first (to avoid issues with replacing while iterating)
57
- const matches: Array<{ full: string; command: string }> = [];
58
- let match = PATTERN.exec(text);
59
- while (match) {
60
- matches.push({ full: match[0], command: match[1] });
61
- match = PATTERN.exec(text);
62
- }
63
-
64
- // Execute each command and collect results
65
- for (const { full, command } of matches) {
66
- try {
67
- const bashResult = await pi.exec("bash", ["-c", command], {
68
- timeout: TIMEOUT_MS,
69
- });
70
-
71
- const output = bashResult.stdout || bashResult.stderr || "";
72
- const trimmed = trimAndLimit(output);
73
-
74
- if (bashResult.code !== 0 && bashResult.stderr) {
75
- expansions.push({
76
- command,
77
- output: trimmed,
78
- error: `exit code ${bashResult.code}`,
79
- });
80
- } else {
81
- expansions.push({ command, output: trimmed });
82
- }
83
-
84
- result = result.replace(full, trimmed);
85
- } catch (err) {
86
- const errorMsg = err instanceof Error ? err.message : String(err);
87
- expansions.push({ command, output: "", error: errorMsg });
88
- result = result.replace(full, `[error: ${errorMsg}]`);
89
- }
90
- }
91
-
92
- // Show what was expanded (if UI available)
93
- if (ctx.hasUI && expansions.length > 0) {
94
- const summary = expansions
95
- .map((e) => {
96
- const status = e.error ? ` (${e.error})` : "";
97
- const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
98
- return `!{${e.command}}${status} -> "${preview}"`;
99
- })
100
- .join("\n");
101
-
102
- ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, "info");
103
- }
104
-
105
- return { action: "transform", text: result, images: event.images };
106
- });
20
+ const PATTERN = /!\{([^}]+)\}/g;
21
+ const TIMEOUT_MS = 30000;
22
+ const MAX_OUTPUT_CHARS = 50000;
23
+
24
+ const trimAndLimit = (output: string) => {
25
+ const trimmed = output.trim();
26
+ if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed;
27
+ return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}\n[inline-bash output truncated after ${MAX_OUTPUT_CHARS} characters]`;
28
+ };
29
+
30
+ pi.on("input", async (event, ctx) => {
31
+ const text = event.text;
32
+
33
+ // Only expand prompts supplied by the user/client. Extension-injected follow-ups
34
+ // should not get an implicit path to local shell execution.
35
+ if (event.source === "extension") {
36
+ return { action: "continue" };
37
+ }
38
+
39
+ if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) {
40
+ return { action: "continue" };
41
+ }
42
+
43
+ PATTERN.lastIndex = 0;
44
+ const matches: Array<{ start: number; end: number; command: string }> = [];
45
+ let match = PATTERN.exec(text);
46
+ while (match) {
47
+ matches.push({
48
+ start: match.index,
49
+ end: match.index + match[0].length,
50
+ command: match[1],
51
+ });
52
+ match = PATTERN.exec(text);
53
+ }
54
+ if (matches.length === 0) {
55
+ return { action: "continue" };
56
+ }
57
+
58
+ const parts: string[] = [];
59
+ let cursor = 0;
60
+ const expansions: Array<{ command: string; output: string; error?: string }> = [];
61
+
62
+ for (const { start, end, command } of matches) {
63
+ parts.push(text.slice(cursor, start));
64
+ cursor = end;
65
+ try {
66
+ const bashResult = await pi.exec("bash", ["-c", command], {
67
+ timeout: TIMEOUT_MS,
68
+ });
69
+
70
+ const output = bashResult.stdout || bashResult.stderr || "";
71
+ const trimmed = trimAndLimit(output);
72
+
73
+ if (bashResult.code !== 0 && bashResult.stderr) {
74
+ expansions.push({
75
+ command,
76
+ output: trimmed,
77
+ error: `exit code ${bashResult.code}`,
78
+ });
79
+ } else {
80
+ expansions.push({ command, output: trimmed });
81
+ }
82
+
83
+ parts.push(trimmed);
84
+ } catch (err) {
85
+ const errorMsg = err instanceof Error ? err.message : String(err);
86
+ expansions.push({ command, output: "", error: errorMsg });
87
+ parts.push(`[error: ${errorMsg}]`);
88
+ }
89
+ }
90
+ parts.push(text.slice(cursor));
91
+ const result = parts.join("");
92
+
93
+ if (ctx.hasUI && expansions.length > 0) {
94
+ const summary = expansions
95
+ .map((e) => {
96
+ const status = e.error ? ` (${e.error})` : "";
97
+ const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
98
+ return `!{${e.command}}${status} -> "${preview}"`;
99
+ })
100
+ .join("\n");
101
+
102
+ ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, "info");
103
+ }
104
+
105
+ return { action: "transform", text: result, images: event.images };
106
+ });
107
107
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-inline-bash",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "A pi extension that expands inline bash commands in user prompts.",
5
5
  "keywords": [
6
6
  "pi-package",