@oh-my-pi/pi-coding-agent 3.0.1337 → 3.3.1337
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/CHANGELOG.md +17 -0
- package/docs/extension-loading.md +2 -2
- package/docs/sdk.md +2 -2
- package/package.json +4 -4
- package/src/core/skills.ts +54 -68
- package/src/core/tools/task/agents.ts +21 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/discovery.ts +21 -0
- package/src/core/tools/task/executor.ts +16 -1
- package/src/core/tools/task/index.ts +25 -0
- package/src/core/tools/task/types.ts +4 -0
- package/src/modes/interactive/interactive-mode.ts +167 -126
- package/src/modes/print-mode.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.3.1337] - 2026-01-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Improved `/status` command output formatting to use consistent column alignment across all sections
|
|
10
|
+
- Updated version update notification to suggest `omp update` instead of manual npm install command
|
|
11
|
+
|
|
12
|
+
## [3.1.1337] - 2026-01-03
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `spawns` frontmatter field for agent definitions to control which sub-agents can be spawned
|
|
16
|
+
- Added spawn restriction enforcement preventing agents from spawning unauthorized sub-agents
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed duplicate skill loading when the same SKILL.md file was discovered through multiple paths
|
|
21
|
+
|
|
5
22
|
## [3.0.1337] - 2026-01-03
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -775,7 +775,7 @@ Update argument parsing:
|
|
|
775
775
|
result.noHooks = true;
|
|
776
776
|
```
|
|
777
777
|
|
|
778
|
-
Add subcommand handling for `
|
|
778
|
+
Add subcommand handling for `omp install`, `omp remove`, `omp update`.
|
|
779
779
|
|
|
780
780
|
### `src/core/hooks/loader.ts`
|
|
781
781
|
|
|
@@ -949,7 +949,7 @@ if (settings.skills?.customDirectories) {
|
|
|
949
949
|
5. **Phase 5: CLI updates**
|
|
950
950
|
- Add new flags to args.ts
|
|
951
951
|
- Update help text
|
|
952
|
-
- Add `
|
|
952
|
+
- Add `omp install`, `omp remove`, `omp update` subcommands
|
|
953
953
|
|
|
954
954
|
6. **Phase 6: Integration**
|
|
955
955
|
- Update sdk.ts
|
package/docs/sdk.md
CHANGED
|
@@ -54,7 +54,7 @@ The main factory function. Creates an `AgentSession` with configurable options.
|
|
|
54
54
|
|
|
55
55
|
**Philosophy:** "Omit to discover, provide to override."
|
|
56
56
|
|
|
57
|
-
- Omit an option →
|
|
57
|
+
- Omit an option → omp discovers/loads from standard locations
|
|
58
58
|
- Provide an option → your value is used, discovery skipped for that option
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
@@ -405,7 +405,7 @@ const { session } = await createAgentSession({
|
|
|
405
405
|
|
|
406
406
|
**When you don't need factories:**
|
|
407
407
|
|
|
408
|
-
- If you omit `tools`,
|
|
408
|
+
- If you omit `tools`, omp automatically creates them with the correct `cwd`
|
|
409
409
|
- If you use `process.cwd()` as your `cwd`, the pre-built instances work fine
|
|
410
410
|
|
|
411
411
|
**When you must use factories:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.3.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.3.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.3.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
package/src/core/skills.ts
CHANGED
|
@@ -46,6 +46,30 @@ export interface LoadSkillsFromDirOptions {
|
|
|
46
46
|
export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
|
|
47
47
|
const skills: Skill[] = [];
|
|
48
48
|
const warnings: SkillWarning[] = [];
|
|
49
|
+
const seenPaths = new Set<string>();
|
|
50
|
+
|
|
51
|
+
function addSkill(skillFile: string, skillDir: string, dirName: string) {
|
|
52
|
+
if (seenPaths.has(skillFile)) return;
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
55
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
56
|
+
const name = (frontmatter.name as string) || dirName;
|
|
57
|
+
const description = frontmatter.description as string;
|
|
58
|
+
|
|
59
|
+
if (description) {
|
|
60
|
+
seenPaths.add(skillFile);
|
|
61
|
+
skills.push({
|
|
62
|
+
name,
|
|
63
|
+
description,
|
|
64
|
+
filePath: skillFile,
|
|
65
|
+
baseDir: skillDir,
|
|
66
|
+
source: options.source,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Skip invalid skills
|
|
71
|
+
}
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
function scanDir(dir: string) {
|
|
51
75
|
try {
|
|
@@ -59,45 +83,14 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
59
83
|
try {
|
|
60
84
|
const stat = statSync(skillFile);
|
|
61
85
|
if (stat.isFile()) {
|
|
62
|
-
|
|
63
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
64
|
-
const name = (frontmatter.name as string) || entry.name;
|
|
65
|
-
const description = frontmatter.description as string;
|
|
66
|
-
|
|
67
|
-
if (description) {
|
|
68
|
-
skills.push({
|
|
69
|
-
name,
|
|
70
|
-
description,
|
|
71
|
-
filePath: skillFile,
|
|
72
|
-
baseDir: fullPath,
|
|
73
|
-
source: options.source,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
86
|
+
addSkill(skillFile, fullPath, entry.name);
|
|
76
87
|
}
|
|
77
88
|
} catch {
|
|
78
|
-
//
|
|
89
|
+
// No SKILL.md in this directory
|
|
79
90
|
}
|
|
80
|
-
|
|
81
91
|
scanDir(fullPath);
|
|
82
92
|
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
83
|
-
|
|
84
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
85
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
86
|
-
const name = (frontmatter.name as string) || basename(dir);
|
|
87
|
-
const description = frontmatter.description as string;
|
|
88
|
-
|
|
89
|
-
if (description) {
|
|
90
|
-
skills.push({
|
|
91
|
-
name,
|
|
92
|
-
description,
|
|
93
|
-
filePath: fullPath,
|
|
94
|
-
baseDir: dir,
|
|
95
|
-
source: options.source,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Skip invalid skills
|
|
100
|
-
}
|
|
93
|
+
addSkill(fullPath, dir, basename(dir));
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
96
|
} catch (err) {
|
|
@@ -117,6 +110,30 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
117
110
|
function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
118
111
|
const skills: Skill[] = [];
|
|
119
112
|
const warnings: SkillWarning[] = [];
|
|
113
|
+
const seenPaths = new Set<string>();
|
|
114
|
+
|
|
115
|
+
function addSkill(skillFile: string, skillDir: string, dirName: string) {
|
|
116
|
+
if (seenPaths.has(skillFile)) return;
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
119
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
120
|
+
const name = (frontmatter.name as string) || dirName;
|
|
121
|
+
const description = frontmatter.description as string;
|
|
122
|
+
|
|
123
|
+
if (description) {
|
|
124
|
+
seenPaths.add(skillFile);
|
|
125
|
+
skills.push({
|
|
126
|
+
name,
|
|
127
|
+
description,
|
|
128
|
+
filePath: skillFile,
|
|
129
|
+
baseDir: skillDir,
|
|
130
|
+
source: "custom",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Skip invalid skills
|
|
135
|
+
}
|
|
136
|
+
}
|
|
120
137
|
|
|
121
138
|
function scanDir(currentDir: string) {
|
|
122
139
|
try {
|
|
@@ -130,45 +147,14 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
|
130
147
|
try {
|
|
131
148
|
const stat = statSync(skillFile);
|
|
132
149
|
if (stat.isFile()) {
|
|
133
|
-
|
|
134
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
135
|
-
const name = (frontmatter.name as string) || entry.name;
|
|
136
|
-
const description = frontmatter.description as string;
|
|
137
|
-
|
|
138
|
-
if (description) {
|
|
139
|
-
skills.push({
|
|
140
|
-
name,
|
|
141
|
-
description,
|
|
142
|
-
filePath: skillFile,
|
|
143
|
-
baseDir: fullPath,
|
|
144
|
-
source: "custom",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
150
|
+
addSkill(skillFile, fullPath, entry.name);
|
|
147
151
|
}
|
|
148
152
|
} catch {
|
|
149
|
-
//
|
|
153
|
+
// No SKILL.md in this directory
|
|
150
154
|
}
|
|
151
|
-
|
|
152
155
|
scanDir(fullPath);
|
|
153
156
|
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
154
|
-
|
|
155
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
156
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
157
|
-
const name = (frontmatter.name as string) || basename(currentDir);
|
|
158
|
-
const description = frontmatter.description as string;
|
|
159
|
-
|
|
160
|
-
if (description) {
|
|
161
|
-
skills.push({
|
|
162
|
-
name,
|
|
163
|
-
description,
|
|
164
|
-
filePath: fullPath,
|
|
165
|
-
baseDir: currentDir,
|
|
166
|
-
source: "custom",
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Skip invalid skills
|
|
171
|
-
}
|
|
157
|
+
addSkill(fullPath, currentDir, basename(currentDir));
|
|
172
158
|
}
|
|
173
159
|
}
|
|
174
160
|
} catch (err) {
|
|
@@ -68,6 +68,26 @@ function parseAgent(fileName: string, content: string, source: AgentSource): Age
|
|
|
68
68
|
.map((t) => t.trim())
|
|
69
69
|
.filter(Boolean);
|
|
70
70
|
|
|
71
|
+
// Parse spawns field
|
|
72
|
+
let spawns: string[] | "*" | undefined;
|
|
73
|
+
if (frontmatter.spawns !== undefined) {
|
|
74
|
+
const spawnsRaw = frontmatter.spawns.trim();
|
|
75
|
+
if (spawnsRaw === "*") {
|
|
76
|
+
spawns = "*";
|
|
77
|
+
} else if (spawnsRaw) {
|
|
78
|
+
spawns = spawnsRaw
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((s) => s.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
if (spawns.length === 0) spawns = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
87
|
+
if (spawns === undefined && tools?.includes("task")) {
|
|
88
|
+
spawns = "*";
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
const recursive =
|
|
72
92
|
frontmatter.recursive === undefined ? false : frontmatter.recursive === "true" || frontmatter.recursive === "1";
|
|
73
93
|
|
|
@@ -75,6 +95,7 @@ function parseAgent(fileName: string, content: string, source: AgentSource): Age
|
|
|
75
95
|
name: frontmatter.name,
|
|
76
96
|
description: frontmatter.description,
|
|
77
97
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
98
|
+
spawns,
|
|
78
99
|
model: frontmatter.model,
|
|
79
100
|
recursive,
|
|
80
101
|
systemPrompt: body,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Code review specialist for quality and security analysis
|
|
4
|
-
tools: read, grep, find, ls, bash,
|
|
4
|
+
tools: read, grep, find, ls, bash, report_finding, submit_review
|
|
5
|
+
spawns: explore
|
|
5
6
|
model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -106,6 +106,26 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
|
|
|
106
106
|
.map((t) => t.trim())
|
|
107
107
|
.filter(Boolean);
|
|
108
108
|
|
|
109
|
+
// Parse spawns field
|
|
110
|
+
let spawns: string[] | "*" | undefined;
|
|
111
|
+
if (frontmatter.spawns !== undefined) {
|
|
112
|
+
const spawnsRaw = frontmatter.spawns.trim();
|
|
113
|
+
if (spawnsRaw === "*") {
|
|
114
|
+
spawns = "*";
|
|
115
|
+
} else if (spawnsRaw) {
|
|
116
|
+
spawns = spawnsRaw
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((s) => s.trim())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
if (spawns.length === 0) spawns = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
125
|
+
if (spawns === undefined && tools?.includes("task")) {
|
|
126
|
+
spawns = "*";
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
const recursive =
|
|
110
130
|
frontmatter.recursive === undefined
|
|
111
131
|
? undefined
|
|
@@ -115,6 +135,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
|
|
|
115
135
|
name: frontmatter.name,
|
|
116
136
|
description: frontmatter.description,
|
|
117
137
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
138
|
+
spawns,
|
|
118
139
|
model: frontmatter.model,
|
|
119
140
|
recursive,
|
|
120
141
|
systemPrompt: body,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
MAX_OUTPUT_BYTES,
|
|
20
20
|
MAX_OUTPUT_LINES,
|
|
21
21
|
OMP_BLOCKED_AGENT_ENV,
|
|
22
|
+
OMP_SPAWNS_ENV,
|
|
22
23
|
type SingleResult,
|
|
23
24
|
} from "./types";
|
|
24
25
|
|
|
@@ -193,7 +194,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
193
194
|
|
|
194
195
|
// Add tools if specified
|
|
195
196
|
if (agent.tools && agent.tools.length > 0) {
|
|
196
|
-
|
|
197
|
+
let toolList = agent.tools;
|
|
198
|
+
// Auto-include task tool if spawns defined but task not in tools
|
|
199
|
+
if (agent.spawns !== undefined && !toolList.includes("task")) {
|
|
200
|
+
toolList = [...toolList, "task"];
|
|
201
|
+
}
|
|
202
|
+
args.push("--tools", toolList.join(","));
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
// Resolve and add model
|
|
@@ -220,6 +226,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
220
226
|
env[OMP_BLOCKED_AGENT_ENV] = agent.name;
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
// Propagate spawn restrictions to subprocess
|
|
230
|
+
if (agent.spawns === undefined) {
|
|
231
|
+
env[OMP_SPAWNS_ENV] = ""; // No spawns = deny all
|
|
232
|
+
} else if (agent.spawns === "*") {
|
|
233
|
+
env[OMP_SPAWNS_ENV] = "*";
|
|
234
|
+
} else {
|
|
235
|
+
env[OMP_SPAWNS_ENV] = agent.spawns.join(",");
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
// Spawn subprocess
|
|
224
239
|
const proc = spawn(OMP_CMD, args, {
|
|
225
240
|
cwd,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
MAX_PARALLEL_TASKS,
|
|
28
28
|
OMP_BLOCKED_AGENT_ENV,
|
|
29
29
|
OMP_NO_SUBAGENTS_ENV,
|
|
30
|
+
OMP_SPAWNS_ENV,
|
|
30
31
|
type TaskToolDetails,
|
|
31
32
|
taskSchema,
|
|
32
33
|
} from "./types";
|
|
@@ -321,6 +322,30 @@ export function createTaskTool(
|
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
// Check spawn restrictions from parent
|
|
326
|
+
const parentSpawns = process.env[OMP_SPAWNS_ENV];
|
|
327
|
+
const isSpawnAllowed = (agentName: string): boolean => {
|
|
328
|
+
if (parentSpawns === undefined) return true; // Root = allow all
|
|
329
|
+
if (parentSpawns === "") return false; // Empty = deny all
|
|
330
|
+
if (parentSpawns === "*") return true; // Wildcard = allow all
|
|
331
|
+
const allowed = new Set(parentSpawns.split(",").map((s) => s.trim()));
|
|
332
|
+
return allowed.has(agentName);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
for (const task of tasks) {
|
|
336
|
+
if (!isSpawnAllowed(task.agent)) {
|
|
337
|
+
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: `Cannot spawn '${task.agent}'. Allowed: ${allowed}` }],
|
|
340
|
+
details: {
|
|
341
|
+
projectAgentsDir,
|
|
342
|
+
results: [],
|
|
343
|
+
totalDurationMs: Date.now() - startTime,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
324
349
|
// Initialize progress for all tasks
|
|
325
350
|
for (let i = 0; i < tasks.length; i++) {
|
|
326
351
|
const agentCfg = getAgent(agents, tasks[i].agent);
|
|
@@ -33,6 +33,9 @@ export const OMP_NO_SUBAGENTS_ENV = "OMP_NO_SUBAGENTS";
|
|
|
33
33
|
/** Environment variable containing blocked agent name (self-recursion prevention) */
|
|
34
34
|
export const OMP_BLOCKED_AGENT_ENV = "OMP_BLOCKED_AGENT";
|
|
35
35
|
|
|
36
|
+
/** Environment variable containing allowed spawn list (propagated to subprocesses) */
|
|
37
|
+
export const OMP_SPAWNS_ENV = "OMP_SPAWNS";
|
|
38
|
+
|
|
36
39
|
/** Task tool parameters */
|
|
37
40
|
export const taskSchema = Type.Object({
|
|
38
41
|
context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
|
|
@@ -74,6 +77,7 @@ export interface AgentDefinition {
|
|
|
74
77
|
description: string;
|
|
75
78
|
systemPrompt: string;
|
|
76
79
|
tools?: string[];
|
|
80
|
+
spawns?: string[] | "*";
|
|
77
81
|
model?: string;
|
|
78
82
|
recursive?: boolean;
|
|
79
83
|
source: AgentSource;
|
|
@@ -1617,7 +1617,7 @@ export class InteractiveMode {
|
|
|
1617
1617
|
theme.bold(theme.fg("warning", "Update Available")) +
|
|
1618
1618
|
"\n" +
|
|
1619
1619
|
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
1620
|
-
theme.fg("accent", "
|
|
1620
|
+
theme.fg("accent", "omp update"),
|
|
1621
1621
|
1,
|
|
1622
1622
|
0,
|
|
1623
1623
|
),
|
|
@@ -2391,14 +2391,26 @@ export class InteractiveMode {
|
|
|
2391
2391
|
}
|
|
2392
2392
|
|
|
2393
2393
|
private handleStatusCommand(): void {
|
|
2394
|
-
const sections: string[] = [];
|
|
2395
|
-
|
|
2396
2394
|
type StatusSource =
|
|
2397
2395
|
| { provider: string; level: string }
|
|
2398
2396
|
| { mcpServer: string; provider?: string }
|
|
2399
2397
|
| "builtin"
|
|
2400
2398
|
| "unknown";
|
|
2401
2399
|
|
|
2400
|
+
type StatusLine = {
|
|
2401
|
+
name: string;
|
|
2402
|
+
sourceText: string;
|
|
2403
|
+
nameWithSource: string;
|
|
2404
|
+
desc?: string;
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
type LineSection = {
|
|
2408
|
+
title: string;
|
|
2409
|
+
lines: StatusLine[];
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
type Section = { kind: "lines"; section: LineSection } | { kind: "text"; text: string };
|
|
2413
|
+
|
|
2402
2414
|
const capitalize = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1);
|
|
2403
2415
|
|
|
2404
2416
|
const resolveSourceText = (source: StatusSource): string => {
|
|
@@ -2414,138 +2426,157 @@ export class InteractiveMode {
|
|
|
2414
2426
|
|
|
2415
2427
|
const renderSourceText = (text: string): string => text.replace(/\bvia\b/, theme.italic("via"));
|
|
2416
2428
|
|
|
2417
|
-
const truncateText = (text: string,
|
|
2418
|
-
|
|
2419
|
-
if (
|
|
2420
|
-
|
|
2429
|
+
const truncateText = (text: string, maxWidth: number): string => {
|
|
2430
|
+
const textWidth = visibleWidth(text);
|
|
2431
|
+
if (textWidth <= maxWidth) return text;
|
|
2432
|
+
if (maxWidth <= 3) {
|
|
2433
|
+
let acc = "";
|
|
2434
|
+
let width = 0;
|
|
2435
|
+
for (const char of text) {
|
|
2436
|
+
const charWidth = visibleWidth(char);
|
|
2437
|
+
if (width + charWidth > maxWidth) break;
|
|
2438
|
+
width += charWidth;
|
|
2439
|
+
acc += char;
|
|
2440
|
+
}
|
|
2441
|
+
return acc;
|
|
2442
|
+
}
|
|
2443
|
+
const targetWidth = maxWidth - 3;
|
|
2444
|
+
let acc = "";
|
|
2445
|
+
let width = 0;
|
|
2446
|
+
for (const char of text) {
|
|
2447
|
+
const charWidth = visibleWidth(char);
|
|
2448
|
+
if (width + charWidth > targetWidth) break;
|
|
2449
|
+
width += charWidth;
|
|
2450
|
+
acc += char;
|
|
2451
|
+
}
|
|
2452
|
+
return `${acc}...`;
|
|
2421
2453
|
};
|
|
2422
2454
|
|
|
2423
|
-
|
|
2424
|
-
const formatSection = <T>(
|
|
2455
|
+
const buildLineSection = <T>(
|
|
2425
2456
|
title: string,
|
|
2426
2457
|
items: readonly T[],
|
|
2427
2458
|
getName: (item: T) => string,
|
|
2428
2459
|
getDesc: (item: T) => string | undefined,
|
|
2429
2460
|
getSource: (item: T) => StatusSource,
|
|
2430
|
-
):
|
|
2431
|
-
if (items.length === 0) return
|
|
2461
|
+
): LineSection | null => {
|
|
2462
|
+
if (items.length === 0) return null;
|
|
2432
2463
|
|
|
2433
|
-
const
|
|
2464
|
+
const lines = items.map((item) => {
|
|
2434
2465
|
const name = getName(item);
|
|
2435
|
-
const desc = getDesc(item);
|
|
2466
|
+
const desc = getDesc(item)?.trim();
|
|
2436
2467
|
const sourceText = resolveSourceText(getSource(item));
|
|
2437
2468
|
const nameWithSource = sourceText ? `${name} ${sourceText}` : name;
|
|
2438
2469
|
return { name, sourceText, nameWithSource, desc };
|
|
2439
2470
|
});
|
|
2440
2471
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2472
|
+
return { title, lines };
|
|
2473
|
+
};
|
|
2474
|
+
|
|
2475
|
+
const renderLineSection = (section: LineSection, maxNameWidth: number): string => {
|
|
2476
|
+
const formattedLines = section.lines.map((line) => {
|
|
2443
2477
|
let nameText = line.name;
|
|
2444
2478
|
let sourceText = line.sourceText;
|
|
2445
2479
|
|
|
2446
2480
|
if (sourceText) {
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
sourceText = truncateText(sourceText, Math.max(0, maxNameWidth - 4));
|
|
2450
|
-
availableForName = maxNameWidth - sourceText.length - 1;
|
|
2451
|
-
}
|
|
2452
|
-
nameText = truncateText(nameText, Math.max(1, availableForName));
|
|
2453
|
-
} else {
|
|
2454
|
-
nameText = truncateText(nameText, maxNameWidth);
|
|
2481
|
+
const maxSourceWidth = Math.max(0, maxNameWidth - 2);
|
|
2482
|
+
sourceText = truncateText(sourceText, maxSourceWidth);
|
|
2455
2483
|
}
|
|
2484
|
+
const sourceWidth = sourceText ? visibleWidth(sourceText) : 0;
|
|
2485
|
+
const availableForName = sourceText ? Math.max(1, maxNameWidth - sourceWidth - 1) : maxNameWidth;
|
|
2486
|
+
nameText = truncateText(nameText, availableForName);
|
|
2456
2487
|
|
|
2457
2488
|
const nameWithSourcePlain = sourceText ? `${nameText} ${sourceText}` : nameText;
|
|
2458
2489
|
const sourceRendered = sourceText ? renderSourceText(sourceText) : "";
|
|
2459
2490
|
const nameRendered = sourceText ? `${theme.bold(nameText)} ${sourceRendered}` : theme.bold(nameText);
|
|
2460
|
-
const pad = Math.max(0, maxNameWidth - nameWithSourcePlain
|
|
2461
|
-
const desc = line.desc
|
|
2462
|
-
const descPart = desc
|
|
2491
|
+
const pad = Math.max(0, maxNameWidth - visibleWidth(nameWithSourcePlain));
|
|
2492
|
+
const desc = line.desc;
|
|
2493
|
+
const descPart = desc
|
|
2494
|
+
? ` ${theme.fg("dim", desc.slice(0, 50) + (desc.length > 50 ? "..." : ""))}`
|
|
2495
|
+
: "";
|
|
2463
2496
|
return ` ${nameRendered}${" ".repeat(pad)}${descPart}`;
|
|
2464
2497
|
});
|
|
2465
2498
|
|
|
2466
|
-
return `${theme.bold(theme.fg("accent", title))}\n${formattedLines.join("\n")}`;
|
|
2499
|
+
return `${theme.bold(theme.fg("accent", section.title))}\n${formattedLines.join("\n")}`;
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2502
|
+
const sections: Section[] = [];
|
|
2503
|
+
const pushLineSection = <T>(
|
|
2504
|
+
title: string,
|
|
2505
|
+
items: readonly T[],
|
|
2506
|
+
getName: (item: T) => string,
|
|
2507
|
+
getDesc: (item: T) => string | undefined,
|
|
2508
|
+
getSource: (item: T) => StatusSource,
|
|
2509
|
+
): void => {
|
|
2510
|
+
const section = buildLineSection(title, items, getName, getDesc, getSource);
|
|
2511
|
+
if (section) {
|
|
2512
|
+
sections.push({ kind: "lines", section });
|
|
2513
|
+
}
|
|
2467
2514
|
};
|
|
2468
2515
|
|
|
2469
2516
|
// Loaded context files
|
|
2470
2517
|
const contextFilesResult = loadSync(contextFileCapability.id, { cwd: process.cwd() });
|
|
2471
2518
|
const contextFiles = contextFilesResult.items as ContextFile[];
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
(f) => ({ provider: f._source.providerName, level: f.level }),
|
|
2480
|
-
),
|
|
2481
|
-
);
|
|
2482
|
-
}
|
|
2519
|
+
pushLineSection(
|
|
2520
|
+
"Context Files",
|
|
2521
|
+
contextFiles,
|
|
2522
|
+
(f) => basename(f.path),
|
|
2523
|
+
() => undefined,
|
|
2524
|
+
(f) => ({ provider: f._source.providerName, level: f.level }),
|
|
2525
|
+
);
|
|
2483
2526
|
|
|
2484
2527
|
// Loaded skills
|
|
2485
2528
|
const skillsSettings = this.session.skillsSettings;
|
|
2486
2529
|
if (skillsSettings?.enabled !== false) {
|
|
2487
2530
|
const { skills, warnings: skillWarnings } = loadSkills(skillsSettings ?? {});
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
(s) => (s._source ? { provider: s._source.providerName, level: s._source.level } : "unknown"),
|
|
2496
|
-
),
|
|
2497
|
-
);
|
|
2498
|
-
}
|
|
2531
|
+
pushLineSection(
|
|
2532
|
+
"Skills",
|
|
2533
|
+
skills,
|
|
2534
|
+
(s) => s.name,
|
|
2535
|
+
(s) => s.description,
|
|
2536
|
+
(s) => (s._source ? { provider: s._source.providerName, level: s._source.level } : "unknown"),
|
|
2537
|
+
);
|
|
2499
2538
|
if (skillWarnings.length > 0) {
|
|
2500
2539
|
sections.push(
|
|
2501
|
-
|
|
2502
|
-
"
|
|
2503
|
-
|
|
2540
|
+
{
|
|
2541
|
+
kind: "text",
|
|
2542
|
+
text:
|
|
2543
|
+
theme.bold(theme.fg("warning", "Skill Warnings")) +
|
|
2544
|
+
"\n" +
|
|
2545
|
+
skillWarnings.map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)).join("\n"),
|
|
2546
|
+
},
|
|
2504
2547
|
);
|
|
2505
2548
|
}
|
|
2506
2549
|
}
|
|
2507
2550
|
|
|
2508
2551
|
// Loaded rules
|
|
2509
2552
|
const rulesResult = loadSync<Rule>(ruleCapability.id, { cwd: process.cwd() });
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
(r) => ({ provider: r._source.providerName, level: r._source.level }),
|
|
2518
|
-
),
|
|
2519
|
-
);
|
|
2520
|
-
}
|
|
2553
|
+
pushLineSection(
|
|
2554
|
+
"Rules",
|
|
2555
|
+
rulesResult.items,
|
|
2556
|
+
(r) => r.name,
|
|
2557
|
+
(r) => r.description,
|
|
2558
|
+
(r) => ({ provider: r._source.providerName, level: r._source.level }),
|
|
2559
|
+
);
|
|
2521
2560
|
|
|
2522
2561
|
// Loaded prompts
|
|
2523
2562
|
const promptsResult = loadSync<Prompt>(promptCapability.id, { cwd: process.cwd() });
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
(p) => ({ provider: p._source.providerName, level: p._source.level }),
|
|
2532
|
-
),
|
|
2533
|
-
);
|
|
2534
|
-
}
|
|
2563
|
+
pushLineSection(
|
|
2564
|
+
"Prompts",
|
|
2565
|
+
promptsResult.items,
|
|
2566
|
+
(p) => p.name,
|
|
2567
|
+
() => undefined,
|
|
2568
|
+
(p) => ({ provider: p._source.providerName, level: p._source.level }),
|
|
2569
|
+
);
|
|
2535
2570
|
|
|
2536
2571
|
// Loaded instructions
|
|
2537
2572
|
const instructionsResult = loadSync<Instruction>(instructionCapability.id, { cwd: process.cwd() });
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
(i) => ({ provider: i._source.providerName, level: i._source.level }),
|
|
2546
|
-
),
|
|
2547
|
-
);
|
|
2548
|
-
}
|
|
2573
|
+
pushLineSection(
|
|
2574
|
+
"Instructions",
|
|
2575
|
+
instructionsResult.items,
|
|
2576
|
+
(i) => i.name,
|
|
2577
|
+
(i) => (i.applyTo ? `applies to: ${i.applyTo}` : undefined),
|
|
2578
|
+
(i) => ({ provider: i._source.providerName, level: i._source.level }),
|
|
2579
|
+
);
|
|
2549
2580
|
|
|
2550
2581
|
// Loaded custom tools - split MCP from non-MCP
|
|
2551
2582
|
if (this.customTools.size > 0) {
|
|
@@ -2555,55 +2586,47 @@ export class InteractiveMode {
|
|
|
2555
2586
|
|
|
2556
2587
|
// MCP Tools section
|
|
2557
2588
|
if (mcpTools.length > 0) {
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
(
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
},
|
|
2572
|
-
),
|
|
2589
|
+
pushLineSection(
|
|
2590
|
+
"MCP Tools",
|
|
2591
|
+
mcpTools,
|
|
2592
|
+
(ct) => ct.tool.label || ct.tool.name,
|
|
2593
|
+
() => undefined,
|
|
2594
|
+
(ct) => {
|
|
2595
|
+
const match = ct.path.match(/^mcp:(.+?) via (.+)$/);
|
|
2596
|
+
if (match) {
|
|
2597
|
+
const [, serverName, providerName] = match;
|
|
2598
|
+
return { mcpServer: serverName, provider: providerName };
|
|
2599
|
+
}
|
|
2600
|
+
return ct.path.startsWith("mcp:") ? { mcpServer: ct.path.slice(4) } : "unknown";
|
|
2601
|
+
},
|
|
2573
2602
|
);
|
|
2574
2603
|
}
|
|
2575
2604
|
|
|
2576
2605
|
// Custom Tools section
|
|
2577
2606
|
if (customTools.length > 0) {
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
(ct)
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
},
|
|
2589
|
-
),
|
|
2607
|
+
pushLineSection(
|
|
2608
|
+
"Custom Tools",
|
|
2609
|
+
customTools,
|
|
2610
|
+
(ct) => ct.tool.label || ct.tool.name,
|
|
2611
|
+
(ct) => ct.tool.description,
|
|
2612
|
+
(ct) => {
|
|
2613
|
+
if (ct.source?.provider === "builtin") return "builtin";
|
|
2614
|
+
if (ct.path === "<exa>") return "builtin";
|
|
2615
|
+
return ct.source ? { provider: ct.source.providerName, level: ct.source.level } : "unknown";
|
|
2616
|
+
},
|
|
2590
2617
|
);
|
|
2591
2618
|
}
|
|
2592
2619
|
}
|
|
2593
2620
|
|
|
2594
2621
|
// Loaded slash commands (file-based)
|
|
2595
2622
|
const fileCommands = this.session.fileCommands;
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
(cmd) => (cmd._source ? { provider: cmd._source.providerName, level: cmd._source.level } : "unknown"),
|
|
2604
|
-
),
|
|
2605
|
-
);
|
|
2606
|
-
}
|
|
2623
|
+
pushLineSection(
|
|
2624
|
+
"Slash Commands",
|
|
2625
|
+
fileCommands,
|
|
2626
|
+
(cmd) => `/${cmd.name}`,
|
|
2627
|
+
(cmd) => cmd.description,
|
|
2628
|
+
(cmd) => (cmd._source ? { provider: cmd._source.providerName, level: cmd._source.level } : "unknown"),
|
|
2629
|
+
);
|
|
2607
2630
|
|
|
2608
2631
|
// Loaded hooks
|
|
2609
2632
|
const hookRunner = this.session.hookRunner;
|
|
@@ -2611,12 +2634,30 @@ export class InteractiveMode {
|
|
|
2611
2634
|
const hookPaths = hookRunner.getHookPaths();
|
|
2612
2635
|
if (hookPaths.length > 0) {
|
|
2613
2636
|
sections.push(
|
|
2614
|
-
|
|
2637
|
+
{
|
|
2638
|
+
kind: "text",
|
|
2639
|
+
text:
|
|
2640
|
+
`${theme.bold(theme.fg("accent", "Hooks"))}\n` +
|
|
2641
|
+
hookPaths.map((p) => ` ${theme.bold(basename(p))} ${theme.fg("dim", "hook")}`).join("\n"),
|
|
2642
|
+
},
|
|
2615
2643
|
);
|
|
2616
2644
|
}
|
|
2617
2645
|
}
|
|
2618
2646
|
|
|
2619
|
-
|
|
2647
|
+
const lineSections = sections.filter((section): section is { kind: "lines"; section: LineSection } => {
|
|
2648
|
+
return section.kind === "lines";
|
|
2649
|
+
});
|
|
2650
|
+
const allLines = lineSections.flatMap((section) => section.section.lines);
|
|
2651
|
+
const maxNameWidth = allLines.length
|
|
2652
|
+
? Math.min(60, Math.max(...allLines.map((line) => visibleWidth(line.nameWithSource))))
|
|
2653
|
+
: 0;
|
|
2654
|
+
const renderedSections = sections
|
|
2655
|
+
.map((section) =>
|
|
2656
|
+
section.kind === "lines" ? renderLineSection(section.section, maxNameWidth) : section.text,
|
|
2657
|
+
)
|
|
2658
|
+
.filter((section) => section.length > 0);
|
|
2659
|
+
|
|
2660
|
+
if (renderedSections.length === 0) {
|
|
2620
2661
|
this.chatContainer.addChild(new Spacer(1));
|
|
2621
2662
|
this.chatContainer.addChild(new Text(theme.fg("muted", "No extensions loaded."), 1, 0));
|
|
2622
2663
|
} else {
|
|
@@ -2624,7 +2665,7 @@ export class InteractiveMode {
|
|
|
2624
2665
|
this.chatContainer.addChild(new DynamicBorder());
|
|
2625
2666
|
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Loaded Extensions")), 1, 0));
|
|
2626
2667
|
this.chatContainer.addChild(new Spacer(1));
|
|
2627
|
-
for (const section of
|
|
2668
|
+
for (const section of renderedSections) {
|
|
2628
2669
|
this.chatContainer.addChild(new Text(section, 1, 0));
|
|
2629
2670
|
this.chatContainer.addChild(new Spacer(1));
|
|
2630
2671
|
}
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Print mode (single-shot): Send prompts, output result, exit.
|
|
3
3
|
*
|
|
4
4
|
* Used for:
|
|
5
|
-
* - `
|
|
6
|
-
* - `
|
|
5
|
+
* - `omp -p "prompt"` - text output
|
|
6
|
+
* - `omp --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|