@diegopetrucci/pi-extensions 0.1.74 → 0.1.78

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 (108) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +17 -12
  3. package/extensions/agent-workflow-audit/README.md +1 -1
  4. package/extensions/agent-workflow-audit/index.ts +1631 -1009
  5. package/extensions/agent-workflow-audit/package.json +1 -1
  6. package/extensions/annotate-git-diff/README.md +3 -1
  7. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  8. package/extensions/annotate-git-diff/git.ts +846 -768
  9. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  10. package/extensions/annotate-git-diff/index.ts +712 -671
  11. package/extensions/annotate-git-diff/package.json +1 -1
  12. package/extensions/annotate-git-diff/prompt.ts +45 -45
  13. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  14. package/extensions/annotate-git-diff/review-server.ts +337 -311
  15. package/extensions/annotate-git-diff/types.ts +132 -132
  16. package/extensions/annotate-git-diff/ui.ts +27 -27
  17. package/extensions/annotate-git-diff/watch.ts +74 -74
  18. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  19. package/extensions/annotate-git-diff/web/index.html +5 -6
  20. package/extensions/annotate-last-message/README.md +1 -1
  21. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  22. package/extensions/annotate-last-message/index.ts +210 -206
  23. package/extensions/annotate-last-message/package.json +1 -1
  24. package/extensions/annotate-last-message/prompt.ts +87 -66
  25. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  26. package/extensions/annotate-last-message/session.ts +94 -89
  27. package/extensions/annotate-last-message/theme.ts +199 -193
  28. package/extensions/annotate-last-message/types.ts +23 -23
  29. package/extensions/annotate-last-message/ui.ts +27 -25
  30. package/extensions/annotate-last-message/web/app.js +4 -7
  31. package/extensions/annotate-last-message/web/index.html +3 -3
  32. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  33. package/extensions/brrr/README.md +1 -1
  34. package/extensions/brrr/index.ts +213 -191
  35. package/extensions/brrr/package.json +1 -1
  36. package/extensions/claude-fast/README.md +5 -79
  37. package/extensions/claude-fast/index.ts +31 -289
  38. package/extensions/claude-fast/package.json +2 -3
  39. package/extensions/code-reviewer/README.md +6 -6
  40. package/extensions/code-reviewer/index.ts +1825 -1373
  41. package/extensions/code-reviewer/package.json +1 -1
  42. package/extensions/confirm-destructive/README.md +1 -1
  43. package/extensions/confirm-destructive/index.ts +50 -52
  44. package/extensions/confirm-destructive/package.json +1 -1
  45. package/extensions/context-cap/README.md +9 -9
  46. package/extensions/context-cap/index.ts +313 -305
  47. package/extensions/context-cap/package.json +2 -2
  48. package/extensions/context-inspector/index.ts +1211 -1064
  49. package/extensions/context-inspector/package.json +1 -1
  50. package/extensions/contrarian/README.md +4 -3
  51. package/extensions/contrarian/index.ts +1863 -1737
  52. package/extensions/contrarian/package.json +1 -1
  53. package/extensions/dirty-repo-guard/index.ts +37 -41
  54. package/extensions/dirty-repo-guard/package.json +1 -1
  55. package/extensions/dynamic-context-pruning/README.md +11 -43
  56. package/extensions/dynamic-context-pruning/index.ts +2131 -2105
  57. package/extensions/dynamic-context-pruning/package.json +1 -1
  58. package/extensions/fast/README.md +52 -14
  59. package/extensions/fast/index.ts +513 -317
  60. package/extensions/fast/package.json +1 -1
  61. package/extensions/git-footer/README.md +1 -1
  62. package/extensions/git-footer/index.ts +458 -411
  63. package/extensions/git-footer/package.json +2 -3
  64. package/extensions/gnosis/index.ts +180 -130
  65. package/extensions/gnosis/package.json +1 -1
  66. package/extensions/inline-bash/index.ts +87 -103
  67. package/extensions/inline-bash/package.json +1 -1
  68. package/extensions/librarian/README.md +1 -1
  69. package/extensions/librarian/index.ts +1877 -1419
  70. package/extensions/librarian/package.json +1 -1
  71. package/extensions/minimal-footer/README.md +5 -5
  72. package/extensions/minimal-footer/index.ts +987 -1012
  73. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  74. package/extensions/minimal-footer/openai-usage.ts +195 -190
  75. package/extensions/minimal-footer/package.json +1 -1
  76. package/extensions/notify/index.ts +222 -230
  77. package/extensions/notify/package.json +1 -1
  78. package/extensions/openai-fast/README.md +5 -73
  79. package/extensions/openai-fast/index.ts +32 -272
  80. package/extensions/openai-fast/package.json +2 -3
  81. package/extensions/oracle/README.md +4 -3
  82. package/extensions/oracle/index.ts +1740 -1627
  83. package/extensions/oracle/package.json +1 -1
  84. package/extensions/permission-gate/README.md +3 -0
  85. package/extensions/permission-gate/index.ts +785 -612
  86. package/extensions/permission-gate/package.json +1 -1
  87. package/extensions/permission-gate/powershell-safety.ts +840 -793
  88. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  89. package/extensions/project-mcp-json/README.md +199 -0
  90. package/extensions/project-mcp-json/config.ts +564 -0
  91. package/extensions/project-mcp-json/index.ts +119 -0
  92. package/extensions/project-mcp-json/package.json +38 -0
  93. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  94. package/extensions/quiet-tools/README.md +30 -19
  95. package/extensions/quiet-tools/index.ts +376 -291
  96. package/extensions/quiet-tools/package.json +2 -2
  97. package/extensions/review/README.md +2 -2
  98. package/extensions/review/index.ts +1745 -1872
  99. package/extensions/review/package.json +1 -1
  100. package/extensions/todo/index.ts +280 -280
  101. package/extensions/todo/package.json +1 -1
  102. package/extensions/triage-comments/README.md +6 -4
  103. package/extensions/triage-comments/index.ts +2620 -1888
  104. package/extensions/triage-comments/package.json +1 -1
  105. package/package.json +9 -6
  106. package/assets/social-preview.png +0 -0
  107. package/extensions/claude-fast/claude-fast.example.json +0 -4
  108. 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",
