pi-landstrip 0.17.35 → 0.17.36

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 +28 -3
  2. package/dist/index.ts +369 -81
  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,8 +208,33 @@ 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` contains normal Pi settings, such as the `pi-landstrip` package
212
+ entry, plus optional `landstrip.opencode` import flags:
213
+
214
+ ```json
215
+ {
216
+ "packages": ["npm:pi-landstrip"],
217
+ "landstrip": {
218
+ "opencode": {
219
+ "showGlobalAgents": false,
220
+ "showLocalAgents": false
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ Both flags default to `false`. When enabled:
227
+
228
+ - `showGlobalAgents` imports Markdown agents from the OpenCode global config
229
+ directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
230
+ `~/.config/opencode`) under `agent/` and `agents/`.
231
+ - `showLocalAgents` imports Markdown agents from the trusted project's
232
+ `.opencode/agent/` and `.opencode/agents/` directories.
233
+
234
+ OpenCode local agents override OpenCode global agents. Pi agent definitions
235
+ from `subagents.json` (built-in, global, and local) take precedence over OpenCode
236
+ imports with the same name; conflicts are silent. Local OpenCode agents are
237
+ skipped when the project is untrusted, regardless of the flag.
213
238
 
214
239
  `subagents.json` accepts only top-level `maxSubagents` and `subagents`; sandbox
215
240
  fields belong in `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 existsSync6,
7
7
  mkdirSync as mkdirSync3,
8
8
  mkdtempSync as mkdtempSync2,
9
- readFileSync as readFileSync4,
9
+ readFileSync as readFileSync6,
10
10
  realpathSync as realpathSync2,
11
11
  rmSync as rmSync2,
12
12
  writeFileSync as writeFileSync3
@@ -18,13 +18,13 @@ 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 join7, 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 {
26
26
  createBashToolDefinition,
27
- getAgentDir as getAgentDir4,
27
+ getAgentDir as getAgentDir5,
28
28
  getShellConfig,
29
29
  SettingsManager,
30
30
  withFileMutationQueue as withFileMutationQueue2
@@ -70,21 +70,21 @@ function encodeLandstripContext(context) {
70
70
  // subagents.ts
71
71
  import { randomUUID } from "node:crypto";
72
72
  import {
73
- existsSync as existsSync3,
73
+ existsSync as existsSync5,
74
74
  lstatSync,
75
75
  mkdirSync as mkdirSync2,
76
76
  mkdtempSync,
77
- readFileSync as readFileSync3,
77
+ readFileSync as readFileSync5,
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 join6, relative as relative2, resolve, sep as sep2 } from "node:path";
85
85
  import { fileURLToPath as fileURLToPath2 } from "node:url";
86
86
  import {
87
- getAgentDir as getAgentDir3,
87
+ getAgentDir as getAgentDir4,
88
88
  keyHint,
89
89
  SessionManager
90
90
  } from "@earendil-works/pi-coding-agent";
@@ -92,10 +92,10 @@ 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 existsSync4, readFileSync as readFileSync4 } from "node:fs";
96
+ import { join as join5 } from "node:path";
97
97
  import { minimatch } from "minimatch";
98
- import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
98
+ import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
99
99
 
100
100
  // config.ts
101
101
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -242,6 +242,267 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
242
242
  return { ...config, agentSources };
243
243
  }
244
244
 
245
+ // opencode-agents.ts
246
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
247
+ import { homedir as homedir2 } from "node:os";
248
+ import { basename, extname, join as join3, relative, sep } from "node:path";
249
+ import { parse as parseYaml } from "yaml";
250
+ var AGENT_DIR_PREFIXES = ["agent/", "agents/"];
251
+ function openCodeGlobalConfigDir() {
252
+ if (process.env.OPENCODE_CONFIG_DIR)
253
+ return process.env.OPENCODE_CONFIG_DIR;
254
+ const xdg = process.env.XDG_CONFIG_HOME;
255
+ return join3(xdg && xdg.length > 0 ? xdg : join3(homedir2(), ".config"), "opencode");
256
+ }
257
+ function listMarkdownFiles(root) {
258
+ if (!existsSync2(root))
259
+ return [];
260
+ const results = [];
261
+ const visit = (dir) => {
262
+ let entries;
263
+ try {
264
+ entries = readdirSync(dir);
265
+ } catch {
266
+ return;
267
+ }
268
+ for (const entry of entries) {
269
+ const path = join3(dir, entry);
270
+ let stats;
271
+ try {
272
+ stats = statSync(path);
273
+ } catch {
274
+ continue;
275
+ }
276
+ if (stats.isDirectory())
277
+ visit(path);
278
+ else if (stats.isFile() && extname(entry).toLowerCase() === ".md")
279
+ results.push(path);
280
+ }
281
+ };
282
+ visit(root);
283
+ return results;
284
+ }
285
+ function agentNameFromPath(dir, filePath) {
286
+ const relativePath = relative(dir, filePath).split(sep).join("/");
287
+ for (const prefix of AGENT_DIR_PREFIXES) {
288
+ if (relativePath.startsWith(prefix)) {
289
+ const candidate = relativePath.slice(prefix.length);
290
+ const extension2 = extname(candidate);
291
+ return extension2.length > 0 ? candidate.slice(0, -extension2.length) : candidate;
292
+ }
293
+ }
294
+ const base = basename(filePath);
295
+ const extension = extname(base);
296
+ return extension.length > 0 ? base.slice(0, -extension.length) : base;
297
+ }
298
+ function parseFrontmatter(content) {
299
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
300
+ if (!match)
301
+ return { data: {}, body: content };
302
+ const parsed = parseYaml(match[1] ?? "");
303
+ if (parsed === null || parsed === undefined)
304
+ return { data: {}, body: match[2] ?? "" };
305
+ if (!isRecord2(parsed))
306
+ throw new Error("frontmatter must be a YAML mapping");
307
+ return { data: { ...parsed }, body: match[2] ?? "" };
308
+ }
309
+ function convertToolsToPermission(tools) {
310
+ if (!isRecord2(tools))
311
+ throw new Error("tools must be a map of booleans");
312
+ const permission = {};
313
+ for (const [tool, enabled] of Object.entries(tools)) {
314
+ if (typeof enabled !== "boolean") {
315
+ throw new Error(`tools.${tool} must be a boolean`);
316
+ }
317
+ const action = enabled ? "allow" : "deny";
318
+ if (tool === "write" || tool === "edit" || tool === "patch") {
319
+ permission.edit = action;
320
+ continue;
321
+ }
322
+ permission[tool] = action;
323
+ }
324
+ return permission;
325
+ }
326
+ function normalizeOpenCodeAgentRaw(data, prompt) {
327
+ const known = new Set([
328
+ "name",
329
+ "model",
330
+ "variant",
331
+ "prompt",
332
+ "description",
333
+ "temperature",
334
+ "top_p",
335
+ "mode",
336
+ "hidden",
337
+ "color",
338
+ "steps",
339
+ "maxSteps",
340
+ "options",
341
+ "permission",
342
+ "disable",
343
+ "tools"
344
+ ]);
345
+ const options = isRecord2(data.options) ? { ...data.options } : {};
346
+ for (const [key, value] of Object.entries(data)) {
347
+ if (!known.has(key))
348
+ options[key] = value;
349
+ }
350
+ let permission = {};
351
+ if (data.tools !== undefined) {
352
+ permission = convertToolsToPermission(data.tools);
353
+ }
354
+ if (data.permission !== undefined) {
355
+ if (!isRecord2(data.permission))
356
+ throw new Error("permission must be an object");
357
+ permission = { ...permission, ...data.permission };
358
+ }
359
+ const steps = data.steps ?? data.maxSteps;
360
+ const raw = {
361
+ description: data.description,
362
+ prompt,
363
+ mode: data.mode,
364
+ model: data.model,
365
+ variant: data.variant,
366
+ hidden: data.hidden,
367
+ disable: data.disable,
368
+ color: data.color,
369
+ temperature: data.temperature,
370
+ top_p: data.top_p
371
+ };
372
+ if (steps !== undefined)
373
+ raw.steps = steps;
374
+ if (Object.keys(permission).length > 0)
375
+ raw.permission = permission;
376
+ if (Object.keys(options).length > 0)
377
+ raw.options = options;
378
+ for (const key of Object.keys(raw)) {
379
+ if (raw[key] === undefined)
380
+ delete raw[key];
381
+ }
382
+ return raw;
383
+ }
384
+ function loadAgentsFromDir(dir, source, agents, diagnostics) {
385
+ for (const subdir of ["agent", "agents"]) {
386
+ const root = join3(dir, subdir);
387
+ for (const filePath of listMarkdownFiles(root)) {
388
+ try {
389
+ const content = readFileSync2(filePath, "utf8");
390
+ const { data, body } = parseFrontmatter(content);
391
+ const name = agentNameFromPath(dir, filePath);
392
+ if (!name) {
393
+ diagnostics.push(`${filePath}: agent name is empty`);
394
+ continue;
395
+ }
396
+ const raw = normalizeOpenCodeAgentRaw(data, body.trim());
397
+ agents.set(name, { name, source, path: filePath, raw });
398
+ } catch (error) {
399
+ diagnostics.push(`${filePath}: ${formatError(error)}`);
400
+ }
401
+ }
402
+ }
403
+ }
404
+ function loadOpenCodeAgents(options) {
405
+ const agents = new Map;
406
+ const diagnostics = [];
407
+ if (options.includeGlobal) {
408
+ loadAgentsFromDir(options.globalConfigDir ?? openCodeGlobalConfigDir(), "global", agents, diagnostics);
409
+ }
410
+ if (options.includeProject) {
411
+ loadAgentsFromDir(join3(options.cwd, ".opencode"), "local", agents, diagnostics);
412
+ }
413
+ return { agents, diagnostics };
414
+ }
415
+
416
+ // settings.ts
417
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
418
+ import { join as join4 } from "node:path";
419
+ import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
420
+ var DEFAULT_OPENCODE_SETTINGS = {
421
+ showGlobalAgents: false,
422
+ showLocalAgents: false
423
+ };
424
+ var LANDSTRIP_SETTING_KEYS = new Set(["opencode"]);
425
+ var OPENCODE_SETTING_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
426
+ function readJsonObject(path) {
427
+ if (!existsSync3(path))
428
+ return {};
429
+ try {
430
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
431
+ if (!isRecord2(parsed)) {
432
+ return { warning: `${path}: settings.json must contain a JSON object` };
433
+ }
434
+ return { value: parsed };
435
+ } catch (error) {
436
+ const message = error instanceof Error ? error.message : String(error);
437
+ return { warning: `${path}: ${message}` };
438
+ }
439
+ }
440
+ function readBoolean(section, key, path, source, warnings) {
441
+ if (!(key in section))
442
+ return;
443
+ const value = section[key];
444
+ if (typeof value === "boolean")
445
+ return value;
446
+ warnings.push(`${source}: ${path} must be a boolean`);
447
+ return;
448
+ }
449
+ function readOpenCodeSection(section, source, warnings) {
450
+ if (!("opencode" in section))
451
+ return {};
452
+ if (!isRecord2(section.opencode)) {
453
+ warnings.push(`${source}: landstrip.opencode must be a JSON object`);
454
+ return {};
455
+ }
456
+ const opencode = section.opencode;
457
+ for (const key of Object.keys(opencode)) {
458
+ if (!OPENCODE_SETTING_KEYS.has(key)) {
459
+ warnings.push(`${source}: unknown landstrip.opencode setting ${key}`);
460
+ }
461
+ }
462
+ const settings = {};
463
+ const showGlobalAgents = readBoolean(opencode, "showGlobalAgents", "landstrip.opencode.showGlobalAgents", source, warnings);
464
+ if (showGlobalAgents !== undefined)
465
+ settings.showGlobalAgents = showGlobalAgents;
466
+ const showLocalAgents = readBoolean(opencode, "showLocalAgents", "landstrip.opencode.showLocalAgents", source, warnings);
467
+ if (showLocalAgents !== undefined)
468
+ settings.showLocalAgents = showLocalAgents;
469
+ return settings;
470
+ }
471
+ function readLandstripSection(root, source, warnings) {
472
+ if (!root || !("landstrip" in root))
473
+ return {};
474
+ if (!isRecord2(root.landstrip)) {
475
+ warnings.push(`${source}: landstrip must be a JSON object`);
476
+ return {};
477
+ }
478
+ const section = root.landstrip;
479
+ for (const key of Object.keys(section)) {
480
+ if (!LANDSTRIP_SETTING_KEYS.has(key)) {
481
+ warnings.push(`${source}: unknown landstrip setting ${key}`);
482
+ }
483
+ }
484
+ return readOpenCodeSection(section, source, warnings);
485
+ }
486
+ function resolveLandstripSettings(cwd, includeProject = true, piAgentDir = getAgentDir2()) {
487
+ const warnings = [];
488
+ const globalPath = join4(piAgentDir, "settings.json");
489
+ const projectPath = join4(cwd, ".pi", "settings.json");
490
+ const globalFile = readJsonObject(globalPath);
491
+ if (globalFile.warning)
492
+ warnings.push(globalFile.warning);
493
+ const projectFile = includeProject ? readJsonObject(projectPath) : {};
494
+ if (projectFile.warning)
495
+ warnings.push(projectFile.warning);
496
+ const settings = {
497
+ opencode: {
498
+ ...DEFAULT_OPENCODE_SETTINGS,
499
+ ...readLandstripSection(globalFile.value, globalPath, warnings),
500
+ ...includeProject ? readLandstripSection(projectFile.value, projectPath, warnings) : {}
501
+ }
502
+ };
503
+ return { settings, warnings };
504
+ }
505
+
245
506
  // agents.ts
246
507
  var AGENT_FIELDS = new Set([
247
508
  "name",
@@ -290,11 +551,11 @@ function normalizePermissions(value) {
290
551
  return Object.entries(value).flatMap(([permission, rules]) => permissionEntries(permission, rules));
291
552
  }
292
553
  function legacyConfigWarnings(piAgentDir) {
293
- const path = join3(piAgentDir, "settings.json");
294
- if (!existsSync2(path))
554
+ const path = join5(piAgentDir, "settings.json");
555
+ if (!existsSync4(path))
295
556
  return [];
296
557
  try {
297
- const value = JSON.parse(readFileSync2(path, "utf8"));
558
+ const value = JSON.parse(readFileSync4(path, "utf8"));
298
559
  if (!isRecord2(value))
299
560
  return [];
300
561
  const fields = ["agent", "permission", "subagents", "maxSubagents"].filter((field) => value[field] !== undefined);
@@ -357,10 +618,10 @@ function normalizeAgent(name, raw, source) {
357
618
  providerOptions
358
619
  };
359
620
  }
360
- function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = true) {
621
+ function loadAgentCatalog(cwd, piAgentDir = getAgentDir3(), includeProject = true) {
361
622
  const warnings = [
362
623
  ...legacyConfigWarnings(piAgentDir),
363
- ...includeProject ? legacyConfigWarnings(join3(cwd, ".pi")) : []
624
+ ...includeProject ? legacyConfigWarnings(join5(cwd, ".pi")) : []
364
625
  ];
365
626
  const diagnostics = [];
366
627
  let maxSubagents = 0;
@@ -374,7 +635,24 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
374
635
  } catch (error) {
375
636
  diagnostics.push(formatError(error));
376
637
  }
638
+ const { settings, warnings: settingsWarnings } = resolveLandstripSettings(cwd, includeProject, piAgentDir);
639
+ warnings.push(...settingsWarnings);
640
+ const openCode = loadOpenCodeAgents({
641
+ cwd,
642
+ includeGlobal: settings.opencode.showGlobalAgents,
643
+ includeProject: settings.opencode.showLocalAgents && includeProject
644
+ });
645
+ diagnostics.push(...openCode.diagnostics);
377
646
  const normalized = new Map;
647
+ for (const imported of openCode.agents.values()) {
648
+ try {
649
+ const agent = normalizeAgent(imported.name, imported.raw, imported.source);
650
+ if (agent)
651
+ normalized.set(imported.name, agent);
652
+ } catch (error) {
653
+ diagnostics.push(`${imported.path}: ${formatError(error)}`);
654
+ }
655
+ }
378
656
  if (subagents.agent !== undefined && !isRecord2(subagents.agent)) {
379
657
  diagnostics.push("subagents.agent must be an object");
380
658
  } else if (isRecord2(subagents.agent)) {
@@ -388,6 +666,8 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
388
666
  const agent = normalizeAgent(name, value, agentSources.get(name) ?? "built-in");
389
667
  if (agent)
390
668
  normalized.set(name, agent);
669
+ else
670
+ normalized.delete(name);
391
671
  } catch (error) {
392
672
  diagnostics.push(formatError(error));
393
673
  }
@@ -878,14 +1158,22 @@ var MAX_DEPTH = 3;
878
1158
  var packageDir2 = dirname2(fileURLToPath2(import.meta.url));
879
1159
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
880
1160
  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;
1161
+ var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
884
1162
  var cachedPiPackage;
885
1163
  var piPackageResolved = false;
886
1164
  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);
1165
+ if (version.length !== 3 || !version.every((part) => Number.isInteger(part) && part >= 0)) {
1166
+ return false;
1167
+ }
1168
+ for (let i = 0;i < 3; i++) {
1169
+ const part = version[i];
1170
+ const min = MIN_SUPPORTED_PI_VERSION[i];
1171
+ if (part > min)
1172
+ return true;
1173
+ if (part < min)
1174
+ return false;
1175
+ }
1176
+ return true;
889
1177
  }
890
1178
  function resolvePiPackage() {
891
1179
  if (piPackageResolved)
@@ -895,8 +1183,8 @@ function resolvePiPackage() {
895
1183
  const entry = fileURLToPath2(import.meta.resolve("@earendil-works/pi-coding-agent"));
896
1184
  let dir2 = dirname2(entry);
897
1185
  for (;; ) {
898
- const pkgPath = join4(dir2, "package.json");
899
- if (existsSync3(pkgPath)) {
1186
+ const pkgPath = join6(dir2, "package.json");
1187
+ if (existsSync5(pkgPath)) {
900
1188
  const pkg = readPiPackage(pkgPath);
901
1189
  if (pkg)
902
1190
  return pkg;
@@ -909,8 +1197,8 @@ function resolvePiPackage() {
909
1197
  } catch {}
910
1198
  let dir = packageDir2;
911
1199
  for (;; ) {
912
- const pkgPath = join4(dir, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
913
- if (existsSync3(pkgPath)) {
1200
+ const pkgPath = join6(dir, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
1201
+ if (existsSync5(pkgPath)) {
914
1202
  const pkg = readPiPackage(pkgPath);
915
1203
  if (pkg)
916
1204
  return pkg;
@@ -923,7 +1211,7 @@ function resolvePiPackage() {
923
1211
  }
924
1212
  function readPiPackage(pkgPath) {
925
1213
  try {
926
- const value = JSON.parse(readFileSync3(pkgPath, "utf8"));
1214
+ const value = JSON.parse(readFileSync5(pkgPath, "utf8"));
927
1215
  if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
928
1216
  return;
929
1217
  }
@@ -933,8 +1221,8 @@ function readPiPackage(pkgPath) {
933
1221
  const binFile = typeof value.bin === "string" ? value.bin : isRecord2(value.bin) && typeof value.bin.pi === "string" ? value.bin.pi : undefined;
934
1222
  if (!binFile)
935
1223
  return;
936
- const cliEntry = join4(dirname2(pkgPath), binFile);
937
- if (!existsSync3(cliEntry))
1224
+ const cliEntry = join6(dirname2(pkgPath), binFile);
1225
+ if (!existsSync5(cliEntry))
938
1226
  return;
939
1227
  cachedPiPackage = { cliEntry, version: parts.slice(0, 3) };
940
1228
  return cachedPiPackage;
@@ -1008,7 +1296,7 @@ function messageContentText(message) {
1008
1296
  `);
1009
1297
  }
