pi-landstrip 0.17.39 → 0.17.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -18
- package/dist/index.ts +145 -58
- package/dist/subagents.json +8 -3
- package/package.json +2 -2
- package/subagents.json +8 -3
package/README.md
CHANGED
|
@@ -111,8 +111,10 @@ global or trusted-project concurrency limit.
|
|
|
111
111
|
|
|
112
112
|
The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by
|
|
113
113
|
default. Build has normal development access; plan asks before shell commands
|
|
114
|
-
and file changes.
|
|
115
|
-
and
|
|
114
|
+
and file changes. Built-in colors match OpenCode's agent palette (`#034cff` for
|
|
115
|
+
build, `#a753ae` for plan) and appear in the status line and agent selectors.
|
|
116
|
+
The selection controls the root system prompt and permissions and is restored
|
|
117
|
+
with the session.
|
|
116
118
|
|
|
117
119
|
## Subagents
|
|
118
120
|
|
|
@@ -216,14 +218,15 @@ Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi'
|
|
|
216
218
|
}
|
|
217
219
|
```
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
OpenCode Markdown agent import flags live in `subagents.json` as top-level
|
|
222
|
+
`opencode` (both default to `true`):
|
|
220
223
|
|
|
221
224
|
```json
|
|
222
225
|
{
|
|
223
226
|
"maxSubagents": 0,
|
|
224
227
|
"opencode": {
|
|
225
|
-
"showGlobalAgents":
|
|
226
|
-
"showLocalAgents":
|
|
228
|
+
"showGlobalAgents": true,
|
|
229
|
+
"showLocalAgents": true
|
|
227
230
|
},
|
|
228
231
|
"subagents": {
|
|
229
232
|
"agent": {}
|
|
@@ -231,18 +234,17 @@ Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
|
|
|
231
234
|
}
|
|
232
235
|
```
|
|
233
236
|
|
|
234
|
-
Both flags default to `false`. When enabled:
|
|
235
|
-
|
|
236
237
|
- `showGlobalAgents` imports Markdown agents from the OpenCode global config
|
|
237
238
|
directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
|
|
238
239
|
`~/.config/opencode`) under `agent/` and `agents/`.
|
|
239
240
|
- `showLocalAgents` imports Markdown agents from the trusted project's
|
|
240
241
|
`.opencode/agent/` and `.opencode/agents/` directories.
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
from `subagents.json` (built-in,
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
Set a flag to `false` to disable that source. OpenCode local agents override
|
|
244
|
+
OpenCode global agents. Pi agent definitions from `subagents.json` (built-in,
|
|
245
|
+
global, and local) take precedence over OpenCode imports with the same name;
|
|
246
|
+
conflicts are silent. Local OpenCode agents are skipped when the project is
|
|
247
|
+
untrusted, regardless of the flag.
|
|
246
248
|
|
|
247
249
|
`subagents.json` accepts top-level `maxSubagents`, `opencode`, and `subagents`;
|
|
248
250
|
sandbox fields belong in `sandbox.json`.
|
|
@@ -320,13 +322,18 @@ The packaged default is 0, which removes the `task` tool while retaining primary
|
|
|
320
322
|
roles. There is no separate subagent enable switch. The Settings tab in
|
|
321
323
|
`/agents` edits this limit for global and trusted-project configuration.
|
|
322
324
|
|
|
323
|
-
Agent modes, hidden/disabled agents, prompts, and ordered
|
|
324
|
-
permissions apply to primary agents and subagents.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
325
|
+
Agent modes, hidden/disabled agents, prompts, colors, and ordered
|
|
326
|
+
`allow`/`ask`/`deny` permissions apply to primary agents and subagents. Prompt
|
|
327
|
+
strings may include OpenCode-style `{file:path}` tokens; relative paths resolve
|
|
328
|
+
against the `subagents.json` file that defines them. `color` accepts `#RRGGBB`
|
|
329
|
+
or OpenCode theme names (`primary`, `secondary`, `accent`, `success`, `warning`,
|
|
330
|
+
`error`, `info`). `hidden` only hides an agent from user-facing primary pickers;
|
|
331
|
+
the model can still invoke a hidden subagent via `task`. Subagent workers also
|
|
332
|
+
honor model selection, supported Pi thinking-level variants, and step limits.
|
|
333
|
+
Primary agent activation currently changes the prompt and permissions only.
|
|
334
|
+
Later matching permission rules win. Put provider-specific values under
|
|
335
|
+
`options`; unknown agent fields are rejected. Agent permissions cannot weaken an
|
|
336
|
+
enabled OS sandbox, grant filesystem access, or grant network access.
|
|
330
337
|
|
|
331
338
|
## Configuration migration
|
|
332
339
|
|
package/dist/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
existsSync as existsSync5,
|
|
7
7
|
mkdirSync as mkdirSync3,
|
|
8
8
|
mkdtempSync as mkdtempSync2,
|
|
9
|
-
readFileSync as
|
|
9
|
+
readFileSync as readFileSync6,
|
|
10
10
|
realpathSync as realpathSync2,
|
|
11
11
|
rmSync as rmSync2,
|
|
12
12
|
writeFileSync as writeFileSync3
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
Socket as NetSocket
|
|
20
20
|
} from "node:net";
|
|
21
21
|
import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
|
|
22
|
-
import { basename as basename3, dirname as dirname3, isAbsolute as
|
|
22
|
+
import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute3, join as join6, parse, resolve as resolve3 } from "node:path";
|
|
23
23
|
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
24
24
|
import { fileURLToPath as fileURLToPath3, URL } from "node:url";
|
|
25
25
|
import {
|
|
@@ -74,14 +74,14 @@ import {
|
|
|
74
74
|
lstatSync,
|
|
75
75
|
mkdirSync as mkdirSync2,
|
|
76
76
|
mkdtempSync,
|
|
77
|
-
readFileSync as
|
|
77
|
+
readFileSync as readFileSync5,
|
|
78
78
|
readlinkSync,
|
|
79
79
|
realpathSync,
|
|
80
80
|
rmSync,
|
|
81
81
|
writeFileSync as writeFileSync2
|
|
82
82
|
} from "node:fs";
|
|
83
83
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
84
|
-
import { basename as basename2, dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
84
|
+
import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
85
85
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
86
86
|
import {
|
|
87
87
|
getAgentDir as getAgentDir3,
|
|
@@ -92,20 +92,21 @@ import { matchesKey, Text, truncateToWidth as truncateToWidth2 } from "@earendil
|
|
|
92
92
|
import { Type } from "typebox";
|
|
93
93
|
|
|
94
94
|
// agents.ts
|
|
95
|
-
import { existsSync as existsSync3, readFileSync as
|
|
95
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
96
96
|
import { join as join4 } from "node:path";
|
|
97
97
|
import { minimatch } from "minimatch";
|
|
98
98
|
import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
|
|
99
99
|
|
|
100
100
|
// config.ts
|
|
101
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
101
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
102
102
|
import { dirname, join as join2 } from "node:path";
|
|
103
103
|
import { fileURLToPath } from "node:url";
|
|
104
104
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
105
105
|
|
|
106
106
|
// util.ts
|
|
107
|
+
import { readFileSync } from "node:fs";
|
|
107
108
|
import { homedir } from "node:os";
|
|
108
|
-
import { join } from "node:path";
|
|
109
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
109
110
|
function expandHomePath(path) {
|
|
110
111
|
if (path === "~" || path === "$HOME")
|
|
111
112
|
return homedir();
|
|
@@ -115,20 +116,85 @@ function expandHomePath(path) {
|
|
|
115
116
|
return join(homedir(), path.slice(6));
|
|
116
117
|
return path;
|
|
117
118
|
}
|
|
119
|
+
function expandFileReferences(text, baseDir) {
|
|
120
|
+
if (!text.includes("{file:"))
|
|
121
|
+
return text;
|
|
122
|
+
const matches = Array.from(text.matchAll(/\{file:[^}]+\}/g));
|
|
123
|
+
if (matches.length === 0)
|
|
124
|
+
return text;
|
|
125
|
+
let out = "";
|
|
126
|
+
let cursor = 0;
|
|
127
|
+
for (const match of matches) {
|
|
128
|
+
const token = match[0];
|
|
129
|
+
const index = match.index ?? 0;
|
|
130
|
+
out += text.slice(cursor, index);
|
|
131
|
+
let filePath = token.slice("{file:".length, -1);
|
|
132
|
+
if (filePath.startsWith("~/"))
|
|
133
|
+
filePath = join(homedir(), filePath.slice(2));
|
|
134
|
+
const resolved = isAbsolute(filePath) ? filePath : resolve(baseDir, filePath);
|
|
135
|
+
try {
|
|
136
|
+
out += readFileSync(resolved, "utf8").trim();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const detail = error instanceof Error && "code" in error && error.code === "ENOENT" ? `${resolved} does not exist` : error instanceof Error ? error.message : String(error);
|
|
139
|
+
throw new Error(`bad file reference: "${token}" ${detail}`);
|
|
140
|
+
}
|
|
141
|
+
cursor = index + token.length;
|
|
142
|
+
}
|
|
143
|
+
out += text.slice(cursor);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
118
146
|
function formatError(error) {
|
|
119
147
|
return error instanceof Error ? error.message : String(error);
|
|
120
148
|
}
|
|
121
149
|
function isRecord2(value) {
|
|
122
150
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
151
|
}
|
|
152
|
+
var OPENCODE_THEME_COLORS = new Set([
|
|
153
|
+
"primary",
|
|
154
|
+
"secondary",
|
|
155
|
+
"accent",
|
|
156
|
+
"success",
|
|
157
|
+
"warning",
|
|
158
|
+
"error",
|
|
159
|
+
"info"
|
|
160
|
+
]);
|
|
161
|
+
var THEME_COLOR_ALIASES = {
|
|
162
|
+
primary: "accent",
|
|
163
|
+
secondary: "muted",
|
|
164
|
+
accent: "accent",
|
|
165
|
+
success: "success",
|
|
166
|
+
warning: "warning",
|
|
167
|
+
error: "error",
|
|
168
|
+
info: "accent"
|
|
169
|
+
};
|
|
170
|
+
function isAgentColor(value) {
|
|
171
|
+
if (/^#[0-9a-fA-F]{6}$/.test(value))
|
|
172
|
+
return true;
|
|
173
|
+
return OPENCODE_THEME_COLORS.has(value);
|
|
174
|
+
}
|
|
175
|
+
function hexFg(hex, text) {
|
|
176
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
177
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
178
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
179
|
+
return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
|
|
180
|
+
}
|
|
181
|
+
function colorizeAgentText(color, text, themeFg, fallback = "accent") {
|
|
182
|
+
if (!color)
|
|
183
|
+
return themeFg ? themeFg(fallback, text) : text;
|
|
184
|
+
if (color.startsWith("#"))
|
|
185
|
+
return hexFg(color, text);
|
|
186
|
+
if (!themeFg)
|
|
187
|
+
return text;
|
|
188
|
+
return themeFg(THEME_COLOR_ALIASES[color] ?? fallback, text);
|
|
189
|
+
}
|
|
124
190
|
|
|
125
191
|
class AsyncQueue {
|
|
126
192
|
tail = Promise.resolve();
|
|
127
193
|
async acquire(signal, cancellationMessage = "Request cancelled") {
|
|
128
194
|
let release;
|
|
129
195
|
const previous = this.tail;
|
|
130
|
-
this.tail = new Promise((
|
|
131
|
-
release =
|
|
196
|
+
this.tail = new Promise((resolve2) => {
|
|
197
|
+
release = resolve2;
|
|
132
198
|
});
|
|
133
199
|
if (!signal) {
|
|
134
200
|
await previous;
|
|
@@ -161,8 +227,8 @@ class AsyncQueue {
|
|
|
161
227
|
var MAX_SUBAGENTS = 16;
|
|
162
228
|
var packageDir = dirname(fileURLToPath(import.meta.url));
|
|
163
229
|
var DEFAULT_OPENCODE = {
|
|
164
|
-
showGlobalAgents:
|
|
165
|
-
showLocalAgents:
|
|
230
|
+
showGlobalAgents: true,
|
|
231
|
+
showLocalAgents: true
|
|
166
232
|
};
|
|
167
233
|
var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
|
|
168
234
|
function readBooleanField(section, key, path) {
|
|
@@ -192,12 +258,28 @@ function normalizeOpenCode(value, path) {
|
|
|
192
258
|
result.showLocalAgents = showLocalAgents;
|
|
193
259
|
return result;
|
|
194
260
|
}
|
|
261
|
+
function expandAgentPromptReferences(config, path) {
|
|
262
|
+
if (!isRecord2(config.subagents) || !isRecord2(config.subagents.agent))
|
|
263
|
+
return;
|
|
264
|
+
const baseDir = dirname(path);
|
|
265
|
+
for (const [name, value] of Object.entries(config.subagents.agent)) {
|
|
266
|
+
if (!isRecord2(value) || typeof value.prompt !== "string")
|
|
267
|
+
continue;
|
|
268
|
+
if (!value.prompt.includes("{file:"))
|
|
269
|
+
continue;
|
|
270
|
+
try {
|
|
271
|
+
value.prompt = expandFileReferences(value.prompt, baseDir);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new Error(`${path}: agent ${name}: ${formatError(error)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
195
277
|
function readConfig(path, isGlobal = false) {
|
|
196
278
|
if (!existsSync(path))
|
|
197
279
|
return {};
|
|
198
280
|
let value;
|
|
199
281
|
try {
|
|
200
|
-
value = JSON.parse(
|
|
282
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
201
283
|
} catch (error) {
|
|
202
284
|
const message = formatError(error);
|
|
203
285
|
throw new Error(`${path}: ${message}`);
|
|
@@ -233,6 +315,7 @@ function readConfig(path, isGlobal = false) {
|
|
|
233
315
|
if ("opencode" in value) {
|
|
234
316
|
config.opencode = normalizeOpenCode(value.opencode, `${path}: opencode`);
|
|
235
317
|
}
|
|
318
|
+
expandAgentPromptReferences(config, path);
|
|
236
319
|
return config;
|
|
237
320
|
}
|
|
238
321
|
function mergeValue(base, override) {
|
|
@@ -306,7 +389,7 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
|
|
|
306
389
|
}
|
|
307
390
|
|
|
308
391
|
// opencode-agents.ts
|
|
309
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
392
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
310
393
|
import { homedir as homedir2 } from "node:os";
|
|
311
394
|
import { basename, extname, join as join3, relative, sep } from "node:path";
|
|
312
395
|
import { parse as parseYaml } from "yaml";
|
|
@@ -449,7 +532,7 @@ function loadAgentsFromDir(dir, source, agents, diagnostics) {
|
|
|
449
532
|
const root = join3(dir, subdir);
|
|
450
533
|
for (const filePath of listMarkdownFiles(root)) {
|
|
451
534
|
try {
|
|
452
|
-
const content =
|
|
535
|
+
const content = readFileSync3(filePath, "utf8");
|
|
453
536
|
const { data, body } = parseFrontmatter(content);
|
|
454
537
|
const name = agentNameFromPath(dir, filePath);
|
|
455
538
|
if (!name) {
|
|
@@ -528,7 +611,7 @@ function legacyConfigWarnings(piAgentDir) {
|
|
|
528
611
|
if (!existsSync3(path))
|
|
529
612
|
return [];
|
|
530
613
|
try {
|
|
531
|
-
const value = JSON.parse(
|
|
614
|
+
const value = JSON.parse(readFileSync4(path, "utf8"));
|
|
532
615
|
if (!isRecord2(value))
|
|
533
616
|
return [];
|
|
534
617
|
const fields = ["agent", "permission", "subagents", "maxSubagents", "landstrip"].filter((field) => value[field] !== undefined);
|
|
@@ -542,11 +625,14 @@ function legacyConfigWarnings(piAgentDir) {
|
|
|
542
625
|
}
|
|
543
626
|
}
|
|
544
627
|
function normalizeAgent(name, raw, source) {
|
|
545
|
-
for (const field of ["description", "prompt", "model", "variant"]) {
|
|
628
|
+
for (const field of ["description", "prompt", "model", "variant", "color"]) {
|
|
546
629
|
if (raw[field] !== undefined && typeof raw[field] !== "string") {
|
|
547
630
|
throw new Error(`agent ${name} ${field} must be a string`);
|
|
548
631
|
}
|
|
549
632
|
}
|
|
633
|
+
if (typeof raw.color === "string" && !isAgentColor(raw.color)) {
|
|
634
|
+
throw new Error(`agent ${name} color must be #RRGGBB or one of primary, secondary, accent, success, warning, error, info`);
|
|
635
|
+
}
|
|
550
636
|
for (const field of ["hidden", "disable"]) {
|
|
551
637
|
if (raw[field] !== undefined && typeof raw[field] !== "boolean") {
|
|
552
638
|
throw new Error(`agent ${name} ${field} must be a boolean`);
|
|
@@ -586,6 +672,7 @@ function normalizeAgent(name, raw, source) {
|
|
|
586
672
|
model: typeof raw.model === "string" ? raw.model : undefined,
|
|
587
673
|
variant: typeof raw.variant === "string" ? raw.variant : undefined,
|
|
588
674
|
hidden: raw.hidden === true,
|
|
675
|
+
color: typeof raw.color === "string" ? raw.color : undefined,
|
|
589
676
|
steps: typeof raw.steps === "number" ? raw.steps : undefined,
|
|
590
677
|
permissions: normalizePermissions(raw.permission),
|
|
591
678
|
providerOptions
|
|
@@ -721,7 +808,7 @@ function defaultSpawn(command, args, options) {
|
|
|
721
808
|
return spawn(command, [...args], options);
|
|
722
809
|
}
|
|
723
810
|
function delay(milliseconds) {
|
|
724
|
-
return new Promise((
|
|
811
|
+
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
725
812
|
}
|
|
726
813
|
|
|
727
814
|
class RpcProcess {
|
|
@@ -774,19 +861,19 @@ class RpcProcess {
|
|
|
774
861
|
});
|
|
775
862
|
child.stdin.on("error", (error) => this.handleChildError(child, error, "RPC stdin error"));
|
|
776
863
|
child.on("error", (error) => this.handleChildError(child, error, "RPC process error"));
|
|
777
|
-
this.exitPromise = new Promise((
|
|
864
|
+
this.exitPromise = new Promise((resolve2) => {
|
|
778
865
|
child.once("exit", (code, signal) => {
|
|
779
866
|
if (this.child === child) {
|
|
780
867
|
this.child = null;
|
|
781
868
|
this.failAll(new Error(this.exitMessage(code, signal)));
|
|
782
869
|
}
|
|
783
|
-
|
|
870
|
+
resolve2();
|
|
784
871
|
});
|
|
785
872
|
});
|
|
786
|
-
await new Promise((
|
|
873
|
+
await new Promise((resolve2, reject) => {
|
|
787
874
|
const onSpawn = () => {
|
|
788
875
|
cleanup();
|
|
789
|
-
|
|
876
|
+
resolve2();
|
|
790
877
|
};
|
|
791
878
|
const onError = (error) => {
|
|
792
879
|
cleanup();
|
|
@@ -830,12 +917,12 @@ class RpcProcess {
|
|
|
830
917
|
if (this.fatalError)
|
|
831
918
|
throw this.fatalError;
|
|
832
919
|
const id = `rpc-${++this.nextRequestId}`;
|
|
833
|
-
const response = await new Promise((
|
|
920
|
+
const response = await new Promise((resolve2, reject) => {
|
|
834
921
|
const timer = setTimeout(() => {
|
|
835
922
|
this.pending.delete(id);
|
|
836
923
|
reject(this.withStderr(`Timed out waiting for ${type} response`));
|
|
837
924
|
}, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
838
|
-
this.pending.set(id, { command: type, resolve, reject, timer });
|
|
925
|
+
this.pending.set(id, { command: type, resolve: resolve2, reject, timer });
|
|
839
926
|
this.write({ ...fields, id, type }, (error) => {
|
|
840
927
|
if (!error)
|
|
841
928
|
return;
|
|
@@ -1035,12 +1122,12 @@ class RpcProcess {
|
|
|
1035
1122
|
}
|
|
1036
1123
|
waitForAgentSettled() {
|
|
1037
1124
|
let waiter;
|
|
1038
|
-
const promise = new Promise((
|
|
1125
|
+
const promise = new Promise((resolve2, reject) => {
|
|
1039
1126
|
const timer = setTimeout(() => {
|
|
1040
1127
|
this.settledWaiters.delete(waiter);
|
|
1041
1128
|
reject(this.withStderr("Timed out waiting for agent_settled"));
|
|
1042
1129
|
}, this.options.settleTimeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS);
|
|
1043
|
-
waiter = { resolve, reject, timer };
|
|
1130
|
+
waiter = { resolve: resolve2, reject, timer };
|
|
1044
1131
|
this.settledWaiters.add(waiter);
|
|
1045
1132
|
});
|
|
1046
1133
|
return {
|
|
@@ -1186,7 +1273,7 @@ function resolvePiPackage() {
|
|
|
1186
1273
|
}
|
|
1187
1274
|
function readPiPackage(pkgPath) {
|
|
1188
1275
|
try {
|
|
1189
|
-
const value = JSON.parse(
|
|
1276
|
+
const value = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1190
1277
|
if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
|
|
1191
1278
|
return;
|
|
1192
1279
|
}
|
|
@@ -1350,7 +1437,7 @@ class Semaphore {
|
|
|
1350
1437
|
this.active += 1;
|
|
1351
1438
|
return () => this.release();
|
|
1352
1439
|
}
|
|
1353
|
-
await new Promise((
|
|
1440
|
+
await new Promise((resolve3, reject) => {
|
|
1354
1441
|
const abort = () => {
|
|
1355
1442
|
const index = this.waiters.indexOf(start);
|
|
1356
1443
|
if (index >= 0)
|
|
@@ -1360,7 +1447,7 @@ class Semaphore {
|
|
|
1360
1447
|
const start = () => {
|
|
1361
1448
|
signal?.removeEventListener("abort", abort);
|
|
1362
1449
|
this.active += 1;
|
|
1363
|
-
|
|
1450
|
+
resolve3();
|
|
1364
1451
|
};
|
|
1365
1452
|
this.waiters.push(start);
|
|
1366
1453
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -1508,7 +1595,7 @@ function canonicalPermissionPath(path, seen = new Set) {
|
|
|
1508
1595
|
if (seen.has(existing))
|
|
1509
1596
|
return path;
|
|
1510
1597
|
seen.add(existing);
|
|
1511
|
-
const target =
|
|
1598
|
+
const target = resolve2(dirname2(existing), readlinkSync(existing), ...missing);
|
|
1512
1599
|
return canonicalPermissionPath(target, seen);
|
|
1513
1600
|
}
|
|
1514
1601
|
break;
|
|
@@ -1521,7 +1608,7 @@ function canonicalPermissionPath(path, seen = new Set) {
|
|
|
1521
1608
|
}
|
|
1522
1609
|
}
|
|
1523
1610
|
try {
|
|
1524
|
-
return
|
|
1611
|
+
return resolve2(realpathSync(existing), ...missing);
|
|
1525
1612
|
} catch {
|
|
1526
1613
|
return path;
|
|
1527
1614
|
}
|
|
@@ -1536,10 +1623,10 @@ function permissionResource(tool, input, cwd) {
|
|
|
1536
1623
|
}
|
|
1537
1624
|
if (typeof input.path !== "string")
|
|
1538
1625
|
return "*";
|
|
1539
|
-
const projectRoot = canonicalPermissionPath(
|
|
1540
|
-
const absolutePath = canonicalPermissionPath(
|
|
1626
|
+
const projectRoot = canonicalPermissionPath(resolve2(cwd));
|
|
1627
|
+
const absolutePath = canonicalPermissionPath(resolve2(cwd, input.path));
|
|
1541
1628
|
const projectPath = relative2(projectRoot, absolutePath);
|
|
1542
|
-
if (projectPath && !
|
|
1629
|
+
if (projectPath && !isAbsolute2(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
|
|
1543
1630
|
return projectPath.split(sep2).join("/");
|
|
1544
1631
|
}
|
|
1545
1632
|
return absolutePath.split(sep2).join("/");
|
|
@@ -1801,7 +1888,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1801
1888
|
const cursor = index === selectedAgent ? theme.fg("accent", "›") : " ";
|
|
1802
1889
|
const name = `@${agent.name}`.padEnd(nameWidth);
|
|
1803
1890
|
const description = `${agent.description ?? "No description"}${agent.hidden ? " [hidden]" : ""}`;
|
|
1804
|
-
lines2.push(`${cursor} ${theme.fg(
|
|
1891
|
+
lines2.push(`${cursor} ${colorizeAgentText(agent.color, name, (c, t) => theme.fg(c, t))} ${theme.fg("dim", agent.source.padEnd(8))} ${theme.fg("text", description)}`);
|
|
1805
1892
|
}
|
|
1806
1893
|
if (agents.length === 0)
|
|
1807
1894
|
lines2.push(" No subagent types are configured.");
|
|
@@ -1950,7 +2037,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1950
2037
|
createTaskTool(parentTask, callerRules, boundCtx) {
|
|
1951
2038
|
const cwd = boundCtx?.cwd ?? process.cwd();
|
|
1952
2039
|
const catalog = this.loadCatalog(cwd, getAgentDir3(), boundCtx ? isProjectTrusted(boundCtx) : false);
|
|
1953
|
-
const agents = availableSubagents(catalog).filter((agent) =>
|
|
2040
|
+
const agents = availableSubagents(catalog).filter((agent) => permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
|
|
1954
2041
|
const descriptions = agents.map((agent) => `${agent.name}: ${agent.description ?? "No description"}`).join(`
|
|
1955
2042
|
`);
|
|
1956
2043
|
return {
|
|
@@ -2772,7 +2859,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
|
|
|
2772
2859
|
if (persist)
|
|
2773
2860
|
this.pi.appendEntry(PRIMARY_AGENT_ENTRY, { name: agent.name });
|
|
2774
2861
|
if (ctx.hasUI) {
|
|
2775
|
-
ctx.ui.setStatus("landstrip-agent", `@${agent.name}`);
|
|
2862
|
+
ctx.ui.setStatus("landstrip-agent", colorizeAgentText(agent.color, `@${agent.name}`));
|
|
2776
2863
|
if (persist)
|
|
2777
2864
|
ctx.ui.notify(`Primary agent: ${agent.name}`, "info");
|
|
2778
2865
|
}
|
|
@@ -2896,18 +2983,18 @@ function loadSandboxConfig(cwd, includeProject) {
|
|
|
2896
2983
|
if (!existsSync5(globalConfigPath)) {
|
|
2897
2984
|
const templatePath = join6(packageDir3, "sandbox.json");
|
|
2898
2985
|
mkdirSync3(dirname3(globalConfigPath), { recursive: true });
|
|
2899
|
-
writeFileSync3(globalConfigPath,
|
|
2986
|
+
writeFileSync3(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
|
|
2900
2987
|
}
|
|
2901
|
-
let globalConfig = JSON.parse(
|
|
2988
|
+
let globalConfig = JSON.parse(readFileSync6(join6(packageDir3, "sandbox.json"), "utf-8"));
|
|
2902
2989
|
try {
|
|
2903
|
-
const override = JSON.parse(
|
|
2990
|
+
const override = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
|
|
2904
2991
|
globalConfig = deepMerge(globalConfig, override);
|
|
2905
2992
|
} catch (error) {
|
|
2906
2993
|
console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
|
|
2907
2994
|
}
|
|
2908
2995
|
if (includeProject && existsSync5(projectConfigPath)) {
|
|
2909
2996
|
try {
|
|
2910
|
-
const projectConfig = JSON.parse(
|
|
2997
|
+
const projectConfig = JSON.parse(readFileSync6(projectConfigPath, "utf-8"));
|
|
2911
2998
|
const merged = deepMerge(globalConfig, projectConfig);
|
|
2912
2999
|
merged.windows.backend = globalConfig.windows.backend;
|
|
2913
3000
|
return merged;
|
|
@@ -2985,7 +3072,7 @@ function readOrEmptyConfig(configPath) {
|
|
|
2985
3072
|
if (!existsSync5(configPath))
|
|
2986
3073
|
return {};
|
|
2987
3074
|
try {
|
|
2988
|
-
return JSON.parse(
|
|
3075
|
+
return JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
2989
3076
|
} catch {
|
|
2990
3077
|
return {};
|
|
2991
3078
|
}
|
|
@@ -3064,7 +3151,7 @@ function shouldPromptForWrite(path, allowWrite, cwd) {
|
|
|
3064
3151
|
return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
|
|
3065
3152
|
}
|
|
3066
3153
|
function expandPath(filePath, cwd) {
|
|
3067
|
-
return
|
|
3154
|
+
return resolve3(cwd, expandHomePath(filePath));
|
|
3068
3155
|
}
|
|
3069
3156
|
function canonicalizePath(filePath, cwd) {
|
|
3070
3157
|
const abs = expandPath(filePath, cwd);
|
|
@@ -3081,7 +3168,7 @@ function canonicalizePath(filePath, cwd) {
|
|
|
3081
3168
|
probe = parent;
|
|
3082
3169
|
}
|
|
3083
3170
|
try {
|
|
3084
|
-
return
|
|
3171
|
+
return resolve3(realpathSync2.native(probe), ...tail);
|
|
3085
3172
|
} catch {
|
|
3086
3173
|
return abs;
|
|
3087
3174
|
}
|
|
@@ -3104,7 +3191,7 @@ function matchesPattern(filePath, patterns, cwd) {
|
|
|
3104
3191
|
}
|
|
3105
3192
|
function normalizeBlockedPath(path, cwd) {
|
|
3106
3193
|
const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
|
|
3107
|
-
return canonicalizePath(
|
|
3194
|
+
return canonicalizePath(isAbsolute3(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
|
|
3108
3195
|
}
|
|
3109
3196
|
function pathUnderDirectory(filePath, dir) {
|
|
3110
3197
|
const normalizedFilePath = normalizePathSeparators(filePath);
|
|
@@ -3326,13 +3413,13 @@ async function stopPreparedChild(child) {
|
|
|
3326
3413
|
child.kill("SIGKILL");
|
|
3327
3414
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
3328
3415
|
return;
|
|
3329
|
-
await new Promise((
|
|
3416
|
+
await new Promise((resolve4) => {
|
|
3330
3417
|
let settleTimer;
|
|
3331
3418
|
const done = () => {
|
|
3332
3419
|
if (settleTimer)
|
|
3333
3420
|
clearTimeout(settleTimer);
|
|
3334
3421
|
child.removeListener("exit", done);
|
|
3335
|
-
|
|
3422
|
+
resolve4();
|
|
3336
3423
|
};
|
|
3337
3424
|
child.once("exit", done);
|
|
3338
3425
|
settleTimer = setTimeout(done, 1000);
|
|
@@ -3545,7 +3632,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3545
3632
|
if (!id)
|
|
3546
3633
|
throw new Error("Worker extension id must not be empty");
|
|
3547
3634
|
const requestedEntry = extension.entry.startsWith("file:") ? fileURLToPath3(extension.entry) : extension.entry;
|
|
3548
|
-
if (!
|
|
3635
|
+
if (!isAbsolute3(requestedEntry)) {
|
|
3549
3636
|
throw new Error("Worker extension entry must be an absolute path or file URL");
|
|
3550
3637
|
}
|
|
3551
3638
|
if (!existsSync5(requestedEntry)) {
|
|
@@ -3599,11 +3686,11 @@ function createLandstripIntegration(options = {}) {
|
|
|
3599
3686
|
function getEffectiveDenyRead(config, cwd) {
|
|
3600
3687
|
if (process.platform !== "win32")
|
|
3601
3688
|
return config.filesystem.denyRead;
|
|
3602
|
-
const volumeRoot = parse(
|
|
3689
|
+
const volumeRoot = parse(resolve3(cwd)).root;
|
|
3603
3690
|
return [...new Set([...config.filesystem.denyRead, volumeRoot])];
|
|
3604
3691
|
}
|
|
3605
3692
|
function hasBaselineWindowsApplicationAccess(filePath) {
|
|
3606
|
-
const normalizedPath = normalizePathSeparators(
|
|
3693
|
+
const normalizedPath = normalizePathSeparators(resolve3(filePath)).toLowerCase();
|
|
3607
3694
|
const systemRoots = [
|
|
3608
3695
|
process.env.ProgramFiles,
|
|
3609
3696
|
process.env["ProgramFiles(x86)"],
|
|
@@ -3613,13 +3700,13 @@ function createLandstripIntegration(options = {}) {
|
|
|
3613
3700
|
return systemRoots.some((root) => {
|
|
3614
3701
|
if (!root)
|
|
3615
3702
|
return false;
|
|
3616
|
-
return pathUnderDirectory(normalizedPath, normalizePathSeparators(
|
|
3703
|
+
return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve3(root)).toLowerCase());
|
|
3617
3704
|
});
|
|
3618
3705
|
}
|
|
3619
3706
|
function withWindowsProcessReadAccess(allowances, command, cwd, additionalPaths = []) {
|
|
3620
3707
|
if (process.platform !== "win32")
|
|
3621
3708
|
return allowances;
|
|
3622
|
-
const executable =
|
|
3709
|
+
const executable = resolve3(cwd, command);
|
|
3623
3710
|
const executablePaths = hasBaselineWindowsApplicationAccess(executable) ? [] : [executable];
|
|
3624
3711
|
return {
|
|
3625
3712
|
...allowances,
|
|
@@ -3833,7 +3920,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3833
3920
|
}
|
|
3834
3921
|
const server = createServer(handleClient);
|
|
3835
3922
|
let stopped = false;
|
|
3836
|
-
return new Promise((
|
|
3923
|
+
return new Promise((resolve4, reject) => {
|
|
3837
3924
|
const listen = (port) => {
|
|
3838
3925
|
server.once("error", (error) => {
|
|
3839
3926
|
if (error.code === "EADDRINUSE" && portRange && port < portRange.high) {
|
|
@@ -3845,7 +3932,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3845
3932
|
server.listen(port, "127.0.0.1", () => {
|
|
3846
3933
|
server.removeAllListeners("error");
|
|
3847
3934
|
const address = server.address();
|
|
3848
|
-
|
|
3935
|
+
resolve4({
|
|
3849
3936
|
port: address.port,
|
|
3850
3937
|
stop: () => new Promise((done) => {
|
|
3851
3938
|
if (stopped) {
|
|
@@ -3864,7 +3951,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3864
3951
|
});
|
|
3865
3952
|
}
|
|
3866
3953
|
function createSocketPair() {
|
|
3867
|
-
return new Promise((
|
|
3954
|
+
return new Promise((resolve4, reject) => {
|
|
3868
3955
|
const server = createServer();
|
|
3869
3956
|
server.on("error", reject);
|
|
3870
3957
|
let client = null;
|
|
@@ -3873,7 +3960,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3873
3960
|
server.close();
|
|
3874
3961
|
if (client) {
|
|
3875
3962
|
client.removeListener("error", reject);
|
|
3876
|
-
|
|
3963
|
+
resolve4([client, serverEnd]);
|
|
3877
3964
|
}
|
|
3878
3965
|
});
|
|
3879
3966
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -4147,7 +4234,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
4147
4234
|
};
|
|
4148
4235
|
let landstripArgs;
|
|
4149
4236
|
try {
|
|
4150
|
-
envFile = writeEnvFile({ ...process.env, ...env, PWD:
|
|
4237
|
+
envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve3(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
|
|
4151
4238
|
processAllowances = withWindowsProcessReadAccess(allowances, shell, cwd, [envFile.path]);
|
|
4152
4239
|
policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
|
|
4153
4240
|
const wrappedCommand = `source '${envFile.path}' && ${command}`;
|
|
@@ -4771,7 +4858,7 @@ ${message}
|
|
|
4771
4858
|
const selected = index === selectedAgent;
|
|
4772
4859
|
const cursor = selected ? accent("›") : " ";
|
|
4773
4860
|
const active = agent.name === activeAgent ? theme.fg("success", "●") : dim("○");
|
|
4774
|
-
const name =
|
|
4861
|
+
const name = colorizeAgentText(agent.color, agent.name, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
|
|
4775
4862
|
const model = dim(` ${agent.model ?? "current model"}`);
|
|
4776
4863
|
const description = agent.description ? dim(` ${agent.description}`) : "";
|
|
4777
4864
|
lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
|
|
@@ -4787,7 +4874,7 @@ ${message}
|
|
|
4787
4874
|
const index = start + offset;
|
|
4788
4875
|
const selected = index === selectedSubagent;
|
|
4789
4876
|
const cursor = selected ? accent("›") : " ";
|
|
4790
|
-
const name =
|
|
4877
|
+
const name = colorizeAgentText(agent2.color, `@${agent2.name}`, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
|
|
4791
4878
|
const flags = [agent2.model ?? "current model", agent2.mode];
|
|
4792
4879
|
if (agent2.hidden)
|
|
4793
4880
|
flags.push("hidden");
|
package/dist/subagents.json
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
"build": {
|
|
5
5
|
"description": "Default primary agent with full development access",
|
|
6
6
|
"mode": "primary",
|
|
7
|
+
"color": "#034cff",
|
|
7
8
|
"prompt": "Work in build mode. Implement, test, and complete the requested changes."
|
|
8
9
|
},
|
|
9
10
|
"plan": {
|
|
10
11
|
"description": "Planning agent that asks before commands or file changes",
|
|
11
12
|
"mode": "primary",
|
|
13
|
+
"color": "#a753ae",
|
|
12
14
|
"prompt": "Work in plan mode. Analyze the problem and produce a clear plan before making changes.",
|
|
13
15
|
"permission": { "edit": "ask", "bash": "ask", "task": "ask" }
|
|
14
16
|
},
|
|
@@ -36,9 +38,9 @@
|
|
|
36
38
|
}
|
|
37
39
|
},
|
|
38
40
|
"explore": {
|
|
39
|
-
"description": "Fast
|
|
41
|
+
"description": "Fast agent specialized for exploring codebases",
|
|
40
42
|
"mode": "subagent",
|
|
41
|
-
"prompt": "Explore the codebase without modifying it. Report findings with file references.",
|
|
43
|
+
"prompt": "Explore the codebase without modifying it. Report findings with file references. Use bash and network fetch tools when needed for git, PR metadata, or remote docs; do not edit files.",
|
|
42
44
|
"permission": {
|
|
43
45
|
"*": "deny",
|
|
44
46
|
"read": {
|
|
@@ -49,7 +51,10 @@
|
|
|
49
51
|
},
|
|
50
52
|
"glob": "allow",
|
|
51
53
|
"grep": "allow",
|
|
52
|
-
"list": "allow"
|
|
54
|
+
"list": "allow",
|
|
55
|
+
"bash": "allow",
|
|
56
|
+
"webfetch": "allow",
|
|
57
|
+
"websearch": "allow"
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.41",
|
|
4
4
|
"description": "Sandbox-aware Pi subagents and interactive command permissions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"ci:test": "npm run test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@landstrip/landstrip": "^0.17.
|
|
43
|
+
"@landstrip/landstrip": "^0.17.41",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"yaml": "^2.9.0"
|
|
46
46
|
},
|
package/subagents.json
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
"build": {
|
|
5
5
|
"description": "Default primary agent with full development access",
|
|
6
6
|
"mode": "primary",
|
|
7
|
+
"color": "#034cff",
|
|
7
8
|
"prompt": "Work in build mode. Implement, test, and complete the requested changes."
|
|
8
9
|
},
|
|
9
10
|
"plan": {
|
|
10
11
|
"description": "Planning agent that asks before commands or file changes",
|
|
11
12
|
"mode": "primary",
|
|
13
|
+
"color": "#a753ae",
|
|
12
14
|
"prompt": "Work in plan mode. Analyze the problem and produce a clear plan before making changes.",
|
|
13
15
|
"permission": { "edit": "ask", "bash": "ask", "task": "ask" }
|
|
14
16
|
},
|
|
@@ -36,9 +38,9 @@
|
|
|
36
38
|
}
|
|
37
39
|
},
|
|
38
40
|
"explore": {
|
|
39
|
-
"description": "Fast
|
|
41
|
+
"description": "Fast agent specialized for exploring codebases",
|
|
40
42
|
"mode": "subagent",
|
|
41
|
-
"prompt": "Explore the codebase without modifying it. Report findings with file references.",
|
|
43
|
+
"prompt": "Explore the codebase without modifying it. Report findings with file references. Use bash and network fetch tools when needed for git, PR metadata, or remote docs; do not edit files.",
|
|
42
44
|
"permission": {
|
|
43
45
|
"*": "deny",
|
|
44
46
|
"read": {
|
|
@@ -49,7 +51,10 @@
|
|
|
49
51
|
},
|
|
50
52
|
"glob": "allow",
|
|
51
53
|
"grep": "allow",
|
|
52
|
-
"list": "allow"
|
|
54
|
+
"list": "allow",
|
|
55
|
+
"bash": "allow",
|
|
56
|
+
"webfetch": "allow",
|
|
57
|
+
"websearch": "allow"
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
},
|