pi-landstrip 0.18.41 → 0.18.43

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/api.ts +12 -14
  2. package/dist/index.ts +237 -294
  3. package/package.json +2 -2
package/api.ts CHANGED
@@ -213,13 +213,13 @@ export function useLandstrip(
213
213
  }
214
214
  }
215
215
 
216
- export function provideLandstripShell(
216
+ function provideExtensionProvider(
217
217
  pi: ExtensionAPI,
218
- provider: LandstripShellProvider,
218
+ register: (runtime: PiLandstripRuntimeV2) => () => void,
219
219
  ): () => void {
220
220
  let unregisterProvider: (() => void) | undefined;
221
221
  const stopDiscovery = useLandstrip(pi, (runtime) => {
222
- const unregisterNext = runtime.registerShellProvider(provider);
222
+ const unregisterNext = register(runtime);
223
223
  unregisterProvider?.();
224
224
  unregisterProvider = unregisterNext;
225
225
  });
@@ -229,20 +229,18 @@ export function provideLandstripShell(
229
229
  };
230
230
  }
231
231
 
232
+ export function provideLandstripShell(
233
+ pi: ExtensionAPI,
234
+ provider: LandstripShellProvider,
235
+ ): () => void {
236
+ return provideExtensionProvider(pi, (runtime) => runtime.registerShellProvider(provider));
237
+ }
238
+
232
239
  export function provideLandstripPermissionAsk(
233
240
  pi: ExtensionAPI,
234
241
  provider: LandstripPermissionAskProvider,
235
242
  ): () => void {
236
- let unregisterProvider: (() => void) | undefined;
237
- const stopDiscovery = useLandstrip(pi, (runtime) => {
238
- const unregisterNext = runtime.registerPermissionAskProvider(provider);
239
- unregisterProvider?.();
240
- unregisterProvider = unregisterNext;
241
- });
242
- return () => {
243
- stopDiscovery();
244
- unregisterProvider?.();
245
- };
243
+ return provideExtensionProvider(pi, (runtime) => runtime.registerPermissionAskProvider(provider));
246
244
  }
247
245
 
248
246
  export function publishLandstripRuntime(
@@ -288,7 +286,7 @@ export function contextFromEnvironment(
288
286
  !['enabled', 'disabled', 'unavailable'].includes(String(value.sandbox)) ||
289
287
  typeof value.cwd !== 'string' ||
290
288
  !Number.isInteger(value.depth) ||
291
- Number(value.depth) < 0 ||
289
+ (value.depth as number) < 0 ||
292
290
  ['sessionId', 'taskId', 'parentTaskId', 'agent'].some(
293
291
  (key) => value[key] !== undefined && typeof value[key] !== 'string',
294
292
  )
package/dist/index.ts CHANGED
@@ -111,10 +111,11 @@ import {
111
111
  SelectList,
112
112
  SettingsList,
113
113
  Text,
114
- truncateToWidth as truncateToWidth2,
115
- visibleWidth as visibleWidth2,
114
+ truncateToWidth,
115
+ visibleWidth,
116
116
  wrapTextWithAnsi
117
117
  } from "@earendil-works/pi-tui";
118
+ import { canonicalizeHost } from "@landstrip/landstrip-api/shared";
118
119
  import { Type } from "typebox";
119
120
 
120
121
  // agents.ts
@@ -133,78 +134,6 @@ import {
133
134
  printParseErrorCode
134
135
  } from "jsonc-parser";
135
136
 
136
- // built-in-agents.ts
137
- var BUILT_IN_LANDSTRIP_CONFIG = {
138
- maxSubagents: 0,
139
- toolFilesystemPolicy: "host",
140
- agent: {
141
- build: {
142
- description: "Implement and test requested changes",
143
- mode: "primary",
144
- color: "success",
145
- prompt: "Implement, test, and complete the request."
146
- },
147
- plan: {
148
- description: "Plan before running commands or editing files",
149
- mode: "primary",
150
- color: "warning",
151
- prompt: "Analyze the request and produce a clear plan before making changes.",
152
- permission: { edit: "ask", bash: "ask", task: "ask" }
153
- },
154
- general: {
155
- description: "General-purpose agent for complex tasks",
156
- mode: "subagent",
157
- prompt: "Complete the task and return a concise result.",
158
- permission: { todowrite: "deny" }
159
- },
160
- scout: {
161
- description: "Read-only codebase reconnaissance",
162
- mode: "subagent",
163
- prompt: "Inspect the codebase without editing. Return concise findings with exact file references and a recommended starting point.",
164
- permission: {
165
- "*": "deny",
166
- read: {
167
- "*": "allow",
168
- "**/*.env": "ask",
169
- "**/*.env.*": "ask",
170
- "**/*.env.example": "allow"
171
- },
172
- glob: "allow",
173
- grep: "allow",
174
- list: "allow"
175
- }
176
- },
177
- explore: {
178
- description: "Codebase research with shell and web access",
179
- mode: "subagent",
180
- prompt: "Explore without editing. Use shell or web tools when needed, then return concise findings with file references.",
181
- permission: {
182
- "*": "deny",
183
- read: {
184
- "*": "allow",
185
- "**/*.env": "ask",
186
- "**/*.env.*": "ask",
187
- "**/*.env.example": "allow"
188
- },
189
- glob: "allow",
190
- grep: "allow",
191
- list: "allow",
192
- bash: "allow",
193
- webfetch: "allow",
194
- websearch: "allow"
195
- }
196
- }
197
- },
198
- permission: {
199
- "*": "allow",
200
- read: {
201
- "**/*.env": "ask",
202
- "**/*.env.*": "ask",
203
- "**/*.env.example": "allow"
204
- }
205
- }
206
- };
207
-
208
137
  // util.ts
209
138
  import { readFileSync } from "node:fs";
210
139
  import { homedir } from "node:os";
@@ -272,17 +201,15 @@ function isAgentColor(value) {
272
201
  return true;
273
202
  return OPENCODE_THEME_COLORS.has(value);
274
203
  }
275
- function hexFg(hex, text) {
276
- const r = Number.parseInt(hex.slice(1, 3), 16);
277
- const g = Number.parseInt(hex.slice(3, 5), 16);
278
- const b = Number.parseInt(hex.slice(5, 7), 16);
279
- return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
280
- }
281
204
  function colorizeAgentText(color, text, themeFg, fallback = "accent") {
282
205
  if (!color)
283
206
  return themeFg ? themeFg(fallback, text) : text;
284
- if (color.startsWith("#"))
285
- return hexFg(color, text);
207
+ if (color.startsWith("#")) {
208
+ const r = Number.parseInt(color.slice(1, 3), 16);
209
+ const g = Number.parseInt(color.slice(3, 5), 16);
210
+ const b = Number.parseInt(color.slice(5, 7), 16);
211
+ return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
212
+ }
286
213
  if (themeFg)
287
214
  return themeFg(THEME_COLOR_ALIASES[color] ?? fallback, text);
288
215
  const ansi = NAMED_COLOR_ANSI[color];
@@ -412,6 +339,76 @@ class PermissionPromptCoordinator {
412
339
  }
413
340
 
414
341
  // config.ts
342
+ var BUILT_IN_LANDSTRIP_CONFIG = {
343
+ maxSubagents: 0,
344
+ toolFilesystemPolicy: "host",
345
+ agent: {
346
+ build: {
347
+ description: "Implement and test requested changes",
348
+ mode: "primary",
349
+ color: "success",
350
+ prompt: "Implement, test, and complete the request."
351
+ },
352
+ plan: {
353
+ description: "Plan before running commands or editing files",
354
+ mode: "primary",
355
+ color: "warning",
356
+ prompt: "Analyze the request and produce a clear plan before making changes.",
357
+ permission: { edit: "ask", bash: "ask", task: "ask" }
358
+ },
359
+ general: {
360
+ description: "General-purpose agent for complex tasks",
361
+ mode: "subagent",
362
+ prompt: "Complete the task and return a concise result.",
363
+ permission: { todowrite: "deny" }
364
+ },
365
+ scout: {
366
+ description: "Read-only codebase reconnaissance",
367
+ mode: "subagent",
368
+ prompt: "Inspect the codebase without editing. Return concise findings with exact file references and a recommended starting point.",
369
+ permission: {
370
+ "*": "deny",
371
+ read: {
372
+ "*": "allow",
373
+ "**/*.env": "ask",
374
+ "**/*.env.*": "ask",
375
+ "**/*.env.example": "allow"
376
+ },
377
+ glob: "allow",
378
+ grep: "allow",
379
+ list: "allow"
380
+ }
381
+ },
382
+ explore: {
383
+ description: "Codebase research with shell and web access",
384
+ mode: "subagent",
385
+ prompt: "Explore without editing. Use shell or web tools when needed, then return concise findings with file references.",
386
+ permission: {
387
+ "*": "deny",
388
+ read: {
389
+ "*": "allow",
390
+ "**/*.env": "ask",
391
+ "**/*.env.*": "ask",
392
+ "**/*.env.example": "allow"
393
+ },
394
+ glob: "allow",
395
+ grep: "allow",
396
+ list: "allow",
397
+ bash: "allow",
398
+ webfetch: "allow",
399
+ websearch: "allow"
400
+ }
401
+ }
402
+ },
403
+ permission: {
404
+ "*": "allow",
405
+ read: {
406
+ "**/*.env": "ask",
407
+ "**/*.env.*": "ask",
408
+ "**/*.env.example": "allow"
409
+ }
410
+ }
411
+ };
415
412
  function isProjectTrusted(ctx) {
416
413
  const trustContext = ctx;
417
414
  return trustContext.isProjectTrusted?.() ?? false;
@@ -420,9 +417,6 @@ var MAX_SUBAGENTS = 16;
420
417
  var LANDSTRIP_KEYS = ["maxSubagents", "agent", "permission", "toolFilesystemPolicy"];
421
418
  var JSON_FORMAT = { formattingOptions: { insertSpaces: true, tabSize: 2, eol: `
422
419
  ` } };
423
- function isLandstripKey(key) {
424
- return LANDSTRIP_KEYS.includes(key);
425
- }
426
420
  function ensureFinalNewline(content) {
427
421
  return content.endsWith(`
428
422
  `) ? content : `${content}
@@ -465,8 +459,9 @@ function parseLandstripConfig(value, path, kind) {
465
459
  if (!isRecord2(value))
466
460
  throw new Error(`${path}: ${label} must be a JSON object`);
467
461
  for (const key of Object.keys(value)) {
468
- if (!isLandstripKey(key))
462
+ if (!LANDSTRIP_KEYS.includes(key)) {
469
463
  throw new Error(`${path}: ${label} has an unknown field ${key}`);
464
+ }
470
465
  }
471
466
  const field = (name) => kind === "settings" ? `landstrip.${name}` : name;
472
467
  const config = {};
@@ -760,11 +755,6 @@ function listMarkdownFiles(root) {
760
755
  visit(root);
761
756
  return results;
762
757
  }
763
- function agentNameFromPath(dir, filePath) {
764
- const relativePath = relative(dir, filePath).split(sep).join("/");
765
- const extension = extname(relativePath);
766
- return extension.length > 0 ? relativePath.slice(0, -extension.length) : relativePath;
767
- }
768
758
  function parseFrontmatter(content) {
769
759
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
770
760
  if (!match)
@@ -863,7 +853,9 @@ function loadPiMarkdownAgents(options) {
863
853
  try {
864
854
  const content = readFileSync3(filePath, "utf8");
865
855
  const raw = parseMarkdownAgentRaw(content);
866
- const name = agentNameFromPath(directory.path, filePath);
856
+ const relativePath = relative(directory.path, filePath).split(sep).join("/");
857
+ const extension = extname(relativePath);
858
+ const name = extension.length > 0 ? relativePath.slice(0, -extension.length) : relativePath;
867
859
  if (!name) {
868
860
  diagnostics.push(`${filePath}: agent name is empty`);
869
861
  continue;
@@ -897,9 +889,6 @@ var AGENT_FIELDS = new Set([
897
889
  function normalizeAbsolutePath(value) {
898
890
  return /^(?:[A-Za-z]:[\\/]|\\\\)/.test(value) ? value.replaceAll("\\", "/") : value;
899
891
  }
900
- function expandPattern(pattern) {
901
- return normalizeAbsolutePath(expandHomePath(pattern));
902
- }
903
892
  function permissionEntries(permission, value) {
904
893
  if (value === "allow" || value === "ask" || value === "deny") {
905
894
  return [{ permission, pattern: "*", action: value }];
@@ -911,7 +900,7 @@ function permissionEntries(permission, value) {
911
900
  if (action !== "allow" && action !== "ask" && action !== "deny") {
912
901
  throw new Error(`permission ${permission} pattern ${pattern} has an invalid action`);
913
902
  }
914
- return { permission, pattern: expandPattern(pattern), action };
903
+ return { permission, pattern: normalizeAbsolutePath(expandHomePath(pattern)), action };
915
904
  });
916
905
  }
917
906
  function normalizePermissions(value) {
@@ -1134,17 +1123,9 @@ function valueAtPath(root, path) {
1134
1123
  }
1135
1124
  return value;
1136
1125
  }
1137
- function configKindForPath(path) {
1138
- return basename(path) === "landstrip.json" ? "dedicated" : "settings";
1139
- }
1140
1126
  function agentJsonPath(kind, name) {
1141
1127
  return kind === "settings" ? ["landstrip", "agent", name] : ["agent", name];
1142
1128
  }
1143
- function editorSnippet(name, raw, kind) {
1144
- const agent = { agent: { [name]: raw } };
1145
- return `${JSON.stringify(kind === "settings" ? { landstrip: agent } : agent, null, 2)}
1146
- `;
1147
- }
1148
1129
  function parseEditorSnippet(content, name, kind) {
1149
1130
  const root = parseJsonDocument(content, "Edited agent");
1150
1131
  const parentPath = kind === "settings" ? ["landstrip"] : [];
@@ -1245,36 +1226,27 @@ function relocateGlobalPrompt(raw, agent) {
1245
1226
  }
1246
1227
  return { ...raw, prompt: expandFileReferences(raw.prompt, dirname2(agent.origin.path)) };
1247
1228
  }
1248
- function safeRelative(root, path) {
1249
- const value = relative2(root, path);
1250
- return value && value !== ".." && !value.startsWith(`..${sep2}`) ? value : undefined;
1251
- }
1252
1229
  function projectMarkdownPath(cwd, agentDir, agent) {
1253
1230
  const origin = agent.origin;
1254
1231
  if (origin.source === "local" && origin.path)
1255
1232
  return origin.path;
1256
1233
  if (origin.kind === "pi-markdown" && origin.path) {
1257
- const suffix = safeRelative(join5(agentDir, "agents"), origin.path) ?? `${agent.name}.md`;
1234
+ const relativePath = relative2(join5(agentDir, "agents"), origin.path);
1235
+ const suffix = relativePath && relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) ? relativePath : `${agent.name}.md`;
1258
1236
  return join5(cwd, ".pi", "agents", suffix);
1259
1237
  }
1260
1238
  throw new Error(`Agent ${agent.name} has no Markdown source`);
1261
1239
  }
1262
- function projectConfigTarget(cwd, agentDir) {
1263
- return getLandstripConfigTarget(cwd, "project", agentDir);
1264
- }
1265
1240
  function assertProjectConfigTarget(cwd, agentDir, target) {
1266
- const current = projectConfigTarget(cwd, agentDir);
1241
+ const current = getLandstripConfigTarget(cwd, "project", agentDir);
1267
1242
  if (current.path !== target.path || current.kind !== target.kind) {
1268
1243
  throw new Error("Landstrip project configuration source changed while updating it");
1269
1244
  }
1270
1245
  }
1271
- function projectConfigHasAgent(cwd, name, agentDir) {
1272
- const target = projectConfigTarget(cwd, agentDir);
1273
- return readAgentNode(target.path, target.kind, name) !== undefined;
1274
- }
1275
1246
  function canDeleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1276
1247
  try {
1277
- if (projectConfigHasAgent(cwd, agent.name, agentDir))
1248
+ const target = getLandstripConfigTarget(cwd, "project", agentDir);
1249
+ if (readAgentNode(target.path, target.kind, agent.name) !== undefined)
1278
1250
  return true;
1279
1251
  } catch {
1280
1252
  return false;
@@ -1283,7 +1255,7 @@ function canDeleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1283
1255
  return Boolean(origin?.source === "local" && origin.path && origin.kind === "pi-markdown");
1284
1256
  }
1285
1257
  async function deleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1286
- const target = projectConfigTarget(cwd, agentDir);
1258
+ const target = getLandstripConfigTarget(cwd, "project", agentDir);
1287
1259
  const containers = target.kind === "settings" ? [["landstrip", "agent"], ["landstrip"]] : [["agent"]];
1288
1260
  const deletedOverride = await deleteJsonNode(target.path, agentJsonPath(target.kind, agent.name), containers, () => assertProjectConfigTarget(cwd, agentDir, target), target.kind === "dedicated");
1289
1261
  const origin = agent.origin;
@@ -1312,12 +1284,13 @@ function prepareProjectAgentEditor(cwd, agent, agentDir = getAgentDir3()) {
1312
1284
  };
1313
1285
  }
1314
1286
  const sourcePath = origin?.path;
1315
- const sourceRaw = origin?.kind === "config" && sourcePath ? readAgentNode(sourcePath, configKindForPath(sourcePath), agent.name) : undefined;
1316
- const raw = relocateGlobalPrompt(sourceRaw ? sourceRaw : agent.raw ? { ...agent.raw } : normalizedAgentRaw(agent), agent);
1317
- const target = projectConfigTarget(cwd, agentDir);
1287
+ const sourceRaw = origin?.kind === "config" && sourcePath ? readAgentNode(sourcePath, basename(sourcePath) === "landstrip.json" ? "dedicated" : "settings", agent.name) : undefined;
1288
+ const raw = relocateGlobalPrompt(sourceRaw ?? (agent.raw ? { ...agent.raw } : normalizedAgentRaw(agent)), agent);
1289
+ const target = getLandstripConfigTarget(cwd, "project", agentDir);
1318
1290
  return {
1319
1291
  title: `Edit @${agent.name}`,
1320
- content: editorSnippet(agent.name, raw, target.kind),
1292
+ content: `${JSON.stringify(target.kind === "settings" ? { landstrip: { agent: { [agent.name]: raw } } } : { agent: { [agent.name]: raw } }, null, 2)}
1293
+ `,
1321
1294
  async save(content) {
1322
1295
  const edited = parseEditorSnippet(content, agent.name, target.kind);
1323
1296
  validateAgentRaw(agent.name, edited);
@@ -1326,28 +1299,6 @@ function prepareProjectAgentEditor(cwd, agent, agentDir = getAgentDir3()) {
1326
1299
  };
1327
1300
  }
1328
1301
 
1329
- // box.ts
1330
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1331
- function dialogTabs(theme, tabs, active) {
1332
- return tabs.map((tab) => tab === active ? theme.fg("accent", theme.bold(`[${tab}]`)) : theme.fg("muted", tab)).join(" ");
1333
- }
1334
- function dialogKeys(theme, hints) {
1335
- const separator = theme.fg("dim", " · ");
1336
- return hints.map(([key, description]) => `${theme.fg("accent", key)} ${theme.fg("muted", description)}`).join(separator);
1337
- }
1338
- function paneTop(theme, width, title) {
1339
- if (width < 3)
1340
- return truncateToWidth(theme.fg("accent", title), Math.max(1, width));
1341
- const label = theme.fg("accent", theme.bold(` ${title} `));
1342
- return `${label}${theme.fg("border", "─".repeat(Math.max(0, width - visibleWidth(label))))}`;
1343
- }
1344
- function paneRow(_theme, width, content = "") {
1345
- if (width < 2)
1346
- return " ".repeat(Math.max(0, width));
1347
- const line = truncateToWidth(content, Math.max(1, width - 2));
1348
- return ` ${line}${" ".repeat(Math.max(0, width - visibleWidth(line) - 1))}`;
1349
- }
1350
-
1351
1302
  // commands.ts
1352
1303
  import { fuzzyFilter } from "@earendil-works/pi-tui";
1353
1304
  var DEFAULT_SUBCOMMAND = "status";
@@ -1362,9 +1313,6 @@ function parseArgv(args) {
1362
1313
  trailing: /\s$/.test(args)
1363
1314
  };
1364
1315
  }
1365
- function sandboxOverview(ctx, integration) {
1366
- return integration.sandboxCallbacks?.overview?.(ctx.cwd, isProjectTrusted(ctx));
1367
- }
1368
1316
  function taskItems(tasks, prefix) {
1369
1317
  return tasks.map((task) => ({
1370
1318
  value: `${prefix}${task.id}`,
@@ -1383,7 +1331,7 @@ var SUBCOMMANDS = [
1383
1331
  summary: "Inspect sandbox & agent runtime status",
1384
1332
  pane: (argv) => argv.rest ? undefined : "overview",
1385
1333
  run: (_argv, { ctx, runtime, integration }) => {
1386
- const overview = sandboxOverview(ctx, integration);
1334
+ const overview = integration.sandboxCallbacks?.overview?.(ctx.cwd, isProjectTrusted(ctx));
1387
1335
  const sandbox = !overview ? "unavailable" : !overview.enabled ? "disabled" : overview.running ? "active" : "enabled (inactive)";
1388
1336
  const primary = runtime.primaryAgentName();
1389
1337
  const tasks = runtime.getTasks();
@@ -1399,14 +1347,14 @@ var SUBCOMMANDS = [
1399
1347
  {
1400
1348
  name: "sandbox",
1401
1349
  summary: "Inspect or toggle sandbox OS isolation",
1402
- pane: (argv, { ctx, integration }) => !argv.rest && sandboxOverview(ctx, integration) ? "overview" : undefined,
1350
+ pane: (argv, { ctx, integration }) => !argv.rest && integration.sandboxCallbacks?.overview?.(ctx.cwd, isProjectTrusted(ctx)) ? "overview" : undefined,
1403
1351
  complete: () => [
1404
1352
  { value: "sandbox on", label: "on", description: "Enable OS sandbox isolation" },
1405
1353
  { value: "sandbox off", label: "off", description: "Disable OS sandbox isolation" }
1406
1354
  ],
1407
1355
  run: async (argv, { ctx, integration }) => {
1408
1356
  const callbacks = integration.sandboxCallbacks;
1409
- const overview = sandboxOverview(ctx, integration);
1357
+ const overview = integration.sandboxCallbacks?.overview?.(ctx.cwd, isProjectTrusted(ctx));
1410
1358
  if (!callbacks || !overview) {
1411
1359
  ctx.ui.notify("Sandbox controls are unavailable", "warning");
1412
1360
  return;
@@ -1456,7 +1404,7 @@ Scope: ${scope} configuration`);
1456
1404
  if (!runtime || !ctx)
1457
1405
  return [];
1458
1406
  try {
1459
- return visibleAgents(runtime.getAgentCatalog(ctx)).map((agent) => ({
1407
+ return [...runtime.getAgentCatalog(ctx).agents.values()].filter((agent) => !agent.hidden).map((agent) => ({
1460
1408
  value: `agents @${agent.name}`,
1461
1409
  label: `@${agent.name}`,
1462
1410
  description: agent.description ?? (agent.model ? `Model: ${agent.model}` : "Primary agent")
@@ -1467,7 +1415,7 @@ Scope: ${scope} configuration`);
1467
1415
  },
1468
1416
  run: async (argv, { ctx, runtime }) => {
1469
1417
  if (!argv.rest) {
1470
- const names = visibleAgents(runtime.getAgentCatalog(ctx)).map((agent) => `@${agent.name}`).join(", ");
1418
+ const names = [...runtime.getAgentCatalog(ctx).agents.values()].filter((agent) => !agent.hidden).map((agent) => `@${agent.name}`).join(", ");
1471
1419
  ctx.ui.notify(`Available agents: ${names || "none"}`, "info");
1472
1420
  return;
1473
1421
  }
@@ -1482,7 +1430,7 @@ Scope: ${scope} configuration`);
1482
1430
  summary: "Inspect configured process subagents",
1483
1431
  pane: (argv) => argv.rest ? undefined : "subagent",
1484
1432
  run: (_argv, { ctx, runtime }) => {
1485
- const names = visibleAgents(runtime.getAgentCatalog(ctx)).filter((agent) => agent.mode === "subagent").map((agent) => `@${agent.name}`).join(", ");
1433
+ const names = [...runtime.getAgentCatalog(ctx).agents.values()].filter((agent) => !agent.hidden).filter((agent) => agent.mode === "subagent").map((agent) => `@${agent.name}`).join(", ");
1486
1434
  ctx.ui.notify(`Subagents: ${names || "none"}`, "info");
1487
1435
  }
1488
1436
  },
@@ -1547,9 +1495,6 @@ Scope: ${scope} configuration`);
1547
1495
  }
1548
1496
  }
1549
1497
  ];
1550
- function visibleAgents(catalog) {
1551
- return [...catalog.agents.values()].filter((agent) => !agent.hidden);
1552
- }
1553
1498
  function itemText(item) {
1554
1499
  return item.search ?? item.label;
1555
1500
  }
@@ -2049,6 +1994,25 @@ function listRowBudget(terminalRows, floor, chrome) {
2049
1994
  return floor;
2050
1995
  return Math.max(floor, Math.floor(terminalRows * OVERLAY_HEIGHT_RATIO) - chrome);
2051
1996
  }
1997
+ function dialogTabs(theme, tabs, active) {
1998
+ return tabs.map((tab) => tab === active ? theme.fg("accent", theme.bold(`[${tab}]`)) : theme.fg("muted", tab)).join(" ");
1999
+ }
2000
+ function dialogKeys(theme, hints) {
2001
+ const separator = theme.fg("dim", " · ");
2002
+ return hints.map(([key, description]) => `${theme.fg("accent", key)} ${theme.fg("muted", description)}`).join(separator);
2003
+ }
2004
+ function paneTop(theme, width, title) {
2005
+ if (width < 3)
2006
+ return truncateToWidth(theme.fg("accent", title), Math.max(1, width));
2007
+ const label = theme.fg("accent", theme.bold(` ${title} `));
2008
+ return `${label}${theme.fg("border", "─".repeat(Math.max(0, width - visibleWidth(label))))}`;
2009
+ }
2010
+ function paneRow(_theme, width, content = "") {
2011
+ if (width < 2)
2012
+ return " ".repeat(Math.max(0, width));
2013
+ const line = truncateToWidth(content, Math.max(1, width - 2));
2014
+ return ` ${line}${" ".repeat(Math.max(0, width - visibleWidth(line) - 1))}`;
2015
+ }
2052
2016
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
2053
2017
  var AGENTS_HELP_ROWS = [
2054
2018
  ["Tab", "Next tab"],
@@ -2314,18 +2278,14 @@ function extractCwdFromTaskJsonFile(filePath) {
2314
2278
  return {};
2315
2279
  }
2316
2280
  }
2317
- function defaultSessionDirForCwd(cwd, agentDir = getAgentDir4()) {
2318
- const resolvedCwd = resolve2(cwd);
2319
- const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
2320
- return join6(agentDir, "sessions", safePath);
2321
- }
2322
2281
  function isSubagentSessionDirForCwd(parentSessionDir, targetCwd) {
2323
2282
  const files = findFilesRecursively(parentSessionDir);
2324
2283
  if (files.length === 0) {
2325
2284
  return true;
2326
2285
  }
2327
2286
  const resolvedTarget = resolve2(targetCwd);
2328
- const targetSessionDir = resolve2(defaultSessionDirForCwd(targetCwd));
2287
+ const safePath = `--${resolvedTarget.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
2288
+ const targetSessionDir = resolve2(join6(getAgentDir4(), "sessions", safePath));
2329
2289
  let hasTargetCwd = false;
2330
2290
  let hasOtherCwd = false;
2331
2291
  for (const file of files) {
@@ -2399,9 +2359,6 @@ function taskProgress(task) {
2399
2359
  progress.push(`retry ${task.retryAttempt}`);
2400
2360
  return progress;
2401
2361
  }
2402
- function taskOutput(details, fallback) {
2403
- return details.error ?? details.output ?? fallback;
2404
- }
2405
2362
  function workerDialogTitle(task, title) {
2406
2363
  return `@${task.agent} · ${task.description} · ${task.id.slice(0, 8)}
2407
2364
  ${title}`;
@@ -2456,8 +2413,27 @@ function dependencyRoot(path) {
2456
2413
  const index = path.lastIndexOf(marker);
2457
2414
  return index < 0 ? undefined : path.slice(0, index + marker.length - 1);
2458
2415
  }
2416
+ function endpointHost(baseUrl) {
2417
+ if (!baseUrl)
2418
+ return;
2419
+ try {
2420
+ return canonicalizeHost(new URL(baseUrl).hostname) ?? undefined;
2421
+ } catch {
2422
+ return;
2423
+ }
2424
+ }
2425
+ function modelEndpointDomains(ctx, selectedModel) {
2426
+ const models = ctx.modelRegistry?.getAll() ?? [];
2427
+ const qualified = models.find((model) => `${model.provider}/${model.id}` === selectedModel);
2428
+ const byId = models.filter((model) => model.id === selectedModel);
2429
+ const registered = qualified ?? (byId.length === 1 ? byId[0] : undefined);
2430
+ const activeName = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
2431
+ const active = selectedModel === activeName || selectedModel === ctx.model?.id ? ctx.model : undefined;
2432
+ const host = endpointHost(registered?.baseUrl ?? active?.baseUrl);
2433
+ return host ? [host] : [];
2434
+ }
2459
2435
  function agentBootstrapPaths(agentDir) {
2460
- return [
2436
+ const entries = [
2461
2437
  "settings.json",
2462
2438
  "landstrip.json",
2463
2439
  "models.json",
@@ -2474,7 +2450,8 @@ function agentBootstrapPaths(agentDir) {
2474
2450
  "bin",
2475
2451
  "npm",
2476
2452
  "git"
2477
- ].map((path) => join6(agentDir, path));
2453
+ ];
2454
+ return [agentDir, ...entries.map((entry) => join6(agentDir, entry))];
2478
2455
  }
2479
2456
 
2480
2457
  class Semaphore {
@@ -3312,7 +3289,7 @@ ${this.primaryAgent.prompt}` };
3312
3289
  const listedAgents = agentsForTab();
3313
3290
  const agentRows = listRowBudget(tui.terminal?.rows, AGENT_LIST_ROWS, AGENT_TAB_CHROME_ROWS);
3314
3291
  const { shown, start, status } = listWindow(listedAgents, selectedAgent, agentRows);
3315
- const nameWidth = Math.min(20, Math.max(5, ...listedAgents.map((agent2) => visibleWidth2(`@${agent2.name}`))));
3292
+ const nameWidth = Math.min(20, Math.max(5, ...listedAgents.map((agent2) => visibleWidth(`@${agent2.name}`))));
3316
3293
  const modelWidth = Math.max(10, Math.min(24, Math.floor((contentWidth - 2) / 3)));
3317
3294
  const enabledWidth = 13;
3318
3295
  const runtimeWidth = Math.max(12, contentWidth - 2 - nameWidth - 1 - modelWidth - 1 - enabledWidth - 1);
@@ -3531,7 +3508,7 @@ ${this.primaryAgent.prompt}` };
3531
3508
  const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
3532
3509
  const changePath = overview.changeScope === "project" ? overview.paths.project : overview.paths.global;
3533
3510
  const item = (label, value) => ` ${theme.fg("dim", label.padEnd(16))} ${value}`;
3534
- lines.push(`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`, item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")), item("Runtime", overview.running ? theme.fg("success", "Running") : theme.fg("dim", "Not running")), "", theme.fg("accent", "Protection"), item("Network", networkMode), item("Shell reads", shellReadMode), item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`), item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length)), item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)), "", theme.fg("accent", "Configuration"), item("Change scope", overview.changeScope === "project" ? "Project" : "Global"), item("Change file", truncateToWidth2(changePath, Math.max(10, contentWidth - 20))), item("Project file", truncateToWidth2(overview.paths.project, Math.max(10, contentWidth - 20))), item("Global file", truncateToWidth2(overview.paths.global, Math.max(10, contentWidth - 20))), item("Landstrip binary", truncateToWidth2(overview.paths.binary, Math.max(10, contentWidth - 20))));
3511
+ lines.push(`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`, item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")), item("Runtime", overview.running ? theme.fg("success", "Running") : theme.fg("dim", "Not running")), "", theme.fg("accent", "Protection"), item("Network", networkMode), item("Shell reads", shellReadMode), item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`), item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length)), item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)), "", theme.fg("accent", "Configuration"), item("Change scope", overview.changeScope === "project" ? "Project" : "Global"), item("Change file", truncateToWidth(changePath, Math.max(10, contentWidth - 20))), item("Project file", truncateToWidth(overview.paths.project, Math.max(10, contentWidth - 20))), item("Global file", truncateToWidth(overview.paths.global, Math.max(10, contentWidth - 20))), item("Landstrip binary", truncateToWidth(overview.paths.binary, Math.max(10, contentWidth - 20))));
3535
3512
  if (overview.windowsMode !== undefined)
3536
3513
  lines.push(item("Windows mode", overview.windowsMode));
3537
3514
  lines.push("", theme.fg("accent", "Network"), item("Allowed domains", listValue(overview.config.network.allowedDomains)), item("Denied domains", listValue(overview.config.network.deniedDomains)), item("Unix sockets", overview.config.network.allowAllUnixSockets ? "all" : listValue(overview.config.network.allowUnixSockets)), "", theme.fg("accent", "Filesystem"), item("Shell reads", shellReadMode), item("Read rules for", overview.readRuleScope), item("Denied reads", listValue(overview.config.filesystem.denyRead)), item("Allowed reads", listValue(overview.config.filesystem.allowRead)), item("Allowed writes", listValue(overview.config.filesystem.allowWrite)), item("Denied writes", listValue(overview.config.filesystem.denyWrite)), "", theme.fg("accent", "Session grants"), item("Domains", listValue(overview.sessionDomains)), item("Read paths", listValue(overview.sessionReadPaths)), item("Write paths", listValue(overview.sessionWritePaths)));
@@ -3636,7 +3613,7 @@ ${this.primaryAgent.prompt}` };
3636
3613
  lines.push(theme.fg("dim", `${scroll + 1}–${scroll + shown.length} of ${transcript.length}`));
3637
3614
  }
3638
3615
  if (steering) {
3639
- const input = steering.render(Math.max(2, contentWidth - visibleWidth2("Steer ")))[0] ?? "";
3616
+ const input = steering.render(Math.max(2, contentWidth - visibleWidth("Steer ")))[0] ?? "";
3640
3617
  lines.push(`${theme.fg("accent", "Steer")} ${input.replace(/^>/, "›")}`);
3641
3618
  } else if (task.state === "queued" || task.state === "running") {
3642
3619
  lines.push(dialogKeys(theme, [
@@ -3645,17 +3622,17 @@ ${this.primaryAgent.prompt}` };
3645
3622
  ["Esc", "tasks"]
3646
3623
  ]));
3647
3624
  }
3648
- return pane(lines.map((line) => truncateToWidth2(line, contentWidth)));
3625
+ return pane(lines.map((line) => truncateToWidth(line, contentWidth)));
3649
3626
  },
3650
3627
  help: (frame) => {
3651
3628
  const { tabs, pane, contentWidth, pad } = frame;
3652
- const shortcutWidth = Math.max(visibleWidth2("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth2(shortcut)));
3629
+ const shortcutWidth = Math.max(visibleWidth("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth(shortcut)));
3653
3630
  const descriptionWidth = Math.max(1, contentWidth - shortcutWidth - 4);
3654
3631
  const lines = [
3655
3632
  tabs,
3656
3633
  "",
3657
3634
  theme.fg("dim", ` ${pad("Shortcut", shortcutWidth)} Description`),
3658
- ...AGENTS_HELP_ROWS.map(([shortcut, description]) => ` ${theme.fg("accent", pad(shortcut, shortcutWidth))} ${theme.fg("text", truncateToWidth2(description, descriptionWidth))}`)
3635
+ ...AGENTS_HELP_ROWS.map(([shortcut, description]) => ` ${theme.fg("accent", pad(shortcut, shortcutWidth))} ${theme.fg("text", truncateToWidth(description, descriptionWidth))}`)
3659
3636
  ];
3660
3637
  return pane(lines);
3661
3638
  }
@@ -3858,12 +3835,12 @@ ${this.primaryAgent.prompt}` };
3858
3835
  return rows.slice(0, paneHeight);
3859
3836
  };
3860
3837
  const pad = (value, cellWidth) => {
3861
- const clipped = truncateToWidth2(value, cellWidth);
3862
- return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
3838
+ const clipped = truncateToWidth(value, cellWidth);
3839
+ return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth(clipped)))}`;
3863
3840
  };
3864
3841
  const listValue = (values) => {
3865
3842
  const value = values.join(", ") || "none";
3866
- return truncateToWidth2(value, Math.max(10, contentWidth - 20));
3843
+ return truncateToWidth(value, Math.max(10, contentWidth - 20));
3867
3844
  };
3868
3845
  refreshTasks();
3869
3846
  const tabs = dialogTabs(theme, TAB_ORDER.map((key) => TAB_LABELS[key]), TAB_LABELS[tab]);
@@ -4051,7 +4028,7 @@ ${descriptions}` : ""),
4051
4028
  if (metrics.length > 0)
4052
4029
  text += `
4053
4030
  ${theme.fg("dim", metrics.join(" · "))}`;
4054
- const output = taskOutput(details, "");
4031
+ const output = details.error ?? details.output ?? "";
4055
4032
  if (output) {
4056
4033
  const lines = output.trimEnd().split(`
4057
4034
  `);
@@ -4482,6 +4459,7 @@ ${catalog.diagnostics.join(`
4482
4459
  TEMP: temp
4483
4460
  },
4484
4461
  ctx,
4462
+ domains: modelEndpointDomains(ctx, model),
4485
4463
  readPaths: [
4486
4464
  ...new Set([
4487
4465
  ctx.cwd,
@@ -4776,15 +4754,15 @@ ${renderTaskResult(task.id, "error", message)}`);
4776
4754
  const progress = taskProgress(record);
4777
4755
  const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
4778
4756
  const indentWidth = 3 + Math.max(0, record.depth) * 3;
4779
- const descriptionWidth = Math.max(1, width - indentWidth - visibleWidth2(`${state} ${agent} ${suffix}`));
4780
- const description = theme.fg("text", truncateToWidth2(task.description, descriptionWidth));
4757
+ const descriptionWidth = Math.max(1, width - indentWidth - visibleWidth(`${state} ${agent} ${suffix}`));
4758
+ const description = theme.fg("text", truncateToWidth(task.description, descriptionWidth));
4781
4759
  return `${state} ${agent} ${description}${suffix}`;
4782
4760
  });
4783
4761
  const shown = tree.slice(0, 8);
4784
4762
  if (tree.length > shown.length) {
4785
4763
  shown.push(theme.fg("dim", ` … ${tree.length - shown.length} more`));
4786
4764
  }
4787
- return [header, ...shown].map((line) => truncateToWidth2(line, Math.max(1, width)));
4765
+ return [header, ...shown].map((line) => truncateToWidth(line, Math.max(1, width)));
4788
4766
  },
4789
4767
  invalidate() {}
4790
4768
  }));
@@ -5202,13 +5180,10 @@ function windowsStatus() {
5202
5180
  }
5203
5181
  return status;
5204
5182
  }
5205
- function activeWindowsImplementation() {
5206
- return windowsStatus().active;
5207
- }
5208
5183
  function shouldStartProxy(config) {
5209
5184
  if (config.network.allowNetwork)
5210
5185
  return false;
5211
- return process.platform !== "win32" || activeWindowsImplementation() === "restrictedUser" || config.windows.allowLoopback;
5186
+ return process.platform !== "win32" || windowsStatus().active === "restrictedUser" || config.windows.allowLoopback;
5212
5187
  }
5213
5188
  function restrictedUserProxyPortRange() {
5214
5189
  if (process.platform !== "win32")
@@ -5221,9 +5196,6 @@ function restrictedUserProxyPortRange() {
5221
5196
  }
5222
5197
  return { low: status.proxyPortLow, high: status.proxyPortHigh };
5223
5198
  }
5224
- function getConfigPaths(cwd) {
5225
- return getPiConfigPaths(cwd, "sandbox.json");
5226
- }
5227
5199
  function readOrEmptyConfig(configPath) {
5228
5200
  if (!existsSync5(configPath))
5229
5201
  return {};
@@ -5239,11 +5211,11 @@ function writeConfigFile(configPath, config) {
5239
5211
  `, "utf-8");
5240
5212
  }
5241
5213
  function getSandboxConfigWriteTarget(cwd, includeProject = true) {
5242
- const { globalPath, projectPath } = getConfigPaths(cwd);
5214
+ const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
5243
5215
  return includeProject ? { scope: "project", path: projectPath } : { scope: "global", path: globalPath };
5244
5216
  }
5245
5217
  async function writeSandboxConfigEnabled(cwd, enabled, scope) {
5246
- const paths = getConfigPaths(cwd);
5218
+ const paths = getPiConfigPaths(cwd, "sandbox.json");
5247
5219
  const path = scope === "project" ? paths.projectPath : paths.globalPath;
5248
5220
  await withFileMutationQueue3(path, async () => {
5249
5221
  const config = readOrEmptyConfig(path);
@@ -5255,7 +5227,7 @@ async function writeSandboxConfigEnabled(cwd, enabled, scope) {
5255
5227
  });
5256
5228
  }
5257
5229
  function loadSandboxEnabledSettings(cwd, includeProject) {
5258
- const { projectPath } = getConfigPaths(cwd);
5230
+ const { projectPath } = getPiConfigPaths(cwd, "sandbox.json");
5259
5231
  const global = loadSandboxConfig(cwd, false).enabled;
5260
5232
  if (!includeProject || !existsSync5(projectPath))
5261
5233
  return { global };
@@ -5272,25 +5244,6 @@ async function addConfigValue(configPath, value, readValues, writeValues) {
5272
5244
  writeConfigFile(configPath, config);
5273
5245
  });
5274
5246
  }
5275
- function addDomainToConfig(configPath, domain) {
5276
- return addConfigValue(configPath, domain, (config) => config.network?.allowedDomains ?? [], (config, allowedDomains) => {
5277
- config.network = {
5278
- ...config.network,
5279
- allowedDomains,
5280
- deniedDomains: config.network?.deniedDomains ?? []
5281
- };
5282
- });
5283
- }
5284
- function addReadPathToConfig(configPath, pathToAdd) {
5285
- return addConfigValue(configPath, pathToAdd, (config) => config.filesystem?.allowRead ?? [], (config, allowRead) => {
5286
- config.filesystem = { ...config.filesystem, allowRead };
5287
- });
5288
- }
5289
- function addWritePathToConfig(configPath, pathToAdd) {
5290
- return addConfigValue(configPath, pathToAdd, (config) => config.filesystem?.allowWrite ?? [], (config, allowWrite) => {
5291
- config.filesystem = { ...config.filesystem, allowWrite };
5292
- });
5293
- }
5294
5247
  function mergeAllowances(base, session, execution) {
5295
5248
  return [...base, ...session, ...execution ?? []];
5296
5249
  }
@@ -5456,28 +5409,10 @@ ${option.hint ?? "This changes persisted sandbox policy."}`, { signal });
5456
5409
  }
5457
5410
  return option.action;
5458
5411
  }
5459
- function promptDomainBlock(ctx, domain, signal) {
5460
- return showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, signal);
5461
- }
5462
5412
  function promptReadBlock(ctx, filePath, reason, signal) {
5463
5413
  const title = reason ? `Read blocked: "${filePath}" is in denyRead (${reason})` : `Read blocked: "${filePath}" is not in allowRead`;
5464
5414
  return showPermissionPrompt(ctx, title, PERMISSION_OPTIONS, signal);
5465
5415
  }
5466
- function promptWriteBlock(ctx, filePath, signal) {
5467
- return showPermissionPrompt(ctx, `Write blocked: "${filePath}" is not in allowWrite`, PERMISSION_OPTIONS, signal);
5468
- }
5469
- function promptFilesystemToolBlock(ctx, accesses, authorizationNote, signal) {
5470
- const paths = accesses.map(({ operation, path }) => `${operation}: ${path}`).join(`
5471
- `);
5472
- const note = authorizationNote ? `
5473
-
5474
- ${authorizationNote}` : "";
5475
- return showPermissionPrompt(ctx, `Filesystem policy blocked this tool call:
5476
- ${paths}${note}`, PERMISSION_OPTIONS, signal);
5477
- }
5478
- function promptNetworkBlock(ctx, operation, target, signal) {
5479
- return showPermissionPrompt(ctx, `Network blocked: ${operation} "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
5480
- }
5481
5416
  function writeEnvFile(env, proxyPort, proxyToken) {
5482
5417
  const lines = [];
5483
5418
  for (const [key, value] of Object.entries(env)) {
@@ -5569,11 +5504,6 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5569
5504
  function loadConfig(cwd) {
5570
5505
  return loadSandboxConfig(cwd, projectConfigTrusted);
5571
5506
  }
5572
- function createPlainBashTool(cwd) {
5573
- return createBashToolDefinition(cwd, {
5574
- shellPath: SettingsManager.create(cwd).getShellPath()
5575
- });
5576
- }
5577
5507
  let sandboxEnabled = false;
5578
5508
  let sandboxReady = false;
5579
5509
  let sandboxState = "unavailable";
@@ -5709,16 +5639,6 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5709
5639
  function getWorkerExtensions() {
5710
5640
  return [...workerExtensions].map(([id, extension]) => ({ id, entry: extension.entry }));
5711
5641
  }
5712
- function resetSessionAllowances() {
5713
- sessionAllowedDomains.length = 0;
5714
- sessionAllowedReadPaths.length = 0;
5715
- sessionAllowedWritePaths.length = 0;
5716
- sessionAllowedTargets.length = 0;
5717
- unsandboxedWorkerWarningShown = false;
5718
- }
5719
- function getEffectiveAllowedDomains(config, allowances) {
5720
- return mergeAllowances(config.network.allowedDomains, sessionAllowedDomains, allowances?.domains);
5721
- }
5722
5642
  function getEffectiveAllowRead(config, cwd, allowances) {
5723
5643
  const paths = mergeAllowances(config.filesystem.allowRead, sessionAllowedReadPaths, allowances?.readPaths);
5724
5644
  if (process.platform !== "win32")
@@ -5760,11 +5680,17 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5760
5680
  };
5761
5681
  }
5762
5682
  async function applyDomainChoice(choice, domain, cwd, allowances) {
5763
- const { globalPath, projectPath } = getConfigPaths(cwd);
5764
- if (choice === "project")
5765
- await addDomainToConfig(projectPath, domain);
5766
- if (choice === "global")
5767
- await addDomainToConfig(globalPath, domain);
5683
+ const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
5684
+ const configPath = choice === "project" ? projectPath : choice === "global" ? globalPath : undefined;
5685
+ if (configPath) {
5686
+ await addConfigValue(configPath, domain, (config) => config.network?.allowedDomains ?? [], (config, allowedDomains) => {
5687
+ config.network = {
5688
+ ...config.network,
5689
+ allowedDomains,
5690
+ deniedDomains: config.network?.deniedDomains ?? []
5691
+ };
5692
+ });
5693
+ }
5768
5694
  const target = choice === "once" ? allowances?.domains : sessionAllowedDomains;
5769
5695
  if (target && !target.includes(domain))
5770
5696
  target.push(domain);
@@ -5774,24 +5700,28 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5774
5700
  notify(ctx, `${verb} allowed (${choice}): ${scope}`, "info");
5775
5701
  }
5776
5702
  async function applyReadChoice(ctx, choice, filePath, cwd, allowances) {
5777
- const { globalPath, projectPath } = getConfigPaths(cwd);
5703
+ const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
5778
5704
  const scope = sessionScopeFor(filePath, cwd);
5779
- if (choice === "project")
5780
- await addReadPathToConfig(projectPath, scope);
5781
- if (choice === "global")
5782
- await addReadPathToConfig(globalPath, scope);
5705
+ const configPath = choice === "project" ? projectPath : choice === "global" ? globalPath : undefined;
5706
+ if (configPath) {
5707
+ await addConfigValue(configPath, scope, (config) => config.filesystem?.allowRead ?? [], (config, allowRead) => {
5708
+ config.filesystem = { ...config.filesystem, allowRead };
5709
+ });
5710
+ }
5783
5711
  const target = choice === "once" ? allowances?.readPaths : sessionAllowedReadPaths;
5784
5712
  if (target && !target.includes(scope))
5785
5713
  target.push(scope);
5786
5714
  noteScope(ctx, "Read", choice, filePath, scope);
5787
5715
  }
5788
5716
  async function applyWriteChoice(ctx, choice, filePath, cwd, allowances) {
5789
- const { globalPath, projectPath } = getConfigPaths(cwd);
5717
+ const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
5790
5718
  const scope = sessionScopeFor(filePath, cwd);
5791
- if (choice === "project")
5792
- await addWritePathToConfig(projectPath, scope);
5793
- if (choice === "global")
5794
- await addWritePathToConfig(globalPath, scope);
5719
+ const configPath = choice === "project" ? projectPath : choice === "global" ? globalPath : undefined;
5720
+ if (configPath) {
5721
+ await addConfigValue(configPath, scope, (config) => config.filesystem?.allowWrite ?? [], (config, allowWrite) => {
5722
+ config.filesystem = { ...config.filesystem, allowWrite };
5723
+ });
5724
+ }
5795
5725
  const target = choice === "once" ? allowances?.writePaths : sessionAllowedWritePaths;
5796
5726
  if (target && !target.includes(scope))
5797
5727
  target.push(scope);
@@ -5827,7 +5757,13 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5827
5757
  return permissionPrompts.resolve(current, async (promptSignal) => {
5828
5758
  const config = loadConfig(ctx.cwd);
5829
5759
  const blocked = normalized.filter((access) => access.operation === "read" ? !readAllowed(access.path, getEffectiveAllowRead(config, ctx.cwd), getEffectiveDenyRead(config, ctx.cwd), ctx.cwd) : shouldPromptForWrite(access.path, getEffectiveAllowWrite(config), ctx.cwd));
5830
- const choice = await promptFilesystemToolBlock(ctx, blocked, options2.authorizationNote, promptSignal);
5760
+ const paths = blocked.map(({ operation, path }) => `${operation}: ${path}`).join(`
5761
+ `);
5762
+ const note = options2.authorizationNote ? `
5763
+
5764
+ ${options2.authorizationNote}` : "";
5765
+ const choice = await showPermissionPrompt(ctx, `Filesystem policy blocked this tool call:
5766
+ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
5831
5767
  if (choice === "abort" || promptSignal.aborted) {
5832
5768
  return { allowed: false, prompted: true, reason: "Filesystem access denied" };
5833
5769
  }
@@ -5846,14 +5782,15 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5846
5782
  const config = loadConfig(cwd);
5847
5783
  if (domainMatchesAny(domain, config.network.deniedDomains))
5848
5784
  return false;
5849
- if (domainMatchesAny(domain, getEffectiveAllowedDomains(config, allowances)))
5785
+ const allowedDomains = mergeAllowances(config.network.allowedDomains, sessionAllowedDomains, allowances?.domains);
5786
+ if (domainMatchesAny(domain, allowedDomains))
5850
5787
  return true;
5851
5788
  return;
5852
5789
  };
5853
5790
  if (!ctx.hasUI)
5854
5791
  return current() ?? false;
5855
5792
  return permissionPrompts.resolve(current, async (promptSignal) => {
5856
- const choice = await promptDomainBlock(ctx, domain, promptSignal);
5793
+ const choice = await showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, promptSignal);
5857
5794
  if (choice === "abort" || promptSignal.aborted)
5858
5795
  return false;
5859
5796
  await applyDomainChoice(choice, domain, cwd, allowances);
@@ -5929,7 +5866,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5929
5866
  if (!ctx.hasUI || !promptOnBlock)
5930
5867
  return { action: "deny", reason: "unprompted" };
5931
5868
  return permissionPrompts.resolve(current2, async (promptSignal) => {
5932
- const choice = await promptNetworkBlock(ctx, trap.operation, trap.target, promptSignal);
5869
+ const choice = await showPermissionPrompt(ctx, `Network blocked: ${trap.operation} "${trap.target}"`, NETWORK_PERMISSION_OPTIONS, promptSignal);
5933
5870
  if (choice === "abort" || promptSignal.aborted)
5934
5871
  return { action: "deny", reason: "rejected" };
5935
5872
  const targets = choice === "once" ? allowances.targets : sessionAllowedTargets;
@@ -5954,7 +5891,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5954
5891
  return { action: "deny", reason: "unprompted" };
5955
5892
  return permissionPrompts.resolve(current, async (promptSignal) => {
5956
5893
  const config = loadConfig(cwd);
5957
- const choice = trap.operation === "read" ? await promptReadBlock(ctx, path, matchesPattern(path, config.filesystem.denyRead, cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await promptWriteBlock(ctx, path, promptSignal);
5894
+ const choice = trap.operation === "read" ? await promptReadBlock(ctx, path, matchesPattern(path, config.filesystem.denyRead, cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await showPermissionPrompt(ctx, `Write blocked: "${path}" is not in allowWrite`, PERMISSION_OPTIONS, promptSignal);
5958
5895
  if (choice === "abort" || promptSignal.aborted)
5959
5896
  return { action: "deny", reason: "rejected" };
5960
5897
  if (trap.operation === "read") {
@@ -6061,7 +5998,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6061
5998
  return prepareUnsandboxedRpcWorker(options2);
6062
5999
  }
6063
6000
  const allowances = {
6064
- domains: [],
6001
+ domains: [...options2.domains ?? []],
6065
6002
  readPaths: [...options2.readPaths],
6066
6003
  writePaths: [...options2.writePaths],
6067
6004
  targets: []
@@ -6554,7 +6491,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6554
6491
  const retryWithAccess = async (operation, blockedPath) => {
6555
6492
  if (!ctx.hasUI)
6556
6493
  return null;
6557
- const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
6494
+ const { globalPath, projectPath } = getPiConfigPaths(ctx.cwd, "sandbox.json");
6558
6495
  const current = () => {
6559
6496
  const config2 = loadConfig(ctx.cwd);
6560
6497
  if (operation === "write" && matchesPattern(blockedPath, config2.filesystem.denyWrite, ctx.cwd)) {
@@ -6565,7 +6502,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
6565
6502
  };
6566
6503
  const access = await permissionPrompts.resolve(current, async (promptSignal) => {
6567
6504
  const config2 = loadConfig(ctx.cwd);
6568
- const choice = operation === "read" ? await promptReadBlock(ctx, blockedPath, matchesPattern(blockedPath, config2.filesystem.denyRead, ctx.cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await promptWriteBlock(ctx, blockedPath, promptSignal);
6505
+ const choice = operation === "read" ? await promptReadBlock(ctx, blockedPath, matchesPattern(blockedPath, config2.filesystem.denyRead, ctx.cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await showPermissionPrompt(ctx, `Write blocked: "${blockedPath}" is not in allowWrite`, PERMISSION_OPTIONS, promptSignal);
6569
6506
  if (choice === "abort" || promptSignal.aborted)
6570
6507
  return false;
6571
6508
  if (operation === "read") {
@@ -6664,7 +6601,7 @@ ${message}
6664
6601
  return;
6665
6602
  let implementation;
6666
6603
  try {
6667
- implementation = activeWindowsImplementation();
6604
+ implementation = windowsStatus().active;
6668
6605
  } catch (error) {
6669
6606
  notify(ctx, formatError(error), "error");
6670
6607
  return;
@@ -6684,7 +6621,7 @@ ${message}
6684
6621
  const theme = ctx.ui.theme;
6685
6622
  const dot = theme.fg("success", "●");
6686
6623
  const label = theme.fg("text", "Sandbox");
6687
- const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
6624
+ const windowsImplementation = process.platform === "win32" ? windowsStatus().active : undefined;
6688
6625
  let networkLabel;
6689
6626
  let networkColor;
6690
6627
  if (config.network.allowNetwork) {
@@ -6759,7 +6696,9 @@ ${message}
6759
6696
  return true;
6760
6697
  }
6761
6698
  function createBashTool(cwd) {
6762
- const localBash = createPlainBashTool(cwd);
6699
+ const localBash = createBashToolDefinition(cwd, {
6700
+ shellPath: SettingsManager.create(cwd).getShellPath()
6701
+ });
6763
6702
  return {
6764
6703
  ...localBash,
6765
6704
  label: "bash (landstrip)",
@@ -6811,7 +6750,11 @@ ${message}
6811
6750
  pi.on("session_start", async (_event, ctx) => {
6812
6751
  activeContext = ctx;
6813
6752
  permissionPrompts.reset();
6814
- resetSessionAllowances();
6753
+ sessionAllowedDomains.length = 0;
6754
+ sessionAllowedReadPaths.length = 0;
6755
+ sessionAllowedWritePaths.length = 0;
6756
+ sessionAllowedTargets.length = 0;
6757
+ unsandboxedWorkerWarningShown = false;
6815
6758
  const trustContext = ctx;
6816
6759
  projectConfigTrusted = trustContext.isProjectTrusted?.() ?? false;
6817
6760
  if (getNoSandboxFlag()) {
@@ -6849,10 +6792,10 @@ ${message}
6849
6792
  enableSandbox(ctx);
6850
6793
  }
6851
6794
  }
6852
- function sandboxOverview2(cwd, includeProject) {
6795
+ function sandboxOverview(cwd, includeProject) {
6853
6796
  const config = loadSandboxConfig(cwd, includeProject);
6854
- const { globalPath, projectPath } = getConfigPaths(cwd);
6855
- const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
6797
+ const { globalPath, projectPath } = getPiConfigPaths(cwd, "sandbox.json");
6798
+ const windowsImplementation = process.platform === "win32" ? windowsStatus().active : undefined;
6856
6799
  return {
6857
6800
  enabled: config.enabled,
6858
6801
  running: sandboxEnabled && sandboxReady,
@@ -6873,7 +6816,7 @@ ${message}
6873
6816
  load: loadSandboxEnabledSettings,
6874
6817
  setEnabled: (ctx, enabled, scope) => applySandboxSetting(ctx, enabled, scope),
6875
6818
  clearProject: (ctx) => applySandboxSetting(ctx, undefined, "project"),
6876
- overview: sandboxOverview2
6819
+ overview: sandboxOverview
6877
6820
  };
6878
6821
  const integration = {
6879
6822
  version: LANDSTRIP_RUNTIME_VERSION,
@@ -6895,25 +6838,25 @@ ${message}
6895
6838
  return integration;
6896
6839
  }
6897
6840
  export {
6898
- writeEnvFile,
6899
- shouldPromptForWrite,
6900
- sessionScopeFor,
6901
- resolveProcessReadPolicy,
6902
- readAllowed,
6903
- parseTrapLine,
6904
- matchesPattern,
6905
- isQueryTrap,
6906
- formatLandstripTraps,
6907
- extractRetryableNativeReadDeniedPath,
6908
- extractNativeWriteDeniedPath,
6909
- extractNativeDeniedPath,
6910
- enforcesShellReadPolicy,
6911
- domainMatchesAny,
6912
- pi_landstrip_default as default,
6913
- createWindowsDenyRead,
6914
- createPosixShellProvider,
6915
- createLandstripLauncherEnvironment,
6916
- createLandstripIntegration,
6841
+ POSIX_SHELL_PROVIDER_ID,
6917
6842
  controlResponseLine,
6918
- POSIX_SHELL_PROVIDER_ID
6843
+ createLandstripIntegration,
6844
+ createLandstripLauncherEnvironment,
6845
+ createPosixShellProvider,
6846
+ createWindowsDenyRead,
6847
+ pi_landstrip_default as default,
6848
+ domainMatchesAny,
6849
+ enforcesShellReadPolicy,
6850
+ extractNativeDeniedPath,
6851
+ extractNativeWriteDeniedPath,
6852
+ extractRetryableNativeReadDeniedPath,
6853
+ formatLandstripTraps,
6854
+ isQueryTrap,
6855
+ matchesPattern,
6856
+ parseTrapLine,
6857
+ readAllowed,
6858
+ resolveProcessReadPolicy,
6859
+ sessionScopeFor,
6860
+ shouldPromptForWrite,
6861
+ writeEnvFile
6919
6862
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.41",
3
+ "version": "0.18.43",
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-api": "^0.18.41",
42
+ "@landstrip/landstrip-api": "^0.18.43",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"