pi-landstrip 0.18.11 → 0.18.13

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 +0 -5
  2. package/dist/index.ts +146 -287
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -143,11 +143,6 @@ Pi Markdown agents are loaded from `~/.pi/agent/agents/` and `.pi/agents/`.
143
143
  `landstrip.agent` settings override Markdown agents with the same name.
144
144
  Project definitions override global definitions.
145
145
 
146
- OpenCode agent imports are enabled by default. Set
147
- `landstrip.opencode.showGlobalAgents` or `showLocalAgents` to `false` to disable
148
- a source. OpenCode agents fill missing names; Pi agents take precedence.
149
- Project imports require a trusted project.
150
-
151
146
  See the main [Landstrip documentation](https://github.com/landstrip/landstrip#readme)
152
147
  for `sandbox.json` fields and platform policy semantics.
153
148
 
package/dist/index.ts CHANGED
@@ -18,8 +18,8 @@ import {
18
18
  isIP,
19
19
  Socket as NetSocket
20
20
  } from "node:net";
21
- import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
22
- import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute3, join as join7, parse, resolve as resolve3 } from "node:path";
21
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
22
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute3, join as join7, parse, resolve as resolve3 } from "node:path";
23
23
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
24
24
  import { fileURLToPath as fileURLToPath2, URL } from "node:url";