1010
1298
  function taskTranscript(task) {
1011
- if (!task.sessionFile || !existsSync3(task.sessionFile)) {
1299
+ if (!task.sessionFile || !existsSync5(task.sessionFile)) {
1012
1300
  const fallback = task.error ?? task.output ?? "Child session is not available yet.";
1013
1301
  return fallback.split(`
1014
1302
  `);
@@ -1038,7 +1326,7 @@ function isProjectTrusted(ctx) {
1038
1326
  return trustContext.isProjectTrusted?.() ?? false;
1039
1327
  }
1040
1328
  function dependencyRoot(path) {
1041
- const marker = `${sep}node_modules${sep}`;
1329
+ const marker = `${sep2}node_modules${sep2}`;
1042
1330
  const index = path.lastIndexOf(marker);
1043
1331
  return index < 0 ? undefined : path.slice(0, index + marker.length - 1);
1044
1332
  }
@@ -1059,7 +1347,7 @@ function agentBootstrapPaths(agentDir) {
1059
1347
  "bin",
1060
1348
  "npm",
1061
1349
  "git"
1062
- ].map((path) => join4(agentDir, path));
1350
+ ].map((path) => join6(agentDir, path));
1063
1351
  }
1064
1352
 
1065
1353
  class Semaphore {
@@ -1253,7 +1541,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1253
1541
  const parent = dirname2(existing);
1254
1542
  if (parent === existing)
1255
1543
  return path;
1256
- missing.unshift(basename(existing));
1544
+ missing.unshift(basename2(existing));
1257
1545
  existing = parent;
1258
1546
  }
1259
1547
  }
