pi-landstrip 0.17.35 → 0.17.37

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 (3) hide show
  1. package/README.md +40 -5
  2. package/dist/index.ts +328 -76
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -16,7 +16,7 @@ The extension includes a shared [sandbox policy](./sandbox.json) and separate
16
16
  [agent configuration](./subagents.json). Global and trusted-project files can
17
17
  override both.
18
18
 
19
- Process-backed subagents require Pi >= 0.80.6, and Node.js >= 22.19.0.
19
+ Process-backed subagents require Pi >= 0.82.0, and Node.js >= 22.19.0.
20
20
 
21
21
  ## Installation
22
22
 
@@ -208,11 +208,44 @@ merged over the packaged [defaults](./subagents.json). The packaged subagent typ
208
208
  are `explore`, `general`, and the OpenCode-compatible `scout` reconnaissance agent.
209
209
 
210
210
  Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi's
211
- `settings.json` contains only normal Pi settings, such as the `pi-landstrip` package
212
- entry.
211
+ `settings.json` only needs the normal package entry for the extension:
213
212
 
214
- `subagents.json` accepts only top-level `maxSubagents` and `subagents`; sandbox
215
- fields belong in `sandbox.json`.
213
+ ```json
214
+ {
215
+ "packages": ["npm:pi-landstrip"]
216
+ }
217
+ ```
218
+
219
+ Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
220
+
221
+ ```json
222
+ {
223
+ "maxSubagents": 0,
224
+ "opencode": {
225
+ "showGlobalAgents": false,
226
+ "showLocalAgents": false
227
+ },
228
+ "subagents": {
229
+ "agent": {}
230
+ }
231
+ }
232
+ ```
233
+
234
+ Both flags default to `false`. When enabled:
235
+
236
+ - `showGlobalAgents` imports Markdown agents from the OpenCode global config
237
+ directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
238
+ `~/.config/opencode`) under `agent/` and `agents/`.
239
+ - `showLocalAgents` imports Markdown agents from the trusted project's
240
+ `.opencode/agent/` and `.opencode/agents/` directories.
241
+
242
+ OpenCode local agents override OpenCode global agents. Pi agent definitions
243
+ from `subagents.json` (built-in, global, and local) take precedence over OpenCode
244
+ imports with the same name; conflicts are silent. Local OpenCode agents are
245
+ skipped when the project is untrusted, regardless of the flag.
246
+
247
+ `subagents.json` accepts top-level `maxSubagents`, `opencode`, and `subagents`;
248
+ sandbox fields belong in `sandbox.json`.
216
249
 
217
250
  The Windows sandbox fields are:
218
251
 
@@ -306,6 +339,8 @@ follows, then remove the old fields:
306
339
  - Convert legacy `tools` booleans to explicit `permission` rules.
307
340
  - Move Markdown agent prompts into each `subagents.agent.<name>.prompt` string.
308
341
  - Put the worker limit in top-level `maxSubagents`; zero disables delegation.
342
+ - Move `landstrip.opencode` flags from Pi `settings.json` to top-level
343
+ `opencode` in `subagents.json`.
309
344
  - Use `~/.pi/agent/subagents.json` for global configuration and
310
345
  `.pi/subagents.json` for trusted project configuration.
