pi-landstrip 0.17.40 → 0.17.42
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 +26 -18
- package/dist/index.ts +155 -58
- package/dist/subagents.json +8 -3
- package/package.json +2 -2
- package/subagents.json +8 -3
package/README.md
CHANGED
|
@@ -111,8 +111,11 @@ 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 TUI agent palette
|
|
115
|
+
(`secondary` for build, `accent` for plan) and appear in the status line and
|
|
116
|
+
agent selectors.
|
|
117
|
+
The selection controls the root system prompt and permissions and is restored
|
|
118
|
+
with the session.
|
|
116
119
|
|
|
117
120
|
## Subagents
|
|
118
121
|
|
|
@@ -216,14 +219,15 @@ Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi'
|
|
|
216
219
|
}
|
|
217
220
|
```
|
|
218
221
|
|
|
219
|
-
|
|
222
|
+
OpenCode Markdown agent import flags live in `subagents.json` as top-level
|
|
223
|
+
`opencode` (both default to `true`):
|
|
220
224
|
|
|
221
225
|
```json
|
|
222
226
|
{
|
|
223
227
|
"maxSubagents": 0,
|
|
224
228
|
"opencode": {
|
|
225
|
-
"showGlobalAgents":
|
|
226
|
-
"showLocalAgents":
|
|
229
|
+
"showGlobalAgents": true,
|
|
230
|
+
"showLocalAgents": true
|
|
227
231
|
},
|
|
228
232
|
"subagents": {
|
|
229
233
|
"agent": {}
|
|
@@ -231,18 +235,17 @@ Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
|
|
|
231
235
|
}
|
|
232
236
|
```
|
|
233
237
|
|
|
234
|
-
Both flags default to `false`. When enabled:
|
|
235
|
-
|
|
236
238
|
- `showGlobalAgents` imports Markdown agents from the OpenCode global config
|
|
237
239
|
directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
|
|
238
240
|
`~/.config/opencode`) under `agent/` and `agents/`.
|
|
239
241
|
- `showLocalAgents` imports Markdown agents from the trusted project's
|
|
240
242
|
`.opencode/agent/` and `.opencode/agents/` directories.
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
from `subagents.json` (built-in,
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
Set a flag to `false` to disable that source. OpenCode local agents override
|
|
245
|
+
OpenCode global agents. Pi agent definitions from `subagents.json` (built-in,
|
|
246
|
+
global, and local) take precedence over OpenCode imports with the same name;
|
|
247
|
+
conflicts are silent. Local OpenCode agents are skipped when the project is
|
|
248
|
+
untrusted, regardless of the flag.
|
|
246
249
|
|
|
247
250
|
`subagents.json` accepts top-level `maxSubagents`, `opencode`, and `subagents`;
|
|
248
251
|
sandbox fields belong in `sandbox.json`.
|
|
@@ -320,13 +323,18 @@ The packaged default is 0, which removes the `task` tool while retaining primary
|
|
|
320
323
|
roles. There is no separate subagent enable switch. The Settings tab in
|
|
321
324
|
`/agents` edits this limit for global and trusted-project configuration.
|
|
322
325
|
|
|
323
|
-
Agent modes, hidden/disabled agents, prompts, and ordered
|
|
324
|
-
permissions apply to primary agents and subagents.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
Agent modes, hidden/disabled agents, prompts, colors, and ordered
|
|
327
|
+
`allow`/`ask`/`deny` permissions apply to primary agents and subagents. Prompt
|
|
328
|
+
strings may include OpenCode-style `{file:path}` tokens; relative paths resolve
|
|
329
|
+
against the `subagents.json` file that defines them. `color` accepts `#RRGGBB`
|
|
330
|
+
or OpenCode theme names (`primary`, `secondary`, `accent`, `success`, `warning`,
|
|
331
|
+
`error`, `info`). `hidden` only hides an agent from user-facing primary pickers;
|
|
332
|
+
the model can still invoke a hidden subagent via `task`. Subagent workers also
|
|
333
|
+
honor model selection, supported Pi thinking-level variants, and step limits.
|
|
334
|
+
Primary agent activation currently changes the prompt and permissions only.
|
|
335
|
+
Later matching permission rules win. Put provider-specific values under
|
|
336
|
+
`options`; unknown agent fields are rejected. Agent permissions cannot weaken an
|
|
337
|
+
enabled OS sandbox, grant filesystem access, or grant network access.
|
|
330
338
|
|
|
331
339
|
## Configuration migration
|
|
332
340
|
|
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,95 @@ 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: "customMessageLabel",
|
|
164
|
+
accent: "accent",
|
|
165
|
+
success: "success",
|
|
166
|
+
warning: "warning",
|
|
167
|
+
error: "error",
|
|
168
|
+
info: "accent"
|
|
169
|
+
};
|
|
170
|
+
var NAMED_COLOR_ANSI = {
|
|
171
|
+
primary: "\x1B[36m",
|
|
172
|
+
secondary: "\x1B[35m",
|
|
173
|
+
accent: "\x1B[36m",
|
|
174
|
+
success: "\x1B[32m",
|
|
175
|
+
warning: "\x1B[33m",
|
|
176
|
+
error: "\x1B[31m",
|
|
177
|
+
info: "\x1B[36m"
|
|
178
|
+
};
|
|
179
|
+
function isAgentColor(value) {
|
|
180
|
+
if (/^#[0-9a-fA-F]{6}$/.test(value))
|
|
181
|
+
return true;
|
|
182
|
+
return OPENCODE_THEME_COLORS.has(value);
|
|
183
|
+
}
|
|
184
|
+
function hexFg(hex, text) {
|
|
185
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
186
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
187
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
188
|
+
return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
|
|
189
|
+
}
|
|
190
|
+
function colorizeAgentText(color, text, themeFg, fallback = "accent") {
|
|
191
|
+
if (!color)
|
|
192
|
+
return themeFg ? themeFg(fallback, text) : text;
|
|
193
|
+
if (color.startsWith("#"))
|
|
194
|
+
return hexFg(color, text);
|
|
195
|
+
if (themeFg)
|
|
196
|
+
return themeFg(THEME_COLOR_ALIASES[color] ?? fallback, text);
|
|
197
|
+
const ansi = NAMED_COLOR_ANSI[color];
|
|
198
|
+
return ansi ? `${ansi}${text}\x1B[39m` : text;
|
|
199
|
+
}
|
|
124
200
|
|
|
125
201
|
class AsyncQueue {
|
|
126
202
|
tail = Promise.resolve();
|
|
127
203
|
async acquire(signal, cancellationMessage = "Request cancelled") {
|
|
128
204
|
let release;
|
|
129
205
|
const previous = this.tail;
|
|
130
|
-
this.tail = new Promise((
|
|
131
|
-
release =
|
|
206
|
+
this.tail = new Promise((resolve2) => {
|
|
207
|
+
release = resolve2;
|
|
132
208
|
});
|
|
133
209
|
if (!signal) {
|
|
134
210
|
await previous;
|
|
@@ -161,8 +237,8 @@ class AsyncQueue {
|
|
|
161
237
|
var MAX_SUBAGENTS = 16;
|
|
162
238
|
var packageDir = dirname(fileURLToPath(import.meta.url));
|
|
163
239
|
var DEFAULT_OPENCODE = {
|
|
164
|
-
showGlobalAgents:
|
|
165
|
-
showLocalAgents:
|
|
240
|
+
showGlobalAgents: true,
|
|
241
|
+
showLocalAgents: true
|
|
166
242
|
};
|
|
167
243
|
var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
|
|
168
244
|
function readBooleanField(section, key, path) {
|
|
@@ -192,12 +268,28 @@ function normalizeOpenCode(value, path) {
|
|
|
192
268
|
result.showLocalAgents = showLocalAgents;
|
|
193
269
|
return result;
|
|
194
270
|
}
|
|
271
|
+
function expandAgentPromptReferences(config, path) {
|
|
272
|
+
if (!isRecord2(config.subagents) || !isRecord2(config.subagents.agent))
|
|
273
|
+
return;
|
|
274
|
+
const baseDir = dirname(path);
|
|
275
|
+
for (const [name, value] of Object.entries(config.subagents.agent)) {
|
|
276
|
+
if (!isRecord2(value) || typeof value.prompt !== "string")
|
|
277
|
+
continue;
|
|
278
|
+
if (!value.prompt.includes("{file:"))
|
|
279
|
+
continue;
|
|
280
|
+
try {
|
|
281
|
+
value.prompt = expandFileReferences(value.prompt, baseDir);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw new Error(`${path}: agent ${name}: ${formatError(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
195
287
|
function readConfig(path, isGlobal = false) {
|
|
196
288
|
if (!existsSync(path))
|
|
197
289
|
return {};
|
|
198
290
|
let value;
|
|
199
291
|
try {
|
|
200
|
-
value = JSON.parse(
|
|
292
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
201
293
|
} catch (error) {
|
|
202
294
|
const message = formatError(error);
|
|
203
295
|
throw new Error(`${path}: ${message}`);
|
|
@@ -233,6 +325,7 @@ function readConfig(path, isGlobal = false) {
|
|
|
233
325
|
if ("opencode" in value) {
|
|
234
326
|
config.opencode = normalizeOpenCode(value.opencode, `${path}: opencode`);
|
|
235
327
|
}
|
|
328
|
+
expandAgentPromptReferences(config, path);
|
|
236
329
|
return config;
|
|
237
330
|
}
|
|
238
331
|
function mergeValue(base, override) {
|
|
@@ -306,7 +399,7 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
|
|
|
306
399
|
}
|
|
307
400
|
|
|
308
401
|
// opencode-agents.ts
|
|
309
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
402
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
310
403
|
import { homedir as homedir2 } from "node:os";
|
|
311
404
|
import { basename, extname, join as join3, relative, sep } from "node:path";
|
|
312
405
|
import { parse as parseYaml } from "yaml";
|
|
@@ -449,7 +542,7 @@ function loadAgentsFromDir(dir, source, agents, diagnostics) {
|
|
|
449
542
|
const root = join3(dir, subdir);
|
|
450
543
|
for (const filePath of listMarkdownFiles(root)) {
|
|
451
544
|
try {
|
|
452
|
-
const content =
|
|
545
|
+
const content = readFileSync3(filePath, "utf8");
|
|
453
546
|
const { data, body } = parseFrontmatter(content);
|
|
454
547
|
const name = agentNameFromPath(dir, filePath);
|
|
455
548
|
if (!name) {
|
|
@@ -528,7 +621,7 @@ function legacyConfigWarnings(piAgentDir) {
|
|
|
528
621
|
if (!existsSync3(path))
|
|
529
622
|
return [];
|
|
530
623
|
try {
|
|
531
|
-
const value = JSON.parse(
|
|
624
|
+
const value = JSON.parse(readFileSync4(path, "utf8"));
|
|
532
625
|
if (!isRecord2(value))
|
|
533
626
|
return [];
|
|
534
627
|
const fields = ["agent", "permission", "subagents", "maxSubagents", "landstrip"].filter((field) => value[field] !== undefined);
|
|
@@ -542,11 +635,14 @@ function legacyConfigWarnings(piAgentDir) {
|
|
|
542
635
|
}
|
|
543
636
|
}
|
|
544
637
|
function normalizeAgent(name, raw, source) {
|
|
545
|
-
for (const field of ["description", "prompt", "model", "variant"]) {
|
|
638
|
+
for (const field of ["description", "prompt", "model", "variant", "color"]) {
|
|
546
639
|
if (raw[field] !== undefined && typeof raw[field] !== "string") {
|
|
547
640
|
throw new Error(`agent ${name} ${field} must be a string`);
|
|
548
641
|
}
|
|
549
642
|
}
|
|
643
|
+
if (typeof raw.color === "string" && !isAgentColor(raw.color)) {
|
|
644
|
+
throw new Error(`agent ${name} color must be #RRGGBB or one of primary, secondary, accent, success, warning, error, info`);
|
|
645
|
+
}
|
|
550
646
|
for (const field of ["hidden", "disable"]) {
|
|
551
647
|
if (raw[field] !== undefined && typeof raw[field] !== "boolean") {
|
|
552
648
|
throw new Error(`agent ${name} ${field} must be a boolean`);
|
|
@@ -586,6 +682,7 @@ function normalizeAgent(name, raw, source) {
|
|
|
586
682
|
model: typeof raw.model === "string" ? raw.model : undefined,
|
|
587
683
|
variant: typeof raw.variant === "string" ? raw.variant : undefined,
|
|
588
684
|
hidden: raw.hidden === true,
|
|
685
|
+
color: typeof raw.color === "string" ? raw.color : undefined,
|
|
589
686
|
steps: typeof raw.steps === "number" ? raw.steps : undefined,
|
|
590
687
|
permissions: normalizePermissions(raw.permission),
|
|
591
688
|
providerOptions
|
|
@@ -721,7 +818,7 @@ function defaultSpawn(command, args, options) {
|
|
|
721
818
|
return spawn(command, [...args], options);
|
|
722
819
|
}
|
|
723
820
|
function delay(milliseconds) {
|
|
724
|
-
return new Promise((
|
|
821
|
+
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
725
822
|
}
|
|
726
823
|
|
|
727
824
|
class RpcProcess {
|
|
@@ -774,19 +871,19 @@ class RpcProcess {
|
|
|
774
871
|
});
|
|
775
872
|
child.stdin.on("error", (error) => this.handleChildError(child, error, "RPC stdin error"));
|
|
776
873
|
child.on("error", (error) => this.handleChildError(child, error, "RPC process error"));
|
|
777
|
-
this.exitPromise = new Promise((
|
|
874
|
+
this.exitPromise = new Promise((resolve2) => {
|
|
778
875
|
child.once("exit", (code, signal) => {
|
|
779
876
|
if (this.child === child) {
|
|
780
877
|
this.child = null;
|
|
781
878
|
this.failAll(new Error(this.exitMessage(code, signal)));
|
|
782
879
|
}
|
|
783
|
-
|
|
880
|
+
resolve2();
|
|
784
881
|
});
|
|
785
882
|
});
|
|
786
|
-
await new Promise((
|
|
883
|
+
await new Promise((resolve2, reject) => {
|
|
787
884
|
const onSpawn = () => {
|
|
788
885
|
cleanup();
|
|
789
|
-
|
|
886
|
+
resolve2();
|
|
790
887
|
};
|
|
791
888
|
const onError = (error) => {
|
|
792
889
|
cleanup();
|
|
@@ -830,12 +927,12 @@ class RpcProcess {
|
|
|
830
927
|
if (this.fatalError)
|
|
831
928
|
throw this.fatalError;
|
|
832
929
|
const id = `rpc-${++this.nextRequestId}`;
|
|
833
|
-
const response = await new Promise((
|
|
930
|
+
const response = await new Promise((resolve2, reject) => {
|
|
834
931
|
const timer = setTimeout(() => {
|
|
835
932
|
this.pending.delete(id);
|
|
836
933
|
reject(this.withStderr(`Timed out waiting for ${type} response`));
|
|
837
934
|
}, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
838
|
-
this.pending.set(id, { command: type, resolve, reject, timer });
|
|
935
|
+
this.pending.set(id, { command: type, resolve: resolve2, reject, timer });
|
|
839
936
|
this.write({ ...fields, id, type }, (error) => {
|
|
840
937
|
if (!error)
|
|
841
938
|
return;
|
|
@@ -1035,12 +1132,12 @@ class RpcProcess {
|
|
|
1035
1132
|
}
|
|
1036
1133
|
waitForAgentSettled() {
|
|
1037
1134
|
let waiter;
|
|
1038
|
-
const promise = new Promise((
|
|
1135
|
+
const promise = new Promise((resolve2, reject) => {
|
|
1039
1136
|
const timer = setTimeout(() => {
|
|
1040
1137
|
this.settledWaiters.delete(waiter);
|
|
1041
1138
|
reject(this.withStderr("Timed out waiting for agent_settled"));
|
|
1042
1139
|
}, this.options.settleTimeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS);
|
|
1043
|
-
waiter = { resolve, reject, timer };
|
|
1140
|
+
waiter = { resolve: resolve2, reject, timer };
|
|
1044
1141
|
this.settledWaiters.add(waiter);
|
|
1045
1142
|
});
|
|
1046
1143
|
return {
|
|
@@ -1186,7 +1283,7 @@ function resolvePiPackage() {
|
|
|
1186
1283
|
}
|
|
1187
1284
|
function readPiPackage(pkgPath) {
|
|
1188
1285
|
try {
|
|
1189
|
-
const value = JSON.parse(
|
|
1286
|
+
const value = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1190
1287
|
if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
|
|
1191
1288
|
return;
|
|
1192
1289
|
}
|
|
@@ -1350,7 +1447,7 @@ class Semaphore {
|
|
|
1350
1447
|
this.active += 1;
|
|
1351
1448
|
return () => this.release();
|
|
1352
1449
|
}
|
|
1353
|
-
await new Promise((
|
|
1450
|
+
await new Promise((resolve3, reject) => {
|
|
1354
1451
|
const abort = () => {
|
|
1355
1452
|
const index = this.waiters.indexOf(start);
|
|
1356
1453
|
if (index >= 0)
|
|
@@ -1360,7 +1457,7 @@ class Semaphore {
|
|
|
1360
1457
|
const start = () => {
|
|
1361
1458
|
signal?.removeEventListener("abort", abort);
|
|
1362
1459
|
this.active += 1;
|
|
1363
|
-
|
|
1460
|
+
resolve3();
|
|
1364
1461
|
};
|
|
1365
1462
|
this.waiters.push(start);
|
|
1366
1463
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -1508,7 +1605,7 @@ function canonicalPermissionPath(path, seen = new Set) {
|
|
|
1508
1605
|
if (seen.has(existing))
|
|
1509
1606
|
return path;
|
|
1510
1607
|
seen.add(existing);
|
|
1511
|
-
const target =
|
|
1608
|
+
const target = resolve2(dirname2(existing), readlinkSync(existing), ...missing);
|
|
1512
1609
|
return canonicalPermissionPath(target, seen);
|
|
1513
1610
|
}
|
|
1514
1611
|
break;
|
|
@@ -1521,7 +1618,7 @@ function canonicalPermissionPath(path, seen = new Set) {
|
|
|
1521
1618
|
}
|
|
1522
1619
|
}
|
|
1523
1620
|
try {
|
|
1524
|
-
return
|
|
1621
|
+
return resolve2(realpathSync(existing), ...missing);
|
|
1525
1622
|
} catch {
|
|
1526
1623
|
return path;
|
|
1527
1624
|
}
|
|
@@ -1536,10 +1633,10 @@ function permissionResource(tool, input, cwd) {
|
|
|
1536
1633
|
}
|
|
1537
1634
|
if (typeof input.path !== "string")
|
|
1538
1635
|
return "*";
|
|
1539
|
-
const projectRoot = canonicalPermissionPath(
|
|
1540
|
-
const absolutePath = canonicalPermissionPath(
|
|
1636
|
+
const projectRoot = canonicalPermissionPath(resolve2(cwd));
|
|
1637
|
+
const absolutePath = canonicalPermissionPath(resolve2(cwd, input.path));
|
|
1541
1638
|
const projectPath = relative2(projectRoot, absolutePath);
|
|
1542
|
-
if (projectPath && !
|
|
1639
|
+
if (projectPath && !isAbsolute2(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
|
|
1543
1640
|
return projectPath.split(sep2).join("/");
|
|
1544
1641
|
}
|
|
1545
1642
|
return absolutePath.split(sep2).join("/");
|
|
@@ -1801,7 +1898,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1801
1898
|
const cursor = index === selectedAgent ? theme.fg("accent", "›") : " ";
|
|
1802
1899
|
const name = `@${agent.name}`.padEnd(nameWidth);
|
|
1803
1900
|
const description = `${agent.description ?? "No description"}${agent.hidden ? " [hidden]" : ""}`;
|
|
1804
|
-
lines2.push(`${cursor} ${theme.fg(
|
|
1901
|
+
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
1902
|
}
|
|
1806
1903
|
if (agents.length === 0)
|
|
1807
1904
|
lines2.push(" No subagent types are configured.");
|
|
@@ -1950,7 +2047,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
1950
2047
|
createTaskTool(parentTask, callerRules, boundCtx) {
|
|
1951
2048
|
const cwd = boundCtx?.cwd ?? process.cwd();
|
|
1952
2049
|
const catalog = this.loadCatalog(cwd, getAgentDir3(), boundCtx ? isProjectTrusted(boundCtx) : false);
|
|
1953
|
-
const agents = availableSubagents(catalog).filter((agent) =>
|
|
2050
|
+
const agents = availableSubagents(catalog).filter((agent) => permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
|
|
1954
2051
|
const descriptions = agents.map((agent) => `${agent.name}: ${agent.description ?? "No description"}`).join(`
|
|
1955
2052
|
`);
|
|
1956
2053
|
return {
|
|
@@ -2772,7 +2869,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
|
|
|
2772
2869
|
if (persist)
|
|
2773
2870
|
this.pi.appendEntry(PRIMARY_AGENT_ENTRY, { name: agent.name });
|
|
2774
2871
|
if (ctx.hasUI) {
|
|
2775
|
-
ctx.ui.setStatus("landstrip-agent", `@${agent.name}`);
|
|
2872
|
+
ctx.ui.setStatus("landstrip-agent", colorizeAgentText(agent.color, `@${agent.name}`));
|
|
2776
2873
|
if (persist)
|
|
2777
2874
|
ctx.ui.notify(`Primary agent: ${agent.name}`, "info");
|
|
2778
2875
|
}
|
|
@@ -2896,18 +2993,18 @@ function loadSandboxConfig(cwd, includeProject) {
|
|
|
2896
2993
|
if (!existsSync5(globalConfigPath)) {
|
|
2897
2994
|
const templatePath = join6(packageDir3, "sandbox.json");
|
|
2898
2995
|
mkdirSync3(dirname3(globalConfigPath), { recursive: true });
|
|
2899
|
-
writeFileSync3(globalConfigPath,
|
|
2996
|
+
writeFileSync3(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
|
|
2900
2997
|
}
|
|
2901
|
-
let globalConfig = JSON.parse(
|
|
2998
|
+
let globalConfig = JSON.parse(readFileSync6(join6(packageDir3, "sandbox.json"), "utf-8"));
|
|
2902
2999
|
try {
|
|
2903
|
-
const override = JSON.parse(
|
|
3000
|
+
const override = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
|
|
2904
3001
|
globalConfig = deepMerge(globalConfig, override);
|
|
2905
3002
|
} catch (error) {
|
|
2906
3003
|
console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
|
|
2907
3004
|
}
|
|
2908
3005
|
if (includeProject && existsSync5(projectConfigPath)) {
|
|
2909
3006
|
try {
|
|
2910
|
-
const projectConfig = JSON.parse(
|
|
3007
|
+
const projectConfig = JSON.parse(readFileSync6(projectConfigPath, "utf-8"));
|
|
2911
3008
|
const merged = deepMerge(globalConfig, projectConfig);
|
|
2912
3009
|
merged.windows.backend = globalConfig.windows.backend;
|
|
2913
3010
|
return merged;
|
|
@@ -2985,7 +3082,7 @@ function readOrEmptyConfig(configPath) {
|
|
|
2985
3082
|
if (!existsSync5(configPath))
|
|
2986
3083
|
return {};
|
|
2987
3084
|
try {
|
|
2988
|
-
return JSON.parse(
|
|
3085
|
+
return JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
2989
3086
|
} catch {
|
|
2990
3087
|
return {};
|
|
2991
3088
|
}
|
|
@@ -3064,7 +3161,7 @@ function shouldPromptForWrite(path, allowWrite, cwd) {
|
|
|
3064
3161
|
return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
|
|
3065
3162
|
}
|
|
3066
3163
|
function expandPath(filePath, cwd) {
|
|
3067
|
-
return
|
|
3164
|
+
return resolve3(cwd, expandHomePath(filePath));
|
|
3068
3165
|
}
|
|
3069
3166
|
function canonicalizePath(filePath, cwd) {
|
|
3070
3167
|
const abs = expandPath(filePath, cwd);
|
|
@@ -3081,7 +3178,7 @@ function canonicalizePath(filePath, cwd) {
|
|
|
3081
3178
|
probe = parent;
|
|
3082
3179
|
}
|
|
3083
3180
|
try {
|
|
3084
|
-
return
|
|
3181
|
+
return resolve3(realpathSync2.native(probe), ...tail);
|
|
3085
3182
|
} catch {
|
|
3086
3183
|
return abs;
|
|
3087
3184
|
}
|
|
@@ -3104,7 +3201,7 @@ function matchesPattern(filePath, patterns, cwd) {
|
|
|
3104
3201
|
}
|
|
3105
3202
|
function normalizeBlockedPath(path, cwd) {
|
|
3106
3203
|
const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
|
|
3107
|
-
return canonicalizePath(
|
|
3204
|
+
return canonicalizePath(isAbsolute3(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
|
|
3108
3205
|
}
|
|
3109
3206
|
function pathUnderDirectory(filePath, dir) {
|
|
3110
3207
|
const normalizedFilePath = normalizePathSeparators(filePath);
|
|
@@ -3326,13 +3423,13 @@ async function stopPreparedChild(child) {
|
|
|
3326
3423
|
child.kill("SIGKILL");
|
|
3327
3424
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
3328
3425
|
return;
|
|
3329
|
-
await new Promise((
|
|
3426
|
+
await new Promise((resolve4) => {
|
|
3330
3427
|
let settleTimer;
|
|
3331
3428
|
const done = () => {
|
|
3332
3429
|
if (settleTimer)
|
|
3333
3430
|
clearTimeout(settleTimer);
|
|
3334
3431
|
child.removeListener("exit", done);
|
|
3335
|
-
|
|
3432
|
+
resolve4();
|
|
3336
3433
|
};
|
|
3337
3434
|
child.once("exit", done);
|
|
3338
3435
|
settleTimer = setTimeout(done, 1000);
|
|
@@ -3545,7 +3642,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3545
3642
|
if (!id)
|
|
3546
3643
|
throw new Error("Worker extension id must not be empty");
|
|
3547
3644
|
const requestedEntry = extension.entry.startsWith("file:") ? fileURLToPath3(extension.entry) : extension.entry;
|
|
3548
|
-
if (!
|
|
3645
|
+
if (!isAbsolute3(requestedEntry)) {
|
|
3549
3646
|
throw new Error("Worker extension entry must be an absolute path or file URL");
|
|
3550
3647
|
}
|
|
3551
3648
|
if (!existsSync5(requestedEntry)) {
|
|
@@ -3599,11 +3696,11 @@ function createLandstripIntegration(options = {}) {
|
|
|
3599
3696
|
function getEffectiveDenyRead(config, cwd) {
|
|
3600
3697
|
if (process.platform !== "win32")
|
|
3601
3698
|
return config.filesystem.denyRead;
|
|
3602
|
-
const volumeRoot = parse(
|
|
3699
|
+
const volumeRoot = parse(resolve3(cwd)).root;
|
|
3603
3700
|
return [...new Set([...config.filesystem.denyRead, volumeRoot])];
|
|
3604
3701
|
}
|
|
3605
3702
|
function hasBaselineWindowsApplicationAccess(filePath) {
|
|
3606
|
-
const normalizedPath = normalizePathSeparators(
|
|
3703
|
+
const normalizedPath = normalizePathSeparators(resolve3(filePath)).toLowerCase();
|
|
3607
3704
|
const systemRoots = [
|
|
3608
3705
|
process.env.ProgramFiles,
|
|
3609
3706
|
process.env["ProgramFiles(x86)"],
|
|
@@ -3613,13 +3710,13 @@ function createLandstripIntegration(options = {}) {
|
|
|
3613
3710
|
return systemRoots.some((root) => {
|
|
3614
3711
|
if (!root)
|
|
3615
3712
|
return false;
|
|
3616
|
-
return pathUnderDirectory(normalizedPath, normalizePathSeparators(
|
|
3713
|
+
return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve3(root)).toLowerCase());
|
|
3617
3714
|
});
|
|
3618
3715
|
}
|
|
3619
3716
|
function withWindowsProcessReadAccess(allowances, command, cwd, additionalPaths = []) {
|
|
3620
3717
|
if (process.platform !== "win32")
|
|
3621
3718
|
return allowances;
|
|
3622
|
-
const executable =
|
|
3719
|
+
const executable = resolve3(cwd, command);
|
|
3623
3720
|
const executablePaths = hasBaselineWindowsApplicationAccess(executable) ? [] : [executable];
|
|
3624
3721
|
return {
|
|
3625
3722
|
...allowances,
|
|
@@ -3833,7 +3930,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3833
3930
|
}
|
|
3834
3931
|
const server = createServer(handleClient);
|
|
3835
3932
|
let stopped = false;
|
|
3836
|
-
return new Promise((
|
|
3933
|
+
return new Promise((resolve4, reject) => {
|
|
3837
3934
|
const listen = (port) => {
|
|
3838
3935
|
server.once("error", (error) => {
|
|
3839
3936
|
if (error.code === "EADDRINUSE" && portRange && port < portRange.high) {
|
|
@@ -3845,7 +3942,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3845
3942
|
server.listen(port, "127.0.0.1", () => {
|
|
3846
3943
|
server.removeAllListeners("error");
|
|
3847
3944
|
const address = server.address();
|
|
3848
|
-
|
|
3945
|
+
resolve4({
|
|
3849
3946
|
port: address.port,
|
|
3850
3947
|
stop: () => new Promise((done) => {
|
|
3851
3948
|
if (stopped) {
|
|
@@ -3864,7 +3961,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3864
3961
|
});
|
|
3865
3962
|
}
|
|
3866
3963
|
function createSocketPair() {
|
|
3867
|
-
return new Promise((
|
|
3964
|
+
return new Promise((resolve4, reject) => {
|
|
3868
3965
|
const server = createServer();
|
|
3869
3966
|
server.on("error", reject);
|
|
3870
3967
|
let client = null;
|
|
@@ -3873,7 +3970,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
3873
3970
|
server.close();
|
|
3874
3971
|
if (client) {
|
|
3875
3972
|
client.removeListener("error", reject);
|
|
3876
|
-
|
|
3973
|
+
resolve4([client, serverEnd]);
|
|
3877
3974
|
}
|
|
3878
3975
|
});
|
|
3879
3976
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -4147,7 +4244,7 @@ function createLandstripIntegration(options = {}) {
|
|
|
4147
4244
|
};
|
|
4148
4245
|
let landstripArgs;
|
|
4149
4246
|
try {
|
|
4150
|
-
envFile = writeEnvFile({ ...process.env, ...env, PWD:
|
|
4247
|
+
envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve3(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
|
|
4151
4248
|
processAllowances = withWindowsProcessReadAccess(allowances, shell, cwd, [envFile.path]);
|
|
4152
4249
|
policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
|
|
4153
4250
|
const wrappedCommand = `source '${envFile.path}' && ${command}`;
|
|
@@ -4771,7 +4868,7 @@ ${message}
|
|
|
4771
4868
|
const selected = index === selectedAgent;
|
|
4772
4869
|
const cursor = selected ? accent("›") : " ";
|
|
4773
4870
|
const active = agent.name === activeAgent ? theme.fg("success", "●") : dim("○");
|
|
4774
|
-
const name =
|
|
4871
|
+
const name = colorizeAgentText(agent.color, agent.name, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
|
|
4775
4872
|
const model = dim(` ${agent.model ?? "current model"}`);
|
|
4776
4873
|
const description = agent.description ? dim(` ${agent.description}`) : "";
|
|
4777
4874
|
lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
|
|
@@ -4787,7 +4884,7 @@ ${message}
|
|
|
4787
4884
|
const index = start + offset;
|
|
4788
4885
|
const selected = index === selectedSubagent;
|
|
4789
4886
|
const cursor = selected ? accent("›") : " ";
|
|
4790
|
-
const name =
|
|
4887
|
+
const name = colorizeAgentText(agent2.color, `@${agent2.name}`, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
|
|
4791
4888
|
const flags = [agent2.model ?? "current model", agent2.mode];
|
|
4792
4889
|
if (agent2.hidden)
|
|
4793
4890
|
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": "secondary",
|
|
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": "accent",
|
|
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.42",
|
|
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.42",
|
|
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": "secondary",
|
|
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": "accent",
|
|
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
|
},
|