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 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
- if (pattern === '~' || pattern === '$HOME') return normalizeAbsolutePath(homedir());
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 (!isObject(value)) {
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 (!isObject(value)) throw new Error('permission must be allow, ask, deny, or a map');
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 (!isObject(value)) return [];
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 && !isObject(raw.options)) {
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> = isObject(raw.options) ? { ...raw.options } : {};
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(error instanceof Error ? error.message : String(error));
181
+ diagnostics.push(formatError(error));
193
182
  }
194
183
 
195
184
  const normalized = new Map<string, AgentDefinition>();
196
- if (subagents.agent !== undefined && !isObject(subagents.agent)) {
185
+ if (subagents.agent !== undefined && !isRecord(subagents.agent)) {
197
186
  diagnostics.push('subagents.agent must be an object');
198
- } else if (isObject(subagents.agent)) {
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 (!isObject(value)) {
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(error instanceof Error ? error.message : String(error));
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(error instanceof Error ? error.message : String(error));
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 = error instanceof Error ? error.message : String(error);
34
+ const message = formatError(error);
37
35
  throw new Error(`${path}: ${message}`);
38
36
  }
39
- if (!isObject(value)) throw new Error(`${path} must contain a JSON object`);
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 (isObject(base) && isObject(override)) {
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 getLandstripConfigPaths(
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, 'subagents.json'),
69
- projectPath: join(cwd, '.pi', 'subagents.json'),
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 } = getLandstripConfigPaths(cwd, agentDir);
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 } = getLandstripConfigPaths(cwd, agentDir);
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 } = getLandstripConfigPaths(cwd, agentDir);
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 (!isObject(config.subagents)) throw new Error('subagents must be an object');
121
+ if (!isRecord(config.subagents)) throw new Error('subagents must be an object');
123
122
  return config as LandstripConfig;
124
123
  }