pi-gsd 1.12.2 → 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,21 +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" };
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
+ }
88
129
 
89
- const text = event.text;
90
- try { ctx.ui.notify("[GSD:DBG] len=" + String(text?.length) + " text=" + String(text).slice(0, 300), "info"); } catch { ctx.ui.notify("[GSD:DBG] text log failed", "error"); }
130
+ for (const part of parts) {
131
+ const p = part.trim();
91
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
+ }
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) => {
92
182
  const includePattern = /<gsd-include\s+path="([^"]+)"(?:\s+select="([^"]*)")?\s*\/>/g;
93
- const includes = [...text.matchAll(includePattern)];
94
- ctx.ui.notify(`[GSD:DBG] includes=${includes.length}`, "info");
95
- if (includes.length === 0) return { action: "continue" };
96
183
 
97
184
  // Package harness fallback path
98
185
  const extFile = typeof __filename !== "undefined" ? __filename : "";
@@ -101,116 +188,46 @@ export default function (pi: ExtensionAPI) {
101
188
  : "";
102
189
 
103
190
  const errors: string[] = [];
104
- let transformed = text;
105
-
106
- for (const match of includes) {
107
- const fullMatch = match[0];
108
- const filePath = match[1];
109
- const selectExpr = match[2] ?? "";
110
-
111
- // ── Resolve file path ───────────────────────────────────────
112
- const subPath = filePath.replace(/^\.pi\/gsd\//, "");
113
- const candidates = [
114
- join(ctx.cwd, filePath),
115
- ...(filePath.startsWith(".pi/gsd/") && pkgHarness
116
- ? [join(pkgHarness, subPath)]
117
- : []),
118
- ];
119
-
120
- let raw: string | null = null;
121
- for (const c of candidates) {
122
- try {
123
- if (existsSync(c)) { raw = readFileSync(c, "utf8"); break; }
124
- } catch { /* try next */ }
125
- }
126
- if (raw === null) {
127
- errors.push(`File not found: ${filePath}`);
128
- continue;
129
- }
130
-
131
- // ── Apply selector ─────────────────────────────────────────
132
- let result = raw;
133
- if (selectExpr) {
134
- const parts = selectExpr.split("|");
135
- if (parts.length > 2) {
136
- errors.push(`Invalid selector (max 2 segments): ${selectExpr}`);
137
- continue;
138
- }
139
- // lines: must be standalone — reject any chain involving lines
140
- if (parts.length > 1 && parts.some((p) => p.trim().startsWith("lines:"))) {
141
- errors.push(`lines: cannot be chained — use it alone: ${selectExpr}`);
142
- continue;
143
- }
191
+ const messages = event.messages;
144
192
 
145
- for (let i = 0; i < parts.length; i++) {
146
- const part = parts[i].trim();
147
- const prev = i > 0 ? parts[i - 1].trim().split(":")[0] : null;
193
+ for (const msg of messages) {
194
+ if (msg.role !== "user") continue;
148
195
 
149
- if (part.startsWith("tag:")) {
150
- const tagName = part.slice(4);
151
- const tagRe = new RegExp(
152
- `<${tagName}>([\\s\\S]*?)</${tagName}>`,
153
- "i",
154
- );
155
- const tagMatch = result.match(tagRe);
156
- if (!tagMatch) {
157
- errors.push(`Tag <${tagName}> not found in ${filePath}`);
158
- result = "";
159
- break;
160
- }
161
- 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;
162
200
 
163
- } else if (part.startsWith("heading:")) {
164
- const headingText = part.slice(8);
165
- const headingRe = new RegExp(
166
- `(^|\\n)(#{1,6})\\s+${headingText.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\s*\\n`,
167
- );
168
- const hMatch = result.match(headingRe);
169
- if (!hMatch) {
170
- errors.push(`Heading \"${headingText}\" not found in ${filePath}`);
171
- result = "";
172
- break;
173
- }
174
- const level = hMatch[2].length;
175
- const startIdx = (hMatch.index ?? 0) + hMatch[0].length;
176
- const nextHeading = result.slice(startIdx).search(
177
- new RegExp(`\\n#{1,${level}}\\s`),
178
- );
179
- result = nextHeading === -1
180
- ? result.slice(startIdx).trim()
181
- : result.slice(startIdx, startIdx + nextHeading).trim();
182
-
183
- } else if (part.startsWith("lines:")) {
184
- const rangeMatch = part.match(/^lines:(\d+)-(\d+)$/);
185
- if (!rangeMatch) {
186
- errors.push(`Invalid lines selector: ${part}`);
187
- result = "";
188
- break;
189
- }
190
- const start = parseInt(rangeMatch[1], 10) - 1;
191
- const end = parseInt(rangeMatch[2], 10);
192
- result = result.split("\n").slice(start, end).join("\n");
193
-
194
- } else {
195
- errors.push(`Unknown selector: ${part}`);
196
- result = "";
197
- 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);
198
219
  }
220
+ block.text = transformed;
199
221
  }
200
- if (result === "") continue; // error already logged
201
222
  }
202
-
203
- transformed = transformed.replace(fullMatch, result);
204
223
  }
205
224
 
206
225
  if (errors.length > 0) {
207
- ctx.ui.notify("\u274c GSD include failed:\n" + errors.map((e) => " \u2022 " + e).join("\n"),
208
- "error",
209
- );
210
- 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
211
228
  }
212
229
 
213
- return { action: "transform", text: transformed };
230
+ return { messages };
214
231
  });
215
232
 
216
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.2",
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": {