pi-open-agents 0.1.32 → 0.1.33

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-agents",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Unified agent and subagent management for pi coding agent, with OpenCode-compatible agent definitions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -115,6 +115,12 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
115
115
  // Skip disabled agents entirely
116
116
  if (agent.disable) continue;
117
117
 
118
+ // Resolve body-level include directives (fail-soft, never throws)
119
+ agent.prompt = await resolveIncludes(agent.prompt, path.dirname(filePath), {
120
+ fs: fileSystem,
121
+ reportedMissing: new Set(),
122
+ }, new Set([path.resolve(filePath)]), 0);
123
+
118
124
  // Add to the appropriate map
119
125
  if (discPath.source === "project") {
120
126
  projectAgents.set(agent.name, agent);
@@ -153,6 +159,119 @@ function defaultAgentDir(): string {
153
159
  return path.join(process.env.HOME ?? "", ".pi", "agent");
154
160
  }
155
161
 
162
+ // ─── Include Directive Resolution ───────────────────────────────────────────
163
+
164
+ /** Max recursion depth for include directives. */
165
+ const INCLUDE_MAX_DEPTH = 8;
166
+
167
+ /**
168
+ * A full-line include directive: `include: "path"`, `include: 'path'`,
169
+ * or `include: path`. Case-sensitive, must start at column 0 and occupy
170
+ * the whole line (trailing whitespace allowed). Occurrences inside
171
+ * sentences, indented lines, or fenced code blocks never match.
172
+ */
173
+ const INCLUDE_LINE_RE = /^include:[ \t]+(?:"([^"]*)"|'([^']*)'|([^\s"']+))[ \t]*$/;
174
+
175
+ /** Opening/closing fenced code block markers (``` or ~~~, optionally indented). */
176
+ const FENCE_RE = /^[ \t]*(```|~~~)/;
177
+
178
+ /** Shared state for one loadAgents run. */
179
+ interface IncludeContext {
180
+ /** Filesystem abstraction (honors the custom fs used for tests). */
181
+ fs: AgentFs;
182
+ /** Resolved paths already warned about (one warning per missing path). */
183
+ reportedMissing: Set<string>;
184
+ }
185
+
186
+ function warnInclude(message: string): void {
187
+ console.error(`[pi-open-agents] include: ${message}`);
188
+ }
189
+
190
+ /**
191
+ * Resolve body-level include directives in agent markdown.
192
+ *
193
+ * Each directive line is replaced by the content of the referenced file,
194
+ * resolved relative to the directory of the file containing the directive.
195
+ * Includes are recursive under the same rule. Cycles are detected via the
196
+ * set of absolute paths on the current expansion path, and nesting is
197
+ * capped at INCLUDE_MAX_DEPTH.
198
+ *
199
+ * Fail-soft: missing files, cycles, and over-deep nesting keep the directive
200
+ * line literal and warn on stderr, but never throw. Keeping the line (instead
201
+ * of dropping it) preserves visibility for debugging and for any downstream
202
+ * loader that might resolve it later.
203
+ *
204
+ * @param content Markdown body to process
205
+ * @param baseDir Directory of the file containing the directives
206
+ * @param ctx Shared per-load state
207
+ * @param visited Absolute paths on the current expansion path (cycle guard)
208
+ * @param depth Current inclusion depth (0 = the agent file itself)
209
+ */
210
+ async function resolveIncludes(
211
+ content: string,
212
+ baseDir: string,
213
+ ctx: IncludeContext,
214
+ visited: Set<string>,
215
+ depth: number,
216
+ ): Promise<string> {
217
+ const lines = content.split("\n");
218
+ const out: string[] = [];
219
+ let inFence = false;
220
+
221
+ for (const line of lines) {
222
+ if (FENCE_RE.test(line)) {
223
+ inFence = !inFence;
224
+ out.push(line);
225
+ continue;
226
+ }
227
+
228
+ if (inFence) {
229
+ out.push(line);
230
+ continue;
231
+ }
232
+
233
+ const match = INCLUDE_LINE_RE.exec(line);
234
+ const includePath = match ? (match[1] ?? match[2] ?? match[3]) : undefined;
235
+ if (!includePath) {
236
+ out.push(line);
237
+ continue;
238
+ }
239
+
240
+ const resolved = path.resolve(baseDir, includePath);
241
+
242
+ if (visited.has(resolved)) {
243
+ warnInclude(`cycle detected for "${includePath}" (${resolved}); keeping directive literal`);
244
+ out.push(line);
245
+ continue;
246
+ }
247
+
248
+ if (depth >= INCLUDE_MAX_DEPTH) {
249
+ warnInclude(`max depth ${INCLUDE_MAX_DEPTH} exceeded for "${includePath}"; keeping directive literal`);
250
+ out.push(line);
251
+ continue;
252
+ }
253
+
254
+ let raw: string;
255
+ try {
256
+ raw = await ctx.fs.readFile(resolved);
257
+ } catch {
258
+ if (!ctx.reportedMissing.has(resolved)) {
259
+ ctx.reportedMissing.add(resolved);
260
+ warnInclude(`file not found: "${includePath}" (${resolved}); keeping directive literal`);
261
+ }
262
+ out.push(line);
263
+ continue;
264
+ }
265
+
266
+ visited.add(resolved);
267
+ const expanded = await resolveIncludes(raw, path.dirname(resolved), ctx, visited, depth + 1);
268
+ visited.delete(resolved);
269
+ out.push(expanded);
270
+ }
271
+
272
+ return out.join("\n");
273
+ }
274
+
156
275
  // ─── Filtering Utilities ────────────────────────────────────────────────────
157
276
 
158
277
  /**