pi-landstrip 0.18.32 → 0.18.34

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/README.md CHANGED
@@ -136,12 +136,16 @@ forms at one scope is an error.
136
136
  Agent definitions accept `name`, `description`, `prompt`, `mode`, `model`,
137
137
  `variant`, `temperature`, `top_p`, `steps`, `color`, `hidden`, `disable`,
138
138
  `options`, and `permission`. `mode` is `primary`, `subagent`, or `all` and
139
- defaults to `all`.
139
+ defaults to `all`. `/agents` Primary lists only `mode: primary` agents. The
140
+ Subagent tab lists `mode: subagent` and `mode: all` agents, including JSON and
141
+ Markdown definitions.
140
142
 
141
143
  The built-in primary agents are `build` and `plan`; built-in subagents are
142
144
  `general`, `scout`, and `explore`. Pi also loads Markdown agents from
143
- `~/.pi/agent/agents/` and `.pi/agents/`. Project definitions override global
144
- definitions, and configured agents override Markdown agents with the same name.
145
+ `~/.pi/agent/agents/` and `.pi/agents/`. Project definitions overlay global
146
+ definitions. Configured fields overlay Markdown agents with the same name, so a
147
+ model-only `landstrip.json` entry keeps the Markdown agent's mode, prompt, and
148
+ permissions.
145
149
 
146
150
  ## Commands and tasks
147
151
 
package/api.ts CHANGED
@@ -6,6 +6,8 @@ import type { Readable, Writable } from 'node:stream';
6
6
 
7
7
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
8
8
 
9
+ import { isRecord } from '@landstrip/landstrip/shared';
10
+
9
11
  export const LANDSTRIP_CONTEXT_ENV = 'LANDSTRIP_CONTEXT';
10
12
  export const LANDSTRIP_RUNTIME_VERSION = 2;
11
13
 
@@ -168,10 +170,6 @@ function eventBus(pi: ExtensionAPI): EventBusLike | undefined {
168
170
  return (pi as ExtensionAPI & { events?: EventBusLike }).events;
169
171
  }
170
172
 
171
- function isRecord(value: unknown): value is Record<string, unknown> {
172
- return typeof value === 'object' && value !== null && !Array.isArray(value);
173
- }
174
-
175
173
  function isRuntime(value: unknown): value is PiLandstripRuntimeV2 {
176
174
  return (
177
175
  isRecord(value) &&
package/dist/index.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  isIP,
21
21
  Socket as NetSocket
22
22
  } from "node:net";
23
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
23
+ import { tmpdir as tmpdir2 } from "node:os";
24
24
  import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute3, join as join7, parse, resolve as resolve3, sep as sep4 } from "node:path";
25
25
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
26
26
  import { fileURLToPath as fileURLToPath2, URL } from "node:url";
@@ -35,8 +35,22 @@ import { matchesKey as matchesKey2, truncateToWidth as truncateToWidth3 } from "
35
35
  import {
36
36
  binaryPath
37
37
  } from "@landstrip/landstrip";
38
+ import {
39
+ allowsAllDomains,
40
+ controlResponseLine,
41
+ domainMatchesAny,
42
+ formatLandstripTraps,
43
+ isDenialTrap,
44
+ isFilesystemTrap,
45
+ isQueryTrap,
46
+ parseLandstripTraps,
47
+ parseTrapLine,
48
+ pathUnderDirectory,
49
+ sessionScopeFor
50
+ } from "@landstrip/landstrip/shared";
38
51
 
39
52
  // api.ts
53
+ import { isRecord } from "@landstrip/landstrip/shared";
40
54
  var LANDSTRIP_CONTEXT_ENV = "LANDSTRIP_CONTEXT";
41
55
  var LANDSTRIP_RUNTIME_VERSION = 2;
42
56
  var RUNTIME_REGISTER_EVENT = "landstrip:runtime:register:v2";
@@ -44,9 +58,6 @@ var RUNTIME_DISCOVER_EVENT = "landstrip:runtime:discover:v2";
44
58
  function eventBus(pi) {
45
59
  return pi.events;
46
60
  }
