pi-gsd 1.12.3 → 1.12.4

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.
@@ -78,20 +78,108 @@ const ensureHarnessSymlink = (cwd: string): void => {
78
78
 
79
79
 
80
80
  export default function (pi: ExtensionAPI) {
81
- // ── input: <gsd-include> injection ───────────────────────────────────
82
- // Replaces <gsd-include path="..." /> tags with actual file contents.
83
- // Supports selectors: tag:NAME, heading:TEXT, lines:N-M
84
- // Valid chains: tag|heading, tag|lines, heading|lines, heading|tag
85
- // On ANY failure: red error + abort (action:"handled"). No partial injection.
86
- pi.on("input", async (event, ctx) => {
87
- if (event.source === "extension") return { action: "continue" };
88
-
89
- const text = event.text;
90
- const includePattern = /<gsd-include\s+path="([^"]+)"(?:\s+select="([^"]*)")?\s*\/>/g;
91
- const includes = [...text.matchAll(includePattern)];
92
- ctx.ui.notify("[GSD] len=" + String(text?.length) + " inc=" + includes.length + " txt=[" + String(text).slice(0, 250) + "]", "info");
93
- if (includes.length === 0) return { action: "continue" };
81
+ /** Resolve a single <gsd-include> match: file lookup + selector extraction. */
82
+ function resolveGsdInclude(
83
+ match: RegExpMatchArray,
84
+ cwd: string,
85
+ pkgHarness: string,
86
+ errors: string[],
87
+ ): string | null {
88
+ const filePath = match[1];
89
+ const selectExpr = match[2] ?? "";
90
+
91
+ // ── Resolve file path ───────────────────────────────────────
92
+ const subPath = filePath.replace(/^\.pi\/gsd\//, "");
93
+ const candidates = [
94
+ join(cwd, filePath),
95
+ ...(filePath.startsWith(".pi/gsd/") && pkgHarness
96
+ ? [join(pkgHarness, subPath)]
97
+ : []),
98
+ ];
99
+
100
+ let raw: string | null = null;
101
+ for (const c of candidates) {
102
+ try {
103
+ if (existsSync(c)) {
104
+ raw = readFileSync(c, "utf8");
105
+ break;
106
+ }
107
+ } catch {
108
+ /* try next */
109
+ }
110
+ }
111
+ if (raw === null) {
112
+ errors.push("File not found: " + filePath);
113
+ return null;
114
+ }
115
+
116
+ // ── Apply selector ─────────────────────────────────────────
117
+ let result = raw;
118
+ if (!selectExpr) return result;
119
+
120
+ const parts = selectExpr.split("|");
121
+ if (parts.length > 2) {
122
+ errors.push("Invalid selector (max 2 segments): " + selectExpr);
123
+ return null;
124
+ }
125
+ if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
126
+ errors.push("lines: cannot be chained — use it alone: " + selectExpr);
127
+ return null;
128
+ }
129
+
130
+ for (const part of parts) {
131
+ const p = part.trim();
132
+
133
+ if (p.startsWith("tag:")) {
134
+ const tagName = p.slice(4);
135
+ const tagRe = new RegExp("<" + tagName + ">([\\s\\S]*?)</" + tagName + ">", "i");
136
+ const tagMatch = result.match(tagRe);
137
+ if (!tagMatch) {
138
+ errors.push("Tag <" + tagName + "> not found in " + filePath);
139
+ return null;
140
+ }
141
+ result = tagMatch[1].trim();
142
+ } else if (p.startsWith("heading:")) {
143
+ const headingText = p.slice(8);
144
+ const escaped = headingText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
145
+ const headingRe = new RegExp("(^|\\n)(#{1,6})\\s+" + escaped + "\\s*\\n");
146
+ const hMatch = result.match(headingRe);
147
+ if (!hMatch) {
148
+ errors.push('Heading "' + headingText + '" not found in ' + filePath);
149
+ return null;
150
+ }
151
+ const level = hMatch[2].length;
152
+ const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
153
+ const nextHeading = result.slice(startIdx).search(new RegExp("\\n#{1," + level + "}\\s"));
154
+ result =
155
+ nextHeading === -1
156
+ ? result.slice(startIdx).trim()
157
+ : result.slice(startIdx, startIdx + nextHeading).trim();
158
+ } else if (p.startsWith("lines:")) {
159
+ const rangeMatch = p.match(/^lines:(\d+)-(\d+)$/);
160
+ if (!rangeMatch) {
161
+ errors.push("Invalid lines selector: " + p);
162
+ return null;
163
+ }
164
+ const start = parseInt(rangeMatch[1], 10) - 1;
165
+ const end = parseInt(rangeMatch[2], 10);
166
+ result = result.split("\n").slice(start, end).join("\n");
167
+ } else {
168
+ errors.push("Unknown selector: " + p);
169
+ return null;
170
+ }
171
+ }
94
172
 
173
+ return result;
174
+ }
175
+
176
+ // ── context: <gsd-include> injection ────────────────────────────────────────
177
+ // Fires AFTER template expansion, before each LLM call.
178
+ // Scans user messages for <gsd-include path="..." select="..." /> tags,
179
+ // resolves files, applies selectors, replaces tags with content.
180
+ // On ANY failure: red error + return empty messages to block the LLM call.
181
+ pi.on("context", async (event, ctx) => {
182
+ const includePattern = /<gsd-include\s+path="([^"]+)"(?:\s+select="([^"]*)")?\s*\/>/g;
95
183
 
96
184
  // Package harness fallback path
97
185
  const extFile = typeof __filename !== "undefined" ? __filename : "";
@@ -100,116 +188,46 @@ export default function (pi: ExtensionAPI) {
100
188
  : "";
101
189
 
102
190
  const errors: string[] = [];
103
- let transformed = text;
104
-
105
- for (const match of includes) {
106
- const fullMatch = match[0];
107
- const filePath = match[1];
108
- const selectExpr = match[2] ?? "";
109
-
110
- // ── Resolve file path ───────────────────────────────────────
111
- const subPath = filePath.replace(/^\.pi\/gsd\//, "");
112
- const candidates = [
113
- join(ctx.cwd, filePath),
114
- ...(filePath.startsWith(".pi/gsd/") && pkgHarness
115
- ? [join(pkgHarness, subPath)]
116
- : []),
117
- ];
118
-
119
- let raw: string | null = null;
120
- for (const c of candidates) {
121
- try {
122
- if (existsSync(c)) { raw = readFileSync(c, "utf8"); break; }
123
- } catch { /* try next */ }
124
- }
125
- if (raw === null) {
126
- errors.push(`File not found: ${filePath}`);
127
- continue;
128
- }
129
-
130
- // ── Apply selector ─────────────────────────────────────────
131
- let result = raw;
132
- if (selectExpr) {
133
- const parts = selectExpr.split("|");
134
- if (parts.length > 2) {
135
- errors.push(`Invalid selector (max 2 segments): ${selectExpr}`);
136
- continue;
137
- }
138
- // lines: must be standalone — reject any chain involving lines
139
- if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
140
- errors.push(`lines: cannot be chained — use it alone: ${selectExpr}`);
141
- continue;
142
- }
191
+ const messages = event.messages;
143
192
 
144
- for (let i = 0; i < parts.length; i++) {
145
- const part = parts[i].trim();
146
- const prev = i > 0 ? parts[i - 1].trim().split(":")[0] : null;
193
+ for (const msg of messages) {
194
+ if (msg.role !== "user") continue;
147
195
 
148
- if (part.startsWith("tag:")) {
149
- const tagName = part.slice(4);
150
- const tagRe = new RegExp(
151
- `<${tagName}>([\\s\\S]*?)</${tagName}>`,
152
- "i",
153
- );
154
- const tagMatch = result.match(tagRe);
155
- if (!tagMatch) {
156
- errors.push(`Tag <${tagName}> not found in ${filePath}`);
157
- result = "";
158
- break;
159
- }
160
- result = tagMatch[1].trim();
196
+ // Handle both string content and content block arrays
197
+ if (typeof msg.content === "string") {
198
+ const includes = [...msg.content.matchAll(includePattern)];
199
+ if (includes.length === 0) continue;
161
200
 
162
- } else if (part.startsWith("heading:")) {
163
- const headingText = part.slice(8);
164
- const headingRe = new RegExp(
165
- `(^|\\n)(#{1,6})\\s+${headingText.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*\\n`,
166
- );
167
- const hMatch = result.match(headingRe);
168
- if (!hMatch) {
169
- errors.push(`Heading \"${headingText}\" not found in ${filePath}`);
170
- result = "";
171
- break;
172
- }
173
- const level = hMatch[2].length;
174
- const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
175
- const nextHeading = result.slice(startIdx).search(
176
- new RegExp(`\\n#{1,${level}}\\s`),
177
- );
178
- result = nextHeading === -1
179
- ? result.slice(startIdx).trim()
180
- : result.slice(startIdx, startIdx + nextHeading).trim();
181
-
182
- } else if (part.startsWith("lines:")) {
183
- const rangeMatch = part.match(/^lines:(\d+)-(\d+)$/);
184
- if (!rangeMatch) {
185
- errors.push(`Invalid lines selector: ${part}`);
186
- result = "";
187
- break;
188
- }
189
- const start = parseInt(rangeMatch[1], 10) - 1;
190
- const end = parseInt(rangeMatch[2], 10);
191
- result = result.split("\n").slice(start, end).join("\n");
192
-
193
- } else {
194
- errors.push(`Unknown selector: ${part}`);
195
- result = "";
196
- break;
201
+ let transformed = msg.content;
202
+ for (const match of includes) {
203
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
204
+ if (replacement === null) continue;
205
+ transformed = transformed.replace(match[0], replacement);
206
+ }
207
+ msg.content = transformed;
208
+ } else if (Array.isArray(msg.content)) {
209
+ for (const block of msg.content) {
210
+ if (block.type !== "text" || !block.text) continue;
211
+ const includes = [...block.text.matchAll(includePattern)];
212
+ if (includes.length === 0) continue;
213
+
214
+ let transformed = block.text;
215
+ for (const match of includes) {
216
+ const replacement = resolveGsdInclude(match, ctx.cwd, pkgHarness, errors);
217
+ if (replacement === null) continue;
218
+ transformed = transformed.replace(match[0], replacement);
197
219
  }
220
+ block.text = transformed;
198
221
  }
199
- if (result === "") continue; // error already logged
200
222
  }
201
-
202
- transformed = transformed.replace(fullMatch, result);
203
223
  }
204
224
 
205
225
  if (errors.length > 0) {
206
- ctx.ui.notify("\u274c GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"),
207
- "error",
208
- );
209
- return { action: "handled" };
226
+ ctx.ui.notify("\u274c GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"), "error");
227
+ return { messages: [] }; // block LLM call
210
228
  }
211
229
 
212
- return { action: "transform", text: transformed };
230
+ return { messages };
213
231
  });
214
232
 
215
233
  // ── session_start: GSD update check ──────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-gsd",
3
- "version": "1.12.3",
3
+ "version": "1.12.4",
4
4
  "description": "Get Shit Done - Unofficial port of the renowned AI-native project-planning spec-driven toolkit",
5
5
  "main": "dist/pi-gsd-tools.js",
6
6
  "bin": {