311
346
  - Leave sandbox policy in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`;
package/dist/index.ts CHANGED
@@ -3,10 +3,10 @@ import { spawn as spawn2, spawnSync } from "node:child_process";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { lookup } from "node:dns/promises";
5
5
  import {
6
- existsSync as existsSync4,
6
+ existsSync as existsSync5,
7
7
  mkdirSync as mkdirSync3,
8
8
  mkdtempSync as mkdtempSync2,
9
- readFileSync as readFileSync4,
9
+ readFileSync as readFileSync5,
10
10
  realpathSync as realpathSync2,
11
11
  rmSync as rmSync2,
12
12
  writeFileSync as writeFileSync3
@@ -18,8 +18,8 @@ import {
18
18
  isIP,
19
19
  Socket as NetSocket
20
20
  } from "node:net";
21
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
22
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, parse, resolve as resolve2 } from "node:path";
21
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
22
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute2, join as join6, parse, resolve as resolve2 } from "node:path";
23
23
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
24
24
  import { fileURLToPath as fileURLToPath3, URL } from "node:url";
25
25
  import {
@@ -70,18 +70,18 @@ function encodeLandstripContext(context) {
70
70
  // subagents.ts
71
71
  import { randomUUID } from "node:crypto";
72
72
  import {
73
- existsSync as existsSync3,
73
+ existsSync as existsSync4,
74
74
  lstatSync,
75
75
  mkdirSync as mkdirSync2,
76
76
  mkdtempSync,
77
- readFileSync as readFileSync3,
77
+ readFileSync as readFileSync4,
78
78
  readlinkSync,
79
79
  realpathSync,
80
80
  rmSync,
81
81
  writeFileSync as writeFileSync2
82
82
  } from "node:fs";
83
- import { homedir as homedir2, tmpdir } from "node:os";
84
- import { basename, dirname as dirname2, isAbsolute, join as join4, relative, resolve, sep } from "node:path";
83
+ import { homedir as homedir3, tmpdir } from "node:os";
84
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
85
85
  import { fileURLToPath as fileURLToPath2 } from "node:url";
86
86
  import {
87
87
  getAgentDir as getAgentDir3,
@@ -92,8 +92,8 @@ import { matchesKey, Text, truncateToWidth as truncateToWidth2 } from "@earendil
92
92
  import { Type } from "typebox";
93
93
 
94
94
  // agents.ts
95
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
96
- import { join as join3 } from "node:path";
95
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
96
+ import { join as join4 } from "node:path";
97
97
  import { minimatch } from "minimatch";
98
98
  import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
99
99
 
@@ -160,6 +160,38 @@ class AsyncQueue {
160
160
  // config.ts
161
161
  var MAX_SUBAGENTS = 16;
162
162
  var packageDir = dirname(fileURLToPath(import.meta.url));
163
+ var DEFAULT_OPENCODE = {
164
+ showGlobalAgents: false,
165
+ showLocalAgents: false
166
+ };
167
+ var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
168
+ function readBooleanField(section, key, path) {
169
+ if (!(key in section))
170
+ return;
171
+ const value = section[key];
172
+ if (typeof value === "boolean")
173
+ return value;
174
+ throw new Error(`${path}.${key} must be a boolean`);
175
+ }
176
+ function normalizeOpenCode(value, path) {
177
+ if (value === undefined)
178
+ return {};
179
+ if (!isRecord2(value))
180
+ throw new Error(`${path} must be a JSON object`);
181
+ for (const key of Object.keys(value)) {
182
+ if (!OPENCODE_KEYS.has(key)) {
183
+ throw new Error(`${path}: unknown field ${key}`);
184
+ }
185
+ }
186
+ const result = {};
187
+ const showGlobalAgents = readBooleanField(value, "showGlobalAgents", path);
188
+ if (showGlobalAgents !== undefined)
189
+ result.showGlobalAgents = showGlobalAgents;
190
+ const showLocalAgents = readBooleanField(value, "showLocalAgents", path);
191
+ if (showLocalAgents !== undefined)
192
+ result.showLocalAgents = showLocalAgents;
193
+ return result;
194
+ }
163
195
  function readConfig(path) {
164
196
  if (!existsSync(path))
165
197
  return {};
@@ -173,11 +205,27 @@ function readConfig(path) {
173
205
  if (!isRecord2(value))
174
206
  throw new Error(`${path} must contain a JSON object`);
175
207
  for (const key of Object.keys(value)) {
176
- if (key !== "maxSubagents" && key !== "subagents") {
208
+ if (key !== "maxSubagents" && key !== "subagents" && key !== "opencode") {
177
209
  throw new Error(`${path}: unknown top-level field ${key}`);
178
210
  }
179
211
  }
180
- return value;
212
+ const config = {};
213
+ if ("maxSubagents" in value) {
214
+ if (typeof value.maxSubagents !== "number") {
215
+ throw new Error(`${path}: maxSubagents must be a number`);
216
+ }
217
+ config.maxSubagents = value.maxSubagents;
218
+ }
219
+ if ("subagents" in value) {
220
+ if (!isRecord2(value.subagents)) {
221
+ throw new Error(`${path}: subagents must be an object`);
222
+ }
223
+ config.subagents = value.subagents;
224
+ }
225
+ if ("opencode" in value) {
226
+ config.opencode = normalizeOpenCode(value.opencode, `${path}: opencode`);
227
+ }
228
+ return config;
181
229
  }
182
230
  function mergeValue(base, override) {
183
231
  if (override === undefined)
@@ -239,7 +287,182 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
239
287
  }
240
288
  if (!isRecord2(config.subagents))
241
289
  throw new Error("subagents must be an object");
242
- return { ...config, agentSources };
290
+ const opencode = {
291
+ ...DEFAULT_OPENCODE,
292
+ ...config.opencode
293
+ };
294
+ return { ...config, opencode, agentSources };
295
+ }
296
+
297
+ // opencode-agents.ts
298
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
299
+ import { homedir as homedir2 } from "node:os";
300
+ import { basename, extname, join as join3, relative, sep } from "node:path";
301
+ import { parse as parseYaml } from "yaml";
302
+ var AGENT_DIR_PREFIXES = ["agent/", "agents/"];
303
+ function openCodeGlobalConfigDir() {
304
+ if (process.env.OPENCODE_CONFIG_DIR)
305
+ return process.env.OPENCODE_CONFIG_DIR;
306
+ const xdg = process.env.XDG_CONFIG_HOME;
307
+ return join3(xdg && xdg.length > 0 ? xdg : join3(homedir2(), ".config"), "opencode");
308
+ }
309
+ function listMarkdownFiles(root) {
310
+ if (!existsSync2(root))
311
+ return [];
312
+ const results = [];
313
+ const visit = (dir) => {
314
+ let entries;
315
+ try {
316
+ entries = readdirSync(dir);
317
+ } catch {
318
+ return;
319
+ }
320
+ for (const entry of entries) {
321
+ const path = join3(dir, entry);
322
+ let stats;
323
+ try {
324
+ stats = statSync(path);
325
+ } catch {
326
+ continue;
327
+ }
328
+ if (stats.isDirectory())
329
+ visit(path);
330
+ else if (stats.isFile() && extname(entry).toLowerCase() === ".md")
331
+ results.push(path);
332
+ }
333
+ };
334
+ visit(root);
335
+ return results;
336
+ }
337
+ function agentNameFromPath(dir, filePath) {
338
+ const relativePath = relative(dir, filePath).split(sep).join("/");
339
+ for (const prefix of AGENT_DIR_PREFIXES) {
340
+ if (relativePath.startsWith(prefix)) {
341
+ const candidate = relativePath.slice(prefix.length);
342
+ const extension2 = extname(candidate);
343
+ return extension2.length > 0 ? candidate.slice(0, -extension2.length) : candidate;
344
+ }
345
+ }
346
+ const base = basename(filePath);
347
+ const extension = extname(base);
348
+ return extension.length > 0 ? base.slice(0, -extension.length) : base;
349
+ }
350
+ function parseFrontmatter(content) {
351
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
352
+ if (!match)
353
+ return { data: {}, body: content };
354
+ const parsed = parseYaml(match[1] ?? "");
355
+ if (parsed === null || parsed === undefined)
356
+ return { data: {}, body: match[2] ?? "" };
357
+ if (!isRecord2(parsed))
358
+ throw new Error("frontmatter must be a YAML mapping");
359
+ return { data: { ...parsed }, body: match[2] ?? "" };
360
+ }
361
+ function convertToolsToPermission(tools) {
362
+ if (!isRecord2(tools))
363
+ throw new Error("tools must be a map of booleans");
364
+ const permission = {};
365
+ for (const [tool, enabled] of Object.entries(tools)) {
366
+ if (typeof enabled !== "boolean") {
367
+ throw new Error(`tools.${tool} must be a boolean`);
368
+ }
369
+ const action = enabled ? "allow" : "deny";
370
+ if (tool === "write" || tool === "edit" || tool === "patch") {
371
+ permission.edit = action;
372
+ continue;
373
+ }
374
+ permission[tool] = action;
375
+ }
376
+ return permission;
377
+ }
378
+ function normalizeOpenCodeAgentRaw(data, prompt) {
379
+ const known = new Set([
380
+ "name",
381
+ "model",
382
+ "variant",
383
+ "prompt",
384
+ "description",
385
+ "temperature",
386
+ "top_p",
387
+ "mode",
388
+ "hidden",
389
+ "color",
390
+ "steps",
391
+ "maxSteps",
392
+ "options",
393
+ "permission",
394
+ "disable",
395
+ "tools"
396
+ ]);
397
+ const options = isRecord2(data.options) ? { ...data.options } : {};
398
+ for (const [key, value] of Object.entries(data)) {
399
+ if (!known.has(key))
400
+ options[key] = value;
401
+ }
402
+ let permission = {};
403
+ if (data.tools !== undefined) {
404
+ permission = convertToolsToPermission(data.tools);
405
+ }
406
+ if (data.permission !== undefined) {
407
+ if (!isRecord2(data.permission))
408
+ throw new Error("permission must be an object");
409
+ permission = { ...permission, ...data.permission };
410
+ }
411
+ const steps = data.steps ?? data.maxSteps;
412
+ const raw = {
413
+ description: data.description,
414
+ prompt,
415
+ mode: data.mode,
416
+ model: data.model,
417
+ variant: data.variant,
418
+ hidden: data.hidden,
419
+ disable: data.disable,
420
+ color: data.color,
421
+ temperature: data.temperature,
422
+ top_p: data.top_p
423
+ };
424
+ if (steps !== undefined)
425
+ raw.steps = steps;
426
+ if (Object.keys(permission).length > 0)
427
+ raw.permission = permission;
428
+ if (Object.keys(options).length > 0)
429
+ raw.options = options;
430
+ for (const key of Object.keys(raw)) {
431
+ if (raw[key] === undefined)
432
+ delete raw[key];
433
+ }
434
+ return raw;
435
+ }
436
+ function loadAgentsFromDir(dir, source, agents, diagnostics) {
437
+ for (const subdir of ["agent", "agents"]) {
438
+ const root = join3(dir, subdir);
439
+ for (const filePath of listMarkdownFiles(root)) {
440
+ try {
441
+ const content = readFileSync2(filePath, "utf8");
442
+ const { data, body } = parseFrontmatter(content);
443
+ const name = agentNameFromPath(dir, filePath);
444
+ if (!name) {
445
+ diagnostics.push(`${filePath}: agent name is empty`);
446
+ continue;
447
+ }
448
+ const raw = normalizeOpenCodeAgentRaw(data, body.trim());
449
+ agents.set(name, { name, source, path: filePath, raw });
450
+ } catch (error) {
451
+ diagnostics.push(`${filePath}: ${formatError(error)}`);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ function loadOpenCodeAgents(options) {
457
+ const agents = new Map;
458
+ const diagnostics = [];
459
+ if (options.includeGlobal) {
460
+ loadAgentsFromDir(options.globalConfigDir ?? openCodeGlobalConfigDir(), "global", agents, diagnostics);
461
+ }
462
+ if (options.includeProject) {
463
+ loadAgentsFromDir(join3(options.cwd, ".opencode"), "local", agents, diagnostics);
464
+ }
465
+ return { agents, diagnostics };
243
466
  }
244
467
 
245
468
  // agents.ts
@@ -290,14 +513,14 @@ function normalizePermissions(value) {
290
513
  return Object.entries(value).flatMap(([permission, rules]) => permissionEntries(permission, rules));
291
514
  }
292
515
  function legacyConfigWarnings(piAgentDir) {
293
- const path = join3(piAgentDir, "settings.json");
294
- if (!existsSync2(path))
516
+ const path = join4(piAgentDir, "settings.json");
517
+ if (!existsSync3(path))
295
518
  return [];
296
519
  try {
297
- const value = JSON.parse(readFileSync2(path, "utf8"));
520
+ const value = JSON.parse(readFileSync3(path, "utf8"));
298
521
  if (!isRecord2(value))
299
522
  return [];
300
- const fields = ["agent", "permission", "subagents", "maxSubagents"].filter((field) => value[field] !== undefined);
523
+ const fields = ["agent", "permission", "subagents", "maxSubagents", "landstrip"].filter((field) => value[field] !== undefined);
301
524
  if (fields.length === 0)
302
525
  return [];
303
526
  return [
@@ -360,21 +583,40 @@ function normalizeAgent(name, raw, source) {
360
583
  function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = true) {
361
584
  const warnings = [
362
585
  ...legacyConfigWarnings(piAgentDir),
363
- ...includeProject ? legacyConfigWarnings(join3(cwd, ".pi")) : []
586
+ ...includeProject ? legacyConfigWarnings(join4(cwd, ".pi")) : []
364
587
  ];
365
588
  const diagnostics = [];
366
589
  let maxSubagents = 0;
367
590
  let subagents = {};
368
591
  let agentSources = new Map;
592
+ let showGlobalAgents = false;
593
+ let showLocalAgents = false;
369
594
  try {
370
595
  const config = loadLandstripConfig(cwd, includeProject, piAgentDir);
371
596
  maxSubagents = config.maxSubagents;
372
597
  subagents = config.subagents;
373
598
  agentSources = new Map(config.agentSources);
599
+ showGlobalAgents = config.opencode.showGlobalAgents;
600
+ showLocalAgents = config.opencode.showLocalAgents;
374
601
  } catch (error) {
375
602
  diagnostics.push(formatError(error));
376
603
  }
604
+ const openCode = loadOpenCodeAgents({
605
+ cwd,
606
+ includeGlobal: showGlobalAgents,
607
+ includeProject: showLocalAgents && includeProject
608
+ });
609
+ diagnostics.push(...openCode.diagnostics);
377
610
  const normalized = new Map;
611
+ for (const imported of openCode.agents.values()) {
612
+ try {
613
+ const agent = normalizeAgent(imported.name, imported.raw, imported.source);
614
+ if (agent)
615
+ normalized.set(imported.name, agent);
616
+ } catch (error) {
617
+ diagnostics.push(`${imported.path}: ${formatError(error)}`);
618
+ }
619
+ }
378
620
  if (subagents.agent !== undefined && !isRecord2(subagents.agent)) {
379
621
  diagnostics.push("subagents.agent must be an object");
380
622
  } else if (isRecord2(subagents.agent)) {
@@ -388,6 +630,8 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
388
630
  const agent = normalizeAgent(name, value, agentSources.get(name) ?? "built-in");
389
631
  if (agent)
390
632
  normalized.set(name, agent);
633
+ else
634
+ normalized.delete(name);
391
635
  } catch (error) {
392
636
  diagnostics.push(formatError(error));
393
637
  }
@@ -878,14 +1122,22 @@ var MAX_DEPTH = 3;
878
1122
  var packageDir2 = dirname2(fileURLToPath2(import.meta.url));
879
1123
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
880
1124
  var INSPECTOR_BODY_LINES = 16;
881
- var SUPPORTED_PI_MAJOR = 0;
882
- var MIN_SUPPORTED_PI_MINOR = 80;
883
- var MIN_SUPPORTED_PI_PATCH = 6;
1125
+ var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
884
1126
  var cachedPiPackage;
885
1127
  var piPackageResolved = false;
886
1128
  function isSupportedPiVersion(version) {
887
- const [major, minor = -1, patch = -1] = version;
888
- return version.length === 3 && Number.isInteger(major) && Number.isInteger(minor) && Number.isInteger(patch) && major === SUPPORTED_PI_MAJOR && (minor > MIN_SUPPORTED_PI_MINOR || minor === MIN_SUPPORTED_PI_MINOR && patch >= MIN_SUPPORTED_PI_PATCH);
1129
+ if (version.length !== 3 || !version.every((part) => Number.isInteger(part) && part >= 0)) {
1130
+ return false;
1131
+ }
1132
+ for (let i = 0;i < 3; i++) {
1133
+ const part = version[i];
1134
+ const min = MIN_SUPPORTED_PI_VERSION[i];
1135
+ if (part > min)
1136
+ return true;
1137
+ if (part < min)
1138
+ return false;
1139
+ }
1140
+ return true;
889
1141
  }
890
1142
  function resolvePiPackage() {
891
1143
  if (piPackageResolved)
@@ -895,8 +1147,8 @@ function resolvePiPackage() {
895
1147
  const entry = fileURLToPath2(import.meta.resolve("@earendil-works/pi-coding-agent"));
896
1148
  let dir2 = dirname2(entry);
897
1149
  for (;; ) {
898
- const pkgPath = join4(dir2, "package.json");
899
- if (existsSync3(pkgPath)) {
1150
+ const pkgPath = join5(dir2, "package.json");
1151
+ if (existsSync4(pkgPath)) {
900
1152
  const pkg = readPiPackage(pkgPath);
901
1153
  if (pkg)
902
1154
  return pkg;
@@ -909,8 +1161,8 @@ function resolvePiPackage() {
909
1161
  } catch {}
910
1162
  let dir = packageDir2;
911
1163
  for (;; ) {
912
- const pkgPath = join4(dir, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
913
- if (existsSync3(pkgPath)) {
1164
+ const pkgPath = join5(dir, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
1165
+ if (existsSync4(pkgPath)) {
914
1166
  const pkg = readPiPackage(pkgPath);
915
1167
  if (pkg)
916
1168
  return pkg;
@@ -923,7 +1175,7 @@ function resolvePiPackage() {
923
1175
  }
924
1176
  function readPiPackage(pkgPath) {
925
1177
  try {
926
- const value = JSON.parse(readFileSync3(pkgPath, "utf8"));
1178
+ const value = JSON.parse(readFileSync4(pkgPath, "utf8"));
927
1179
  if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
928
1180
  return;
929
1181
  }
@@ -933,8 +1185,8 @@ function readPiPackage(pkgPath) {
933
1185
  const binFile = typeof value.bin === "string" ? value.bin : isRecord2(value.bin) && typeof value.bin.pi === "string" ? value.bin.pi : undefined;
934
1186
  if (!binFile)
935
1187
  return;
936
- const cliEntry = join4(dirname2(pkgPath), binFile);
937
- if (!existsSync3(cliEntry))
1188
+ const cliEntry = join5(dirname2(pkgPath), binFile);
1189
+ if (!existsSync4(cliEntry))
938
1190
  return;
939
1191
  cachedPiPackage = { cliEntry, version: parts.slice(0, 3) };
940
1192
  return cachedPiPackage;
@@ -1008,7 +1260,7 @@ function messageContentText(message) {
1008
1260
  `);
