pi-landstrip 0.17.16 → 0.17.18
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 +85 -82
- package/index.ts +209 -40
- package/package.json +2 -2
- package/subagents.ts +431 -62
package/README.md
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# pi-landstrip
|
|
2
2
|
|
|
3
|
-
`pi-landstrip` is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`pi-landstrip` is a [Pi](https://pi.dev/) extension that provides sandboxed Bash
|
|
4
|
+
execution, OpenCode-compatible primary agents, and sandbox-aware subagents. It
|
|
5
|
+
uses an Anthropic-compatible policy and delegates OS-level enforcement to
|
|
6
|
+
[`landstrip`](https://github.com/landstrip/landstrip).
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
The extension includes a shared [sandbox policy](./sandbox.json) and separate
|
|
9
|
+
[agent configuration](./subagents.json). Global and trusted-project files can
|
|
10
|
+
override both.
|
|
11
11
|
|
|
12
|
-
Pi >=0.80.6 <0.81.0 and Node.js >=22.19.0
|
|
12
|
+
Process-backed subagents require Pi >= 0.80.6 and < 0.81.0, and Node.js >= 22.19.0.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Installation
|
|
15
15
|
|
|
16
16
|
### Automatic install
|
|
17
17
|
|
|
18
|
-
```
|
|
18
|
+
```sh
|
|
19
19
|
pi install npm:pi-landstrip
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
This installs `pi-landstrip` and its `@landstrip/landstrip` dependency. Native
|
|
23
|
-
binaries are
|
|
23
|
+
binaries are published for Linux, macOS, and Windows on x64 and Arm64.
|
|
24
24
|
|
|
25
25
|
### Manual install
|
|
26
26
|
|
|
@@ -33,13 +33,13 @@ Add the extension to `~/.pi/agent/settings.json` (global) or
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Alternatively,
|
|
37
|
-
`.pi/extensions/` (project). See
|
|
38
|
-
[
|
|
36
|
+
Alternatively, place the extension under `~/.pi/agent/extensions/` (global) or
|
|
37
|
+
`.pi/extensions/` (project). See Pi's
|
|
38
|
+
[extension documentation](https://pi.dev/docs/latest/extensions) for details.
|
|
39
39
|
|
|
40
40
|
On unsupported platforms the extension loads but leaves sandboxing disabled.
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Disabling
|
|
43
43
|
|
|
44
44
|
Use the `--no-sandbox` flag, or set `enabled` to `false` in `sandbox.json`:
|
|
45
45
|
|
|
@@ -60,23 +60,23 @@ without a project-trust API use only global configuration.
|
|
|
60
60
|
|
|
61
61
|
## Behavior
|
|
62
62
|
|
|
63
|
-
When
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
When Pi requests a sandboxed permission, the extension sends a host notification
|
|
64
|
+
and opens a dialog. The user can allow once, allow for the session, persist for
|
|
65
|
+
the project or globally, or reject. The dialog shows the exact path or domain
|
|
66
|
+
being approved.
|
|
67
67
|
|
|
68
68
|
Project approvals are written to `.pi/sandbox.json`; global approvals are
|
|
69
69
|
written to `~/.pi/agent/sandbox.json`.
|
|
70
70
|
|
|
71
|
-
The main agent remains a normal Pi process.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
default
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
The main agent remains a normal Pi process. `pi-landstrip` replaces Bash
|
|
72
|
+
execution, including AI `bash` calls and manually typed shell commands (`!` and
|
|
73
|
+
`!!`), with a Landstrip-wrapped implementation. Network traffic uses an
|
|
74
|
+
allowlist proxy when direct access is disabled. Pi's filesystem tools and plugin
|
|
75
|
+
callbacks remain trusted code outside the Landstrip sandbox.
|
|
76
|
+
|
|
77
|
+
By default, Bash and subagent processes have no direct network access. Reads are
|
|
78
|
+
limited to the project, Git configuration, and `/dev/null`; writes are limited
|
|
79
|
+
to the project and `/dev/null`.
|
|
80
80
|
|
|
81
81
|
Subagent startup adds read-only bootstrap access to the selected Pi runtime,
|
|
82
82
|
global settings, model/auth configuration, installed plugins, skills, and the
|
|
@@ -84,32 +84,30 @@ task's dedicated session directory. These paths are required to construct a
|
|
|
84
84
|
normal Pi worker and are not persisted into `sandbox.json`. The worker receives
|
|
85
85
|
write access only to its own session and temporary directories.
|
|
86
86
|
|
|
87
|
-
Use `/sandbox`
|
|
88
|
-
|
|
89
|
-
trusted-project
|
|
87
|
+
Use `/sandbox` to inspect the active policy and toggle sandboxing. Use `/agents`
|
|
88
|
+
to select the primary role, inspect worker configuration and status, and set the
|
|
89
|
+
global or trusted-project concurrency limit.
|
|
90
90
|
|
|
91
91
|
## Primary agents
|
|
92
92
|
|
|
93
|
-
The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by
|
|
94
|
-
Build has normal development access; plan asks before shell commands
|
|
95
|
-
changes. The selection controls the root system prompt and permissions
|
|
96
|
-
restored with the session.
|
|
93
|
+
The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by
|
|
94
|
+
default. Build has normal development access; plan asks before shell commands
|
|
95
|
+
and file changes. The selection controls the root system prompt and permissions
|
|
96
|
+
and is restored with the session.
|
|
97
97
|
|
|
98
98
|
## Subagents
|
|
99
99
|
|
|
100
|
-
Landstrip provides an OpenCode-compatible `task` tool. Each active task
|
|
101
|
-
full `pi --mode rpc` process
|
|
102
|
-
covers Pi
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
Landstrip provides an OpenCode-compatible `task` tool. Each active task runs as a
|
|
101
|
+
full `pi --mode rpc` process inside an outer Landstrip sandbox. The sandbox
|
|
102
|
+
covers Pi, its plugins and tools, model requests, and descendant processes. The
|
|
103
|
+
root Pi process supervises RPC, permissions, nesting, persistence, and result
|
|
104
|
+
delivery.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
instances. Continuing a `task_id` restores its persisted Pi session in a new
|
|
112
|
-
worker process.
|
|
106
|
+
Workers use normal Pi resource discovery and plugin loading, plus the
|
|
107
|
+
`pi-landstrip` worker extension. Requested tools are composed inside the worker,
|
|
108
|
+
so plugins can add or replace implementations. Each task starts a fresh Pi
|
|
109
|
+
process and plugin instances; continuing a `task_id` restores its persisted Pi
|
|
110
|
+
session in a new process.
|
|
113
111
|
|
|
114
112
|
The tool accepts the OpenCode task fields:
|
|
115
113
|
|
|
@@ -125,21 +123,24 @@ The tool accepts the OpenCode task fields:
|
|
|
125
123
|
```
|
|
126
124
|
|
|
127
125
|
Foreground tasks return the child result directly. Background tasks return a
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
queued result and deliver completion automatically. Task rows show lifecycle
|
|
127
|
+
state, current activity, tool-call count, elapsed time, and expandable output.
|
|
128
|
+
Use `/subagents` or the task row's `/subagents <id>` command to inspect a child
|
|
129
|
+
transcript. Completed and failed task metadata remains available after reload,
|
|
130
|
+
and persisted sessions can be continued with `task_id`.
|
|
131
|
+
|
|
132
132
|
Session switching or shutdown stops live workers. After an unclean restart,
|
|
133
|
-
unfinished work is marked interrupted
|
|
134
|
-
|
|
133
|
+
unfinished work is marked interrupted; completed but undelivered background
|
|
134
|
+
results are delivered when the root session resumes.
|
|
135
135
|
|
|
136
|
-
OpenCode-style
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
root session and its descendants.
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
OpenCode-style permissions wrap each worker's composed tools: `deny` blocks,
|
|
137
|
+
`ask` prompts in the root UI, and `allow` runs the tool. Other workers may
|
|
138
|
+
continue while a prompt is open. Forwarded worker dialogs identify the agent,
|
|
139
|
+
task summary, and task ID. An "Allow for this session" decision applies to the
|
|
140
|
+
root session and its descendants.
|
|
141
|
+
|
|
142
|
+
Tool permissions cannot widen the outer Landstrip policy. Sandbox approvals
|
|
143
|
+
continue to use `.pi/sandbox.json` and `~/.pi/agent/sandbox.json`.
|
|
143
144
|
|
|
144
145
|
Subagents fail closed when sandboxing is expected but unavailable, unsupported,
|
|
145
146
|
or missing. An explicit `--no-sandbox` flag or `enabled: false` configuration is
|
|
@@ -172,13 +173,14 @@ first.
|
|
|
172
173
|
|
|
173
174
|
## Configuration
|
|
174
175
|
|
|
175
|
-
Subagent configuration is read from `~/.pi/agent/subagents.json` and, for
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`subagents
|
|
176
|
+
Subagent configuration is read from `~/.pi/agent/subagents.json` and, for trusted
|
|
177
|
+
projects, `.pi/subagents.json`. Project values override global values; both are
|
|
178
|
+
merged over the packaged [defaults](./subagents.json). Sandbox policy remains in
|
|
179
|
+
`~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi's `settings.json` contains
|
|
180
|
+
only normal Pi settings, such as the `pi-landstrip` package entry.
|
|
181
|
+
|
|
182
|
+
`subagents.json` accepts only top-level `maxSubagents` and `subagents`; sandbox
|
|
183
|
+
fields belong in `sandbox.json`.
|
|
182
184
|
|
|
183
185
|
```json
|
|
184
186
|
{
|
|
@@ -205,10 +207,10 @@ package entry. `subagents.json` accepts only top-level `maxSubagents` and
|
|
|
205
207
|
}
|
|
206
208
|
```
|
|
207
209
|
|
|
208
|
-
`maxSubagents` is an integer from 0 through 16 controlling concurrent
|
|
209
|
-
|
|
210
|
-
roles. There is no separate subagent enable switch. The Settings tab in
|
|
211
|
-
edits this limit
|
|
210
|
+
`maxSubagents` is an integer from 0 through 16 controlling concurrent workers.
|
|
211
|
+
The packaged default is 0, which removes the `task` tool while retaining primary
|
|
212
|
+
roles. There is no separate subagent enable switch. The Settings tab in
|
|
213
|
+
`/agents` edits this limit for global and trusted-project configuration.
|
|
212
214
|
|
|
213
215
|
Agent modes, hidden/disabled agents, prompts, and ordered `allow`/`ask`/`deny`
|
|
214
216
|
permissions apply to primary agents and subagents. Subagent workers also honor
|
|
@@ -218,7 +220,7 @@ matching permission rules win. Put provider-specific values under `options`;
|
|
|
218
220
|
unknown agent fields are rejected. Agent permissions cannot weaken an enabled
|
|
219
221
|
OS sandbox, grant filesystem access, or grant network access.
|
|
220
222
|
|
|
221
|
-
##
|
|
223
|
+
## Configuration migration
|
|
222
224
|
|
|
223
225
|
There is no compatibility loader for previous subagent configuration sources.
|
|
224
226
|
Legacy keys in `settings.json` produce a migration warning. Move configuration as
|
|
@@ -234,22 +236,23 @@ follows, then remove the old fields:
|
|
|
234
236
|
- Leave sandbox policy in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`;
|
|
235
237
|
those files are unchanged.
|
|
236
238
|
|
|
237
|
-
Do not put
|
|
238
|
-
|
|
239
|
+
Do not put these fields in Pi's `settings.json`; `pi-landstrip` does not read
|
|
240
|
+
them there.
|
|
239
241
|
|
|
240
242
|
## Limits
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
allows nesting to three levels. A foreground parent
|
|
244
|
-
while waiting
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
`pi-landstrip` grants at most `maxSubagents` scheduler permits to active
|
|
245
|
+
subagent work and allows nesting to three levels. A foreground parent returns
|
|
246
|
+
its permit while waiting for a child and reacquires it before resuming,
|
|
247
|
+
preventing nested-task deadlocks. A worker receives a nested `task` tool only
|
|
248
|
+
when its agent has an explicit `task` permission. Nested tasks are separate Pi
|
|
249
|
+
processes supervised by the root and Landstrip-wrapped unless sandboxing is
|
|
250
|
+
explicitly disabled, so an inactive parent process can remain alive during the
|
|
251
|
+
handoff. Persisted sessions remain resumable by `task_id`.
|
|
248
252
|
|
|
249
253
|
## License
|
|
250
254
|
|
|
251
|
-
`pi-landstrip` is licensed under `Apache-2.0`. See [LICENSE](LICENSE)
|
|
252
|
-
information.
|
|
255
|
+
`pi-landstrip` is licensed under `Apache-2.0`. See [LICENSE](LICENSE).
|
|
253
256
|
|
|
254
257
|
The bundled `@landstrip/landstrip` package is licensed separately as
|
|
255
258
|
`Apache-2.0 AND LGPL-2.1-or-later`.
|
package/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
registerSubagentWorker,
|
|
61
61
|
workerConfigFromEnvironment,
|
|
62
62
|
} from './subagents.ts';
|
|
63
|
-
import { availablePrimaryAgents } from './agents.ts';
|
|
63
|
+
import { availablePrimaryAgents, availableSubagents } from './agents.ts';
|
|
64
64
|
import { loadLandstripConfig, MAX_SUBAGENTS, setMaxSubagentsConfigForScope } from './config.ts';
|
|
65
65
|
|
|
66
66
|
interface SandboxFilesystemConfig {
|
|
@@ -2366,41 +2366,116 @@ export function createLandstripIntegration(
|
|
|
2366
2366
|
if (!ctx.hasUI) return;
|
|
2367
2367
|
const config = loadConfig(ctx.cwd);
|
|
2368
2368
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
:
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2369
|
+
const shouldToggle = await ctx.ui.custom<boolean>(
|
|
2370
|
+
(_tui, theme, _kb, done) => {
|
|
2371
|
+
const dim = (value: string) => theme.fg('dim', value);
|
|
2372
|
+
const muted = (value: string) => theme.fg('muted', value);
|
|
2373
|
+
const accent = (value: string) => theme.fg('accent', value);
|
|
2374
|
+
const text = (value: string) => theme.fg('text', value);
|
|
2375
|
+
|
|
2376
|
+
function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
|
|
2377
|
+
if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
|
|
2378
|
+
if (!config.enabled) return { color: 'warning', label: 'Disabled' };
|
|
2379
|
+
if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
|
|
2380
|
+
return { color: 'success', label: 'Active' };
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function boolValue(value: boolean): string {
|
|
2384
|
+
return value ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
return {
|
|
2388
|
+
render(width: number): string[] {
|
|
2389
|
+
const innerWidth = Math.max(1, width - 4);
|
|
2390
|
+
const row = (content = '') => boxRow(theme, width, content);
|
|
2391
|
+
const lines = [boxTop(theme, width, 'Sandbox')];
|
|
2392
|
+
const status = sandboxStatus();
|
|
2393
|
+
const statusDot = theme.fg(status.color, '●');
|
|
2394
|
+
const pathSnippet = text(
|
|
2395
|
+
truncateToWidth(landstripDisplayPath(), Math.max(20, innerWidth - 28)),
|
|
2396
|
+
);
|
|
2397
|
+
const section = (title: string, detail?: string): void => {
|
|
2398
|
+
lines.push(row(''));
|
|
2399
|
+
lines.push(row(`${accent(title)}${detail ? dim(` · ${detail}`) : ''}`));
|
|
2400
|
+
};
|
|
2401
|
+
const item = (label: string, value: string): void => {
|
|
2402
|
+
lines.push(row(` ${dim('•')} ${muted(label.padEnd(13))} ${value}`));
|
|
2403
|
+
};
|
|
2404
|
+
const listValue = (values: string[]): string => {
|
|
2405
|
+
const value = values.join(', ') || 'none';
|
|
2406
|
+
return text(truncateToWidth(value, Math.max(10, innerWidth - 17)));
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
lines.push(
|
|
2410
|
+
row(
|
|
2411
|
+
`${statusDot} ${text(status.label)} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
|
|
2412
|
+
),
|
|
2413
|
+
);
|
|
2414
|
+
|
|
2415
|
+
section('Config');
|
|
2416
|
+
item('project', text(projectPath));
|
|
2417
|
+
item('global', text(globalPath));
|
|
2418
|
+
|
|
2419
|
+
const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
2420
|
+
section('Network', networkMode);
|
|
2421
|
+
item('allow network', boolValue(config.network.allowNetwork));
|
|
2422
|
+
item('allowed', listValue(config.network.allowedDomains));
|
|
2423
|
+
item('denied', listValue(config.network.deniedDomains));
|
|
2424
|
+
item(
|
|
2425
|
+
'unix sockets',
|
|
2426
|
+
config.network.allowAllUnixSockets
|
|
2427
|
+
? text('all')
|
|
2428
|
+
: listValue(config.network.allowUnixSockets),
|
|
2429
|
+
);
|
|
2430
|
+
if (sessionAllowedDomains.length > 0) {
|
|
2431
|
+
item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
section('Filesystem');
|
|
2435
|
+
item('deny read', listValue(config.filesystem.denyRead));
|
|
2436
|
+
item('allow read', listValue(config.filesystem.allowRead));
|
|
2437
|
+
item('allow write', listValue(config.filesystem.allowWrite));
|
|
2438
|
+
item('deny write', listValue(config.filesystem.denyWrite));
|
|
2439
|
+
|
|
2440
|
+
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
2441
|
+
section('Session grants');
|
|
2442
|
+
if (sessionAllowedReadPaths.length > 0) {
|
|
2443
|
+
item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
|
|
2444
|
+
}
|
|
2445
|
+
if (sessionAllowedWritePaths.length > 0) {
|
|
2446
|
+
item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
lines.push(
|
|
2451
|
+
row(''),
|
|
2452
|
+
row(
|
|
2453
|
+
`${dim('enter')} ${muted(config.enabled ? 'disable' : 'enable')} ${dim('esc')} ${muted('close')}`,
|
|
2454
|
+
),
|
|
2455
|
+
boxBottom(theme, width),
|
|
2456
|
+
);
|
|
2457
|
+
return lines;
|
|
2458
|
+
},
|
|
2459
|
+
|
|
2460
|
+
handleInput(data: string): void {
|
|
2461
|
+
if (matchesKey(data, 'return')) {
|
|
2462
|
+
done(true);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) done(false);
|
|
2466
|
+
},
|
|
2467
|
+
|
|
2468
|
+
invalidate(): void {},
|
|
2469
|
+
};
|
|
2470
|
+
},
|
|
2471
|
+
{
|
|
2472
|
+
overlay: true,
|
|
2473
|
+
overlayOptions: { anchor: 'center', width: 78, margin: 2 },
|
|
2474
|
+
},
|
|
2401
2475
|
);
|
|
2402
|
-
if (!
|
|
2476
|
+
if (!shouldToggle) return;
|
|
2403
2477
|
|
|
2478
|
+
const enabled = !config.enabled;
|
|
2404
2479
|
try {
|
|
2405
2480
|
const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectConfigTrusted);
|
|
2406
2481
|
if (!enabled) {
|
|
@@ -2425,7 +2500,7 @@ export function createLandstripIntegration(
|
|
|
2425
2500
|
if (!ctx.hasUI) return;
|
|
2426
2501
|
await ctx.ui.custom(
|
|
2427
2502
|
(tui, theme, _kb, done) => {
|
|
2428
|
-
let tab: 'agents' | 'settings' = 'agents';
|
|
2503
|
+
let tab: 'agents' | 'subagents' | 'settings' = 'agents';
|
|
2429
2504
|
const initialAgents = runtime
|
|
2430
2505
|
? availablePrimaryAgents(runtime.getAgentCatalog(ctx))
|
|
2431
2506
|
: [];
|
|
@@ -2433,12 +2508,22 @@ export function createLandstripIntegration(
|
|
|
2433
2508
|
0,
|
|
2434
2509
|
initialAgents.findIndex((agent) => agent.name === runtime?.getPrimaryAgent()?.name),
|
|
2435
2510
|
);
|
|
2511
|
+
let selectedSubagent = 0;
|
|
2436
2512
|
let selectedSetting = 0;
|
|
2437
2513
|
let values = [
|
|
2438
2514
|
loadLandstripConfig(ctx.cwd, false).maxSubagents,
|
|
2439
2515
|
loadLandstripConfig(ctx.cwd, projectConfigTrusted).maxSubagents,
|
|
2440
2516
|
];
|
|
2441
2517
|
let editing = false;
|
|
2518
|
+
const supportedVariants = new Set([
|
|
2519
|
+
'off',
|
|
2520
|
+
'minimal',
|
|
2521
|
+
'low',
|
|
2522
|
+
'medium',
|
|
2523
|
+
'high',
|
|
2524
|
+
'xhigh',
|
|
2525
|
+
'max',
|
|
2526
|
+
]);
|
|
2442
2527
|
const dim = (value: string) => theme.fg('dim', value);
|
|
2443
2528
|
const muted = (value: string) => theme.fg('muted', value);
|
|
2444
2529
|
const accent = (value: string) => theme.fg('accent', value);
|
|
@@ -2450,7 +2535,7 @@ export function createLandstripIntegration(
|
|
|
2450
2535
|
const lines = [boxTop(theme, width, 'Agents')];
|
|
2451
2536
|
lines.push(
|
|
2452
2537
|
row(
|
|
2453
|
-
` ${tab === 'agents' ? accent('Agents') : muted('Agents')} ${dim('│')} ${tab === 'settings' ? accent('Settings') : muted('Settings')}`,
|
|
2538
|
+
` ${tab === 'agents' ? accent('Agents') : muted('Agents')} ${dim('│')} ${tab === 'subagents' ? accent('Subagents') : muted('Subagents')} ${dim('│')} ${tab === 'settings' ? accent('Settings') : muted('Settings')}`,
|
|
2454
2539
|
),
|
|
2455
2540
|
row(''),
|
|
2456
2541
|
);
|
|
@@ -2466,8 +2551,73 @@ export function createLandstripIntegration(
|
|
|
2466
2551
|
const cursor = selected ? accent('›') : ' ';
|
|
2467
2552
|
const active = agent.name === activeAgent ? theme.fg('success', '●') : dim('○');
|
|
2468
2553
|
const name = selected ? accent(agent.name) : text(agent.name);
|
|
2554
|
+
const model = dim(` ${agent.model ?? 'current model'}`);
|
|
2469
2555
|
const description = agent.description ? dim(` ${agent.description}`) : '';
|
|
2470
|
-
lines.push(row(`${cursor} ${active} ${name}${description}`));
|
|
2556
|
+
lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
|
|
2557
|
+
}
|
|
2558
|
+
} else if (tab === 'subagents') {
|
|
2559
|
+
const catalog = runtime?.getAgentCatalog(ctx);
|
|
2560
|
+
const agents = catalog ? availableSubagents(catalog) : [];
|
|
2561
|
+
selectedSubagent = Math.min(selectedSubagent, Math.max(0, agents.length - 1));
|
|
2562
|
+
if (agents.length === 0) lines.push(row(muted('No subagents configured')));
|
|
2563
|
+
const start = Math.max(0, Math.min(selectedSubagent - 3, agents.length - 7));
|
|
2564
|
+
for (const [offset, agent] of agents.slice(start, start + 7).entries()) {
|
|
2565
|
+
const index = start + offset;
|
|
2566
|
+
const selected = index === selectedSubagent;
|
|
2567
|
+
const cursor = selected ? accent('›') : ' ';
|
|
2568
|
+
const name = selected ? accent(`@${agent.name}`) : text(`@${agent.name}`);
|
|
2569
|
+
const flags = [agent.model ?? 'current model', agent.mode];
|
|
2570
|
+
if (agent.hidden) flags.push('hidden');
|
|
2571
|
+
lines.push(row(`${cursor} ${name} ${dim(flags.join(' · '))}`));
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
const agent = agents[selectedSubagent];
|
|
2575
|
+
if (agent) {
|
|
2576
|
+
lines.push(row(''));
|
|
2577
|
+
const details = [
|
|
2578
|
+
`model ${agent.model ?? 'current model'}`,
|
|
2579
|
+
agent.variant ? `variant ${agent.variant}` : undefined,
|
|
2580
|
+
agent.steps ? `${agent.steps} steps` : undefined,
|
|
2581
|
+
].filter(Boolean);
|
|
2582
|
+
lines.push(row(` ${text(details.join(' · '))}`));
|
|
2583
|
+
const permissions = [...(catalog?.permissions ?? []), ...agent.permissions];
|
|
2584
|
+
lines.push(row(` ${dim('Permissions')}`));
|
|
2585
|
+
if (permissions.length === 0) lines.push(row(` ${muted('default: ask')}`));
|
|
2586
|
+
for (const rule of permissions.slice(0, 4)) {
|
|
2587
|
+
lines.push(
|
|
2588
|
+
row(
|
|
2589
|
+
` ${text(`${rule.permission}:${rule.pattern}`)} ${dim('→')} ${theme.fg(
|
|
2590
|
+
rule.action === 'deny'
|
|
2591
|
+
? 'error'
|
|
2592
|
+
: rule.action === 'allow'
|
|
2593
|
+
? 'success'
|
|
2594
|
+
: 'warning',
|
|
2595
|
+
rule.action,
|
|
2596
|
+
)}`,
|
|
2597
|
+
),
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
if (permissions.length > 4) {
|
|
2601
|
+
lines.push(row(` ${muted(`… ${permissions.length - 4} more`)}`));
|
|
2602
|
+
}
|
|
2603
|
+
const unsupported = Object.keys(agent.providerOptions);
|
|
2604
|
+
if (agent.variant && !supportedVariants.has(agent.variant)) {
|
|
2605
|
+
unsupported.push(`variant=${agent.variant}`);
|
|
2606
|
+
}
|
|
2607
|
+
const unsupportedText =
|
|
2608
|
+
unsupported.length > 0 ? unsupported.join(', ') : 'none';
|
|
2609
|
+
lines.push(
|
|
2610
|
+
row(` ${dim('Unsupported RPC options:')} ${text(unsupportedText)}`),
|
|
2611
|
+
);
|
|
2612
|
+
}
|
|
2613
|
+
if ((catalog?.diagnostics.length ?? 0) > 0) {
|
|
2614
|
+
lines.push(row(` ${theme.fg('error', 'Catalog diagnostics')}`));
|
|
2615
|
+
for (const diagnostic of catalog?.diagnostics.slice(0, 3) ?? []) {
|
|
2616
|
+
lines.push(row(` ${theme.fg('error', diagnostic)}`));
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
for (const warning of catalog?.warnings.slice(0, 2) ?? []) {
|
|
2620
|
+
lines.push(row(` ${theme.fg('warning', warning)}`));
|
|
2471
2621
|
}
|
|
2472
2622
|
} else {
|
|
2473
2623
|
const scopes = ['Global', 'Project'] as const;
|
|
@@ -2492,8 +2642,10 @@ export function createLandstripIntegration(
|
|
|
2492
2642
|
row(''),
|
|
2493
2643
|
row(
|
|
2494
2644
|
tab === 'agents'
|
|
2495
|
-
? `${dim('↑↓')} ${muted('select')} ${dim('enter')} ${muted('activate')} ${dim('tab')} ${muted('
|
|
2496
|
-
:
|
|
2645
|
+
? `${dim('↑↓')} ${muted('select')} ${dim('enter')} ${muted('activate')} ${dim('tab')} ${muted('subagents')} ${dim('esc')} ${muted('close')}`
|
|
2646
|
+
: tab === 'subagents'
|
|
2647
|
+
? `${dim('↑↓')} ${muted('inspect')} ${dim('tab')} ${muted('settings')} ${dim('esc')} ${muted('close')}`
|
|
2648
|
+
: `${dim('↑↓')} ${muted('scope')} ${dim('0-9/+/-')} ${muted('change')} ${dim('enter')} ${muted('save')} ${dim('tab')} ${muted('agents')} ${dim('esc')} ${muted('close')}`,
|
|
2497
2649
|
),
|
|
2498
2650
|
boxBottom(theme, width),
|
|
2499
2651
|
);
|
|
@@ -2510,7 +2662,10 @@ export function createLandstripIntegration(
|
|
|
2510
2662
|
matchesKey(data, 'left') ||
|
|
2511
2663
|
matchesKey(data, 'right')
|
|
2512
2664
|
) {
|
|
2513
|
-
|
|
2665
|
+
const tabs = ['agents', 'subagents', 'settings'] as const;
|
|
2666
|
+
const current = tabs.indexOf(tab);
|
|
2667
|
+
const offset = matchesKey(data, 'left') ? -1 : 1;
|
|
2668
|
+
tab = tabs[(current + offset + tabs.length) % tabs.length] ?? 'agents';
|
|
2514
2669
|
editing = false;
|
|
2515
2670
|
tui.requestRender();
|
|
2516
2671
|
return;
|
|
@@ -2528,6 +2683,20 @@ export function createLandstripIntegration(
|
|
|
2528
2683
|
tui.requestRender();
|
|
2529
2684
|
return;
|
|
2530
2685
|
}
|
|
2686
|
+
if (tab === 'subagents') {
|
|
2687
|
+
const catalog = runtime?.getAgentCatalog(ctx);
|
|
2688
|
+
const agents = catalog ? availableSubagents(catalog) : [];
|
|
2689
|
+
if (matchesKey(data, 'up')) {
|
|
2690
|
+
selectedSubagent = Math.max(0, selectedSubagent - 1);
|
|
2691
|
+
} else if (matchesKey(data, 'down')) {
|
|
2692
|
+
selectedSubagent = Math.min(
|
|
2693
|
+
Math.max(0, agents.length - 1),
|
|
2694
|
+
selectedSubagent + 1,
|
|
2695
|
+
);
|
|
2696
|
+
} else return;
|
|
2697
|
+
tui.requestRender();
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2531
2700
|
|
|
2532
2701
|
if (matchesKey(data, 'up')) {
|
|
2533
2702
|
selectedSetting = Math.max(0, selectedSetting - 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.18",
|
|
4
4
|
"description": "Sandbox-aware Pi subagents and interactive command permissions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@earendil-works/pi-tui": "^0.80.6",
|
|
41
|
-
"@landstrip/landstrip": "^0.17.
|
|
41
|
+
"@landstrip/landstrip": "^0.17.18",
|
|
42
42
|
"minimatch": "^10.2.5"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|