@@ -1,107 +1,91 @@
1
- /**
2
- * Inline Bash Extension - expands inline bash commands in user prompts.
3
- *
4
- * Start pi with this extension:
5
- * pi -e ./examples/extensions/inline-bash.ts
6
- *
7
- * Then type prompts with inline bash:
8
- * What's in !{pwd}?
9
- * The current branch is !{git branch --show-current} and status: !{git status --short}
10
- * My node version is !{node --version}
11
- *
12
- * The !{command} patterns are executed and replaced with their trimmed output
13
- * before the prompt is sent to the agent. Very large expansions are truncated.
14
- *
15
- * Note: Regular !command syntax (whole-line bash) is preserved and works as before.
16
- */
17
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
2
 
19
3
  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
- });
4
+ const PATTERN = /!\{([^}]+)\}/g;
5
+ const TIMEOUT_MS = 30000;
6
+ const MAX_OUTPUT_CHARS = 50000;
7
+
8
+ const trimAndLimit = (output: string) => {
9
+ const trimmed = output.trim();
10
+ if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed;
11
+ return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}\n[inline-bash output truncated after ${MAX_OUTPUT_CHARS} characters]`;
12
+ };
13
+
14
+ pi.on("input", async (event, ctx) => {
15
+ const text = event.text;
16
+
17
+ // Only expand prompts supplied by the user/client. Extension-injected follow-ups
18
+ // should not get an implicit path to local shell execution.
19
+ if (event.source === "extension") {
20
+ return { action: "continue" };
21
+ }
22
+
23
+ if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) {
24
+ return { action: "continue" };
25
+ }
26
+
27
+ PATTERN.lastIndex = 0;
28
+ const matches: Array<{ start: number; end: number; command: string }> = [];
29
+ let match = PATTERN.exec(text);
30
+ while (match) {
31
+ matches.push({
32
+ start: match.index,
33
+ end: match.index + match[0].length,
34
+ command: match[1],
35
+ });
36
+ match = PATTERN.exec(text);
37
+ }
38
+ if (matches.length === 0) {
39
+ return { action: "continue" };
40
+ }
41
+
42
+ const parts: string[] = [];
43
+ let cursor = 0;
44
+ const expansions: Array<{ command: string; output: string; error?: string }> = [];
45
+
46
+ for (const { start, end, command } of matches) {
47
+ parts.push(text.slice(cursor, start));
48
+ cursor = end;
49
+ try {
50
+ const bashResult = await pi.exec("bash", ["-c", command], {
51
+ timeout: TIMEOUT_MS,
52
+ });
53
+
54
+ const output = bashResult.stdout || bashResult.stderr || "";
55
+ const trimmed = trimAndLimit(output);
56
+
57
+ if (bashResult.code !== 0 && bashResult.stderr) {
58
+ expansions.push({
59
+ command,
60
+ output: trimmed,
61
+ error: `exit code ${bashResult.code}`,
62
+ });
63
+ } else {
64
+ expansions.push({ command, output: trimmed });
65
+ }
66
+
67
+ parts.push(trimmed);
68
+ } catch (err) {
69
+ const errorMsg = err instanceof Error ? err.message : String(err);
70
+ expansions.push({ command, output: "", error: errorMsg });
71
+ parts.push(`[error: ${errorMsg}]`);
72
+ }
73
+ }
74
+ parts.push(text.slice(cursor));
75
+ const result = parts.join("");
76
+
77
+ if (ctx.hasUI && expansions.length > 0) {
78
+ const summary = expansions
79
+ .map((e) => {
80
+ const status = e.error ? ` (${e.error})` : "";
81
+ const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
82
+ return `!{${e.command}}${status} -> "${preview}"`;
83
+ })
84
+ .join("\n");
85
+
86
+ ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, "info");
87
+ }
88
+
89
+ return { action: "transform", text: result, images: event.images };
90
+ });
107
91
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-inline-bash",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "A pi extension that expands inline bash commands in user prompts.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -35,7 +35,7 @@ Then reload pi:
35
35
  ## Behavior
36
36
 
37
37
  - Tool name: `librarian`
38
- - Uses a restricted subagent with `bash` and `read`
38
+ - Uses a restricted subagent with `bash`, `read`, and any configured MCP tools (MCP tools run without confirmation; may have side effects; use for evidence gathering only; requires host Pi >=1.0.4; older hosts degrade to no MCP, no error); the global `mcp.json` always applies; a project `.pi/mcp.json` is never loaded (the librarian subagent runs in a separate temp workspace)
39
39
  - Uses `gh` for GitHub search/API access
40
40
  - Uses cached local checkouts only when enabled
41
41
  - Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`