1009
1261
  }
1010
1262
  function taskTranscript(task) {
1011
- if (!task.sessionFile || !existsSync3(task.sessionFile)) {
1263
+ if (!task.sessionFile || !existsSync4(task.sessionFile)) {
1012
1264
  const fallback = task.error ?? task.output ?? "Child session is not available yet.";
1013
1265
  return fallback.split(`
1014
1266
  `);
@@ -1038,7 +1290,7 @@ function isProjectTrusted(ctx) {
1038
1290
  return trustContext.isProjectTrusted?.() ?? false;
1039
1291
  }
1040
1292
  function dependencyRoot(path) {
1041
- const marker = `${sep}node_modules${sep}`;
1293
+ const marker = `${sep2}node_modules${sep2}`;
1042
1294
  const index = path.lastIndexOf(marker);
1043
1295
  return index < 0 ? undefined : path.slice(0, index + marker.length - 1);
1044
1296
  }
@@ -1059,7 +1311,7 @@ function agentBootstrapPaths(agentDir) {
1059
1311
  "bin",
1060
1312
  "npm",
1061
1313
  "git"
1062
- ].map((path) => join4(agentDir, path));
1314
+ ].map((path) => join5(agentDir, path));
1063
1315
  }
1064
1316
 
1065
1317
  class Semaphore {
@@ -1253,7 +1505,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1253
1505
  const parent = dirname2(existing);
1254
1506
  if (parent === existing)
1255
1507
  return path;
1256
- missing.unshift(basename(existing));
1508
+ missing.unshift(basename2(existing));
1257
1509
  existing = parent;
1258
1510
  }
1259
1511
  }