25
25
  import {
@@ -80,8 +80,8 @@ import {
80
80
  rmSync,
81
81
  writeFileSync as writeFileSync3
82
82
  } from "node:fs";
83
- import { homedir as homedir3, tmpdir } from "node:os";
84
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join6, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
83
+ import { homedir as homedir2, tmpdir } from "node:os";
84
+ import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join6, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
85
85
  import { fileURLToPath } from "node:url";
86
86
  import {
87
87
  getAgentDir as getAgentDir4,
@@ -275,38 +275,69 @@ function colorizeAgentText(color, text, themeFg, fallback = "accent") {
275
275
 
276
276
  class AsyncQueue {
277
277
  tail = Promise.resolve();
278
+ generation = 0;
279
+ resetWaiters = [];
278
280
  async acquire(signal, cancellationMessage = "Request cancelled") {
279
281
  if (signal?.aborted)
280
282
  throw new Error(cancellationMessage);
283
+ const generation = this.generation;
281
284
  let release;
282
285
  const previous = this.tail;
283
286
  this.tail = new Promise((resolve2) => {
284
287
  release = resolve2;
285
288
  });
286
- if (!signal) {
287
- await previous;
288
- return release;
289
- }
290
- let abort;
289
+ const finish = () => {
290
+ release?.();
291
+ };
292
+ let wakeReset;
293
+ const resetSignal = new Promise((resolve2) => {
294
+ wakeReset = resolve2;
295
+ this.resetWaiters.push(resolve2);
296
+ });
297
+ const dropResetWaiter = () => {
298
+ if (!wakeReset)
299
+ return;
300
+ const index = this.resetWaiters.indexOf(wakeReset);
301
+ if (index >= 0)
302
+ this.resetWaiters.splice(index, 1);
303
+ wakeReset = undefined;
304
+ };
291
305
  try {
292
- await Promise.race([
293
- previous,
294
- new Promise((_resolve, reject) => {
295
- abort = () => reject(new Error(cancellationMessage));
296
- signal.addEventListener("abort", abort, { once: true });
297
- })
298
- ]);
306
+ if (!signal) {
307
+ await Promise.race([previous, resetSignal]);
308
+ } else {
309
+ let abort;
310
+ try {
311
+ await Promise.race([
312
+ previous,
313
+ resetSignal,
314
+ new Promise((_resolve, reject) => {
315
+ abort = () => reject(new Error(cancellationMessage));
316
+ signal.addEventListener("abort", abort, { once: true });
317
+ })
318
+ ]);
319
+ } finally {
320
+ if (abort)
321
+ signal.removeEventListener("abort", abort);
322
+ }
323
+ }
324
+ if (this.generation !== generation) {
325
+ throw new Error(cancellationMessage);
326
+ }
327
+ return finish;
299
328
  } catch (error) {
300
- previous.finally(() => release?.());
329
+ previous.finally(finish);
301
330
  throw error;
302
331
  } finally {
303
- if (abort)
304
- signal.removeEventListener("abort", abort);
332
+ dropResetWaiter();
305
333
  }
306
- return release;
307
334
  }
308
335
  reset() {
336
+ this.generation += 1;
309
337
  this.tail = Promise.resolve();
338
+ const waiters = this.resetWaiters.splice(0);
339
+ for (const wake of waiters)
340
+ wake();
310
341
  }
311
342
  }
312
343
 
@@ -332,38 +363,7 @@ class PermissionPromptCoordinator {
332
363
 
333
364
  // config.ts
334
365
  var MAX_SUBAGENTS = 16;
335
- var DEFAULT_OPENCODE = {
336
- showGlobalAgents: true,
337
- showLocalAgents: true
338
- };
339
- var LANDSTRIP_KEYS = new Set(["maxSubagents", "agent", "permission", "opencode"]);
340
- var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
341
- function readBooleanField(section, key, path) {
342
- if (!(key in section))
343
- return;
344
- const value = section[key];
345
- if (typeof value === "boolean")
346
- return value;
347
- throw new Error(`${path}.${key} must be a boolean`);
348
- }
349
- function normalizeOpenCode(value, path) {
350
- if (value === undefined)
351
- return {};
352
- if (!isRecord2(value))
353
- throw new Error(`${path} must be a JSON object`);
354
- for (const key of Object.keys(value)) {
355
- if (!OPENCODE_KEYS.has(key))
356
- throw new Error(`${path}: unknown field ${key}`);
357
- }
358
- const result = {};
359
- const showGlobalAgents = readBooleanField(value, "showGlobalAgents", path);
360
- if (showGlobalAgents !== undefined)
361
- result.showGlobalAgents = showGlobalAgents;
362
- const showLocalAgents = readBooleanField(value, "showLocalAgents", path);
363
- if (showLocalAgents !== undefined)
364
- result.showLocalAgents = showLocalAgents;
365
- return result;
366
- }
366
+ var LANDSTRIP_KEYS = new Set(["maxSubagents", "agent", "permission"]);
367
367
  function readJsonObject(path) {
368
368
  let value;
369
369
  try {
@@ -417,7 +417,6 @@ function readLandstripSettings(path) {
417
417
  }
418
418
  if ("permission" in settings.landstrip)
419
419
  config.permission = settings.landstrip.permission;
420
- config.opencode = normalizeOpenCode(settings.landstrip.opencode, `${path}: landstrip.opencode`);
421
420
  expandAgentPromptReferences(config, path);
422
421
  return config;
423
422
  }
@@ -557,8 +556,7 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
557
556
  }
558
557
  if (!isRecord2(config.agent))
559
558
  throw new Error("landstrip.agent must be an object");
560
- const opencode = { ...DEFAULT_OPENCODE, ...config.opencode };
561
- return { ...config, maxSubagents, agent: config.agent, opencode, agentSources };
559
+ return { ...config, maxSubagents, agent: config.agent, agentSources };
562
560
  }
563
561
  function loadMaxSubagentsSettings(cwd, includeProject = true, agentDir = getAgentDir()) {
564
562
  const global = loadLandstripConfig(cwd, false, agentDir).maxSubagents;
@@ -574,17 +572,8 @@ function loadMaxSubagentsSettings(cwd, includeProject = true, agentDir = getAgen
574
572
 
575
573
  // opencode-agents.ts
576
574
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
577
- import { homedir as homedir2 } from "node:os";
578
- import { dirname as dirname2, extname, join as join3, relative, sep } from "node:path";
575
+ import { extname, join as join3, relative, sep } from "node:path";
579
576
  import { parse as parseYaml } from "yaml";
580
- import { parse as parseJsonc, printParseErrorCode } from "jsonc-parser";
581
- var AGENT_DIR_PREFIXES = ["agent/", "agents/"];
582
- function openCodeGlobalConfigDir() {
583
- if (process.env.OPENCODE_CONFIG_DIR)
584
- return process.env.OPENCODE_CONFIG_DIR;
585
- const xdg = process.env.XDG_CONFIG_HOME;
586
- return join3(xdg && xdg.length > 0 ? xdg : join3(homedir2(), ".config"), "opencode");
587
- }
588
577
  function listMarkdownFiles(root) {
589
578
  if (!existsSync2(root))
590
579
  return [];
@@ -615,13 +604,6 @@ function listMarkdownFiles(root) {
615
604
  }
616
605
  function agentNameFromPath(dir, filePath) {
617
606
  const relativePath = relative(dir, filePath).split(sep).join("/");
618
- for (const prefix of AGENT_DIR_PREFIXES) {
619
- if (relativePath.startsWith(prefix)) {
620
- const candidate = relativePath.slice(prefix.length);
621
- const extension2 = extname(candidate);
622
- return extension2.length > 0 ? candidate.slice(0, -extension2.length) : candidate;
623
- }
624
- }
625
607
  const extension = extname(relativePath);
626
608
  return extension.length > 0 ? relativePath.slice(0, -extension.length) : relativePath;
627
609
  }
@@ -653,7 +635,7 @@ function convertToolsToPermission(tools) {
653
635
  }
654
636
  return permission;
655
637
  }
656
- function normalizeOpenCodeAgentRaw(data, prompt) {
638
+ function normalizeMarkdownAgentRaw(data, prompt) {
657
639
  const known = new Set([
658
640
  "name",
659
641
  "model",
@@ -713,98 +695,27 @@ function normalizeOpenCodeAgentRaw(data, prompt) {
713
695
  }
714
696
  function parseMarkdownAgentRaw(content) {
715
697
  const { data, body } = parseFrontmatter(content);
716
- return normalizeOpenCodeAgentRaw(data, body.trim());
717
- }
718
- function parseOpenCodeConfig(path) {
719
- const errors = [];
720
- const value = parseJsonc(readFileSync3(path, "utf8"), errors, {
721
- allowTrailingComma: true
722
- });
723
- if (errors.length > 0) {
724
- const error = errors[0];
725
- throw new Error(`${printParseErrorCode(error.error)} at offset ${error.offset}`);
726
- }
727
- if (!isRecord2(value))
728
- throw new Error("config must contain a JSON object");
729
- return value;
698
+ return normalizeMarkdownAgentRaw(data, body.trim());
730
699
  }
731
- function loadAgentsFromConfigFile(path, source, agents, diagnostics) {
732
- if (!existsSync2(path))
733
- return;
734
- try {
735
- const config = parseOpenCodeConfig(path);
736
- if (config.agent === undefined)
737
- return;
738
- if (!isRecord2(config.agent))
739
- throw new Error("agent must be a JSON object");
740
- for (const [name, value] of Object.entries(config.agent)) {
700
+ function loadPiMarkdownAgents(options) {
701
+ const agents = new Map;
702
+ const diagnostics = [];
703
+ for (const directory of options.directories) {
704
+ for (const filePath of listMarkdownFiles(directory.path)) {
741
705
  try {
742
- if (!name)
743
- throw new Error("agent name is empty");
744
- if (!isRecord2(value))
745
- throw new Error(`agent ${name} must be a JSON object`);
746
- if (value.prompt !== undefined && typeof value.prompt !== "string") {
747
- throw new Error(`agent ${name} prompt must be a string`);
706
+ const content = readFileSync3(filePath, "utf8");
707
+ const raw = parseMarkdownAgentRaw(content);
708
+ const name = agentNameFromPath(directory.path, filePath);
709
+ if (!name) {
710
+ diagnostics.push(`${filePath}: agent name is empty`);
711
+ continue;
748
712
  }
749
- const prompt = typeof value.prompt === "string" ? expandFileReferences(value.prompt, dirname2(path)) : "";
750
- const raw = normalizeOpenCodeAgentRaw(value, prompt);
751
- agents.set(name, { name, source, path, format: "json", raw });
713
+ agents.set(name, { name, source: directory.source, path: filePath, raw });
752
714
  } catch (error) {
753
- diagnostics.push(`${path}: ${formatError(error)}`);
754
- }
755
- }
756
- } catch (error) {
757
- diagnostics.push(`${path}: ${formatError(error)}`);
758
- }
759
- }
760
- function loadConfigAgentsFromDir(dir, source, agents, diagnostics) {
761
- for (const file of ["opencode.json", "opencode.jsonc"]) {
762
- loadAgentsFromConfigFile(join3(dir, file), source, agents, diagnostics);
763
- }
764
- }
765
- function loadAgentsFromRoot(root, nameRoot, source, agents, diagnostics) {
766
- for (const filePath of listMarkdownFiles(root)) {
767
- try {
768
- const content = readFileSync3(filePath, "utf8");
769
- const raw = parseMarkdownAgentRaw(content);
770
- const name = agentNameFromPath(nameRoot, filePath);
771
- if (!name) {
772
- diagnostics.push(`${filePath}: agent name is empty`);
773
- continue;
715
+ diagnostics.push(`${filePath}: ${formatError(error)}`);
774
716
  }
775
- agents.set(name, { name, source, path: filePath, format: "markdown", raw });
776
- } catch (error) {
777
- diagnostics.push(`${filePath}: ${formatError(error)}`);
778
717
  }
779
718
  }
780
- }
781
- function loadAgentsFromDir(dir, source, agents, diagnostics) {
782
- for (const subdir of ["agent", "agents"]) {
783
- loadAgentsFromRoot(join3(dir, subdir), dir, source, agents, diagnostics);
784
- }
785
- }
786
- function loadPiMarkdownAgents(options) {
787
- const agents = new Map;
788
- const diagnostics = [];
789
- for (const directory of options.directories) {
790
- loadAgentsFromRoot(directory.path, directory.path, directory.source, agents, diagnostics);
791
- }
792
- return { agents, diagnostics };
793
- }
794
- function loadOpenCodeAgents(options) {
795
- const agents = new Map;
796
- const diagnostics = [];
797
- if (options.includeGlobal) {
798
- const globalDir = options.globalConfigDir ?? openCodeGlobalConfigDir();
799
- loadConfigAgentsFromDir(globalDir, "global", agents, diagnostics);
800
- loadAgentsFromDir(globalDir, "global", agents, diagnostics);
801
- }
802
- if (options.includeProject) {
803
- loadConfigAgentsFromDir(options.cwd, "local", agents, diagnostics);
804
- const projectDir = join3(options.cwd, ".opencode");
805
- loadConfigAgentsFromDir(projectDir, "local", agents, diagnostics);
806
- loadAgentsFromDir(projectDir, "local", agents, diagnostics);
807
- }
808
719
  return { agents, diagnostics };
809
720
  }
810
721
 
@@ -919,38 +830,16 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
919
830
  let configuredAgents = {};
920
831
  let configuredPermission;
921
832
  let agentSources = new Map;
922
- let showGlobalAgents = false;
923
- let showLocalAgents = false;
924
833
  try {
925
834
  const config = loadLandstripConfig(cwd, includeProject, piAgentDir);
926
835
  maxSubagents = config.maxSubagents;
927
836
  configuredAgents = config.agent;
928
837
  configuredPermission = config.permission;
929
838
  agentSources = new Map(config.agentSources);
930
- showGlobalAgents = config.opencode.showGlobalAgents;
931
- showLocalAgents = config.opencode.showLocalAgents;
932
839
  } catch (error) {
933
840
  diagnostics.push(formatError(error));
934
841
  }
935
- const openCode = loadOpenCodeAgents({
936
- cwd,
937
- includeGlobal: showGlobalAgents,
938
- includeProject: showLocalAgents && includeProject
939
- });
940
- diagnostics.push(...openCode.diagnostics);
941
842
  const normalized = new Map;
942
- for (const imported of openCode.agents.values()) {
943
- try {
944
- const kind = imported.format === "markdown" ? "opencode-markdown" : "opencode-json";
945
- normalized.set(imported.name, normalizeAgent(imported.name, imported.raw, imported.source, {
946
- kind,
947
- source: imported.source,
948
- path: imported.path
949
- }));
950
- } catch (error) {
951
- diagnostics.push(`${imported.path}: ${formatError(error)}`);
952
- }
953
- }
954
843
  const piMarkdown = loadPiMarkdownAgents({
955
844
  directories: [
956
845
  { path: join4(piAgentDir, "agents"), source: "global" },
@@ -1045,13 +934,13 @@ function agentSupportsMode(agent, mode) {
1045
934
 
1046
935
  // agent-files.ts
1047
936
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1048
- import { basename, dirname as dirname3, join as join5, relative as relative2, sep as sep2 } from "node:path";
937
+ import { dirname as dirname2, join as join5, relative as relative2, sep as sep2 } from "node:path";
1049
938
  import { getAgentDir as getAgentDir3, withFileMutationQueue as withFileMutationQueue2 } from "@earendil-works/pi-coding-agent";
1050
939
  import {
1051
940
  applyEdits,
1052
941
  modify,
1053
- parse as parseJsonc2,
1054
- printParseErrorCode as printParseErrorCode2
942
+ parse as parseJsonc,
943
+ printParseErrorCode
1055
944
  } from "jsonc-parser";
1056
945
  var JSON_FORMAT = {
1057
946
  formattingOptions: { insertSpaces: true, tabSize: 2, eol: `
@@ -1059,10 +948,10 @@ var JSON_FORMAT = {
1059
948
  };
1060
949
  function parseJsonDocument(content, label) {
1061
950
  const errors = [];
1062
- const value = parseJsonc2(content, errors, { allowTrailingComma: true });
951
+ const value = parseJsonc(content, errors, { allowTrailingComma: true });
1063
952
  if (errors.length > 0) {
1064
953
  const error = errors[0];
1065
- throw new Error(`${label}: ${printParseErrorCode2(error.error)} at offset ${error.offset}`);
954
+ throw new Error(`${label}: ${printParseErrorCode(error.error)} at offset ${error.offset}`);
1066
955
  }
1067
956
  if (!isRecord2(value))
1068
957
  throw new Error(`${label} must contain a JSON object`);
@@ -1077,38 +966,22 @@ function valueAtPath(root, path) {
1077
966
  }
1078
967
  return value;
1079
968
  }
1080
- function agentJsonPath(kind, name) {
1081
- return kind === "landstrip" ? ["landstrip", "agent", name] : ["agent", name];
969
+ function agentJsonPath(name) {
970
+ return ["landstrip", "agent", name];
1082
971
  }
1083
- function agentContainerPath(kind) {
1084
- return kind === "landstrip" ? ["landstrip", "agent"] : ["agent"];
1085
- }
1086
- function editorSnippet(kind, name, raw) {
1087
- const value = kind === "landstrip" ? { landstrip: { agent: { [name]: raw } } } : { agent: { [name]: raw } };
1088
- return `${JSON.stringify(value, null, 2)}
972
+ function editorSnippet(name, raw) {
973
+ return `${JSON.stringify({ landstrip: { agent: { [name]: raw } } }, null, 2)}
1089
974
  `;
1090
975
  }
1091
- function parseEditorSnippet(content, kind, name) {
976
+ function parseEditorSnippet(content, name) {
1092
977
  const root = parseJsonDocument(content, "Edited agent");
1093
- const parent = kind === "landstrip" ? valueAtPath(root, ["landstrip"]) : root;
1094
- const container = valueAtPath(root, agentContainerPath(kind));
1095
- const rootKey = kind === "landstrip" ? "landstrip" : "agent";
1096
- if (Object.keys(root).length !== 1 || root[rootKey] === undefined || !isRecord2(parent) || Object.keys(parent).length !== 1 || parent.agent === undefined || !isRecord2(container) || Object.keys(container).length !== 1 || !isRecord2(container[name])) {
978
+ const parent = valueAtPath(root, ["landstrip"]);
979
+ const container = valueAtPath(root, ["landstrip", "agent"]);
980
+ if (Object.keys(root).length !== 1 || root.landstrip === undefined || !isRecord2(parent) || Object.keys(parent).length !== 1 || parent.agent === undefined || !isRecord2(container) || Object.keys(container).length !== 1 || !isRecord2(container[name])) {
1097
981
  throw new Error(`Edited JSON must contain only agent ${name}`);
1098
982
  }
1099
983
  return container[name];
1100
984
  }
1101
- function validateEditedAgent(kind, name, raw) {
1102
- if (kind === "landstrip") {
1103
- validateAgentRaw(name, raw);
1104
- return;
1105
- }
1106
- const prompt = raw.prompt;
1107
- if (prompt !== undefined && typeof prompt !== "string") {
1108
- throw new Error(`agent ${name} prompt must be a string`);
1109
- }
1110
- validateAgentRaw(name, normalizeOpenCodeAgentRaw(raw, typeof prompt === "string" ? prompt : ""));
1111
- }
1112
985
  function ensureFinalNewline(content) {
1113
986
  return content.endsWith(`
1114
987
  `) ? content : `${content}
@@ -1120,7 +993,7 @@ async function writeJsonNode(path, nodePath, value) {
1120
993
  `;
1121
994
  parseJsonDocument(content, path);
1122
995
  const edited = applyEdits(content, modify(content, nodePath, value, JSON_FORMAT));
1123
- mkdirSync2(dirname3(path), { recursive: true });
996
+ mkdirSync2(dirname2(path), { recursive: true });
1124
997
  writeFileSync2(path, ensureFinalNewline(edited), { mode: 384 });
1125
998
  });
1126
999
  }
@@ -1146,11 +1019,11 @@ async function deleteJsonNode(path, nodePath, containerPaths) {
1146
1019
  });
1147
1020
  return deleted;
1148
1021
  }
1149
- function readAgentNode(path, kind, name) {
1022
+ function readAgentNode(path, name) {
1150
1023
  if (!existsSync3(path))
1151
1024
  return;
1152
1025
  const root = parseJsonDocument(readFileSync4(path, "utf8"), path);
1153
- const value = valueAtPath(root, agentJsonPath(kind, name));
1026
+ const value = valueAtPath(root, agentJsonPath(name));
1154
1027
  return isRecord2(value) ? { ...value } : undefined;
1155
1028
  }
1156
1029
  function permissionConfig(agent) {
@@ -1189,7 +1062,7 @@ function relocateGlobalPrompt(raw, agent) {
1189
1062
  if (agent.origin?.source !== "global" || !agent.origin.path || typeof raw.prompt !== "string") {
1190
1063
  return raw;
1191
1064
  }
1192
- return { ...raw, prompt: expandFileReferences(raw.prompt, dirname3(agent.origin.path)) };
1065
+ return { ...raw, prompt: expandFileReferences(raw.prompt, dirname2(agent.origin.path)) };
1193
1066
  }
1194
1067
  function safeRelative(root, path) {
1195
1068
  const value = relative2(root, path);
@@ -1203,19 +1076,13 @@ function projectMarkdownPath(cwd, agentDir, agent) {
1203
1076
  const suffix = safeRelative(join5(agentDir, "agents"), origin.path) ?? `${agent.name}.md`;
1204
1077
  return join5(cwd, ".pi", "agents", suffix);
1205
1078
  }
1206
- if (origin.kind === "opencode-markdown" && origin.path) {
1207
- return join5(cwd, ".opencode", "agents", `${agent.name}.md`);
1208
- }
1209
1079
  throw new Error(`Agent ${agent.name} has no Markdown source`);
1210
1080
  }
1211
- function openCodeProjectPath(cwd, sourcePath) {
1212
- return join5(cwd, ".opencode", basename(sourcePath));
1213
- }
1214
1081
  function projectSettingsPath(cwd, agentDir) {
1215
1082
  return getPiConfigPaths(cwd, "settings.json", agentDir).projectPath;
1216
1083
  }
1217
1084
  function projectSettingsHasAgent(cwd, name, agentDir) {
1218
- return readAgentNode(projectSettingsPath(cwd, agentDir), "landstrip", name) !== undefined;
1085
+ return readAgentNode(projectSettingsPath(cwd, agentDir), name) !== undefined;
1219
1086
  }
1220
1087
  function canDeleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1221
1088
  try {
@@ -1225,21 +1092,16 @@ function canDeleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1225
1092
  return false;
1226
1093
  }
1227
1094
  const origin = agent.origin;
1228
- return Boolean(origin?.source === "local" && origin.path && (origin.kind === "pi-markdown" || origin.kind === "opencode-markdown" || origin.kind === "opencode-json"));
1095
+ return Boolean(origin?.source === "local" && origin.path && origin.kind === "pi-markdown");
1229
1096
  }
1230
1097
  async function deleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1231
1098
  const settingsPath = projectSettingsPath(cwd, agentDir);
1232
- const deletedOverride = await deleteJsonNode(settingsPath, agentJsonPath("landstrip", agent.name), [["landstrip", "agent"], ["landstrip"]]);
1099
+ const deletedOverride = await deleteJsonNode(settingsPath, agentJsonPath(agent.name), [
1100
+ ["landstrip", "agent"],
1101
+ ["landstrip"]
1102
+ ]);
1233
1103
  const origin = agent.origin;
1234
- if (origin?.source !== "local" || !origin.path)
1235
- return deletedOverride;
1236
- if (origin.kind === "opencode-json") {
1237
- const deletedSource = await deleteJsonNode(origin.path, agentJsonPath("opencode", agent.name), [
1238
- ["agent"]
1239
- ]);
1240
- return deletedOverride || deletedSource;
1241
- }
1242
- if (origin.kind !== "pi-markdown" && origin.kind !== "opencode-markdown") {
1104
+ if (origin?.source !== "local" || !origin.path || origin.kind !== "pi-markdown") {
1243
1105
  return deletedOverride;
1244
1106
  }
1245
1107
  if (!existsSync3(origin.path))
@@ -1249,44 +1111,31 @@ async function deleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
1249
1111
  }
1250
1112
  function prepareProjectAgentEditor(cwd, agent, agentDir = getAgentDir3()) {
1251
1113
  const origin = agent.origin;
1252
- const openCodeProjectEnabled = loadLandstripConfig(cwd, true, agentDir).opencode.showLocalAgents;
1253
- if (origin?.path && (origin.kind === "pi-markdown" || origin.kind === "opencode-markdown")) {
1254
- const useMarkdown = origin.kind === "pi-markdown" || origin.source === "local" || openCodeProjectEnabled;
1255
- const target2 = useMarkdown ? projectMarkdownPath(cwd, agentDir, agent) : projectSettingsPath(cwd, agentDir);
1256
- const projectOverride = useMarkdown ? undefined : readAgentNode(target2, "landstrip", agent.name);
1257
- const disabledOverride = projectOverride && Object.keys(projectOverride).length === 1 && typeof projectOverride.disable === "boolean" ? projectOverride.disable : undefined;
1114
+ if (origin?.path && origin.kind === "pi-markdown") {
1115
+ const target2 = projectMarkdownPath(cwd, agentDir, agent);
1258
1116
  return {
1259
1117
  title: `Edit @${agent.name}`,
1260
1118
  content: readFileSync4(origin.path, "utf8"),
1261
1119
  async save(content) {
1262
- const edited = parseMarkdownAgentRaw(content);
1263
- const raw2 = !useMarkdown && edited.disable === undefined && disabledOverride !== undefined ? { ...edited, disable: disabledOverride } : edited;
1264
- validateAgentRaw(agent.name, raw2);
1265
- if (!useMarkdown) {
1266
- await writeJsonNode(target2, agentJsonPath("landstrip", agent.name), raw2);
1267
- return;
1268
- }
1120
+ validateAgentRaw(agent.name, parseMarkdownAgentRaw(content));
1269
1121
  await withFileMutationQueue2(target2, async () => {
1270
- mkdirSync2(dirname3(target2), { recursive: true });
1122
+ mkdirSync2(dirname2(target2), { recursive: true });
1271
1123
  writeFileSync2(target2, ensureFinalNewline(content), { mode: 384 });
1272
1124
  });
1273
1125
  }
1274
1126
  };
1275
1127
  }
1276
- const useOpenCode = origin?.kind === "opencode-json" && origin.path !== undefined && (origin.source === "local" || openCodeProjectEnabled);
1277
- const kind = useOpenCode ? "opencode" : "landstrip";
1278
1128
  const sourcePath = origin?.path;
1279
- const sourceKind = origin?.kind === "opencode-json" ? "opencode" : "landstrip";
1280
- const sourceRaw = sourcePath ? readAgentNode(sourcePath, sourceKind, agent.name) : undefined;
1281
- const raw = relocateGlobalPrompt(sourceKind === kind && sourceRaw ? sourceRaw : agent.raw ? { ...agent.raw } : normalizedAgentRaw(agent), agent);
1282
- const target = kind === "opencode" && sourcePath ? origin?.source === "local" ? sourcePath : openCodeProjectPath(cwd, sourcePath) : projectSettingsPath(cwd, agentDir);
1129
+ const sourceRaw = origin?.kind === "settings" && sourcePath ? readAgentNode(sourcePath, agent.name) : undefined;
1130
+ const raw = relocateGlobalPrompt(sourceRaw ? sourceRaw : agent.raw ? { ...agent.raw } : normalizedAgentRaw(agent), agent);
1131
+ const target = projectSettingsPath(cwd, agentDir);
1283
1132
  return {
1284
1133
  title: `Edit @${agent.name}`,
1285
- content: editorSnippet(kind, agent.name, raw),
1134
+ content: editorSnippet(agent.name, raw),
1286
1135
  async save(content) {
1287
- const edited = parseEditorSnippet(content, kind, agent.name);
1288
- validateEditedAgent(kind, agent.name, edited);
1289
- await writeJsonNode(target, agentJsonPath(kind, agent.name), edited);
1136
+ const edited = parseEditorSnippet(content, agent.name);
1137
+ validateAgentRaw(agent.name, edited);
1138
+ await writeJsonNode(target, agentJsonPath(agent.name), edited);
1290
1139
  }
1291
1140
  };
1292
1141
  }
@@ -1744,7 +1593,7 @@ var PRIMARY_AGENT_ENTRY = "landstrip.primary-agent";
1744
1593
  var WORKER_ENV = "PI_LANDSTRIP_WORKER";
1745
1594
  var CONTROL_TITLE = "pi-landstrip:control:v1";
1746
1595
  var MAX_DEPTH = 3;
1747
- var packageDir = dirname4(fileURLToPath(import.meta.url));
1596
+ var packageDir = dirname3(fileURLToPath(import.meta.url));
1748
1597
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
1749
1598
  var INSPECTOR_BODY_LINES = 16;
1750
1599
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
@@ -1790,7 +1639,7 @@ function resolvePiPackage() {
1790
1639
  piPackageResolved = true;
1791
1640
  try {
1792
1641
  const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
1793
- let dir2 = dirname4(entry);
1642
+ let dir2 = dirname3(entry);
1794
1643
  for (;; ) {
1795
1644
  const pkgPath = join6(dir2, "package.json");
1796
1645
  if (existsSync4(pkgPath)) {
@@ -1798,7 +1647,7 @@ function resolvePiPackage() {
1798
1647
  if (pkg)
1799
1648
  return pkg;
1800
1649
  }
1801
- const parent = dirname4(dir2);
1650
+ const parent = dirname3(dir2);
1802
1651
  if (parent === dir2)
1803
1652
  break;
1804
1653
  dir2 = parent;
@@ -1812,7 +1661,7 @@ function resolvePiPackage() {
1812
1661
  if (pkg)
1813
1662
  return pkg;
1814
1663
  }
1815
- const parent = dirname4(dir);
1664
+ const parent = dirname3(dir);
1816
1665
  if (parent === dir)
1817
1666
  return;
1818
1667
  dir = parent;
@@ -1830,7 +1679,7 @@ function readPiPackage(pkgPath) {
1830
1679
  const binFile = typeof value.bin === "string" ? value.bin : isRecord2(value.bin) && typeof value.bin.pi === "string" ? value.bin.pi : undefined;
1831
1680
  if (!binFile)
1832
1681
  return;
1833
- const cliEntry = join6(dirname4(pkgPath), binFile);
1682
+ const cliEntry = join6(dirname3(pkgPath), binFile);
1834
1683
  if (!existsSync4(cliEntry))
1835
1684
  return;
1836
1685
  cachedPiPackage = { cliEntry, version: parts.slice(0, 3) };
@@ -2085,14 +1934,14 @@ class Semaphore {
2085
1934
  if (this.active >= this.limit)
2086
1935
  return;
2087
1936
  this.active += 1;
2088
- return () => this.release();
1937
+ return this.releaseOnce();
2089
1938
  }
2090
1939
  async acquire(signal) {
2091
1940
  if (signal?.aborted)
2092
1941
  throw new Error("Task cancelled");
2093
1942
  if (this.active < this.limit) {
2094
1943
  this.active += 1;
2095
- return () => this.release();
1944
+ return this.releaseOnce();
2096
1945
  }
2097
1946
  await new Promise((resolve3, reject) => {
2098
1947
  const abort = () => {
@@ -2109,11 +1958,17 @@ class Semaphore {
2109
1958
  this.waiters.push(start);
2110
1959
  signal?.addEventListener("abort", abort, { once: true });
2111
1960
  });
2112
- return () => this.release();
1961
+ return this.releaseOnce();
2113
1962
  }
2114
- release() {
2115
- this.active -= 1;
2116
- this.waiters.shift()?.();
1963
+ releaseOnce() {
1964
+ let released = false;
1965
+ return () => {
1966
+ if (released)
1967
+ return;
1968
+ released = true;
1969
+ this.active -= 1;
1970
+ this.waiters.shift()?.();
1971
+ };
2117
1972
  }
2118
1973
  }
2119
1974
 
@@ -2261,15 +2116,15 @@ function canonicalPermissionPath(path, seen = new Set) {
2261
2116
  if (seen.has(existing))
2262
2117
  return path;
2263
2118
  seen.add(existing);
2264
- const target = resolve2(dirname4(existing), readlinkSync(existing), ...missing);
2119
+ const target = resolve2(dirname3(existing), readlinkSync(existing), ...missing);
2265
2120
  return canonicalPermissionPath(target, seen);
2266
2121
  }
2267
2122
  break;
2268
2123
  } catch {
2269
- const parent = dirname4(existing);
2124
+ const parent = dirname3(existing);
2270
2125
  if (parent === existing)
2271
2126
  return path;
2272
- missing.unshift(basename2(existing));
2127
+ missing.unshift(basename(existing));
2273
2128
  existing = parent;
2274
2129
  }
2275
2130
  }
@@ -2708,7 +2563,9 @@ ${this.primaryAgent.prompt}` };
2708
2563
  const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
2709
2564
  lines2.push(`${cursor} ${value} ${label}${dirty}`);
2710
2565
  }
2711
- lines2.push("", theme.fg("dim", selectedSetting === 0 ? rows[0]?.inherited ? "Sets concurrent subagents. Zero disables the task tool. Using the Global value." : "Sets concurrent subagents. Zero disables the task tool." : rows[1]?.inherited ? "Controls OS sandboxing. Use /sandbox to inspect its policy. Using the Global value." : "Controls OS sandboxing. Use /sandbox to inspect its policy."));
2566
+ if (selectedSetting === 1) {
2567
+ lines2.push("", theme.fg("dim", rows[1]?.inherited ? "Controls OS sandboxing. Use /sandbox to inspect its policy. Using the Global value." : "Controls OS sandboxing. Use /sandbox to inspect its policy."));
2568
+ }
2712
2569
  if (saving)
2713
2570
  lines2.push("", theme.fg("dim", "Saving…"));
2714
2571
  return box(lines2);
@@ -3564,11 +3421,11 @@ ${catalog.diagnostics.join(`
3564
3421
  return;
3565
3422
  });
3566
3423
  try {
3567
- const sessionWritePath = task.sessionDir ?? (task.sessionFile ? dirname4(task.sessionFile) : undefined);
3424
+ const sessionWritePath = task.sessionDir ?? (task.sessionFile ? dirname3(task.sessionFile) : undefined);
3568
3425
  if (!sessionWritePath)
3569
3426
  throw new Error("Subagent task has no session directory or file");
3570
3427
  const cliEntry = invocation.args[0] ?? invocation.command;
3571
- const cliRoot = dependencyRoot(cliEntry) ?? dirname4(dirname4(cliEntry));
3428
+ const cliRoot = dependencyRoot(cliEntry) ?? dirname3(dirname3(cliEntry));
3572
3429
  const extensionRoot = dependencyRoot(packageDir);
3573
3430
  launch = await this.integration.prepareRpcWorker({
3574
3431
  command: invocation.command,
@@ -3588,7 +3445,7 @@ ${catalog.diagnostics.join(`
3588
3445
  ...new Set([
3589
3446
  ctx.cwd,
3590
3447
  ...agentBootstrapPaths(agentDir),
3591
- join6(homedir3(), ".agents", "skills"),
3448
+ join6(homedir2(), ".agents", "skills"),
3592
3449
  packageDir,
3593
3450
  join6(packageDir, "node_modules"),
3594
3451
  invocation.command,
@@ -3596,7 +3453,7 @@ ${catalog.diagnostics.join(`
3596
3453
  extensionRoot,
3597
3454
  ...workerExtensions.flatMap((extension) => [
3598
3455
  extension.entry,
3599
- dirname4(extension.entry),
3456
+ dirname3(extension.entry),
3600
3457
  dependencyRoot(extension.entry)
3601
3458
  ]),
3602
3459
  task.sessionDir,
@@ -3718,7 +3575,9 @@ ${catalog.diagnostics.join(`
3718
3575
  });
3719
3576
  } finally {
3720
3577
  if (handedOff)
3721
- await this.restoreLease(task.id, signal);
3578
+ await this.restoreLease(task.id).catch(() => {
3579
+ return;
3580
+ });
3722
3581
  }
3723
3582
  response = {
3724
3583
  ok: true,
@@ -3808,7 +3667,7 @@ ${renderTaskResult(task.id, "error", message)}`);
3808
3667
  storeTaskText(task, value, kind) {
3809
3668
  if (Buffer.byteLength(value) <= MAX_TASK_OUTPUT_BYTES)
3810
3669
  return value;
3811
- const directory = task.sessionDir ?? (task.sessionFile ? dirname4(task.sessionFile) : undefined);
3670
+ const directory = task.sessionDir ?? (task.sessionFile ? dirname3(task.sessionFile) : undefined);
3812
3671
  if (!directory)
3813
3672
  throw new Error(`Task ${task.id} has no artifact directory`);
3814
3673
  mkdirSync3(directory, { recursive: true });
@@ -4015,11 +3874,11 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
4015
3874
  lease.release = undefined;
4016
3875
  return true;
4017
3876
  }
4018
- async restoreLease(taskId, signal) {
3877
+ async restoreLease(taskId) {
4019
3878
  const lease = this.leases.get(taskId);
4020
3879
  if (!lease || lease.release)
4021
3880
  return;
4022
- const release = await this.semaphore.acquire(signal);
3881
+ const release = await this.semaphore.acquire();
4023
3882
  const current = this.leases.get(taskId);
4024
3883
  if (!current || current.release) {
4025
3884
  release();
@@ -4070,7 +3929,7 @@ var PROXY_ENVIRONMENT_VARIABLES = [
4070
3929
  "https_proxy",
4071
3930
  "all_proxy"
4072
3931
  ];
4073
- var packageDir2 = dirname5(fileURLToPath2(import.meta.url));
3932
+ var packageDir2 = dirname4(fileURLToPath2(import.meta.url));
4074
3933
  var PERMISSION_OPTIONS = [
4075
3934
  { label: "Allow once", action: "once" },
4076
3935
  { label: "Allow for this session", action: "session" },
@@ -4098,7 +3957,7 @@ function loadSandboxConfig(cwd, includeProject) {
4098
3957
  const globalConfigPath = join7(getAgentDir5(), "sandbox.json");
4099
3958
  if (!existsSync5(globalConfigPath)) {
4100
3959
  const templatePath = join7(packageDir2, "sandbox.json");
4101
- mkdirSync4(dirname5(globalConfigPath), { recursive: true });
3960
+ mkdirSync4(dirname4(globalConfigPath), { recursive: true });
4102
3961
  writeFileSync4(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
4103
3962
  }
4104
3963
  let globalConfig = JSON.parse(readFileSync6(join7(packageDir2, "sandbox.json"), "utf-8"));
@@ -4194,12 +4053,12 @@ function readOrEmptyConfig(configPath) {
4194
4053
  return {};
4195
4054
  try {
4196
4055
  return JSON.parse(readFileSync6(configPath, "utf-8"));
4197
- } catch {
4198
- return {};
4056
+ } catch (error) {
4057
+ throw new Error(`${configPath}: ${formatError(error)}`);
4199
4058
  }
4200
4059
  }
4201
4060
  function writeConfigFile(configPath, config) {
4202
- mkdirSync4(dirname5(configPath), { recursive: true });
4061
+ mkdirSync4(dirname4(configPath), { recursive: true });
4203
4062
  writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
4204
4063
  `, "utf-8");
4205
4064
  }
@@ -4295,10 +4154,10 @@ function canonicalizePath(filePath, cwd) {
4295
4154
  const tail = [];
4296
4155
  let probe = abs;
4297
4156
  while (!existsSync5(probe)) {
4298
- const parent = dirname5(probe);
4157
+ const parent = dirname4(probe);
4299
4158
  if (parent === probe)
4300
4159
  return abs;
4301
- tail.unshift(basename3(probe));
4160
+ tail.unshift(basename2(probe));
4302
4161
  probe = parent;
4303
4162
  }
4304
4163
  try {
@@ -4336,9 +4195,9 @@ function pathUnderDirectory(filePath, dir) {
4336
4195
  return normalizedFilePath.startsWith(normalizedDir + sep4);
4337
4196
  }
4338
4197
  function sessionScopeFor(filePath, baseDirectory) {
4339
- const dir = dirname5(filePath);
4198
+ const dir = dirname4(filePath);
4340
4199
  const normalizedDir = normalizePathSeparators(dir);
4341
- const home = homedir4();
4200
+ const home = homedir3();
4342
4201
  const boundaries = new Set;
4343
4202
  if (home)
4344
4203
  boundaries.add(home);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.11",
3
+ "version": "0.18.13",
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.11",
42
+ "@landstrip/landstrip": "^0.18.13",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"