@esso0428/pi-subagents 0.15.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 +638 -0
- package/CONTRIBUTING.md +68 -0
- package/LICENSE +21 -0
- package/README.md +745 -0
- package/SECURITY.md +95 -0
- package/dist/agent-manager.d.ts +144 -0
- package/dist/agent-manager.js +542 -0
- package/dist/agent-runner.d.ts +212 -0
- package/dist/agent-runner.js +850 -0
- package/dist/agent-types.d.ts +67 -0
- package/dist/agent-types.js +168 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +76 -0
- package/dist/custom-agents.d.ts +17 -0
- package/dist/custom-agents.js +156 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +122 -0
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +2209 -0
- package/dist/invocation-config.d.ts +22 -0
- package/dist/invocation-config.js +15 -0
- package/dist/memory.d.ts +53 -0
- package/dist/memory.js +165 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +80 -0
- package/dist/nico-overrides.d.ts +53 -0
- package/dist/nico-overrides.js +169 -0
- package/dist/output-file.d.ts +24 -0
- package/dist/output-file.js +101 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +73 -0
- package/dist/schedule-store.d.ts +38 -0
- package/dist/schedule-store.js +155 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +141 -0
- package/dist/settings.js +162 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +160 -0
- package/dist/ui/agent-widget.js +484 -0
- package/dist/ui/conversation-viewer.d.ts +57 -0
- package/dist/ui/conversation-viewer.js +354 -0
- package/dist/ui/fleet-list.d.ts +106 -0
- package/dist/ui/fleet-list.js +345 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +95 -0
- package/dist/ui/viewer-keys.d.ts +20 -0
- package/dist/ui/viewer-keys.js +17 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/dist/worktree.d.ts +45 -0
- package/dist/worktree.js +160 -0
- package/examples/agent-tool-description.md +42 -0
- package/package.json +56 -0
- package/src/agent-manager.ts +631 -0
- package/src/agent-runner.ts +1014 -0
- package/src/agent-types.ts +202 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +122 -0
- package/src/custom-agents.ts +167 -0
- package/src/default-agents.ts +126 -0
- package/src/enabled-models.ts +180 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +2400 -0
- package/src/invocation-config.ts +40 -0
- package/src/memory.ts +179 -0
- package/src/model-resolver.ts +100 -0
- package/src/nico-overrides.ts +235 -0
- package/src/output-file.ts +110 -0
- package/src/prompts.ts +99 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +288 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +25 -0
- package/src/types.ts +208 -0
- package/src/ui/agent-widget.ts +566 -0
- package/src/ui/conversation-viewer.ts +362 -0
- package/src/ui/fleet-list.ts +380 -0
- package/src/ui/schedule-menu.ts +104 -0
- package/src/ui/viewer-keys.ts +39 -0
- package/src/usage.ts +60 -0
- package/src/worktree.ts +191 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentConfig, IsolationMode, JoinMode, ThinkingLevel } from "./types.js";
|
|
2
|
+
interface AgentInvocationParams {
|
|
3
|
+
model?: string;
|
|
4
|
+
thinking?: string;
|
|
5
|
+
max_turns?: number;
|
|
6
|
+
run_in_background?: boolean;
|
|
7
|
+
inherit_context?: boolean;
|
|
8
|
+
isolated?: boolean;
|
|
9
|
+
isolation?: IsolationMode;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveAgentInvocationConfig(agentConfig: AgentConfig | undefined, params: AgentInvocationParams): {
|
|
12
|
+
modelInput?: string;
|
|
13
|
+
modelFromParams: boolean;
|
|
14
|
+
thinking?: ThinkingLevel;
|
|
15
|
+
maxTurns?: number;
|
|
16
|
+
inheritContext: boolean;
|
|
17
|
+
runInBackground: boolean;
|
|
18
|
+
isolated: boolean;
|
|
19
|
+
isolation?: IsolationMode;
|
|
20
|
+
};
|
|
21
|
+
export declare function resolveJoinMode(defaultJoinMode: JoinMode, runInBackground: boolean): JoinMode | undefined;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function resolveAgentInvocationConfig(agentConfig, params) {
|
|
2
|
+
return {
|
|
3
|
+
modelInput: agentConfig?.model ?? params.model,
|
|
4
|
+
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
5
|
+
thinking: (agentConfig?.thinking ?? params.thinking),
|
|
6
|
+
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
7
|
+
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
8
|
+
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
9
|
+
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
10
|
+
isolation: agentConfig?.isolation ?? params.isolation,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function resolveJoinMode(defaultJoinMode, runInBackground) {
|
|
14
|
+
return runInBackground ? defaultJoinMode : undefined;
|
|
15
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
+
*
|
|
4
|
+
* Memory scopes:
|
|
5
|
+
* - "user" → getAgentDir()/agent-memory/{agent-name}/ (default ~/.pi/agent/agent-memory/, honors $PI_CODING_AGENT_DIR)
|
|
6
|
+
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
+
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
+
*
|
|
9
|
+
* The user scope previously hardcoded ~/.pi/agent-memory/. That legacy location
|
|
10
|
+
* is still honored (read + write) when it exists and the new location doesn't,
|
|
11
|
+
* so existing memories aren't orphaned.
|
|
12
|
+
*/
|
|
13
|
+
import type { MemoryScope } from "./types.js";
|
|
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 declare function isUnsafeName(name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
21
|
+
*/
|
|
22
|
+
export declare function isSymlink(filePath: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Safely read a file, rejecting symlinks.
|
|
25
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
26
|
+
*/
|
|
27
|
+
export declare function safeReadFile(filePath: string): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
30
|
+
* Throws if agentName contains path traversal characters.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveMemoryDir(agentName: string, scope: MemoryScope, cwd: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Ensure the memory directory exists, creating it if needed.
|
|
35
|
+
* Refuses to create directories if any component in the path is a symlink
|
|
36
|
+
* to prevent symlink-based directory traversal attacks.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureMemoryDir(memoryDir: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
41
|
+
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readMemoryIndex(memoryDir: string): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Build the memory block to inject into the agent's system prompt.
|
|
46
|
+
* Also ensures the memory directory exists (creates it if needed).
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Build a read-only memory block for agents that lack write/edit tools.
|
|
51
|
+
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildReadOnlyMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string;
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
+
*
|
|
4
|
+
* Memory scopes:
|
|
5
|
+
* - "user" → getAgentDir()/agent-memory/{agent-name}/ (default ~/.pi/agent/agent-memory/, honors $PI_CODING_AGENT_DIR)
|
|
6
|
+
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
+
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
+
*
|
|
9
|
+
* The user scope previously hardcoded ~/.pi/agent-memory/. That legacy location
|
|
10
|
+
* is still honored (read + write) when it exists and the new location doesn't,
|
|
11
|
+
* so existing memories aren't orphaned.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join, } from "node:path";
|
|
16
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
/** Maximum lines to read from MEMORY.md */
|
|
18
|
+
const MAX_MEMORY_LINES = 200;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
21
|
+
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
22
|
+
*/
|
|
23
|
+
export function isUnsafeName(name) {
|
|
24
|
+
if (!name || name.length > 128)
|
|
25
|
+
return true;
|
|
26
|
+
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
30
|
+
*/
|
|
31
|
+
export function isSymlink(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
return lstatSync(filePath).isSymbolicLink();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Safely read a file, rejecting symlinks.
|
|
41
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
42
|
+
*/
|
|
43
|
+
export function safeReadFile(filePath) {
|
|
44
|
+
if (!existsSync(filePath))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (isSymlink(filePath))
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return readFileSync(filePath, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
57
|
+
* Throws if agentName contains path traversal characters.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveMemoryDir(agentName, scope, cwd) {
|
|
60
|
+
if (isUnsafeName(agentName)) {
|
|
61
|
+
throw new Error(`Unsafe agent name for memory directory: "${agentName}"`);
|
|
62
|
+
}
|
|
63
|
+
switch (scope) {
|
|
64
|
+
case "user": {
|
|
65
|
+
const current = join(getAgentDir(), "agent-memory", agentName);
|
|
66
|
+
// Legacy location from when this path was hardcoded. Keep using it if it
|
|
67
|
+
// already holds this agent's memory and the new location hasn't been
|
|
68
|
+
// created yet — otherwise existing memories would be silently orphaned.
|
|
69
|
+
const legacy = join(homedir(), ".pi", "agent-memory", agentName);
|
|
70
|
+
if (!existsSync(current) && existsSync(legacy) && !isSymlink(legacy)) {
|
|
71
|
+
return legacy;
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
case "project":
|
|
76
|
+
return join(cwd, ".pi", "agent-memory", agentName);
|
|
77
|
+
case "local":
|
|
78
|
+
return join(cwd, ".pi", "agent-memory-local", agentName);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Ensure the memory directory exists, creating it if needed.
|
|
83
|
+
* Refuses to create directories if any component in the path is a symlink
|
|
84
|
+
* to prevent symlink-based directory traversal attacks.
|
|
85
|
+
*/
|
|
86
|
+
export function ensureMemoryDir(memoryDir) {
|
|
87
|
+
// If the directory already exists, verify it's not a symlink
|
|
88
|
+
if (existsSync(memoryDir)) {
|
|
89
|
+
if (isSymlink(memoryDir)) {
|
|
90
|
+
throw new Error(`Refusing to use symlinked memory directory: ${memoryDir}`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
98
|
+
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
99
|
+
*/
|
|
100
|
+
export function readMemoryIndex(memoryDir) {
|
|
101
|
+
// Reject symlinked memory directories
|
|
102
|
+
if (isSymlink(memoryDir))
|
|
103
|
+
return undefined;
|
|
104
|
+
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
105
|
+
const content = safeReadFile(memoryFile);
|
|
106
|
+
if (content === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const lines = content.split("\n");
|
|
109
|
+
if (lines.length > MAX_MEMORY_LINES) {
|
|
110
|
+
return lines.slice(0, MAX_MEMORY_LINES).join("\n") + "\n... (truncated at 200 lines)";
|
|
111
|
+
}
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the memory block to inject into the agent's system prompt.
|
|
116
|
+
* Also ensures the memory directory exists (creates it if needed).
|
|
117
|
+
*/
|
|
118
|
+
export function buildMemoryBlock(agentName, scope, cwd) {
|
|
119
|
+
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
120
|
+
// Create the memory directory so the agent can immediately write to it
|
|
121
|
+
ensureMemoryDir(memoryDir);
|
|
122
|
+
const existingMemory = readMemoryIndex(memoryDir);
|
|
123
|
+
const header = `# Agent Memory
|
|
124
|
+
|
|
125
|
+
You have a persistent memory directory at: ${memoryDir}/
|
|
126
|
+
Memory scope: ${scope}
|
|
127
|
+
|
|
128
|
+
This memory persists across sessions. Use it to build up knowledge over time.`;
|
|
129
|
+
const memoryContent = existingMemory
|
|
130
|
+
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
131
|
+
: `\n\nNo MEMORY.md exists yet. Create one at ${join(memoryDir, "MEMORY.md")} to start building persistent memory.`;
|
|
132
|
+
const instructions = `
|
|
133
|
+
|
|
134
|
+
## Memory Instructions
|
|
135
|
+
- MEMORY.md is an index file — keep it concise (under 200 lines). Lines after 200 are truncated.
|
|
136
|
+
- Store detailed memories in separate files within ${memoryDir}/ and link to them from MEMORY.md.
|
|
137
|
+
- Each memory file should use this frontmatter format:
|
|
138
|
+
\`\`\`markdown
|
|
139
|
+
---
|
|
140
|
+
name: <memory name>
|
|
141
|
+
description: <one-line description>
|
|
142
|
+
type: <user|feedback|project|reference>
|
|
143
|
+
---
|
|
144
|
+
<memory content>
|
|
145
|
+
\`\`\`
|
|
146
|
+
- Update or remove memories that become outdated. Check for existing memories before creating duplicates.
|
|
147
|
+
- You have Read, Write, and Edit tools available for managing memory files.`;
|
|
148
|
+
return header + memoryContent + instructions;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build a read-only memory block for agents that lack write/edit tools.
|
|
152
|
+
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
153
|
+
*/
|
|
154
|
+
export function buildReadOnlyMemoryBlock(agentName, scope, cwd) {
|
|
155
|
+
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
156
|
+
const existingMemory = readMemoryIndex(memoryDir);
|
|
157
|
+
const header = `# Agent Memory (read-only)
|
|
158
|
+
|
|
159
|
+
Memory scope: ${scope}
|
|
160
|
+
You have read-only access to memory. You can reference existing memories but cannot create or modify them.`;
|
|
161
|
+
const memoryContent = existingMemory
|
|
162
|
+
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
163
|
+
: `\n\nNo memory is available yet. Other agents or sessions with write access can create memories for you to consume.`;
|
|
164
|
+
return header + memoryContent;
|
|
165
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model resolution: exact match ("provider/modelId") with fuzzy fallback.
|
|
3
|
+
*/
|
|
4
|
+
export interface ModelEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ModelRegistry {
|
|
10
|
+
find(provider: string, modelId: string): any;
|
|
11
|
+
getAll(): any[];
|
|
12
|
+
getAvailable?(): any[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a model string to a Model instance.
|
|
16
|
+
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
|
17
|
+
* Returns the Model on success, or an error message string on failure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveModel(input: string, registry: ModelRegistry): any | string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model resolution: exact match ("provider/modelId") with fuzzy fallback.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a model string to a Model instance.
|
|
6
|
+
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
|
7
|
+
* Returns the Model on success, or an error message string on failure.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveModel(input, registry) {
|
|
10
|
+
// Available models (those with auth configured)
|
|
11
|
+
const all = (registry.getAvailable?.() ?? registry.getAll());
|
|
12
|
+
const availableSet = new Set(all.map(m => `${m.provider}/${m.id}`.toLowerCase()));
|
|
13
|
+
// 1. Exact match: "provider/modelId" — only if available (has auth)
|
|
14
|
+
const slashIdx = input.indexOf("/");
|
|
15
|
+
if (slashIdx !== -1) {
|
|
16
|
+
const provider = input.slice(0, slashIdx);
|
|
17
|
+
const modelId = input.slice(slashIdx + 1);
|
|
18
|
+
if (availableSet.has(input.toLowerCase())) {
|
|
19
|
+
const found = registry.find(provider, modelId);
|
|
20
|
+
if (found)
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// 2. Fuzzy match against available models. Normalize separators so cosmetic
|
|
25
|
+
// punctuation differences still match — e.g. "claude-haiku-4.5" and
|
|
26
|
+
// "claude-haiku-4-5" (dot vs dash in the version) resolve to the same model.
|
|
27
|
+
const normalize = (s) => s.toLowerCase().replace(/\./g, "-");
|
|
28
|
+
const query = normalize(input);
|
|
29
|
+
// Score each model: prefer exact id match > id contains > name contains > provider+id contains
|
|
30
|
+
let bestMatch;
|
|
31
|
+
let bestScore = 0;
|
|
32
|
+
for (const m of all) {
|
|
33
|
+
const id = normalize(m.id);
|
|
34
|
+
const name = normalize(m.name);
|
|
35
|
+
const full = normalize(`${m.provider}/${m.id}`);
|
|
36
|
+
let score = 0;
|
|
37
|
+
if (id === query || full === query) {
|
|
38
|
+
score = 100; // exact
|
|
39
|
+
}
|
|
40
|
+
else if (id.includes(query) || full.includes(query)) {
|
|
41
|
+
score = 60 + (query.length / id.length) * 30; // substring, prefer tighter matches
|
|
42
|
+
}
|
|
43
|
+
else if (name.includes(query)) {
|
|
44
|
+
score = 40 + (query.length / name.length) * 20;
|
|
45
|
+
}
|
|
46
|
+
else if (
|
|
47
|
+
// A trailing date-stamp token (e.g. "20251001") is optional, so a
|
|
48
|
+
// date-pinned config like "claude-haiku-4-5-20251001" still matches an
|
|
49
|
+
// undated registry id like "claude-haiku-4-5".
|
|
50
|
+
query
|
|
51
|
+
.split(/[\s\-/]+/)
|
|
52
|
+
.every(part => /^\d{8}$/.test(part) || id.includes(part) || name.includes(part) || m.provider.toLowerCase().includes(part))) {
|
|
53
|
+
score = 20; // all parts present somewhere
|
|
54
|
+
}
|
|
55
|
+
if (score > bestScore) {
|
|
56
|
+
bestScore = score;
|
|
57
|
+
bestMatch = m;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (bestMatch && bestScore >= 20) {
|
|
61
|
+
const found = registry.find(bestMatch.provider, bestMatch.id);
|
|
62
|
+
if (found)
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
// 3. Provider fallback: a "provider/modelId" query that didn't match under the
|
|
66
|
+
// named provider (exact or fuzzy above) retries against all providers. The
|
|
67
|
+
// named provider is preferred when present; this only kicks in when it isn't,
|
|
68
|
+
// so the same model from another provider beats falling back to "inherit".
|
|
69
|
+
if (slashIdx !== -1) {
|
|
70
|
+
const bare = resolveModel(input.slice(slashIdx + 1), registry);
|
|
71
|
+
if (typeof bare !== "string")
|
|
72
|
+
return bare;
|
|
73
|
+
}
|
|
74
|
+
// 4. No match — list available models
|
|
75
|
+
const modelList = all
|
|
76
|
+
.map(m => ` ${m.provider}/${m.id}`)
|
|
77
|
+
.sort()
|
|
78
|
+
.join("\n");
|
|
79
|
+
return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nico-overrides.ts — Read and apply agent overrides from `npm:pi-subagents`-style
|
|
3
|
+
* `settings.json` (`subagents.agentOverrides`).
|
|
4
|
+
*
|
|
5
|
+
* Priority (after `@tintinweb/pi-subagents` has resolved its own .md chain):
|
|
6
|
+
* 1. Local JSON (.pi/settings.json) ← highest
|
|
7
|
+
* 2. Global JSON (~/.pi/agent/settings.json)
|
|
8
|
+
*
|
|
9
|
+
* Overrides are applied to matching agents in the registry. If an agent name
|
|
10
|
+
* from the overrides does not exist in the registry, it is auto-registered
|
|
11
|
+
* using the override fields as the full definition (no .md file needed).
|
|
12
|
+
*
|
|
13
|
+
* Skill mapping (Nico string[] → tintinweb true | string[] | false):
|
|
14
|
+
* ["*"] → true (all skills)
|
|
15
|
+
* ["foo"] → ["foo"] (only listed)
|
|
16
|
+
* [] | false → false (none)
|
|
17
|
+
* omitted → true (all, fallback)
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentConfig } from "./types.js";
|
|
20
|
+
export interface NicoAgentOverride {
|
|
21
|
+
model?: string | false;
|
|
22
|
+
thinking?: string | false;
|
|
23
|
+
fallbackModels?: string[];
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
systemPromptMode?: "append" | "replace";
|
|
26
|
+
inheritProjectContext?: boolean;
|
|
27
|
+
inheritSkills?: boolean;
|
|
28
|
+
defaultContext?: "fresh" | "fork" | false;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
skills?: string[] | false;
|
|
31
|
+
tools?: string[] | false;
|
|
32
|
+
completionGuard?: boolean;
|
|
33
|
+
toolBudget?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read subagents.agentOverrides and subagents.defaultModel from both local
|
|
37
|
+
* and global Nico-style settings.json. Local overrides global on key collision.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readNicoAgentOverrides(cwd: string): {
|
|
40
|
+
overrides: Record<string, NicoAgentOverride>;
|
|
41
|
+
defaultModel: string | undefined;
|
|
42
|
+
};
|
|
43
|
+
export declare function resolveNicoSkills(skills: string[] | false | undefined): true | string[] | false;
|
|
44
|
+
/**
|
|
45
|
+
* Apply a Nico-style override to an existing tintinweb AgentConfig.
|
|
46
|
+
* JSON values directly overwrite the config (highest priority).
|
|
47
|
+
*/
|
|
48
|
+
export declare function applyNicoOverride(agent: AgentConfig, override: NicoAgentOverride, nicoDefaultModel?: string): AgentConfig;
|
|
49
|
+
/**
|
|
50
|
+
* Apply all Nico overrides to a map of agents (mutating in-place).
|
|
51
|
+
* Agents that don't exist yet are auto-registered from the override.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyNicoOverridesToMap(agents: Map<string, AgentConfig>, overrides: Record<string, NicoAgentOverride>, defaultModel?: string): void;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nico-overrides.ts — Read and apply agent overrides from `npm:pi-subagents`-style
|
|
3
|
+
* `settings.json` (`subagents.agentOverrides`).
|
|
4
|
+
*
|
|
5
|
+
* Priority (after `@tintinweb/pi-subagents` has resolved its own .md chain):
|
|
6
|
+
* 1. Local JSON (.pi/settings.json) ← highest
|
|
7
|
+
* 2. Global JSON (~/.pi/agent/settings.json)
|
|
8
|
+
*
|
|
9
|
+
* Overrides are applied to matching agents in the registry. If an agent name
|
|
10
|
+
* from the overrides does not exist in the registry, it is auto-registered
|
|
11
|
+
* using the override fields as the full definition (no .md file needed).
|
|
12
|
+
*
|
|
13
|
+
* Skill mapping (Nico string[] → tintinweb true | string[] | false):
|
|
14
|
+
* ["*"] → true (all skills)
|
|
15
|
+
* ["foo"] → ["foo"] (only listed)
|
|
16
|
+
* [] | false → false (none)
|
|
17
|
+
* omitted → true (all, fallback)
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Reader
|
|
25
|
+
// ============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Read subagents.agentOverrides and subagents.defaultModel from both local
|
|
28
|
+
* and global Nico-style settings.json. Local overrides global on key collision.
|
|
29
|
+
*/
|
|
30
|
+
export function readNicoAgentOverrides(cwd) {
|
|
31
|
+
const merged = {};
|
|
32
|
+
let defaultModel;
|
|
33
|
+
// Global: ~/.pi/agent/settings.json
|
|
34
|
+
const globalPath = join(getAgentDir(), "settings.json");
|
|
35
|
+
const globalSettings = readNicoSettingsFile(globalPath);
|
|
36
|
+
if (globalSettings) {
|
|
37
|
+
mergeOverrides(merged, globalSettings.agentOverrides);
|
|
38
|
+
if (defaultModel === undefined)
|
|
39
|
+
defaultModel = globalSettings.defaultModel;
|
|
40
|
+
}
|
|
41
|
+
// Local: .pi/settings.json
|
|
42
|
+
const localPath = join(cwd, ".pi", "settings.json");
|
|
43
|
+
if (existsSync(localPath)) {
|
|
44
|
+
const localSettings = readNicoSettingsFile(localPath);
|
|
45
|
+
if (localSettings) {
|
|
46
|
+
mergeOverrides(merged, localSettings.agentOverrides);
|
|
47
|
+
if (localSettings.defaultModel !== undefined)
|
|
48
|
+
defaultModel = localSettings.defaultModel;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { overrides: merged, defaultModel };
|
|
52
|
+
}
|
|
53
|
+
function readNicoSettingsFile(filePath) {
|
|
54
|
+
if (!existsSync(filePath))
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
58
|
+
const sub = raw?.subagents;
|
|
59
|
+
if (!sub || typeof sub !== "object")
|
|
60
|
+
return undefined;
|
|
61
|
+
return sub;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Silently skip — bad Nico config must not break tintinweb
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function mergeOverrides(target, source) {
|
|
69
|
+
if (!source)
|
|
70
|
+
return;
|
|
71
|
+
for (const [name, override] of Object.entries(source)) {
|
|
72
|
+
target[name] = { ...target[name], ...override };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Skill converter: Nico string[] → tintinweb true | string[] | false
|
|
77
|
+
// ============================================================================
|
|
78
|
+
export function resolveNicoSkills(skills) {
|
|
79
|
+
// Not set → inherit all (tintinweb default)
|
|
80
|
+
if (skills === undefined)
|
|
81
|
+
return true;
|
|
82
|
+
// Explicit false / empty array → none
|
|
83
|
+
if (skills === false || skills.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
// ["*"] → all skills
|
|
86
|
+
if (skills.length === 1 && skills[0] === "*")
|
|
87
|
+
return true;
|
|
88
|
+
// Specific list
|
|
89
|
+
return [...skills];
|
|
90
|
+
}
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Applier — override existing agent
|
|
93
|
+
// ============================================================================
|
|
94
|
+
/**
|
|
95
|
+
* Apply a Nico-style override to an existing tintinweb AgentConfig.
|
|
96
|
+
* JSON values directly overwrite the config (highest priority).
|
|
97
|
+
*/
|
|
98
|
+
export function applyNicoOverride(agent, override, nicoDefaultModel) {
|
|
99
|
+
let modified = false;
|
|
100
|
+
let next = agent;
|
|
101
|
+
// model: explicit override wins; else use defaultModel when agent has none
|
|
102
|
+
if (override.model !== undefined) {
|
|
103
|
+
next = { ...next, model: override.model === false ? undefined : override.model };
|
|
104
|
+
modified = true;
|
|
105
|
+
}
|
|
106
|
+
else if (nicoDefaultModel !== undefined && next.model === undefined) {
|
|
107
|
+
next = { ...next, model: nicoDefaultModel };
|
|
108
|
+
modified = true;
|
|
109
|
+
}
|
|
110
|
+
if (override.thinking !== undefined) {
|
|
111
|
+
next = { ...next, thinking: override.thinking === false ? undefined : override.thinking };
|
|
112
|
+
modified = true;
|
|
113
|
+
}
|
|
114
|
+
if (override.systemPrompt !== undefined) {
|
|
115
|
+
next = { ...next, systemPrompt: override.systemPrompt };
|
|
116
|
+
modified = true;
|
|
117
|
+
}
|
|
118
|
+
if (override.disabled !== undefined) {
|
|
119
|
+
next = { ...next, enabled: !override.disabled };
|
|
120
|
+
modified = true;
|
|
121
|
+
}
|
|
122
|
+
if (override.tools !== undefined) {
|
|
123
|
+
next = { ...next, builtinToolNames: override.tools === false ? [] : [...override.tools] };
|
|
124
|
+
modified = true;
|
|
125
|
+
}
|
|
126
|
+
return modified ? next : agent;
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Auto-register — create AgentConfig from override when agent doesn't exist
|
|
130
|
+
// ============================================================================
|
|
131
|
+
function createAgentFromOverride(name, override, defaultModel) {
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
displayName: name,
|
|
135
|
+
description: `Auto-registered from npm:pi-subagents JSON settings`,
|
|
136
|
+
builtinToolNames: override.tools !== undefined
|
|
137
|
+
? (override.tools === false ? [] : [...override.tools])
|
|
138
|
+
: [...BUILTIN_TOOL_NAMES],
|
|
139
|
+
extensions: true,
|
|
140
|
+
skills: resolveNicoSkills(override.skills),
|
|
141
|
+
model: override.model !== undefined ? (override.model === false ? undefined : override.model) : defaultModel,
|
|
142
|
+
thinking: override.thinking !== undefined
|
|
143
|
+
? (override.thinking === false ? undefined : override.thinking)
|
|
144
|
+
: undefined,
|
|
145
|
+
systemPrompt: override.systemPrompt ?? "",
|
|
146
|
+
promptMode: override.systemPromptMode === "append" ? "append" : "replace",
|
|
147
|
+
enabled: !(override.disabled ?? false),
|
|
148
|
+
source: "global",
|
|
149
|
+
isDefault: false,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Bulk apply
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Apply all Nico overrides to a map of agents (mutating in-place).
|
|
157
|
+
* Agents that don't exist yet are auto-registered from the override.
|
|
158
|
+
*/
|
|
159
|
+
export function applyNicoOverridesToMap(agents, overrides, defaultModel) {
|
|
160
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
161
|
+
const existing = agents.get(name);
|
|
162
|
+
if (existing) {
|
|
163
|
+
agents.set(name, applyNicoOverride(existing, override, defaultModel));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
agents.set(name, createAgentFromOverride(name, override, defaultModel));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 "@earendil-works/pi-coding-agent";
|
|
8
|
+
/**
|
|
9
|
+
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
10
|
+
* - POSIX: "/home/user/project" → "home-user-project"
|
|
11
|
+
* - Windows: "C:\Users\foo\project" → "Users-foo-project"
|
|
12
|
+
* - UNC: "\\\\server\\share\\project" → "server-share-project"
|
|
13
|
+
*/
|
|
14
|
+
export declare function encodeCwd(cwd: string): string;
|
|
15
|
+
/** Create the output file path, ensuring the directory exists.
|
|
16
|
+
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
17
|
+
export declare function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string;
|
|
18
|
+
/** Write the initial user prompt entry. */
|
|
19
|
+
export declare function writeInitialEntry(path: string, agentId: string, prompt: string, cwd: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe to session events and flush new messages to the output file on each turn_end.
|
|
22
|
+
* Returns a cleanup function that does a final flush and unsubscribes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function streamToOutputFile(session: AgentSession, path: string, agentId: string, cwd: string): () => void;
|