@@ -1275,11 +1527,11 @@ function permissionResource(tool, input, cwd) {
1275
1527
  return "*";
1276
1528
  const projectRoot = canonicalPermissionPath(resolve(cwd));
1277
1529
  const absolutePath = canonicalPermissionPath(resolve(cwd, input.path));
1278
- const projectPath = relative(projectRoot, absolutePath);
1279
- if (projectPath && !isAbsolute(projectPath) && !projectPath.startsWith(`..${sep}`) && projectPath !== "..") {
1280
- return projectPath.split(sep).join("/");
1530
+ const projectPath = relative2(projectRoot, absolutePath);
1531
+ if (projectPath && !isAbsolute(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1532
+ return projectPath.split(sep2).join("/");
1281
1533
  }
1282
- return absolutePath.split(sep).join("/");
1534
+ return absolutePath.split(sep2).join("/");
1283
1535
  }
1284
1536
  function permissionResources(tool, input, cwd) {
1285
1537
  if (tool !== "apply_patch" || typeof input.patchText !== "string") {
@@ -1811,7 +2063,7 @@ ${catalog.diagnostics.join(`
1811
2063
  if (task.agent !== agent.name) {
1812
2064
  throw new Error(`Task ${task.id} belongs to agent ${task.agent}, not ${agent.name}`);
1813
2065
  }
1814
- if (!this.running.has(task.id) && (!task.sessionDir || !existsSync3(task.sessionDir)) && (!task.sessionFile || !existsSync3(task.sessionFile))) {
2066
+ if (!this.running.has(task.id) && (!task.sessionDir || !existsSync4(task.sessionDir)) && (!task.sessionFile || !existsSync4(task.sessionFile))) {
1815
2067
  throw new Error(`Task ${task.id} session is unavailable`);
1816
2068
  }
1817
2069
  } else {
@@ -1921,7 +2173,7 @@ ${catalog.diagnostics.join(`
1921
2173
  createRecord(input, ctx, parentTask, agent, depth) {
1922
2174
  const parentSession = parentTask?.sessionFile ?? ctx.sessionManager.getSessionFile();
1923
2175
  const id = randomUUID();
1924
- const sessionDir = join4(getAgentDir3(), "sessions", "pi-landstrip", ctx.sessionManager.getSessionId(), id);
2176
+ const sessionDir = join5(getAgentDir3(), "sessions", "pi-landstrip", ctx.sessionManager.getSessionId(), id);
1925
2177
  mkdirSync2(sessionDir, { recursive: true });
1926
2178
  const task = {
1927
2179
  version: 1,
@@ -2111,7 +2363,7 @@ ${catalog.diagnostics.join(`
2111
2363
  "--mode",
2112
2364
  "rpc",
2113
2365
  "--extension",
2114
- join4(packageDir2, "index.ts"),
2366
+ join5(packageDir2, "index.ts"),
2115
2367
  ...workerExtensions.flatMap((extension) => ["--extension", extension.entry]),
2116
2368
  ...task.sessionFile ? ["--session", task.sessionFile] : task.sessionDir ? ["--session-dir", task.sessionDir] : [],
2117
2369
  "--model",
@@ -2126,7 +2378,7 @@ ${catalog.diagnostics.join(`
2126
2378
  ];
2127
2379
  const config = { rules, task, taskEnabled, steps: agent.steps };
2128
2380
  const publicContext = this.taskContext(task, ctx);
2129
- const temp = mkdtempSync(join4(tmpdir(), `pi-landstrip-task-${task.id}-`));
2381
+ const temp = mkdtempSync(join5(tmpdir(), `pi-landstrip-task-${task.id}-`));
2130
2382
  const agentDir = getAgentDir3();
2131
2383
  let launch;
2132
2384
  let rpc;
@@ -2158,9 +2410,9 @@ ${catalog.diagnostics.join(`
2158
2410
  ...new Set([
2159
2411
  ctx.cwd,
2160
2412
  ...agentBootstrapPaths(agentDir),
2161
- join4(homedir2(), ".agents", "skills"),
2413
+ join5(homedir3(), ".agents", "skills"),
2162
2414
  packageDir2,
2163
- join4(packageDir2, "node_modules"),
2415
+ join5(packageDir2, "node_modules"),
2164
2416
  invocation.command,
2165
2417
  cliRoot,
2166
2418
  extensionRoot,
@@ -2258,7 +2510,7 @@ ${catalog.diagnostics.join(`
2258
2510
  throw new Error("Unable to resolve the running Pi package; process-backed subagents are unavailable");
2259
2511
  }
2260
2512
  if (!isSupportedPiVersion(pkg.version)) {
2261
- throw new Error(`Process-backed subagents require Pi >=0.80.6; found ${pkg.version.join(".")}`);
2513
+ throw new Error(`Process-backed subagents require Pi >=0.82.0; found ${pkg.version.join(".")}`);
2262
2514
  }
2263
2515
  }
2264
2516
  async handleWorkerRequest(task, rules, ctx, request) {
@@ -2382,7 +2634,7 @@ ${renderTaskResult(task.id, "error", message)}`);
2382
2634
  if (!directory)
2383
2635
  throw new Error(`Task ${task.id} has no artifact directory`);
2384
2636
  mkdirSync2(directory, { recursive: true });
2385
- const artifactPath = join4(directory, `${kind}.txt`);
2637
+ const artifactPath = join5(directory, `${kind}.txt`);
2386
2638
  const bounded = boundTaskOutput(value, artifactPath);
2387
2639
  if (kind === "output")
2388
2640
  task.outputFile = artifactPath;
@@ -2628,23 +2880,23 @@ var NETWORK_PERMISSION_OPTIONS = [
2628
2880
  { label: "Abort (keep blocked)", key: "esc", action: "abort" }
2629
2881
  ];
2630
2882
  function loadSandboxConfig(cwd, includeProject) {
2631
- const projectConfigPath = join5(cwd, ".pi", "sandbox.json");
2632
- const globalConfigPath = join5(getAgentDir4(), "sandbox.json");
2633
- if (!existsSync4(globalConfigPath)) {
2634
- const templatePath = join5(packageDir3, "sandbox.json");
2883
+ const projectConfigPath = join6(cwd, ".pi", "sandbox.json");
2884
+ const globalConfigPath = join6(getAgentDir4(), "sandbox.json");
2885
+ if (!existsSync5(globalConfigPath)) {
2886
+ const templatePath = join6(packageDir3, "sandbox.json");
2635
2887
  mkdirSync3(dirname3(globalConfigPath), { recursive: true });
2636
- writeFileSync3(globalConfigPath, readFileSync4(templatePath, "utf-8"), "utf-8");
2888
+ writeFileSync3(globalConfigPath, readFileSync5(templatePath, "utf-8"), "utf-8");
2637
2889
  }
2638
- let globalConfig = JSON.parse(readFileSync4(join5(packageDir3, "sandbox.json"), "utf-8"));
2890
+ let globalConfig = JSON.parse(readFileSync5(join6(packageDir3, "sandbox.json"), "utf-8"));
2639
2891
  try {
2640
- const override = JSON.parse(readFileSync4(globalConfigPath, "utf-8"));
2892
+ const override = JSON.parse(readFileSync5(globalConfigPath, "utf-8"));
2641
2893
  globalConfig = deepMerge(globalConfig, override);
2642
2894
  } catch (error) {
2643
2895
  console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
2644
2896
  }
2645
- if (includeProject && existsSync4(projectConfigPath)) {
2897
+ if (includeProject && existsSync5(projectConfigPath)) {
2646
2898
  try {
2647
- const projectConfig = JSON.parse(readFileSync4(projectConfigPath, "utf-8"));
2899
+ const projectConfig = JSON.parse(readFileSync5(projectConfigPath, "utf-8"));
2648
2900
  const merged = deepMerge(globalConfig, projectConfig);
2649
2901
  merged.windows.backend = globalConfig.windows.backend;
2650
2902
  return merged;
@@ -2719,10 +2971,10 @@ function getConfigPaths(cwd) {
2719
2971
  return getPiConfigPaths(cwd, "sandbox.json");
2720
2972
  }
2721
2973
  function readOrEmptyConfig(configPath) {
2722
- if (!existsSync4(configPath))
2974
+ if (!existsSync5(configPath))
2723
2975
  return {};
2724
2976
  try {
2725
- return JSON.parse(readFileSync4(configPath, "utf-8"));
2977
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
2726
2978
  } catch {
2727
2979
  return {};
2728
2980
  }
@@ -2734,7 +2986,7 @@ function writeConfigFile(configPath, config) {
2734
2986
  }
2735
2987
  function getSandboxConfigWriteTarget(cwd, includeProject = true) {
2736
2988
  const { globalPath, projectPath } = getConfigPaths(cwd);
2737
- if (includeProject && existsSync4(projectPath)) {
2989
+ if (includeProject && existsSync5(projectPath)) {
2738
2990
  return { scope: "project", path: projectPath };
2739
2991
  }
2740
2992
  return { scope: "global", path: globalPath };
@@ -2810,11 +3062,11 @@ function canonicalizePath(filePath, cwd) {
2810
3062
  } catch {
2811
3063
  const tail = [];
2812
3064
  let probe = abs;
2813
- while (!existsSync4(probe)) {
3065
+ while (!existsSync5(probe)) {
2814
3066
  const parent = dirname3(probe);
2815
3067
  if (parent === probe)
2816
3068
  return abs;
2817
- tail.unshift(basename2(probe));
3069
+ tail.unshift(basename3(probe));
2818
3070
  probe = parent;
2819
3071
  }
2820
3072
  try {
@@ -2835,26 +3087,26 @@ function matchesPattern(filePath, patterns, cwd) {
2835
3087
  const escaped = absPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/|\*\*|\*/g, (token) => token === "**/" ? "(?:.*/)?" : token === "**" ? ".*" : "[^/]*");
2836
3088
  return new RegExp(`^${escaped}$`).test(abs);
2837
3089
  }
2838
- const sep2 = absPattern.endsWith("/") ? "" : "/";
2839
- return abs === absPattern || abs.startsWith(absPattern + sep2);
3090
+ const sep3 = absPattern.endsWith("/") ? "" : "/";
3091
+ return abs === absPattern || abs.startsWith(absPattern + sep3);
2840
3092
  });
2841
3093
  }
2842
3094
  function normalizeBlockedPath(path, cwd) {
2843
3095
  const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
2844
- return canonicalizePath(isAbsolute2(nativePath) ? nativePath : join5(cwd, nativePath), cwd);
3096
+ return canonicalizePath(isAbsolute2(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
2845
3097
  }
2846
3098
  function pathUnderDirectory(filePath, dir) {
2847
3099
  const normalizedFilePath = normalizePathSeparators(filePath);
2848
3100
  const normalizedDir = normalizePathSeparators(dir);
2849
3101
  if (normalizedFilePath === normalizedDir)
2850
3102
  return true;
2851
- const sep2 = normalizedDir.endsWith("/") ? "" : "/";
2852
- return normalizedFilePath.startsWith(normalizedDir + sep2);
3103
+ const sep3 = normalizedDir.endsWith("/") ? "" : "/";
3104
+ return normalizedFilePath.startsWith(normalizedDir + sep3);
2853
3105
  }
2854
3106
  function sessionScopeFor(filePath, baseDirectory) {
2855
3107
  const dir = dirname3(filePath);
2856
3108
  const normalizedDir = normalizePathSeparators(dir);
2857
- const home = homedir3();
3109
+ const home = homedir4();
2858
3110
  const boundaries = new Set;
2859
3111
  if (home)
2860
3112
  boundaries.add(home);
@@ -2870,7 +3122,7 @@ function sessionScopeFor(filePath, baseDirectory) {
2870
3122
  const first = rest.split("/")[0];
2871
3123
  if (!first)
2872
3124
  return filePath;
2873
- return join5(boundary, first);
3125
+ return join6(boundary, first);
2874
3126
  }
2875
3127
  }
2876
3128
  if (pathUnderDirectory(dir, baseDirectory))
@@ -3143,8 +3395,8 @@ function writeEnvFile(env, proxyPort, proxyToken) {
3143
3395
  lines.push("export NO_PROXY=''");
3144
3396
  lines.push("export no_proxy=''");
3145
3397
  }
3146
- const dir = mkdtempSync2(join5(tmpdir2(), "pi-landstrip-env-"));
3147
- const path = join5(dir, "env.sh");
3398
+ const dir = mkdtempSync2(join6(tmpdir2(), "pi-landstrip-env-"));
3399
+ const path = join6(dir, "env.sh");
3148
3400
  writeFileSync3(path, lines.join(`
3149
3401
  `), "utf-8");
3150
3402
  return { dir, path };
@@ -3285,7 +3537,7 @@ function createLandstripIntegration(options = {}) {
3285
3537
  if (!isAbsolute2(requestedEntry)) {
3286
3538
  throw new Error("Worker extension entry must be an absolute path or file URL");
3287
3539
  }
3288
- if (!existsSync4(requestedEntry)) {
3540
+ if (!existsSync5(requestedEntry)) {
3289
3541
  throw new Error(`Worker extension entry does not exist: ${requestedEntry}`);
3290
3542
  }
3291
3543
  const entry = realpathSync2(requestedEntry);
@@ -3327,7 +3579,7 @@ function createLandstripIntegration(options = {}) {
3327
3579
  const paths = mergeAllowances(config.filesystem.allowRead, sessionAllowedReadPaths, allowances?.readPaths);
3328
3580
  if (process.platform !== "win32")
3329
3581
  return paths;
3330
- return paths.filter((path) => !path.startsWith("/") && (path.includes("*") || existsSync4(expandPath(path, cwd))));
3582
+ return paths.filter((path) => !path.startsWith("/") && (path.includes("*") || existsSync5(expandPath(path, cwd))));
3331
3583
  }
3332
3584
  function getEffectiveAllowWrite(config, allowances) {
3333
3585
  const paths = mergeAllowances(config.filesystem.allowWrite, sessionAllowedWritePaths, allowances?.writePaths);
@@ -3433,8 +3685,8 @@ function createLandstripIntegration(options = {}) {
3433
3685
  };
3434
3686
  }
3435
3687
  function writePolicyFile(cwd, proxyPort, allowances) {
3436
- const dir = mkdtempSync2(join5(tmpdir2(), "pi-landstrip-"));
3437
- const path = join5(dir, "policy.json");
3688
+ const dir = mkdtempSync2(join6(tmpdir2(), "pi-landstrip-"));
3689
+ const path = join6(dir, "policy.json");
3438
3690
  writeFileSync3(path, JSON.stringify(buildLandstripPolicy(cwd, proxyPort, allowances), null, 2) + `
3439
3691
  `, "utf-8");
3440
3692
  return { dir, path };
@@ -3861,7 +4113,7 @@ function createLandstripIntegration(options = {}) {
3861
4113
  function createLandstripBashOps(ctx, callbacks = {}, allowances = { domains: [], readPaths: [], writePaths: [], targets: [] }) {
3862
4114
  return {
3863
4115
  async exec(command, cwd, { onData, signal, timeout, env }) {
3864
- if (!existsSync4(cwd))
4116
+ if (!existsSync5(cwd))
3865
4117
  throw new Error(`Working directory does not exist: ${cwd}`);
3866
4118
  const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
3867
4119
  const config = loadConfig(cwd);
@@ -4225,7 +4477,7 @@ ${message}
4225
4477
  networkLabel = `${config.network.allowedDomains.length} domains`;
4226
4478
  networkColor = "accent";
4227
4479
  }
4228
- const sep2 = theme.fg("dim", "·");
4480
+ const sep3 = theme.fg("dim", "·");
4229
4481
  const net = theme.fg(networkColor, networkLabel);
4230
4482
  const write = theme.fg("accent", `${config.filesystem.allowWrite.length} write paths`);
4231
4483
  const fields = [`${dot} ${label}`, net, write];
@@ -4235,7 +4487,7 @@ ${message}
4235
4487
  const label2 = backend === "restrictedUser" ? "restricted user" : `${config.windows.appContainerMode} AppContainer`;
4236
4488
  fields.splice(1, 0, theme.fg(standard ? "warning" : "accent", label2));
4237
4489
  }
4238
- setTuiStatus(ctx, "sandbox", fields.join(` ${sep2} `));
4490
+ setTuiStatus(ctx, "sandbox", fields.join(` ${sep3} `));
4239
4491
  }
4240
4492
  const headlessWarnings = new Set;
4241
4493
  function warnUnsandboxed(ctx, message, level) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.17.35",
3
+ "version": "0.17.37",
4
4
  "description": "Sandbox-aware Pi subagents and interactive command permissions",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -40,14 +40,15 @@
40
40
  "ci:test": "npm run test"
41
41
  },
42
42
  "dependencies": {
43
- "@landstrip/landstrip": "^0.17.35",
44
- "minimatch": "^10.2.5"
43
+ "@landstrip/landstrip": "^0.17.37",
44
+ "minimatch": "^10.2.5",
45
+ "yaml": "^2.9.0"
45
46
  },
46
47
  "devDependencies": {
47
- "@earendil-works/pi-coding-agent": "^0.81.1",
48
+ "@earendil-works/pi-coding-agent": "^0.82.0",
48
49
  "@earendil-works/pi-tui": "^0.81.1",
49
50
  "@types/node": "^24.0.0",
50
- "oxfmt": "^0.59.0",
51
+ "oxfmt": "^0.60.0",
51
52
  "oxlint": "^1.68.0",
52
53
  "typebox": "^1.3.6",
53
54
  "typescript": "^5.0.0",