@tintinweb/pi-subagents 0.4.10 → 0.5.0
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 +14 -4
- package/README.md +23 -8
- package/dist/agent-manager.d.ts +18 -4
- package/dist/agent-manager.js +111 -9
- package/dist/agent-runner.d.ts +10 -6
- package/dist/agent-runner.js +81 -27
- package/dist/agent-types.d.ts +10 -0
- package/dist/agent-types.js +23 -1
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +54 -0
- package/dist/custom-agents.js +36 -8
- package/dist/index.js +336 -66
- package/dist/memory.d.ts +49 -0
- package/dist/memory.js +151 -0
- package/dist/output-file.d.ts +17 -0
- package/dist/output-file.js +66 -0
- package/dist/prompts.d.ts +12 -1
- package/dist/prompts.js +15 -3
- package/dist/skill-loader.d.ts +19 -0
- package/dist/skill-loader.js +67 -0
- package/dist/types.d.ts +45 -1
- package/dist/ui/agent-widget.d.ts +21 -0
- package/dist/ui/agent-widget.js +205 -127
- package/dist/ui/conversation-viewer.d.ts +2 -2
- package/dist/ui/conversation-viewer.js +3 -3
- package/dist/ui/conversation-viewer.test.d.ts +1 -0
- package/dist/ui/conversation-viewer.test.js +254 -0
- package/dist/worktree.d.ts +36 -0
- package/dist/worktree.js +139 -0
- package/package.json +8 -6
- package/src/agent-runner.ts +1 -1
- package/src/cross-extension-rpc.ts +57 -23
- package/src/index.ts +4 -3
- package/src/ui/conversation-viewer.ts +1 -1
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
+
*
|
|
4
|
+
* Memory scopes:
|
|
5
|
+
* - "user" → ~/.pi/agent-memory/{agent-name}/
|
|
6
|
+
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
+
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
+
*/
|
|
9
|
+
import type { MemoryScope } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
12
|
+
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isUnsafeName(name: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isSymlink(filePath: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Safely read a file, rejecting symlinks.
|
|
21
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
22
|
+
*/
|
|
23
|
+
export declare function safeReadFile(filePath: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
26
|
+
* Throws if agentName contains path traversal characters.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveMemoryDir(agentName: string, scope: MemoryScope, cwd: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Ensure the memory directory exists, creating it if needed.
|
|
31
|
+
* Refuses to create directories if any component in the path is a symlink
|
|
32
|
+
* to prevent symlink-based directory traversal attacks.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ensureMemoryDir(memoryDir: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
37
|
+
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readMemoryIndex(memoryDir: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Build the memory block to inject into the agent's system prompt.
|
|
42
|
+
* Also ensures the memory directory exists (creates it if needed).
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Build a read-only memory block for agents that lack write/edit tools.
|
|
47
|
+
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildReadOnlyMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string;
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
+
*
|
|
4
|
+
* Memory scopes:
|
|
5
|
+
* - "user" → ~/.pi/agent-memory/{agent-name}/
|
|
6
|
+
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
+
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, } from "node:path";
|
|
12
|
+
/** Maximum lines to read from MEMORY.md */
|
|
13
|
+
const MAX_MEMORY_LINES = 200;
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
16
|
+
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
17
|
+
*/
|
|
18
|
+
export function isUnsafeName(name) {
|
|
19
|
+
if (!name || name.length > 128)
|
|
20
|
+
return true;
|
|
21
|
+
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
25
|
+
*/
|
|
26
|
+
export function isSymlink(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
return lstatSync(filePath).isSymbolicLink();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Safely read a file, rejecting symlinks.
|
|
36
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
37
|
+
*/
|
|
38
|
+
export function safeReadFile(filePath) {
|
|
39
|
+
if (!existsSync(filePath))
|
|
40
|
+
return undefined;
|
|
41
|
+
if (isSymlink(filePath))
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(filePath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
52
|
+
* Throws if agentName contains path traversal characters.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveMemoryDir(agentName, scope, cwd) {
|
|
55
|
+
if (isUnsafeName(agentName)) {
|
|
56
|
+
throw new Error(`Unsafe agent name for memory directory: "${agentName}"`);
|
|
57
|
+
}
|
|
58
|
+
switch (scope) {
|
|
59
|
+
case "user":
|
|
60
|
+
return join(homedir(), ".pi", "agent-memory", agentName);
|
|
61
|
+
case "project":
|
|
62
|
+
return join(cwd, ".pi", "agent-memory", agentName);
|
|
63
|
+
case "local":
|
|
64
|
+
return join(cwd, ".pi", "agent-memory-local", agentName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ensure the memory directory exists, creating it if needed.
|
|
69
|
+
* Refuses to create directories if any component in the path is a symlink
|
|
70
|
+
* to prevent symlink-based directory traversal attacks.
|
|
71
|
+
*/
|
|
72
|
+
export function ensureMemoryDir(memoryDir) {
|
|
73
|
+
// If the directory already exists, verify it's not a symlink
|
|
74
|
+
if (existsSync(memoryDir)) {
|
|
75
|
+
if (isSymlink(memoryDir)) {
|
|
76
|
+
throw new Error(`Refusing to use symlinked memory directory: ${memoryDir}`);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
84
|
+
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
85
|
+
*/
|
|
86
|
+
export function readMemoryIndex(memoryDir) {
|
|
87
|
+
// Reject symlinked memory directories
|
|
88
|
+
if (isSymlink(memoryDir))
|
|
89
|
+
return undefined;
|
|
90
|
+
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
91
|
+
const content = safeReadFile(memoryFile);
|
|
92
|
+
if (content === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
if (lines.length > MAX_MEMORY_LINES) {
|
|
96
|
+
return lines.slice(0, MAX_MEMORY_LINES).join("\n") + "\n... (truncated at 200 lines)";
|
|
97
|
+
}
|
|
98
|
+
return content;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the memory block to inject into the agent's system prompt.
|
|
102
|
+
* Also ensures the memory directory exists (creates it if needed).
|
|
103
|
+
*/
|
|
104
|
+
export function buildMemoryBlock(agentName, scope, cwd) {
|
|
105
|
+
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
106
|
+
// Create the memory directory so the agent can immediately write to it
|
|
107
|
+
ensureMemoryDir(memoryDir);
|
|
108
|
+
const existingMemory = readMemoryIndex(memoryDir);
|
|
109
|
+
const header = `# Agent Memory
|
|
110
|
+
|
|
111
|
+
You have a persistent memory directory at: ${memoryDir}/
|
|
112
|
+
Memory scope: ${scope}
|
|
113
|
+
|
|
114
|
+
This memory persists across sessions. Use it to build up knowledge over time.`;
|
|
115
|
+
const memoryContent = existingMemory
|
|
116
|
+
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
117
|
+
: `\n\nNo MEMORY.md exists yet. Create one at ${join(memoryDir, "MEMORY.md")} to start building persistent memory.`;
|
|
118
|
+
const instructions = `
|
|
119
|
+
|
|
120
|
+
## Memory Instructions
|
|
121
|
+
- MEMORY.md is an index file — keep it concise (under 200 lines). Lines after 200 are truncated.
|
|
122
|
+
- Store detailed memories in separate files within ${memoryDir}/ and link to them from MEMORY.md.
|
|
123
|
+
- Each memory file should use this frontmatter format:
|
|
124
|
+
\`\`\`markdown
|
|
125
|
+
---
|
|
126
|
+
name: <memory name>
|
|
127
|
+
description: <one-line description>
|
|
128
|
+
type: <user|feedback|project|reference>
|
|
129
|
+
---
|
|
130
|
+
<memory content>
|
|
131
|
+
\`\`\`
|
|
132
|
+
- Update or remove memories that become outdated. Check for existing memories before creating duplicates.
|
|
133
|
+
- You have Read, Write, and Edit tools available for managing memory files.`;
|
|
134
|
+
return header + memoryContent + instructions;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build a read-only memory block for agents that lack write/edit tools.
|
|
138
|
+
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
139
|
+
*/
|
|
140
|
+
export function buildReadOnlyMemoryBlock(agentName, scope, cwd) {
|
|
141
|
+
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
142
|
+
const existingMemory = readMemoryIndex(memoryDir);
|
|
143
|
+
const header = `# Agent Memory (read-only)
|
|
144
|
+
|
|
145
|
+
Memory scope: ${scope}
|
|
146
|
+
You have read-only access to memory. You can reference existing memories but cannot create or modify them.`;
|
|
147
|
+
const memoryContent = existingMemory
|
|
148
|
+
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
149
|
+
: `\n\nNo memory is available yet. Other agents or sessions with write access can create memories for you to consume.`;
|
|
150
|
+
return header + memoryContent;
|
|
151
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-file.ts — Streaming JSONL output file for agent transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Creates a per-agent output file that streams conversation turns as JSONL,
|
|
5
|
+
* matching Claude Code's task output file format.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
/** Create the output file path, ensuring the directory exists.
|
|
9
|
+
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
10
|
+
export declare function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string;
|
|
11
|
+
/** Write the initial user prompt entry. */
|
|
12
|
+
export declare function writeInitialEntry(path: string, agentId: string, prompt: string, cwd: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to session events and flush new messages to the output file on each turn_end.
|
|
15
|
+
* Returns a cleanup function that does a final flush and unsubscribes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function streamToOutputFile(session: AgentSession, path: string, agentId: string, cwd: string): () => void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-file.ts — Streaming JSONL output file for agent transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Creates a per-agent output file that streams conversation turns as JSONL,
|
|
5
|
+
* matching Claude Code's task output file format.
|
|
6
|
+
*/
|
|
7
|
+
import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
/** Create the output file path, ensuring the directory exists.
|
|
11
|
+
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
12
|
+
export function createOutputFilePath(cwd, agentId, sessionId) {
|
|
13
|
+
const encoded = cwd.replace(/\//g, "-").replace(/^-/, "");
|
|
14
|
+
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
15
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
16
|
+
chmodSync(root, 0o700);
|
|
17
|
+
const dir = join(root, encoded, sessionId, "tasks");
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
return join(dir, `${agentId}.output`);
|
|
20
|
+
}
|
|
21
|
+
/** Write the initial user prompt entry. */
|
|
22
|
+
export function writeInitialEntry(path, agentId, prompt, cwd) {
|
|
23
|
+
const entry = {
|
|
24
|
+
isSidechain: true,
|
|
25
|
+
agentId,
|
|
26
|
+
type: "user",
|
|
27
|
+
message: { role: "user", content: prompt },
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
cwd,
|
|
30
|
+
};
|
|
31
|
+
writeFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to session events and flush new messages to the output file on each turn_end.
|
|
35
|
+
* Returns a cleanup function that does a final flush and unsubscribes.
|
|
36
|
+
*/
|
|
37
|
+
export function streamToOutputFile(session, path, agentId, cwd) {
|
|
38
|
+
let writtenCount = 1; // initial user prompt already written
|
|
39
|
+
const flush = () => {
|
|
40
|
+
const messages = session.messages;
|
|
41
|
+
while (writtenCount < messages.length) {
|
|
42
|
+
const msg = messages[writtenCount];
|
|
43
|
+
const entry = {
|
|
44
|
+
isSidechain: true,
|
|
45
|
+
agentId,
|
|
46
|
+
type: msg.role === "assistant" ? "assistant" : msg.role === "user" ? "user" : "toolResult",
|
|
47
|
+
message: msg,
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
cwd,
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore write errors */ }
|
|
55
|
+
writtenCount++;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const unsubscribe = session.subscribe((event) => {
|
|
59
|
+
if (event.type === "turn_end")
|
|
60
|
+
flush();
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
flush();
|
|
64
|
+
unsubscribe();
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/prompts.d.ts
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* prompts.ts — System prompt builder for agents.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfig, EnvInfo } from "./types.js";
|
|
5
|
+
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
|
6
|
+
export interface PromptExtras {
|
|
7
|
+
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
|
8
|
+
memoryBlock?: string;
|
|
9
|
+
/** Preloaded skill contents to inject. */
|
|
10
|
+
skillBlocks?: {
|
|
11
|
+
name: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
5
15
|
/**
|
|
6
16
|
* Build the system prompt for an agent from its config.
|
|
7
17
|
*
|
|
@@ -10,5 +20,6 @@ import type { AgentConfig, EnvInfo } from "./types.js";
|
|
|
10
20
|
* - "append" with empty systemPrompt: pure parent clone
|
|
11
21
|
*
|
|
12
22
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
23
|
+
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
13
24
|
*/
|
|
14
|
-
export declare function buildAgentPrompt(config: AgentConfig, cwd: string, env: EnvInfo, parentSystemPrompt?: string): string;
|
|
25
|
+
export declare function buildAgentPrompt(config: AgentConfig, cwd: string, env: EnvInfo, parentSystemPrompt?: string, extras?: PromptExtras): string;
|
package/dist/prompts.js
CHANGED
|
@@ -9,12 +9,24 @@
|
|
|
9
9
|
* - "append" with empty systemPrompt: pure parent clone
|
|
10
10
|
*
|
|
11
11
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
12
|
+
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
12
13
|
*/
|
|
13
|
-
export function buildAgentPrompt(config, cwd, env, parentSystemPrompt) {
|
|
14
|
+
export function buildAgentPrompt(config, cwd, env, parentSystemPrompt, extras) {
|
|
14
15
|
const envBlock = `# Environment
|
|
15
16
|
Working directory: ${cwd}
|
|
16
17
|
${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
|
|
17
18
|
Platform: ${env.platform}`;
|
|
19
|
+
// Build optional extras suffix
|
|
20
|
+
const extraSections = [];
|
|
21
|
+
if (extras?.memoryBlock) {
|
|
22
|
+
extraSections.push(extras.memoryBlock);
|
|
23
|
+
}
|
|
24
|
+
if (extras?.skillBlocks?.length) {
|
|
25
|
+
for (const skill of extras.skillBlocks) {
|
|
26
|
+
extraSections.push(`\n# Preloaded Skill: ${skill.name}\n${skill.content}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const extrasSuffix = extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : "";
|
|
18
30
|
if (config.promptMode === "append") {
|
|
19
31
|
const identity = parentSystemPrompt || genericBase;
|
|
20
32
|
const bridge = `<sub_agent_context>
|
|
@@ -32,14 +44,14 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
32
44
|
const customSection = config.systemPrompt?.trim()
|
|
33
45
|
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
34
46
|
: "";
|
|
35
|
-
return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection;
|
|
47
|
+
return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection + extrasSuffix;
|
|
36
48
|
}
|
|
37
49
|
// "replace" mode — env header + the config's full system prompt
|
|
38
50
|
const replaceHeader = `You are a pi coding agent sub-agent.
|
|
39
51
|
You have been invoked to handle a specific task autonomously.
|
|
40
52
|
|
|
41
53
|
${envBlock}`;
|
|
42
|
-
return replaceHeader + "\n\n" + config.systemPrompt;
|
|
54
|
+
return replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix;
|
|
43
55
|
}
|
|
44
56
|
/** Fallback base prompt when parent system prompt is unavailable in append mode. */
|
|
45
57
|
const genericBase = `# Role
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-loader.ts — Preload specific skill files and inject their content into the system prompt.
|
|
3
|
+
*
|
|
4
|
+
* When skills is a string[], reads each named skill from .pi/skills/ or ~/.pi/skills/
|
|
5
|
+
* and returns their content for injection into the agent's system prompt.
|
|
6
|
+
*/
|
|
7
|
+
export interface PreloadedSkill {
|
|
8
|
+
name: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Attempt to load named skills from project and global skill directories.
|
|
13
|
+
* Looks for: <dir>/<name>.md, <dir>/<name>.txt, <dir>/<name>
|
|
14
|
+
*
|
|
15
|
+
* @param skillNames List of skill names to preload.
|
|
16
|
+
* @param cwd Working directory for project-level skills.
|
|
17
|
+
* @returns Array of loaded skills (missing skills are skipped with a warning comment).
|
|
18
|
+
*/
|
|
19
|
+
export declare function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-loader.ts — Preload specific skill files and inject their content into the system prompt.
|
|
3
|
+
*
|
|
4
|
+
* When skills is a string[], reads each named skill from .pi/skills/ or ~/.pi/skills/
|
|
5
|
+
* and returns their content for injection into the agent's system prompt.
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { isUnsafeName, safeReadFile } from "./memory.js";
|
|
10
|
+
/**
|
|
11
|
+
* Attempt to load named skills from project and global skill directories.
|
|
12
|
+
* Looks for: <dir>/<name>.md, <dir>/<name>.txt, <dir>/<name>
|
|
13
|
+
*
|
|
14
|
+
* @param skillNames List of skill names to preload.
|
|
15
|
+
* @param cwd Working directory for project-level skills.
|
|
16
|
+
* @returns Array of loaded skills (missing skills are skipped with a warning comment).
|
|
17
|
+
*/
|
|
18
|
+
export function preloadSkills(skillNames, cwd) {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const name of skillNames) {
|
|
21
|
+
// Unlike memory (which throws on unsafe names because it's part of agent setup),
|
|
22
|
+
// skills are optional — skip gracefully to avoid blocking agent startup.
|
|
23
|
+
if (isUnsafeName(name)) {
|
|
24
|
+
results.push({ name, content: `(Skill "${name}" skipped: name contains path traversal characters)` });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const content = findAndReadSkill(name, cwd);
|
|
28
|
+
if (content !== undefined) {
|
|
29
|
+
results.push({ name, content });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Include a note about missing skills so the agent knows it was requested but not found
|
|
33
|
+
results.push({ name, content: `(Skill "${name}" not found in .pi/skills/ or ~/.pi/skills/)` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Search for a skill file in project and global directories.
|
|
40
|
+
* Project-level takes priority over global.
|
|
41
|
+
*/
|
|
42
|
+
function findAndReadSkill(name, cwd) {
|
|
43
|
+
const projectDir = join(cwd, ".pi", "skills");
|
|
44
|
+
const globalDir = join(homedir(), ".pi", "skills");
|
|
45
|
+
// Try project first, then global
|
|
46
|
+
for (const dir of [projectDir, globalDir]) {
|
|
47
|
+
const content = tryReadSkillFile(dir, name);
|
|
48
|
+
if (content !== undefined)
|
|
49
|
+
return content;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Try to read a skill file from a directory.
|
|
55
|
+
* Tries extensions in order: .md, .txt, (no extension)
|
|
56
|
+
*/
|
|
57
|
+
function tryReadSkillFile(dir, name) {
|
|
58
|
+
const extensions = [".md", ".txt", ""];
|
|
59
|
+
for (const ext of extensions) {
|
|
60
|
+
const path = join(dir, name + ext);
|
|
61
|
+
// safeReadFile rejects symlinks to prevent reading arbitrary files
|
|
62
|
+
const content = safeReadFile(path);
|
|
63
|
+
if (content !== undefined)
|
|
64
|
+
return content.trim();
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* types.ts — Type definitions for the subagent system.
|
|
3
3
|
*/
|
|
4
|
-
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
5
4
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
5
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
export type { ThinkingLevel };
|
|
7
7
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
8
8
|
export type SubagentType = string;
|
|
9
9
|
/** Names of the three embedded default agents. */
|
|
10
10
|
export declare const DEFAULT_AGENT_NAMES: readonly ["general-purpose", "Explore", "Plan"];
|
|
11
|
+
/** Memory scope for persistent agent memory. */
|
|
12
|
+
export type MemoryScope = "user" | "project" | "local";
|
|
13
|
+
/** Isolation mode for agent execution. */
|
|
14
|
+
export type IsolationMode = "worktree";
|
|
11
15
|
/** Unified agent configuration — used for both default and user-defined agents. */
|
|
12
16
|
export interface AgentConfig {
|
|
13
17
|
name: string;
|
|
14
18
|
displayName?: string;
|
|
15
19
|
description: string;
|
|
16
20
|
builtinToolNames?: string[];
|
|
21
|
+
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
22
|
+
disallowedTools?: string[];
|
|
17
23
|
/** true = inherit all, string[] = only listed, false = none */
|
|
18
24
|
extensions: true | string[] | false;
|
|
19
25
|
/** true = inherit all, string[] = only listed, false = none */
|
|
@@ -29,6 +35,10 @@ export interface AgentConfig {
|
|
|
29
35
|
runInBackground: boolean;
|
|
30
36
|
/** Default for spawn: no extension tools */
|
|
31
37
|
isolated: boolean;
|
|
38
|
+
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
39
|
+
memory?: MemoryScope;
|
|
40
|
+
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
41
|
+
isolation?: IsolationMode;
|
|
32
42
|
/** true = this is an embedded default agent (informational) */
|
|
33
43
|
isDefault?: boolean;
|
|
34
44
|
/** false = agent is hidden from the registry */
|
|
@@ -54,6 +64,40 @@ export interface AgentRecord {
|
|
|
54
64
|
joinMode?: JoinMode;
|
|
55
65
|
/** Set when result was already consumed via get_subagent_result — suppresses completion notification. */
|
|
56
66
|
resultConsumed?: boolean;
|
|
67
|
+
/** Steering messages queued before the session was ready. */
|
|
68
|
+
pendingSteers?: string[];
|
|
69
|
+
/** Worktree info if the agent is running in an isolated worktree. */
|
|
70
|
+
worktree?: {
|
|
71
|
+
path: string;
|
|
72
|
+
branch: string;
|
|
73
|
+
};
|
|
74
|
+
/** Worktree cleanup result after agent completion. */
|
|
75
|
+
worktreeResult?: {
|
|
76
|
+
hasChanges: boolean;
|
|
77
|
+
branch?: string;
|
|
78
|
+
};
|
|
79
|
+
/** The tool_use_id from the original Agent tool call. */
|
|
80
|
+
toolCallId?: string;
|
|
81
|
+
/** Path to the streaming output transcript file. */
|
|
82
|
+
outputFile?: string;
|
|
83
|
+
/** Cleanup function for the output file stream subscription. */
|
|
84
|
+
outputCleanup?: () => void;
|
|
85
|
+
}
|
|
86
|
+
/** Details attached to custom notification messages for visual rendering. */
|
|
87
|
+
export interface NotificationDetails {
|
|
88
|
+
id: string;
|
|
89
|
+
description: string;
|
|
90
|
+
status: string;
|
|
91
|
+
toolUses: number;
|
|
92
|
+
turnCount: number;
|
|
93
|
+
maxTurns?: number;
|
|
94
|
+
totalTokens: number;
|
|
95
|
+
durationMs: number;
|
|
96
|
+
outputFile?: string;
|
|
97
|
+
error?: string;
|
|
98
|
+
resultPreview: string;
|
|
99
|
+
/** Additional agents in a group notification. */
|
|
100
|
+
others?: NotificationDetails[];
|
|
57
101
|
}
|
|
58
102
|
export interface EnvInfo {
|
|
59
103
|
isGitRepo: boolean;
|
|
@@ -36,6 +36,10 @@ export interface AgentActivity {
|
|
|
36
36
|
};
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
|
+
/** Current turn count. */
|
|
40
|
+
turnCount: number;
|
|
41
|
+
/** Effective max turns for this agent (undefined = unlimited). */
|
|
42
|
+
maxTurns?: number;
|
|
39
43
|
}
|
|
40
44
|
/** Metadata attached to Agent tool results for custom rendering. */
|
|
41
45
|
export interface AgentDetails {
|
|
@@ -54,11 +58,17 @@ export interface AgentDetails {
|
|
|
54
58
|
modelName?: string;
|
|
55
59
|
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
56
60
|
tags?: string[];
|
|
61
|
+
/** Current turn count. */
|
|
62
|
+
turnCount?: number;
|
|
63
|
+
/** Effective max turns (undefined = unlimited). */
|
|
64
|
+
maxTurns?: number;
|
|
57
65
|
agentId?: string;
|
|
58
66
|
error?: string;
|
|
59
67
|
}
|
|
60
68
|
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
61
69
|
export declare function formatTokens(count: number): string;
|
|
70
|
+
/** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
|
|
71
|
+
export declare function formatTurns(turnCount: number, maxTurns?: number | null): string;
|
|
62
72
|
/** Format milliseconds as human-readable duration. */
|
|
63
73
|
export declare function formatMs(ms: number): string;
|
|
64
74
|
/** Format duration from start/completed timestamps. */
|
|
@@ -79,6 +89,12 @@ export declare class AgentWidget {
|
|
|
79
89
|
private finishedTurnAge;
|
|
80
90
|
/** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
|
|
81
91
|
private static readonly ERROR_LINGER_TURNS;
|
|
92
|
+
/** Whether the widget callback is currently registered with the TUI. */
|
|
93
|
+
private widgetRegistered;
|
|
94
|
+
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
95
|
+
private tui;
|
|
96
|
+
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
97
|
+
private lastStatusText;
|
|
82
98
|
constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>);
|
|
83
99
|
/** Set the UI context (grabbed from first tool execution). */
|
|
84
100
|
setUICtx(ctx: UICtx): void;
|
|
@@ -95,6 +111,11 @@ export declare class AgentWidget {
|
|
|
95
111
|
markFinished(agentId: string): void;
|
|
96
112
|
/** Render a finished agent line. */
|
|
97
113
|
private renderFinishedLine;
|
|
114
|
+
/**
|
|
115
|
+
* Render the widget content. Called from the registered widget's render() callback,
|
|
116
|
+
* reading live state each time instead of capturing it in a closure.
|
|
117
|
+
*/
|
|
118
|
+
private renderWidget;
|
|
98
119
|
/** Force an immediate widget update. */
|
|
99
120
|
update(): void;
|
|
100
121
|
dispose(): void;
|