pi-landstrip 0.17.18 → 0.17.20
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/agents.ts +13 -24
- package/box.ts +26 -0
- package/config.ts +13 -14
- package/index.ts +254 -372
- package/package.json +4 -2
- package/rpc-process.ts +3 -5
- package/subagents.ts +26 -46
- package/util.ts +61 -0
package/agents.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
|
|
8
7
|
import { minimatch } from 'minimatch';
|
|
9
8
|
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
10
9
|
|
|
11
10
|
import { loadLandstripConfig, type ConfigObject } from './config.ts';
|
|
11
|
+
import { expandHomePath, formatError, isRecord } from './util.ts';
|
|
12
12
|
|
|
13
13
|
export type PermissionAction = 'allow' | 'ask' | 'deny';
|
|
14
14
|
|
|
@@ -58,30 +58,19 @@ const AGENT_FIELDS = new Set([
|
|
|
58
58
|
'color',
|
|
59
59
|
]);
|
|
60
60
|
|
|
61
|
-
function isObject(value: unknown): value is ConfigObject {
|
|
62
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
61
|
function normalizeAbsolutePath(value: string): string {
|
|
66
62
|
return /^(?:[A-Za-z]:[\\/]|\\\\)/.test(value) ? value.replaceAll('\\', '/') : value;
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
function expandPattern(pattern: string): string {
|
|
70
|
-
|
|
71
|
-
if (pattern.startsWith('~/')) {
|
|
72
|
-
return normalizeAbsolutePath(join(homedir(), pattern.slice(2)));
|
|
73
|
-
}
|
|
74
|
-
if (pattern.startsWith('$HOME/')) {
|
|
75
|
-
return normalizeAbsolutePath(join(homedir(), pattern.slice(6)));
|
|
76
|
-
}
|
|
77
|
-
return normalizeAbsolutePath(pattern);
|
|
66
|
+
return normalizeAbsolutePath(expandHomePath(pattern));
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
function permissionEntries(permission: string, value: unknown): PermissionRule[] {
|
|
81
70
|
if (value === 'allow' || value === 'ask' || value === 'deny') {
|
|
82
71
|
return [{ permission, pattern: '*', action: value }];
|
|
83
72
|
}
|
|
84
|
-
if (!
|
|
73
|
+
if (!isRecord(value)) {
|
|
85
74
|
throw new Error(`permission ${permission} must be allow, ask, deny, or a map`);
|
|
86
75
|
}
|
|
87
76
|
return Object.entries(value).map(([pattern, action]) => {
|
|
@@ -97,7 +86,7 @@ function normalizePermissions(value: unknown): PermissionRules {
|
|
|
97
86
|
return [{ permission: '*', pattern: '*', action: value }];
|
|
98
87
|
}
|
|
99
88
|
if (value === undefined) return [];
|
|
100
|
-
if (!
|
|
89
|
+
if (!isRecord(value)) throw new Error('permission must be allow, ask, deny, or a map');
|
|
101
90
|
return Object.entries(value).flatMap(([permission, rules]) =>
|
|
102
91
|
permissionEntries(permission, rules),
|
|
103
92
|
);
|
|
@@ -108,7 +97,7 @@ function legacyConfigWarnings(piAgentDir: string): string[] {
|
|
|
108
97
|
if (!existsSync(path)) return [];
|
|
109
98
|
try {
|
|
110
99
|
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
111
|
-
if (!
|
|
100
|
+
if (!isRecord(value)) return [];
|
|
112
101
|
const fields = ['agent', 'permission', 'subagents', 'maxSubagents'].filter(
|
|
113
102
|
(field) => value[field] !== undefined,
|
|
114
103
|
);
|
|
@@ -135,7 +124,7 @@ function normalizeAgent(name: string, raw: ConfigObject): AgentDefinition | unde
|
|
|
135
124
|
for (const key of Object.keys(raw)) {
|
|
136
125
|
if (!AGENT_FIELDS.has(key)) throw new Error(`agent ${name} has an unknown field ${key}`);
|
|
137
126
|
}
|
|
138
|
-
if (raw.options !== undefined && !
|
|
127
|
+
if (raw.options !== undefined && !isRecord(raw.options)) {
|
|
139
128
|
throw new Error(`agent ${name} options must be an object`);
|
|
140
129
|
}
|
|
141
130
|
for (const field of ['temperature', 'top_p'] as const) {
|
|
@@ -152,7 +141,7 @@ function normalizeAgent(name: string, raw: ConfigObject): AgentDefinition | unde
|
|
|
152
141
|
) {
|
|
153
142
|
throw new Error(`agent ${name} has an invalid mode`);
|
|
154
143
|
}
|
|
155
|
-
const providerOptions: Record<string, unknown> =
|
|
144
|
+
const providerOptions: Record<string, unknown> = isRecord(raw.options) ? { ...raw.options } : {};
|
|
156
145
|
if (typeof raw.temperature === 'number') providerOptions.temperature = raw.temperature;
|
|
157
146
|
if (typeof raw.top_p === 'number') providerOptions.top_p = raw.top_p;
|
|
158
147
|
if (raw.steps !== undefined && (!Number.isInteger(raw.steps) || (raw.steps as number) <= 0)) {
|
|
@@ -189,16 +178,16 @@ export function loadAgentCatalog(
|
|
|
189
178
|
maxSubagents = config.maxSubagents;
|
|
190
179
|
subagents = config.subagents;
|
|
191
180
|
} catch (error) {
|
|
192
|
-
diagnostics.push(
|
|
181
|
+
diagnostics.push(formatError(error));
|
|
193
182
|
}
|
|
194
183
|
|
|
195
184
|
const normalized = new Map<string, AgentDefinition>();
|
|
196
|
-
if (subagents.agent !== undefined && !
|
|
185
|
+
if (subagents.agent !== undefined && !isRecord(subagents.agent)) {
|
|
197
186
|
diagnostics.push('subagents.agent must be an object');
|
|
198
|
-
} else if (
|
|
187
|
+
} else if (isRecord(subagents.agent)) {
|
|
199
188
|
for (const name of Object.keys(subagents.agent).sort()) {
|
|
200
189
|
const value = subagents.agent[name];
|
|
201
|
-
if (!
|
|
190
|
+
if (!isRecord(value)) {
|
|
202
191
|
diagnostics.push(`agent ${name} must be an object`);
|
|
203
192
|
continue;
|
|
204
193
|
}
|
|
@@ -206,7 +195,7 @@ export function loadAgentCatalog(
|
|
|
206
195
|
const agent = normalizeAgent(name, value);
|
|
207
196
|
if (agent) normalized.set(name, agent);
|
|
208
197
|
} catch (error) {
|
|
209
|
-
diagnostics.push(
|
|
198
|
+
diagnostics.push(formatError(error));
|
|
210
199
|
}
|
|
211
200
|
}
|
|
212
201
|
}
|
|
@@ -220,7 +209,7 @@ export function loadAgentCatalog(
|
|
|
220
209
|
try {
|
|
221
210
|
permissions = normalizePermissions(subagents.permission);
|
|
222
211
|
} catch (error) {
|
|
223
|
-
diagnostics.push(
|
|
212
|
+
diagnostics.push(formatError(error));
|
|
224
213
|
}
|
|
225
214
|
return { agents: normalized, permissions, diagnostics, warnings, maxSubagents };
|
|
226
215
|
}
|
package/box.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
|
+
|
|
4
|
+
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
6
|
+
|
|
7
|
+
export function boxTop(theme: Theme, width: number, title: string): string {
|
|
8
|
+
if (width < 5) return truncateToWidth(theme.fg('accent', title), Math.max(1, width));
|
|
9
|
+
const label = theme.fg('accent', ` ${title} `);
|
|
10
|
+
const fill = theme.fg('border', '─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
11
|
+
return `${theme.fg('border', '╭─')}${label}${fill}${theme.fg('border', '─╮')}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function boxRow(theme: Theme, width: number, content = ''): string {
|
|
15
|
+
if (width < 5) return truncateToWidth(content, Math.max(1, width));
|
|
16
|
+
const innerWidth = Math.max(1, width - 4);
|
|
17
|
+
const border = theme.fg('border', '│');
|
|
18
|
+
const line = truncateToWidth(content, innerWidth);
|
|
19
|
+
return `${border} ${line}${' '.repeat(Math.max(0, innerWidth - visibleWidth(line)))} ${border}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function boxBottom(theme: Theme, width: number): string {
|
|
23
|
+
if (width < 5) return '';
|
|
24
|
+
const border = (value: string) => theme.fg('border', value);
|
|
25
|
+
return `${border('╰')}${border('─'.repeat(Math.max(0, width - 2)))}${border('╯')}`;
|
|
26
|
+
}
|
package/config.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
import { getAgentDir, withFileMutationQueue } from '@earendil-works/pi-coding-agent';
|
|
9
9
|
|
|
10
|
+
import { formatError, isRecord } from './util.ts';
|
|
11
|
+
|
|
10
12
|
export type ConfigObject = Record<string, unknown>;
|
|
11
13
|
|
|
12
14
|
export const MAX_SUBAGENTS = 16;
|
|
@@ -23,20 +25,16 @@ export interface LandstripConfig extends LandstripConfigFile {
|
|
|
23
25
|
|
|
24
26
|
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
25
27
|
|
|
26
|
-
function isObject(value: unknown): value is ConfigObject {
|
|
27
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
28
|
function readConfig(path: string): LandstripConfigFile {
|
|
31
29
|
if (!existsSync(path)) return {};
|
|
32
30
|
let value: unknown;
|
|
33
31
|
try {
|
|
34
32
|
value = JSON.parse(readFileSync(path, 'utf8'));
|
|
35
33
|
} catch (error) {
|
|
36
|
-
const message =
|
|
34
|
+
const message = formatError(error);
|
|
37
35
|
throw new Error(`${path}: ${message}`);
|
|
38
36
|
}
|
|
39
|
-
if (!
|
|
37
|
+
if (!isRecord(value)) throw new Error(`${path} must contain a JSON object`);
|
|
40
38
|
for (const key of Object.keys(value)) {
|
|
41
39
|
if (key !== 'maxSubagents' && key !== 'subagents') {
|
|
42
40
|
throw new Error(`${path}: unknown top-level field ${key}`);
|
|
@@ -48,7 +46,7 @@ function readConfig(path: string): LandstripConfigFile {
|
|
|
48
46
|
function mergeValue(base: unknown, override: unknown): unknown {
|
|
49
47
|
if (override === undefined) return base;
|
|
50
48
|
if (Array.isArray(override)) return [...override];
|
|
51
|
-
if (
|
|
49
|
+
if (isRecord(base) && isRecord(override)) {
|
|
52
50
|
const result: ConfigObject = { ...base };
|
|
53
51
|
for (const [key, value] of Object.entries(override)) {
|
|
54
52
|
const merged = mergeValue(result[key], value);
|
|
@@ -60,13 +58,14 @@ function mergeValue(base: unknown, override: unknown): unknown {
|
|
|
60
58
|
return override;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
function
|
|
61
|
+
export function getPiConfigPaths(
|
|
64
62
|
cwd: string,
|
|
63
|
+
fileName: string,
|
|
65
64
|
agentDir = getAgentDir(),
|
|
66
65
|
): { globalPath: string; projectPath: string } {
|
|
67
66
|
return {
|
|
68
|
-
globalPath: join(agentDir,
|
|
69
|
-
projectPath: join(cwd, '.pi',
|
|
67
|
+
globalPath: join(agentDir, fileName),
|
|
68
|
+
projectPath: join(cwd, '.pi', fileName),
|
|
70
69
|
};
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -76,7 +75,7 @@ export async function setMaxSubagentsConfig(
|
|
|
76
75
|
includeProject = true,
|
|
77
76
|
agentDir = getAgentDir(),
|
|
78
77
|
): Promise<'global' | 'project'> {
|
|
79
|
-
const { projectPath } =
|
|
78
|
+
const { projectPath } = getPiConfigPaths(cwd, 'subagents.json', agentDir);
|
|
80
79
|
const scope = includeProject && existsSync(projectPath) ? 'project' : 'global';
|
|
81
80
|
await setMaxSubagentsConfigForScope(cwd, maxSubagents, scope, agentDir);
|
|
82
81
|
return scope;
|
|
@@ -91,7 +90,7 @@ export async function setMaxSubagentsConfigForScope(
|
|
|
91
90
|
if (!Number.isInteger(maxSubagents) || maxSubagents < 0 || maxSubagents > MAX_SUBAGENTS) {
|
|
92
91
|
throw new Error(`maxSubagents must be an integer from 0 to ${MAX_SUBAGENTS}`);
|
|
93
92
|
}
|
|
94
|
-
const { globalPath, projectPath } =
|
|
93
|
+
const { globalPath, projectPath } = getPiConfigPaths(cwd, 'subagents.json', agentDir);
|
|
95
94
|
const path = scope === 'project' ? projectPath : globalPath;
|
|
96
95
|
await withFileMutationQueue(path, async () => {
|
|
97
96
|
const config = readConfig(path);
|
|
@@ -106,7 +105,7 @@ export function loadLandstripConfig(
|
|
|
106
105
|
includeProject = true,
|
|
107
106
|
agentDir = getAgentDir(),
|
|
108
107
|
): LandstripConfig {
|
|
109
|
-
const { globalPath, projectPath } =
|
|
108
|
+
const { globalPath, projectPath } = getPiConfigPaths(cwd, 'subagents.json', agentDir);
|
|
110
109
|
let config = readConfig(join(packageDir, 'subagents.json'));
|
|
111
110
|
config = mergeValue(config, readConfig(globalPath)) as LandstripConfigFile;
|
|
112
111
|
if (includeProject) {
|
|
@@ -119,6 +118,6 @@ export function loadLandstripConfig(
|
|
|
119
118
|
) {
|
|
120
119
|
throw new Error(`maxSubagents must be an integer from 0 to ${MAX_SUBAGENTS}`);
|
|
121
120
|
}
|
|
122
|
-
if (!
|
|
121
|
+
if (!isRecord(config.subagents)) throw new Error('subagents must be an object');
|
|
123
122
|
return config as LandstripConfig;
|
|
124
123
|
}
|