@@ -1275,11 +1563,11 @@ function permissionResource(tool, input, cwd) {
1275
1563
  return "*";
1276
1564
  const projectRoot = canonicalPermissionPath(resolve(cwd));
1277
1565
  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("/");
1566
+ const projectPath = relative2(projectRoot, absolutePath);
1567
+ if (projectPath && !isAbsolute(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1568
+ return projectPath.split(sep2).join("/");
1281
1569
  }
1282
- return absolutePath.split(sep).join("/");
1570
+ return absolutePath.split(sep2).join("/");
1283
1571
  }
1284
1572
  function permissionResources(tool, input, cwd) {
1285
1573
  if (tool !== "apply_patch" || typeof input.patchText !== "string") {
@@ -1472,7 +1760,7 @@ ${this.primaryAgent.prompt}` };
1472
1760
  });
1473
1761
  }
1474
1762
  getAgentCatalog(ctx) {
1475
- return this.loadCatalog(ctx.cwd, getAgentDir3(), isProjectTrusted(ctx));
1763
+ return this.loadCatalog(ctx.cwd, getAgentDir4(), isProjectTrusted(ctx));
1476
1764
  }
1477
1765
  getPrimaryAgent() {
1478
1766
  return this.primaryAgent;
@@ -1686,7 +1974,7 @@ ${this.primaryAgent.prompt}` };
1686
1974
  }
1687
1975
  createTaskTool(parentTask, callerRules, boundCtx) {
1688
1976
  const cwd = boundCtx?.cwd ?? process.cwd();
1689
- const catalog = this.loadCatalog(cwd, getAgentDir3(), boundCtx ? isProjectTrusted(boundCtx) : false);
1977
+ const catalog = this.loadCatalog(cwd, getAgentDir4(), boundCtx ? isProjectTrusted(boundCtx) : false);
1690
1978
  const agents = availableSubagents(catalog).filter((agent) => !agent.hidden && permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
1691
1979
  const descriptions = agents.map((agent) => `${agent.name}: ${agent.description ?? "No description"}`).join(`
1692
1980
  `);
@@ -1771,7 +2059,7 @@ ${theme.fg("dim", `↳ /subagents ${details.taskId.slice(0, 8)} to inspect`)}`;
1771
2059
  async execute(input, ctx, signal, parentTask, callerRules, update) {
1772
2060
  if (!input.prompt.trim())
1773
2061
  throw new Error("Task prompt cannot be empty");
1774
- const catalog = this.loadCatalog(ctx.cwd, getAgentDir3(), isProjectTrusted(ctx));
2062
+ const catalog = this.loadCatalog(ctx.cwd, getAgentDir4(), isProjectTrusted(ctx));
1775
2063
  for (const warning of catalog.warnings)
1776
2064
  ctx.ui.notify(warning, "warning");
1777
2065
  for (const diagnostic of catalog.diagnostics)
@@ -1811,7 +2099,7 @@ ${catalog.diagnostics.join(`
1811
2099
  if (task.agent !== agent.name) {
1812
2100
  throw new Error(`Task ${task.id} belongs to agent ${task.agent}, not ${agent.name}`);
1813
2101
  }
1814
- if (!this.running.has(task.id) && (!task.sessionDir || !existsSync3(task.sessionDir)) && (!task.sessionFile || !existsSync3(task.sessionFile))) {
2102
+ if (!this.running.has(task.id) && (!task.sessionDir || !existsSync5(task.sessionDir)) && (!task.sessionFile || !existsSync5(task.sessionFile))) {
1815
2103
  throw new Error(`Task ${task.id} session is unavailable`);
1816
2104
  }
1817
2105
  } else {
@@ -1921,7 +2209,7 @@ ${catalog.diagnostics.join(`
1921
2209
  createRecord(input, ctx, parentTask, agent, depth) {
1922
2210
  const parentSession = parentTask?.sessionFile ?? ctx.sessionManager.getSessionFile();
1923
2211
  const id = randomUUID();
1924
- const sessionDir = join4(getAgentDir3(), "sessions", "pi-landstrip", ctx.sessionManager.getSessionId(), id);
2212
+ const sessionDir = join6(getAgentDir4(), "sessions", "pi-landstrip", ctx.sessionManager.getSessionId(), id);
1925
2213
  mkdirSync2(sessionDir, { recursive: true });
1926
2214
  const task = {
1927
2215
  version: 1,
@@ -2111,7 +2399,7 @@ ${catalog.diagnostics.join(`
2111
2399
  "--mode",
2112
2400
  "rpc",
2113
2401
  "--extension",
2114
- join4(packageDir2, "index.ts"),
2402
+ join6(packageDir2, "index.ts"),
2115
2403
  ...workerExtensions.flatMap((extension) => ["--extension", extension.entry]),
2116
2404
  ...task.sessionFile ? ["--session", task.sessionFile] : task.sessionDir ? ["--session-dir", task.sessionDir] : [],
2117
2405
  "--model",
@@ -2126,8 +2414,8 @@ ${catalog.diagnostics.join(`
2126
2414
  ];
2127
2415
  const config = { rules, task, taskEnabled, steps: agent.steps };
2128
2416
  const publicContext = this.taskContext(task, ctx);
2129
- const temp = mkdtempSync(join4(tmpdir(), `pi-landstrip-task-${task.id}-`));
2130
- const agentDir = getAgentDir3();
2417
+ const temp = mkdtempSync(join6(tmpdir(), `pi-landstrip-task-${task.id}-`));
2418
+ const agentDir = getAgentDir4();
2131
2419
  let launch;
2132
2420
  let rpc;
2133
2421
  const abortWorker = () => void rpc?.stop().catch(() => {
@@ -2158,9 +2446,9 @@ ${catalog.diagnostics.join(`
2158
2446
  ...new Set([
2159
2447
  ctx.cwd,
2160
2448
  ...agentBootstrapPaths(agentDir),
2161
- join4(homedir2(), ".agents", "skills"),
2449
+ join6(homedir3(), ".agents", "skills"),
2162
2450
  packageDir2,
2163
- join4(packageDir2, "node_modules"),
2451
+ join6(packageDir2, "node_modules"),
2164
2452
  invocation.command,
2165
2453
  cliRoot,
2166
2454
  extensionRoot,
@@ -2258,7 +2546,7 @@ ${catalog.diagnostics.join(`
2258
2546
  throw new Error("Unable to resolve the running Pi package; process-backed subagents are unavailable");
2259
2547
  }
2260
2548
  if (!isSupportedPiVersion(pkg.version)) {
2261
- throw new Error(`Process-backed subagents require Pi >=0.80.6; found ${pkg.version.join(".")}`);
2549
+ throw new Error(`Process-backed subagents require Pi >=0.82.0; found ${pkg.version.join(".")}`);
2262
2550
  }
2263
2551
  }
2264
2552
  async handleWorkerRequest(task, rules, ctx, request) {
@@ -2382,7 +2670,7 @@ ${renderTaskResult(task.id, "error", message)}`);
2382
2670
  if (!directory)
2383
2671
  throw new Error(`Task ${task.id} has no artifact directory`);
2384
2672
  mkdirSync2(directory, { recursive: true });
2385
- const artifactPath = join4(directory, `${kind}.txt`);
2673
+ const artifactPath = join6(directory, `${kind}.txt`);
2386
2674
  const bounded = boundTaskOutput(value, artifactPath);
2387
2675
  if (kind === "output")
2388
2676
  task.outputFile = artifactPath;
@@ -2472,7 +2760,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
2472
2760
  this.primaryAgent = undefined;
2473
2761
  this.primaryRules = undefined;
2474
2762
  this.primaryConfigurationError = false;
2475
- const catalog = this.loadCatalog(ctx.cwd, getAgentDir3(), isProjectTrusted(ctx));
2763
+ const catalog = this.loadCatalog(ctx.cwd, getAgentDir4(), isProjectTrusted(ctx));
2476
2764
  for (const warning of catalog.warnings)
2477
2765
  ctx.ui.notify(warning, "warning");
2478
2766
  this.maxSubagents = catalog.maxSubagents;
@@ -2628,23 +2916,23 @@ var NETWORK_PERMISSION_OPTIONS = [
2628
2916
  { label: "Abort (keep blocked)", key: "esc", action: "abort" }
2629
2917
  ];
2630
2918
  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");
2919
+ const projectConfigPath = join7(cwd, ".pi", "sandbox.json");
2920
+ const globalConfigPath = join7(getAgentDir5(), "sandbox.json");
2921
+ if (!existsSync6(globalConfigPath)) {
2922
+ const templatePath = join7(packageDir3, "sandbox.json");
2635
2923
  mkdirSync3(dirname3(globalConfigPath), { recursive: true });
2636
- writeFileSync3(globalConfigPath, readFileSync4(templatePath, "utf-8"), "utf-8");
2924
+ writeFileSync3(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
2637
2925
  }
2638
- let globalConfig = JSON.parse(readFileSync4(join5(packageDir3, "sandbox.json"), "utf-8"));
2926
+ let globalConfig = JSON.parse(readFileSync6(join7(packageDir3, "sandbox.json"), "utf-8"));
2639
2927
  try {
2640
- const override = JSON.parse(readFileSync4(globalConfigPath, "utf-8"));
2928
+ const override = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
2641
2929
  globalConfig = deepMerge(globalConfig, override);
2642
2930
  } catch (error) {
2643
2931
  console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
2644
2932
  }
2645
- if (includeProject && existsSync4(projectConfigPath)) {
2933
+ if (includeProject && existsSync6(projectConfigPath)) {
2646
2934
  try {
2647
- const projectConfig = JSON.parse(readFileSync4(projectConfigPath, "utf-8"));
2935
+ const projectConfig = JSON.parse(readFileSync6(projectConfigPath, "utf-8"));
2648
2936
  const merged = deepMerge(globalConfig, projectConfig);
2649
2937
  merged.windows.backend = globalConfig.windows.backend;
2650
2938
  return merged;
@@ -2719,10 +3007,10 @@ function getConfigPaths(cwd) {
2719
3007
  return getPiConfigPaths(cwd, "sandbox.json");
2720
3008
  }
2721
3009
  function readOrEmptyConfig(configPath) {
2722
- if (!existsSync4(configPath))
3010
+ if (!existsSync6(configPath))
2723
3011
  return {};
2724
3012
  try {
2725
- return JSON.parse(readFileSync4(configPath, "utf-8"));
3013
+ return JSON.parse(readFileSync6(configPath, "utf-8"));
2726
3014
  } catch {
2727
3015
  return {};
2728
3016
  }
@@ -2734,7 +3022,7 @@ function writeConfigFile(configPath, config) {
2734
3022
  }
2735
3023
  function getSandboxConfigWriteTarget(cwd, includeProject = true) {
2736
3024
  const { globalPath, projectPath } = getConfigPaths(cwd);
2737
- if (includeProject && existsSync4(projectPath)) {
3025
+ if (includeProject && existsSync6(projectPath)) {
2738
3026
  return { scope: "project", path: projectPath };
2739
3027
  }
2740
3028
  return { scope: "global", path: globalPath };
@@ -2810,11 +3098,11 @@ function canonicalizePath(filePath, cwd) {
2810
3098
  } catch {
2811
3099
  const tail = [];
2812
3100
  let probe = abs;
2813
- while (!existsSync4(probe)) {
3101
+ while (!existsSync6(probe)) {
2814
3102
  const parent = dirname3(probe);
2815
3103
  if (parent === probe)
2816
3104
  return abs;
2817
- tail.unshift(basename2(probe));
3105
+ tail.unshift(basename3(probe));
2818
3106
  probe = parent;
2819
3107
  }
2820
3108
  try {
@@ -2835,26 +3123,26 @@ function matchesPattern(filePath, patterns, cwd) {
2835
3123
  const escaped = absPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/|\*\*|\*/g, (token) => token === "**/" ? "(?:.*/)?" : token === "**" ? ".*" : "[^/]*");
2836
3124
  return new RegExp(`^${escaped}$`).test(abs);
2837
3125
  }
2838
- const sep2 = absPattern.endsWith("/") ? "" : "/";
2839
- return abs === absPattern || abs.startsWith(absPattern + sep2);
3126
+ const sep3 = absPattern.endsWith("/") ? "" : "/";
3127
+ return abs === absPattern || abs.startsWith(absPattern + sep3);
2840
3128
  });
2841
3129
  }
2842
3130
  function normalizeBlockedPath(path, cwd) {
2843
3131
  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);
3132
+ return canonicalizePath(isAbsolute2(nativePath) ? nativePath : join7(cwd, nativePath), cwd);
2845
3133
  }
2846
3134
  function pathUnderDirectory(filePath, dir) {
2847
3135
  const normalizedFilePath = normalizePathSeparators(filePath);
2848
3136
  const normalizedDir = normalizePathSeparators(dir);
2849
3137
  if (normalizedFilePath === normalizedDir)
2850
3138
  return true;
2851
- const sep2 = normalizedDir.endsWith("/") ? "" : "/";
2852
- return normalizedFilePath.startsWith(normalizedDir + sep2);
3139
+ const sep3 = normalizedDir.endsWith("/") ? "" : "/";
3140
+ return normalizedFilePath.startsWith(normalizedDir + sep3);
2853
3141
  }
2854
3142
  function sessionScopeFor(filePath, baseDirectory) {
2855
3143
  const dir = dirname3(filePath);
2856
3144
  const normalizedDir = normalizePathSeparators(dir);
2857
- const home = homedir3();
3145
+ const home = homedir4();
2858
3146
  const boundaries = new Set;
2859
3147
  if (home)
2860
3148
  boundaries.add(home);
@@ -2870,7 +3158,7 @@ function sessionScopeFor(filePath, baseDirectory) {
2870
3158
  const first = rest.split("/")[0];
2871
3159
  if (!first)
2872
3160
  return filePath;
2873
- return join5(boundary, first);
3161
+ return join7(boundary, first);
2874
3162
  }
2875
3163
  }
2876
3164
  if (pathUnderDirectory(dir, baseDirectory))
@@ -3143,8 +3431,8 @@ function writeEnvFile(env, proxyPort, proxyToken) {
3143
3431
  lines.push("export NO_PROXY=''");
3144
3432
  lines.push("export no_proxy=''");
3145
3433
  }
3146
- const dir = mkdtempSync2(join5(tmpdir2(), "pi-landstrip-env-"));
3147
- const path = join5(dir, "env.sh");
3434
+ const dir = mkdtempSync2(join7(tmpdir2(), "pi-landstrip-env-"));
3435
+ const path = join7(dir, "env.sh");
3148
3436
  writeFileSync3(path, lines.join(`
3149
3437
  `), "utf-8");
3150
3438
  return { dir, path };
@@ -3285,7 +3573,7 @@ function createLandstripIntegration(options = {}) {
3285
3573
  if (!isAbsolute2(requestedEntry)) {
3286
3574
  throw new Error("Worker extension entry must be an absolute path or file URL");
3287
3575
  }
3288
- if (!existsSync4(requestedEntry)) {
3576
+ if (!existsSync6(requestedEntry)) {
3289
3577
  throw new Error(`Worker extension entry does not exist: ${requestedEntry}`);
3290
3578
  }
3291
3579
  const entry = realpathSync2(requestedEntry);
@@ -3327,7 +3615,7 @@ function createLandstripIntegration(options = {}) {
3327
3615
  const paths = mergeAllowances(config.filesystem.allowRead, sessionAllowedReadPaths, allowances?.readPaths);
3328
3616
  if (process.platform !== "win32")
3329
3617
  return paths;
3330
- return paths.filter((path) => !path.startsWith("/") && (path.includes("*") || existsSync4(expandPath(path, cwd))));
3618
+ return paths.filter((path) => !path.startsWith("/") && (path.includes("*") || existsSync6(expandPath(path, cwd))));
3331
3619
  }
3332
3620
  function getEffectiveAllowWrite(config, allowances) {
3333
3621
  const paths = mergeAllowances(config.filesystem.allowWrite, sessionAllowedWritePaths, allowances?.writePaths);
@@ -3433,8 +3721,8 @@ function createLandstripIntegration(options = {}) {
3433
3721
  };
3434
3722
  }
3435
3723
  function writePolicyFile(cwd, proxyPort, allowances) {
3436
- const dir = mkdtempSync2(join5(tmpdir2(), "pi-landstrip-"));
3437
- const path = join5(dir, "policy.json");
3724
+ const dir = mkdtempSync2(join7(tmpdir2(), "pi-landstrip-"));
3725
+ const path = join7(dir, "policy.json");
3438
3726
  writeFileSync3(path, JSON.stringify(buildLandstripPolicy(cwd, proxyPort, allowances), null, 2) + `
3439
3727
  `, "utf-8");
3440
3728
  return { dir, path };
@@ -3861,7 +4149,7 @@ function createLandstripIntegration(options = {}) {
3861
4149
  function createLandstripBashOps(ctx, callbacks = {}, allowances = { domains: [], readPaths: [], writePaths: [], targets: [] }) {
3862
4150
  return {
3863
4151
  async exec(command, cwd, { onData, signal, timeout, env }) {
3864
- if (!existsSync4(cwd))
4152
+ if (!existsSync6(cwd))
3865
4153
  throw new Error(`Working directory does not exist: ${cwd}`);
3866
4154
  const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
3867
4155
  const config = loadConfig(cwd);
@@ -4225,7 +4513,7 @@ ${message}
4225
4513
  networkLabel = `${config.network.allowedDomains.length} domains`;
4226
4514
  networkColor = "accent";
4227
4515
  }
4228
- const sep2 = theme.fg("dim", "·");
4516
+ const sep3 = theme.fg("dim", "·");
4229
4517
  const net = theme.fg(networkColor, networkLabel);
4230
4518
  const write = theme.fg("accent", `${config.filesystem.allowWrite.length} write paths`);
4231
4519
  const fields = [`${dot} ${label}`, net, write];
@@ -4235,7 +4523,7 @@ ${message}
4235
4523
  const label2 = backend === "restrictedUser" ? "restricted user" : `${config.windows.appContainerMode} AppContainer`;
4236
4524
  fields.splice(1, 0, theme.fg(standard ? "warning" : "accent", label2));
4237
4525
  }
4238
- setTuiStatus(ctx, "sandbox", fields.join(` ${sep2} `));
4526
+ setTuiStatus(ctx, "sandbox", fields.join(` ${sep3} `));
4239
4527
  }
4240
4528
  const headlessWarnings = new Set;
4241
4529
  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.36",
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.36",
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",