47
- function isRecord(value) {
48
- return typeof value === "object" && value !== null && !Array.isArray(value);
49
- }
50
61
  function publishLandstripRuntime(pi, runtime) {
51
62
  const events = eventBus(pi);
52
63
  if (!events)
@@ -198,15 +209,7 @@ var BUILT_IN_LANDSTRIP_CONFIG = {
198
209
  import { readFileSync } from "node:fs";
199
210
  import { homedir } from "node:os";
200
211
  import { isAbsolute, join, resolve } from "node:path";
201
- function expandHomePath(path) {
202
- if (path === "~" || path === "$HOME")
203
- return homedir();
204
- if (path.startsWith("~/"))
205
- return join(homedir(), path.slice(2));
206
- if (path.startsWith("$HOME/"))
207
- return join(homedir(), path.slice(6));
208
- return path;
209
- }
212
+ import { expandHomePath, isRecord as isRecord2 } from "@landstrip/landstrip/shared";
210
213
  function expandFileReferences(text, baseDir) {
211
214
  if (!text.includes("{file:"))
212
215
  return text;
@@ -237,9 +240,6 @@ function expandFileReferences(text, baseDir) {
237
240
  function formatError(error) {
238
241
  return error instanceof Error ? error.message : String(error);
239
242
  }
240
- function isRecord2(value) {
241
- return typeof value === "object" && value !== null && !Array.isArray(value);
242
- }
243
243
  var OPENCODE_THEME_COLORS = new Set([
244
244
  "primary",
245
245
  "secondary",
@@ -996,16 +996,20 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
996
996
  continue;
997
997
  }
998
998
  const imported = normalized.get(name);
999
- if (Object.keys(value).every((key) => key === "disable")) {
1000
- if (imported) {
1001
- normalized.set(name, {
1002
- ...imported,
1003
- disabled: value.disable === true,
1004
- raw: { ...imported.raw, disable: value.disable }
1005
- });
999
+ if (imported) {
1000
+ try {
1001
+ const source = agentSources.get(name) ?? imported.source;
1002
+ normalized.set(name, normalizeAgent(name, { ...imported.raw, ...value }, source, imported.origin ?? {
1003
+ kind: "pi-markdown",
1004
+ source: imported.source
1005
+ }));
1006
+ } catch (error) {
1007
+ diagnostics.push(formatError(error));
1006
1008
  }
1007
1009
  continue;
1008
1010
  }
1011
+ if (Object.keys(value).every((key) => key === "disable"))
1012
+ continue;
1009
1013
  try {
1010
1014
  const source = agentSources.get(name) ?? "built-in";
1011
1015
  normalized.set(name, normalizeAgent(name, value, source, {
@@ -1058,6 +1062,9 @@ function mergePermissionRules(...values) {
1058
1062
  function availableAgents(catalog) {
1059
1063
  return [...catalog.agents.values()].filter((agent) => !agent.hidden && !agent.disabled);
1060
1064
  }
1065
+ function isPrimaryAgent(agent) {
1066
+ return agent.mode === "primary";
1067
+ }
1061
1068
  function agentSupportsMode(agent, mode) {
1062
1069
  return agent.mode === mode || agent.mode === "all";
1063
1070
  }
@@ -2744,7 +2751,7 @@ ${this.primaryAgent.prompt}` };
2744
2751
  let disabledOverrides = loadAgentDisabledOverrides(ctx.cwd, projectTrusted);
2745
2752
  let maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2746
2753
  let tab = requested ? "log" : "primary";
2747
- const agentsForTab = () => agents.filter((agent) => agentSupportsMode(agent, tab === "subagent" ? "subagent" : "primary"));
2754
+ const agentsForTab = () => agents.filter((agent) => tab === "subagent" ? agentSupportsMode(agent, "subagent") : isPrimaryAgent(agent));
2748
2755
  let selectedAgent = Math.max(0, agentsForTab().findIndex((agent) => agent.name === this.primaryAgent?.name));
2749
2756
  let selectedTask = requested ? tasks.findIndex((task) => task.id === requested || task.id.startsWith(requested)) : 0;
2750
2757
  if (selectedTask < 0) {
@@ -3105,7 +3112,7 @@ ${this.primaryAgent.prompt}` };
3105
3112
  tui.requestRender();
3106
3113
  });
3107
3114
  return;
3108
- } else if (tab === "primary" && matchesKey(data, "return") && agent && !agent.disabled && agentSupportsMode(agent, "primary")) {
3115
+ } else if (tab === "primary" && matchesKey(data, "return") && agent && !agent.disabled && isPrimaryAgent(agent)) {
3109
3116
  this.selectPrimaryAgent(agent.name, ctx).then(() => tui.requestRender()).catch((error) => ctx.ui.notify(`Could not select primary agent: ${formatError(error)}`, "error"));
3110
3117
  return;
3111
3118
  } else if (tab === "primary" && matchesKey(data, "return") && agent) {
@@ -3314,7 +3321,7 @@ ${this.primaryAgent.prompt}` };
3314
3321
  ctx.ui.notify(diagnostic, "warning");
3315
3322
  if (catalog.diagnostics.length > 0)
3316
3323
  return false;
3317
- const agent = availableAgents(catalog).find((candidate) => candidate.name === name && agentSupportsMode(candidate, "primary"));
3324
+ const agent = availableAgents(catalog).find((candidate) => candidate.name === name && isPrimaryAgent(candidate));
3318
3325
  if (!agent) {
3319
3326
  ctx.ui.notify(`Unknown primary agent: ${name}`, "error");
3320
3327
  return false;
@@ -3335,7 +3342,7 @@ ${this.primaryAgent.prompt}` };
3335
3342
  ctx.ui.notify(diagnostic, "warning");
3336
3343
  if (catalog.diagnostics.length > 0)
3337
3344
  return;
3338
- const agents = availableAgents(catalog).filter((agent) => agentSupportsMode(agent, "primary"));
3345
+ const agents = availableAgents(catalog).filter(isPrimaryAgent);
3339
3346
  if (agents.length === 0) {
3340
3347
  ctx.ui.notify("No primary agents are available", "warning");
3341
3348
  return;
@@ -4256,7 +4263,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
4256
4263
  if (typeof value?.name === "string")
4257
4264
  name = value.name;
4258
4265
  }
4259
- const agents = availableAgents(catalog).filter((agent2) => agentSupportsMode(agent2, "primary"));
4266
+ const agents = availableAgents(catalog).filter(isPrimaryAgent);
4260
4267
  const agent = agents.find((candidate) => candidate.name === name) ?? agents[0];
4261
4268
  if (agent && !await this.activatePrimaryAgent(agent, catalog, ctx, false)) {
4262
4269
  this.invalidatePrimaryAgent(catalog, ctx);
@@ -4620,23 +4627,6 @@ function addWritePathToConfig(configPath, pathToAdd) {
4620
4627
  function mergeAllowances(base, session, execution) {
4621
4628
  return [...base, ...session, ...execution ?? []];
4622
4629
  }
4623
- function domainMatchesPattern(domain, pattern) {
4624
- const normalizedDomain = domain.toLowerCase().replace(/\.+$/, "");
4625
- const normalizedPattern = pattern.toLowerCase().replace(/\.+$/, "");
4626
- if (normalizedPattern === "*")
4627
- return true;
4628
- if (normalizedPattern.startsWith("*.")) {
4629
- const base = normalizedPattern.slice(2);
4630
- return normalizedDomain === base || normalizedDomain.endsWith(`.${base}`);
4631
- }
4632
- return normalizedDomain === normalizedPattern;
4633
- }
4634
- function domainMatchesAny(domain, patterns) {
4635
- return patterns.some((pattern) => domainMatchesPattern(domain, pattern));
4636
- }
4637
- function allowsAllDomains(allowedDomains) {
4638
- return allowedDomains.includes("*");
4639
- }
4640
4630
  function shouldPromptForWrite(path, allowWrite, cwd) {
4641
4631
  return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
4642
4632
  }
@@ -4703,40 +4693,6 @@ function normalizeBlockedPath(path, cwd) {
4703
4693
  const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
4704
4694
  return canonicalizePath(isAbsolute3(nativePath) ? nativePath : join7(cwd, nativePath), cwd);
4705
4695
  }
4706
- function pathUnderDirectory(filePath, dir) {
4707
- const normalizedFilePath = normalizePathSeparators(filePath);
4708
- const normalizedDir = normalizePathSeparators(dir);
4709
- if (normalizedFilePath === normalizedDir)
4710
- return true;
4711
- const sep5 = normalizedDir.endsWith("/") ? "" : "/";
4712
- return normalizedFilePath.startsWith(normalizedDir + sep5);
4713
- }
4714
- function sessionScopeFor(filePath, baseDirectory) {
4715
- const dir = dirname4(filePath);
4716
- const normalizedDir = normalizePathSeparators(dir);
4717
- const home = homedir3();
4718
- const boundaries = new Set;
4719
- if (home)
4720
- boundaries.add(home);
4721
- try {
4722
- const realHome = realpathSync2.native(home);
4723
- if (realHome)
4724
- boundaries.add(realHome);
4725
- } catch {}
4726
- for (const boundary of boundaries) {
4727
- const normalizedBoundary = normalizePathSeparators(boundary);
4728
- if (pathUnderDirectory(dir, boundary)) {
4729
- const rest = normalizedDir.slice(normalizedBoundary.length).replace(/^\/+/, "");
4730
- const first = rest.split("/")[0];
4731
- if (!first)
4732
- return filePath;
4733
- return join7(boundary, first);
4734
- }
4735
- }
4736
- if (pathUnderDirectory(dir, baseDirectory))
4737
- return baseDirectory;
4738
- return dir;
4739
- }
4740
4696
  function longestPrefixMatch(path, patterns, cwd) {
4741
4697
  let best = -1;
4742
4698
  for (const pattern of patterns) {
@@ -4761,15 +4717,6 @@ function isPathLike(value) {
4761
4717
  function normalizePathMatch(value, cwd) {
4762
4718
  return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
4763
4719
  }
4764
- function isFilesystemTrap(trap) {
4765
- return trap.kind === "filesystem";
4766
- }
4767
- function isDenialTrap(trap) {
4768
- return trap.kind === "filesystem" || trap.kind === "network";
4769
- }
4770
- function isQueryTrap(trap) {
4771
- return isDenialTrap(trap) && trap.state === "query";
4772
- }
4773
4720
  function extractBlockedPath(trapOutput, cwd) {
4774
4721
  const landstripErrors = parseLandstripTraps(trapOutput).filter(isFilesystemTrap);
4775
4722
  if (landstripErrors.length > 0) {
@@ -4815,67 +4762,6 @@ function extractTrapBlockedPath(trapOutput, cwd, operation) {
4815
4762
  }
4816
4763
  return null;
4817
4764
  }
4818
- function isLandstripTrap(value) {
4819
- if (typeof value !== "object" || value === null)
4820
- return false;
4821
- const obj = value;
4822
- switch (obj.kind) {
4823
- case "filesystem":
4824
- return (obj.operation === "read" || obj.operation === "write") && typeof obj.path === "string" && typeof obj.query_id === "string";
4825
- case "network":
4826
- return typeof obj.operation === "string" && typeof obj.target === "string" && typeof obj.query_id === "string";
4827
- case "launch":
4828
- return typeof obj.program === "string" && typeof obj.message === "string";
4829
- case "usage":
4830
- return typeof obj.message === "string";
4831
- case "internal":
4832
- return typeof obj.code === "string" && typeof obj.message === "string";
4833
- default:
4834
- return false;
4835
- }
4836
- }
4837
- function parseTrapLine(line) {
4838
- try {
4839
- const parsed = JSON.parse(line);
4840
- return isLandstripTrap(parsed) ? parsed : null;
4841
- } catch {
4842
- return null;
4843
- }
4844
- }
4845
- function parseLandstripTraps(output) {
4846
- const traps = [];
4847
- for (const line of output.trim().split(`
4848
- `)) {
4849
- const trap = parseTrapLine(line);
4850
- if (trap)
4851
- traps.push(trap);
4852
- }
4853
- return traps;
4854
- }
4855
- function formatLandstripTraps(traps) {
4856
- return traps.map((trap) => {
4857
- switch (trap.kind) {
4858
- case "filesystem":
4859
- return `landstrip: filesystem ${trap.operation} denied: ${trap.path} (${trap.mechanism})`;
4860
- case "network":
4861
- return `landstrip: network ${trap.operation} denied: ${trap.target} (${trap.mechanism})`;
4862
- case "launch":
4863
- return `landstrip: launch failed: ${trap.program}: ${trap.message}`;
4864
- case "usage":
4865
- return `landstrip: usage error: ${trap.message}`;
4866
- case "internal": {
4867
- const mechanism = trap.mechanism ? ` (${trap.mechanism})` : "";
4868
- return `landstrip: ${trap.code}${mechanism}: ${trap.message}`;
4869
- }
4870
- }
4871
- }).join(`
4872
- `);
4873
- }
4874
- function controlResponseLine(queryId, action) {
4875
- const response = { query_id: queryId, action };
4876
- return JSON.stringify(response) + `
4877
- `;
4878
- }
4879
4765
  function notify(ctx, message, level) {
4880
4766
  if (!ctx.hasUI)
4881
4767
  return;
@@ -5763,8 +5649,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5763
5649
  if (options2.signal?.aborted)
5764
5650
  throw new Error("Task cancelled");
5765
5651
  if (!ensureSandboxState(options2.ctx)) {
5766
- const explicitlyDisabled = noSandboxFlag || !loadConfig(options2.cwd).enabled;
5767
- if (!explicitlyDisabled) {
5652
+ if (sandboxState === "unavailable") {
5768
5653
  throw new Error("Sandbox is unavailable; refusing subagent process");
5769
5654
  }
5770
5655
  if (!unsandboxedWorkerWarningShown) {
@@ -5870,7 +5755,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5870
5755
  };
5871
5756
  }
5872
5757
  async function prepareProcess(options2) {
5873
- if (process.platform === "win32" && (noSandboxFlag || !loadConfig(options2.cwd).enabled)) {
5758
+ if (process.platform === "win32" && (getNoSandboxFlag() || !loadConfig(options2.cwd).enabled)) {
5874
5759
  throw new Error("Generic process preparation requires sandboxing on Windows");
5875
5760
  }
5876
5761
  return prepareRpcWorker({
@@ -6450,7 +6335,7 @@ ${message}
6450
6335
  enableStatus(ctx, config);
6451
6336
  return true;
6452
6337
  }
6453
- let noSandboxFlag = false;
6338
+ let getNoSandboxFlag = () => false;
6454
6339
  function disableSandbox(ctx) {
6455
6340
  sandboxEnabled = false;
6456
6341
  sandboxReady = false;
@@ -6458,7 +6343,7 @@ ${message}
6458
6343
  setTuiStatus(ctx, "sandbox", undefined);
6459
6344
  }
6460
6345
  function ensureSandboxState(ctx) {
6461
- if (noSandboxFlag) {
6346
+ if (getNoSandboxFlag()) {
6462
6347
  disableSandbox(ctx);
6463
6348
  return false;
6464
6349
  }
@@ -6504,6 +6389,7 @@ ${message}
6504
6389
  type: "boolean",
6505
6390
  default: false
6506
6391
  });
6392
+ getNoSandboxFlag = () => Boolean(maybePi.getFlag?.("no-sandbox"));
6507
6393
  unpublishRuntime?.();
6508
6394
  unpublishRuntime = publishLandstripRuntime(pi, integration);
6509
6395
  if (shouldRegisterBashTool)
@@ -6526,8 +6412,7 @@ ${message}
6526
6412
  resetSessionAllowances();
6527
6413
  const trustContext = ctx;
6528
6414
  projectConfigTrusted = trustContext.isProjectTrusted?.() ?? false;
6529
- noSandboxFlag = Boolean(maybePi.getFlag?.("no-sandbox"));
6530
- if (noSandboxFlag) {
6415
+ if (getNoSandboxFlag()) {
6531
6416
  disableSandbox(ctx);
6532
6417
  notify(ctx, "Sandbox disabled via --no-sandbox", "warning");
6533
6418
  if (!unpublishRuntime)
@@ -6567,7 +6452,7 @@ ${message}
6567
6452
  const shouldToggle = await ctx.ui.custom((tui, theme, _kb, done) => {
6568
6453
  const { muted, accent, text } = themeColors(theme);
6569
6454
  const sandboxStatus = () => {
6570
- if (noSandboxFlag)
6455
+ if (getNoSandboxFlag())
6571
6456
  return { color: "warning", label: "Disabled by --no-sandbox" };
6572
6457
  if (!config.enabled)
6573
6458
  return { color: "warning", label: "Disabled by configuration" };
@@ -6643,10 +6528,10 @@ ${target.path}`);
6643
6528
  const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectTrusted);
6644
6529
  if (!enabled) {
6645
6530
  disableSandbox(ctx);
6646
- } else if (!noSandboxFlag) {
6531
+ } else if (!getNoSandboxFlag()) {
6647
6532
  enableSandbox(ctx);
6648
6533
  }
6649
- if (enabled && noSandboxFlag) {
6534
+ if (enabled && getNoSandboxFlag()) {
6650
6535
  notify(ctx, "Sandbox remains disabled via --no-sandbox", "warning");
6651
6536
  } else {
6652
6537
  notify(ctx, `Sandbox ${enabled ? "enabled" : "disabled"} in ${scope} config`, "info");
@@ -6662,7 +6547,7 @@ ${target.path}`);
6662
6547
  const effectiveEnabled = loadConfig(ctx.cwd).enabled;
6663
6548
  if (!effectiveEnabled) {
6664
6549
  disableSandbox(ctx);
6665
- } else if (!noSandboxFlag) {
6550
+ } else if (!getNoSandboxFlag()) {
6666
6551
  enableSandbox(ctx);
6667
6552
  }
6668
6553
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.32",
3
+ "version": "0.18.34",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip": "^0.18.32",
42
+ "@landstrip/landstrip": "^0.18.34",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"