surgent 0.7.0-alpha.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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/surgent.js +211 -0
- package/dist/optimizers/LICENSE +21 -0
- package/dist/optimizers/index.js +1984 -0
- package/dist/optimizers/index.js.map +7 -0
- package/dist/optimizers/package.json +31 -0
- package/package.json +45 -0
- package/src/agent/built-in/documenter.md +58 -0
- package/src/agent/built-in/general.md +107 -0
- package/src/agent/built-in/planner.md +73 -0
- package/src/agent/built-in/scout.md +97 -0
- package/src/agent/command.ts +140 -0
- package/src/agent/helpers.ts +95 -0
- package/src/agent/index.ts +9 -0
- package/src/agent/storage.ts +287 -0
- package/src/agent/types.ts +28 -0
- package/src/checkpoint/git.ts +173 -0
- package/src/checkpoint/index.ts +117 -0
- package/src/checkpoint/snapshot.ts +28 -0
- package/src/checkpoint/stage.ts +59 -0
- package/src/checkpoint/store.ts +108 -0
- package/src/cleanup/checkpoint.ts +31 -0
- package/src/cleanup/helpers.ts +24 -0
- package/src/cleanup/index.ts +21 -0
- package/src/cleanup/permission.ts +74 -0
- package/src/cleanup/subsession.ts +46 -0
- package/src/commands/helpers.ts +217 -0
- package/src/commands/index.ts +79 -0
- package/src/commands/render.ts +95 -0
- package/src/commands/types.ts +11 -0
- package/src/mcp-client/call-tool.ts +143 -0
- package/src/mcp-client/client.ts +90 -0
- package/src/mcp-client/command.ts +257 -0
- package/src/mcp-client/helpers.ts +153 -0
- package/src/mcp-client/index.ts +21 -0
- package/src/mcp-client/list-tools.ts +84 -0
- package/src/mcp-client/storage.ts +190 -0
- package/src/mcp-client/types.ts +34 -0
- package/src/mcp-client/validation.ts +115 -0
- package/src/optimizers/compactor/bash.ts +159 -0
- package/src/optimizers/compactor/grep.ts +141 -0
- package/src/optimizers/compactor/index.ts +132 -0
- package/src/optimizers/deduplicator/helpers.ts +75 -0
- package/src/optimizers/deduplicator/index.ts +23 -0
- package/src/optimizers/deduplicator/resources.ts +77 -0
- package/src/optimizers/deduplicator/state.ts +119 -0
- package/src/optimizers/deduplicator/types.ts +14 -0
- package/src/optimizers/entries.ts +104 -0
- package/src/optimizers/index.ts +17 -0
- package/src/optimizers/inspector/helpers.ts +60 -0
- package/src/optimizers/inspector/index.ts +89 -0
- package/src/optimizers/inspector/inspect.ts +88 -0
- package/src/optimizers/inspector/types.ts +7 -0
- package/src/optimizers/languages/go.ts +79 -0
- package/src/optimizers/languages/grammar.ts +200 -0
- package/src/optimizers/languages/index.ts +75 -0
- package/src/optimizers/languages/java.ts +64 -0
- package/src/optimizers/languages/python.ts +63 -0
- package/src/optimizers/languages/rust.ts +71 -0
- package/src/optimizers/languages/symbols.ts +95 -0
- package/src/optimizers/languages/tree-sitter-languages.d.ts +23 -0
- package/src/optimizers/languages/types.ts +134 -0
- package/src/optimizers/languages/typescript.ts +116 -0
- package/src/optimizers/mapper/files.ts +94 -0
- package/src/optimizers/mapper/index.ts +133 -0
- package/src/optimizers/mapper/types.ts +6 -0
- package/src/optimizers/pruner/cleanup.ts +121 -0
- package/src/optimizers/pruner/context.ts +46 -0
- package/src/optimizers/pruner/index.ts +45 -0
- package/src/optimizers/pruner/session.ts +34 -0
- package/src/optimizers/pruner/types.ts +18 -0
- package/src/permission/bash.ts +124 -0
- package/src/permission/command.ts +111 -0
- package/src/permission/components/prompt.ts +255 -0
- package/src/permission/components/rules-list.ts +342 -0
- package/src/permission/constants.ts +48 -0
- package/src/permission/helpers.ts +156 -0
- package/src/permission/index.ts +134 -0
- package/src/permission/pattern.ts +51 -0
- package/src/permission/piignore.ts +148 -0
- package/src/permission/precedence.ts +54 -0
- package/src/permission/resolution.ts +116 -0
- package/src/permission/storage.ts +142 -0
- package/src/permission/types.ts +57 -0
- package/src/questionnaire/component.ts +357 -0
- package/src/questionnaire/helpers.ts +220 -0
- package/src/questionnaire/index.ts +67 -0
- package/src/questionnaire/schemas.ts +50 -0
- package/src/questionnaire/types.ts +47 -0
- package/src/redactor/index.ts +34 -0
- package/src/redactor/patterns.ts +234 -0
- package/src/redactor/secrets.ts +113 -0
- package/src/subagent/helpers.ts +93 -0
- package/src/subagent/index.ts +81 -0
- package/src/subagent/storage.ts +100 -0
- package/src/subagent/subsession.ts +266 -0
- package/src/subagent/types.ts +83 -0
- package/src/subagent/validation.ts +100 -0
- package/src/ui/components/action-select-list.ts +165 -0
- package/src/ui/components/bash-mode.ts +281 -0
- package/src/ui/components/extended-select-list.ts +166 -0
- package/src/ui/components/form-field.ts +184 -0
- package/src/ui/components/form.ts +179 -0
- package/src/ui/components/frame.ts +60 -0
- package/src/ui/components/input-mode-indicator.ts +64 -0
- package/src/ui/components/keybound.ts +150 -0
- package/src/ui/components/lines.ts +27 -0
- package/src/ui/components/placeholder-input.ts +59 -0
- package/src/ui/components/scoped-input.ts +78 -0
- package/src/ui/components/scrollable-view.ts +155 -0
- package/src/ui/index.ts +40 -0
- package/src/utils.ts +206 -0
- package/src/web-tools/index.ts +15 -0
- package/src/web-tools/providers/brave.ts +55 -0
- package/src/web-tools/providers/firecrawl.ts +66 -0
- package/src/web-tools/providers/index.ts +50 -0
- package/src/web-tools/providers/jina.ts +48 -0
- package/src/web-tools/providers/native.ts +57 -0
- package/src/web-tools/providers/tavily.ts +56 -0
- package/src/web-tools/settings.ts +15 -0
- package/src/web-tools/web-fetch/helpers.ts +66 -0
- package/src/web-tools/web-fetch/index.ts +91 -0
- package/src/web-tools/web-fetch/parser.ts +51 -0
- package/src/web-tools/web-fetch/storage.ts +65 -0
- package/src/web-tools/web-fetch/types.ts +8 -0
- package/src/web-tools/web-login/helpers.ts +79 -0
- package/src/web-tools/web-login/index.ts +100 -0
- package/src/web-tools/web-login/types.ts +4 -0
- package/src/web-tools/web-search/helpers.ts +36 -0
- package/src/web-tools/web-search/index.ts +98 -0
- package/src/web-tools/web-search/types.ts +15 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Repository-grounded planner for work spanning multiple components or unclear sequencing and risks; use before complex implementation, not small obvious fixes
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<role>
|
|
6
|
+
You are planning agent. Turn user request into small, execution-ready plan for coding agent.
|
|
7
|
+
</role>
|
|
8
|
+
|
|
9
|
+
<mission>
|
|
10
|
+
Produce plan grounded in current repository. Reduce handoff uncertainty without expanding scope.
|
|
11
|
+
</mission>
|
|
12
|
+
|
|
13
|
+
<rules>
|
|
14
|
+
- Plan only. Never edit files, write artifacts, or propose code patches.
|
|
15
|
+
- Treat latest user request as scope contract. Preserve explicit constraints and exclusions.
|
|
16
|
+
- Inspect repository before conclusions. Never claim behavior from unopened code.
|
|
17
|
+
- Prefer existing patterns, helpers, types, commands, and test setup. Do not invent architecture for hypothetical needs.
|
|
18
|
+
- Ask one focused questionnaire question only when missing answer changes implementation, safety, or scope. State assumption when reasonable answer does not change plan materially.
|
|
19
|
+
- Do not add cleanup, refactors, documentation, dependencies, or tests outside requested work unless correctness requires them.
|
|
20
|
+
</rules>
|
|
21
|
+
|
|
22
|
+
<evidence_workflow>
|
|
23
|
+
1. Extract outcome, constraints, non-scope, and acceptance criteria from request.
|
|
24
|
+
2. Locate likely entry points with narrow searches. Read affected code, related types, and side-effect boundaries.
|
|
25
|
+
3. Identify established implementation and validation patterns. Trace callers only when needed to prove behavior.
|
|
26
|
+
4. Form smallest viable design. Record files to change, dependency order, failure cases, and validation.
|
|
27
|
+
5. Before final answer, remove speculative steps and verify every proposed target has evidence.
|
|
28
|
+
</evidence_workflow>
|
|
29
|
+
|
|
30
|
+
<planning_rules>
|
|
31
|
+
- Name concrete file paths, symbols, and behavior changes.
|
|
32
|
+
- Separate required work from optional follow-up. Omit optional follow-up unless user asked for it.
|
|
33
|
+
- Prefer one shared root-cause change over repeated caller fixes.
|
|
34
|
+
- Include validation that can fail when changed behavior regresses.
|
|
35
|
+
- Flag migrations, data loss, security impact, and incompatible behavior before proposing execution.
|
|
36
|
+
- For ambiguous repository behavior, say what was inspected, what remains unknown, and exact question needed.
|
|
37
|
+
</planning_rules>
|
|
38
|
+
|
|
39
|
+
<output_contract>
|
|
40
|
+
Return Markdown with using this exact template:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
# Plan: [title]
|
|
44
|
+
|
|
45
|
+
## Objective
|
|
46
|
+
One paragraph covering outcome and reason.
|
|
47
|
+
|
|
48
|
+
## Out of scope
|
|
49
|
+
- Only explicit exclusions or tightly related non-goals.
|
|
50
|
+
|
|
51
|
+
## Assumptions
|
|
52
|
+
- Only assumptions that affect correctness.
|
|
53
|
+
Omit when none.
|
|
54
|
+
|
|
55
|
+
## Steps
|
|
56
|
+
Use 3-10 ordered, atomic steps. Each step contains:
|
|
57
|
+
- Goal
|
|
58
|
+
- Changes
|
|
59
|
+
- Targets: exact paths and symbols when known
|
|
60
|
+
- Validation: command, test, or observable behavior
|
|
61
|
+
- Done when
|
|
62
|
+
|
|
63
|
+
## Risks & Mitigations
|
|
64
|
+
- Include only real risks supported by request or repository evidence.
|
|
65
|
+
|
|
66
|
+
## Handoff Packet
|
|
67
|
+
- Hard constraints
|
|
68
|
+
- Acceptance criteria
|
|
69
|
+
|
|
70
|
+
## Open Questions
|
|
71
|
+
- Only non-blocking questions. Omit when none.
|
|
72
|
+
```
|
|
73
|
+
</output_contract>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Read-only codebase researcher for broad or uncertain questions; returns bounded, evidence-backed synthesis at quick, standard, or deep depth and should not handle one known target
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<role>
|
|
6
|
+
Code research agent. Answer assigned question from repository evidence without exposing raw exploration to parent context.
|
|
7
|
+
</role>
|
|
8
|
+
|
|
9
|
+
<depth>
|
|
10
|
+
Read `Depth: quick|standard|deep` from task. Use `standard` when omitted.
|
|
11
|
+
|
|
12
|
+
- `quick`: Follow one primary path. Inspect only decisive symbols. Target at most 250 words and 5 evidence references. Report ambiguity instead of widening scope.
|
|
13
|
+
- `standard`: Trace primary execution path, immediate callers and callees, relevant types, configuration, and tests. Target at most 700 words and 12 evidence references.
|
|
14
|
+
- `deep`: Trace subsystem boundaries, alternate paths, shared abstractions, invariants, side effects, and verification surface. Target at most 1,400 words and 20 evidence references. Stop when assigned question is answered, not when repository is exhausted.
|
|
15
|
+
|
|
16
|
+
Depth controls research breadth, not only answer length. Never silently escalate depth.
|
|
17
|
+
</depth>
|
|
18
|
+
|
|
19
|
+
<research>
|
|
20
|
+
1. Start from concrete paths, symbols, snippets, errors, or commands in request.
|
|
21
|
+
2. Use `ls`, `find`, and `grep` to discover relevant locations and terms.
|
|
22
|
+
3. Use `code_map` to map narrow code areas and identify symbols, dependencies, and public boundaries.
|
|
23
|
+
4. Use `inspect` for full symbol bodies needed to answer question.
|
|
24
|
+
5. Use `read` only for relevant non-code files or code regions `inspect` cannot expose. For code, derive offset and limit from latest `code_map` result.
|
|
25
|
+
6. Follow relationships only to depth required by selected level.
|
|
26
|
+
7. Separate confirmed behavior from inference. Preserve unresolved gaps when they affect confidence.
|
|
27
|
+
8. Synthesize findings. Never return tool calls, raw tool output, or selector JSON.
|
|
28
|
+
</research>
|
|
29
|
+
|
|
30
|
+
<output_contract>
|
|
31
|
+
Return Markdown with exact template for matching selected depth. Replace bracketed guidance; never output brackets.
|
|
32
|
+
|
|
33
|
+
`quick` template:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
## Answer
|
|
37
|
+
[Direct answer and confidence.]
|
|
38
|
+
|
|
39
|
+
## Evidence
|
|
40
|
+
- `path:line` — `symbol`: [Finding and significance.]
|
|
41
|
+
|
|
42
|
+
## Gaps
|
|
43
|
+
[Material uncertainty or `None.`]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`standard` template:
|
|
47
|
+
|
|
48
|
+
```markdown
|
|
49
|
+
## Answer
|
|
50
|
+
[Evidence-backed synthesis.]
|
|
51
|
+
|
|
52
|
+
## Execution flow
|
|
53
|
+
[Primary flow through relevant symbols.]
|
|
54
|
+
|
|
55
|
+
## Relevant locations
|
|
56
|
+
- `path:line` — `symbol`: [Role in behavior.]
|
|
57
|
+
|
|
58
|
+
## Evidence
|
|
59
|
+
- `path:line` — `symbol`: [Finding and significance.]
|
|
60
|
+
|
|
61
|
+
## Gaps
|
|
62
|
+
[Material uncertainty or `None.`]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`deep` template:
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
## Answer
|
|
69
|
+
[Detailed synthesis and confidence.]
|
|
70
|
+
|
|
71
|
+
## Execution flow
|
|
72
|
+
[Primary and alternate flows across boundaries.]
|
|
73
|
+
|
|
74
|
+
## Relevant locations
|
|
75
|
+
- `path:line` — `symbol`: [Role in subsystem.]
|
|
76
|
+
|
|
77
|
+
## Change impact
|
|
78
|
+
[Callers, invariants, side effects, and compatibility concerns.]
|
|
79
|
+
|
|
80
|
+
## Verification
|
|
81
|
+
[Existing checks and smallest checks needed to prove behavior.]
|
|
82
|
+
|
|
83
|
+
## Evidence
|
|
84
|
+
- `path:line` — `symbol`: [Finding and significance.]
|
|
85
|
+
|
|
86
|
+
## Gaps
|
|
87
|
+
[Material uncertainty or `None.`]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rules:
|
|
91
|
+
- Give every important claim a file path plus symbol or line range.
|
|
92
|
+
- Format each evidence item as `- \`path:line\` — \`symbol\`: finding`. Use path plus symbol when line is unavailable.
|
|
93
|
+
- Explain significance instead of repeating file contents.
|
|
94
|
+
- Include no raw code except 1–3 decisive lines when required.
|
|
95
|
+
- Write `None.` under `## Gaps` when no material gap remains.
|
|
96
|
+
- Do not propose implementation unless task asks for change analysis.
|
|
97
|
+
</output_contract>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Agent, AgentMeta } from "./types.js";
|
|
3
|
+
import {
|
|
4
|
+
createAgentFile,
|
|
5
|
+
DEFAULT_AGENT,
|
|
6
|
+
deleteAgentFiles,
|
|
7
|
+
isBuiltIn,
|
|
8
|
+
loadAgents,
|
|
9
|
+
writeAgentMeta,
|
|
10
|
+
} from "./storage.js";
|
|
11
|
+
import { ExtendedSelectList } from "../ui/components/extended-select-list.js";
|
|
12
|
+
import { ScopedInput } from "../ui/components/scoped-input.js";
|
|
13
|
+
import { Form } from "../ui/components/form.js";
|
|
14
|
+
import { getAgentConfigForm } from "./helpers.js";
|
|
15
|
+
import { openInEditor } from "../utils.js";
|
|
16
|
+
|
|
17
|
+
async function showAgentPicker(
|
|
18
|
+
ctx: ExtensionCommandContext,
|
|
19
|
+
agents: Agent[],
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
const items = agents
|
|
22
|
+
.filter((agent) => agent.name !== DEFAULT_AGENT)
|
|
23
|
+
.map((agent) => ({
|
|
24
|
+
value: agent.name,
|
|
25
|
+
label: isBuiltIn(agent.filePath) ? `${agent.name} (built-in)` : agent.name,
|
|
26
|
+
description: agent.meta.description,
|
|
27
|
+
data: agent,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return ctx.ui.custom<string | null>((_tui, theme, _keybindings, done) => {
|
|
31
|
+
const selectList = new ExtendedSelectList(theme, {
|
|
32
|
+
title: "Agents",
|
|
33
|
+
addLabel: "Create new agent",
|
|
34
|
+
items,
|
|
35
|
+
maxVisibleRows: 12,
|
|
36
|
+
canDelete: (item) => item.data !== undefined && !isBuiltIn(item.data.filePath),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
selectList.onAdd = () => done("__new__");
|
|
40
|
+
selectList.onSelect = (item) => done(String(item.value));
|
|
41
|
+
selectList.onCancel = () => done(null);
|
|
42
|
+
selectList.onDeleteBlocked = () => ctx.ui.notify("Built-in agent cannot be deleted", "error");
|
|
43
|
+
selectList.onDelete = (item) => {
|
|
44
|
+
const agentName = item.data?.name ?? String(item.value);
|
|
45
|
+
void deleteAgentFiles(agentName, ctx.cwd)
|
|
46
|
+
.then(() => ctx.ui.notify(`Agent "${agentName}" deleted`, "info"))
|
|
47
|
+
.catch(() => ctx.ui.notify(`Failed to delete agent "${agentName}"`, "error"));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return selectList;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function openAgentConfigEditor(ctx: ExtensionCommandContext, agent: Agent) {
|
|
55
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
|
|
56
|
+
const editor = new Form<AgentMeta>(
|
|
57
|
+
tui,
|
|
58
|
+
keybindings,
|
|
59
|
+
theme,
|
|
60
|
+
getAgentConfigForm(agent.name, agent.meta, isBuiltIn(agent.filePath)),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
editor.onCancel = () => done();
|
|
64
|
+
editor.onSave = async (updatedMeta) => {
|
|
65
|
+
await writeAgentMeta(agent, updatedMeta);
|
|
66
|
+
agent.meta = updatedMeta;
|
|
67
|
+
ctx.ui.notify(`Agent "${agent.name}" config updated`, "info");
|
|
68
|
+
done();
|
|
69
|
+
};
|
|
70
|
+
editor.onSaveError = (error) => {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
ctx.ui.notify(`Failed to update agent config: ${message}`, "error");
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return editor;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleExistingAgent(ctx: ExtensionCommandContext, agent: Agent) {
|
|
80
|
+
while (true) {
|
|
81
|
+
const options = ["Start in new session", "Edit agent config"];
|
|
82
|
+
if (!isBuiltIn(agent.filePath)) {
|
|
83
|
+
options.push("Open in external editor");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const action = await ctx.ui.select(`Agent: ${agent.name}`, options);
|
|
87
|
+
if (!action) return false;
|
|
88
|
+
|
|
89
|
+
if (action === "Start in new session") {
|
|
90
|
+
const result = await ctx.newSession({
|
|
91
|
+
setup: async (sessionManager) => {
|
|
92
|
+
sessionManager.appendCustomEntry("agent", agent.name);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return !result.cancelled;
|
|
96
|
+
}
|
|
97
|
+
if (action === "Edit agent config") {
|
|
98
|
+
await openAgentConfigEditor(ctx, agent);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (action === "Open in external editor") {
|
|
102
|
+
await openInEditor(ctx, agent.filePath);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleNewAgent(ctx: ExtensionCommandContext) {
|
|
109
|
+
const result = await ctx.ui.custom<{ name: string; scope: string } | null>(
|
|
110
|
+
(_tui, theme, _kb, done) => {
|
|
111
|
+
const scopedInput = new ScopedInput(theme, "Agent name");
|
|
112
|
+
scopedInput.onSubmit = ({ scope, value: name }) => done({ name, scope });
|
|
113
|
+
scopedInput.onCancel = () => done(null);
|
|
114
|
+
return scopedInput;
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!result) return;
|
|
119
|
+
const { name, scope } = result;
|
|
120
|
+
const filePath = await createAgentFile(scope === "project" ? ctx.cwd : scope, name);
|
|
121
|
+
|
|
122
|
+
ctx.ui.notify(`Agent created: ${filePath}`, "info");
|
|
123
|
+
await openInEditor(ctx, filePath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function agentsCommandHandler(ctx: ExtensionCommandContext) {
|
|
127
|
+
while (true) {
|
|
128
|
+
const agents = await loadAgents(ctx.cwd);
|
|
129
|
+
const selected = await showAgentPicker(ctx, agents);
|
|
130
|
+
if (!selected) return;
|
|
131
|
+
|
|
132
|
+
if (selected === "__new__") {
|
|
133
|
+
await handleNewAgent(ctx);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const agent = agents.find((candidate) => candidate.name === selected);
|
|
138
|
+
if (agent && (await handleExistingAgent(ctx, agent))) return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AgentMeta } from "./types.js";
|
|
2
|
+
import type { FormConfig } from "../ui/components/form.js";
|
|
3
|
+
import { META_KEYS } from "./storage.js";
|
|
4
|
+
|
|
5
|
+
function parseConfigValues(values: Record<string, string>) {
|
|
6
|
+
const description = (values.description ?? "").trim();
|
|
7
|
+
if (!description) {
|
|
8
|
+
throw new Error("Description cannot be empty.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const updated: AgentMeta = { description };
|
|
12
|
+
for (const field of META_KEYS) {
|
|
13
|
+
if (field === "description") continue;
|
|
14
|
+
|
|
15
|
+
const rawValue = values[field] ?? "";
|
|
16
|
+
if (field === "model") {
|
|
17
|
+
const modelValue = rawValue.trim();
|
|
18
|
+
if (modelValue) {
|
|
19
|
+
updated.model = modelValue;
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (field === "thinking_level") {
|
|
24
|
+
const thinkingLevel = rawValue.trim();
|
|
25
|
+
if (
|
|
26
|
+
thinkingLevel &&
|
|
27
|
+
!["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinkingLevel)
|
|
28
|
+
) {
|
|
29
|
+
throw new Error("Thinking level must be off, minimal, low, medium, high, xhigh, or max.");
|
|
30
|
+
}
|
|
31
|
+
if (thinkingLevel) {
|
|
32
|
+
updated.thinking_level = thinkingLevel as AgentMeta["thinking_level"];
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedValue = rawValue.trim();
|
|
38
|
+
if (!normalizedValue) continue;
|
|
39
|
+
|
|
40
|
+
const entries =
|
|
41
|
+
normalizedValue === "[]"
|
|
42
|
+
? []
|
|
43
|
+
: normalizedValue
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((entry) => entry.trim().replace(/^['\"]|['\"]$/g, ""))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
if (entries.length === 0 && normalizedValue !== "[]") continue;
|
|
48
|
+
|
|
49
|
+
updated[field] = entries;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return updated;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getAgentConfigForm(
|
|
56
|
+
agent: string,
|
|
57
|
+
meta: AgentMeta,
|
|
58
|
+
builtIn: boolean,
|
|
59
|
+
): FormConfig<AgentMeta> {
|
|
60
|
+
return {
|
|
61
|
+
title: `Edit agent config: ${agent}`,
|
|
62
|
+
fields: META_KEYS.filter((field) => !builtIn || field !== "description").map((field) => {
|
|
63
|
+
const value = meta[field];
|
|
64
|
+
let placeholder: string;
|
|
65
|
+
if (field === "description") {
|
|
66
|
+
placeholder = "Describe what this agent does (not included in system prompt)";
|
|
67
|
+
} else if (field === "model") {
|
|
68
|
+
placeholder = "AI model to use for this agent (leave blank to inherit)";
|
|
69
|
+
} else if (field === "thinking_level") {
|
|
70
|
+
placeholder = "off, minimal, low, medium, high, xhigh, or max (leave blank to inherit)";
|
|
71
|
+
} else {
|
|
72
|
+
placeholder = `comma-separated allowed ${field}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
key: field,
|
|
77
|
+
label: field,
|
|
78
|
+
labelWidth: 32,
|
|
79
|
+
mode: {
|
|
80
|
+
type: "input",
|
|
81
|
+
placeholder,
|
|
82
|
+
text: Array.isArray(value)
|
|
83
|
+
? value.length === 0
|
|
84
|
+
? "[]"
|
|
85
|
+
: value.join(", ")
|
|
86
|
+
: (value ?? ""),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
emptyMessage: "No metadata fields available for editing.",
|
|
91
|
+
parseOnSave: builtIn
|
|
92
|
+
? (values) => parseConfigValues({ description: meta.description, ...values })
|
|
93
|
+
: parseConfigValues,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { agentsCommandHandler } from "./command.js";
|
|
3
|
+
|
|
4
|
+
export default function (pi: ExtensionAPI) {
|
|
5
|
+
pi.registerCommand("agent", {
|
|
6
|
+
description: "List, create, edit, and switch agents",
|
|
7
|
+
handler: (_args, ctx) => agentsCommandHandler(ctx),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { readdir, unlink, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path, { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { isMissingFileError, readJson, writeJson } from "../utils.js";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getPiPath } from "../utils.js";
|
|
6
|
+
import type { AgentMeta, Agent, SettingsSchema } from "./types.js";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { loadMcpConfigSet } from "../mcp-client/storage.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_AGENT = "general";
|
|
11
|
+
export const META_KEYS: (keyof AgentMeta)[] = [
|
|
12
|
+
"description",
|
|
13
|
+
"tools",
|
|
14
|
+
"mcp_tools",
|
|
15
|
+
"skills",
|
|
16
|
+
"bash",
|
|
17
|
+
"files.read",
|
|
18
|
+
"files.write",
|
|
19
|
+
"model",
|
|
20
|
+
"thinking_level",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
24
|
+
const LINE_ENDING = /\r?\n/;
|
|
25
|
+
const KEY_VALUE_PAIR = /^([\w.]+):\s*(.*)$/;
|
|
26
|
+
const INLINE_ARRAY = /^\[(.*)\]$/;
|
|
27
|
+
const QUOTED_STRING = /^["']|["']$/g;
|
|
28
|
+
|
|
29
|
+
const ARRAY_KEYS = new Set<keyof AgentMeta>([
|
|
30
|
+
"tools",
|
|
31
|
+
"mcp_tools",
|
|
32
|
+
"skills",
|
|
33
|
+
"bash",
|
|
34
|
+
"files.read",
|
|
35
|
+
"files.write",
|
|
36
|
+
]);
|
|
37
|
+
const STRING_KEYS = new Set<keyof AgentMeta>(["description", "model", "thinking_level"]);
|
|
38
|
+
const META_KEY_SET = new Set<string>(META_KEYS);
|
|
39
|
+
const BUILT_IN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "built-in");
|
|
40
|
+
|
|
41
|
+
function parseAllowList(value: string): string[] | undefined {
|
|
42
|
+
const inlineArray = value.match(INLINE_ARRAY);
|
|
43
|
+
if (inlineArray) {
|
|
44
|
+
return inlineArray[1]!
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((part) => part.trim().replace(QUOTED_STRING, ""))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseAgentConfig(content: string, filePath: string): Agent | null {
|
|
52
|
+
const match = content.match(FRONTMATTER_BLOCK);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
|
|
55
|
+
const frontmatter = match[1]!;
|
|
56
|
+
const body = match[2]!.trim();
|
|
57
|
+
const name = path.basename(filePath, path.extname(filePath));
|
|
58
|
+
const meta: Partial<AgentMeta> = {};
|
|
59
|
+
|
|
60
|
+
for (const line of frontmatter.split(LINE_ENDING)) {
|
|
61
|
+
const kv = line.match(KEY_VALUE_PAIR);
|
|
62
|
+
if (!kv) continue;
|
|
63
|
+
const key = kv[1]!;
|
|
64
|
+
const value = kv[2]!.trim();
|
|
65
|
+
|
|
66
|
+
if (ARRAY_KEYS.has(key as keyof AgentMeta)) {
|
|
67
|
+
const parsedAllowList = parseAllowList(value);
|
|
68
|
+
if (parsedAllowList !== undefined) {
|
|
69
|
+
(meta as Record<string, string[]>)[key] = parsedAllowList;
|
|
70
|
+
}
|
|
71
|
+
} else if (STRING_KEYS.has(key as keyof AgentMeta)) {
|
|
72
|
+
(meta as Record<string, string>)[key] = value.replace(QUOTED_STRING, "");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!meta.description) return null;
|
|
77
|
+
return { meta: meta as AgentMeta, body, filePath, name };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getAgentFiles(cwd: string, name?: string, skipBuiltIn?: boolean): Promise<string[]> {
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
const dirs = [getPiPath("agents", cwd), getPiPath("agents")];
|
|
83
|
+
const files: string[] = [];
|
|
84
|
+
if (!skipBuiltIn) dirs.push(BUILT_IN_DIR);
|
|
85
|
+
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
try {
|
|
88
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
89
|
+
files.push(
|
|
90
|
+
...entries
|
|
91
|
+
.filter((entry) => {
|
|
92
|
+
if (seen.has(entry.name)) return false;
|
|
93
|
+
seen.add(entry.name);
|
|
94
|
+
return (
|
|
95
|
+
entry.isFile() && entry.name.endsWith(".md") && (!name || entry.name === `${name}.md`)
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
.map((entry) => join(dir, entry.name)),
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function appendToolDetails(activeTools: string[], lines: Record<string, string>) {
|
|
109
|
+
const appendContent: string[] = [];
|
|
110
|
+
if (
|
|
111
|
+
(activeTools.includes("call_mcp_tool") || activeTools.includes("list_mcp_tools")) &&
|
|
112
|
+
lines.mcp
|
|
113
|
+
) {
|
|
114
|
+
appendContent.push(lines.mcp);
|
|
115
|
+
}
|
|
116
|
+
if (activeTools.includes("subagent") && lines.subagent) {
|
|
117
|
+
appendContent.push(lines.subagent);
|
|
118
|
+
}
|
|
119
|
+
if (appendContent.length > 0) {
|
|
120
|
+
await writeFile(getPiPath("system"), `${appendContent.join("\n")}\n`, "utf8");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function loadAgents(cwd: string, name?: string): Promise<[Agent, ...Agent[]]> {
|
|
125
|
+
const agents: Agent[] = [];
|
|
126
|
+
const files = await getAgentFiles(cwd, name);
|
|
127
|
+
const settings = await readJson<SettingsSchema>(getPiPath("settings"), {});
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
try {
|
|
131
|
+
const content = await readFile(file, "utf8");
|
|
132
|
+
const parsed = parseAgentConfig(content, file);
|
|
133
|
+
if (!parsed || (!isBuiltIn(file) && parsed.name === DEFAULT_AGENT)) continue;
|
|
134
|
+
if (isBuiltIn(file)) {
|
|
135
|
+
parsed.meta = { ...parsed.meta, ...settings.agent?.meta?.[parsed.name] };
|
|
136
|
+
}
|
|
137
|
+
agents.push(parsed);
|
|
138
|
+
} catch {} // skip unreadable files
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (agents.length === 0) {
|
|
142
|
+
throw new Error("Invalid agent name or files unreachable. Please try again.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return agents as [Agent, ...Agent[]];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function serializeMeta(meta: AgentMeta): string[] {
|
|
149
|
+
const lines: string[] = [];
|
|
150
|
+
|
|
151
|
+
for (const key of META_KEYS) {
|
|
152
|
+
const value = meta[key];
|
|
153
|
+
if (value === undefined) continue;
|
|
154
|
+
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
lines.push(`${key}: [${value.map((entry) => JSON.stringify(entry)).join(", ")}]`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push(`${key}: ${String(value)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function createAgentFile(base: string, name: string): Promise<string> {
|
|
167
|
+
const filePath = join(getPiPath("agents", base), `${name}.md`);
|
|
168
|
+
await writeFile(
|
|
169
|
+
filePath,
|
|
170
|
+
`---\ndescription: Describe what \`${name}\` agent does\n---\n\nWrite \`${name}\` agent's system prompt here`,
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
173
|
+
return filePath;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function writeAgentMeta(agent: Agent, meta: AgentMeta) {
|
|
177
|
+
if (isBuiltIn(agent.filePath)) {
|
|
178
|
+
const settingsPath = getPiPath("settings");
|
|
179
|
+
let settings: SettingsSchema;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
settings = JSON.parse(await readFile(settingsPath, "utf8")) as SettingsSchema;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (!isMissingFileError(error)) throw error;
|
|
185
|
+
settings = {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { description, ...stored } = meta;
|
|
189
|
+
settings.agent = {
|
|
190
|
+
...settings.agent,
|
|
191
|
+
meta: { ...settings.agent?.meta, [agent.name]: stored },
|
|
192
|
+
};
|
|
193
|
+
await writeJson(settingsPath, settings);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const content = await readFile(agent.filePath, "utf8");
|
|
198
|
+
const match = content.match(FRONTMATTER_BLOCK);
|
|
199
|
+
if (!match) {
|
|
200
|
+
throw new Error(`Invalid agent file frontmatter: ${agent.filePath}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const existingFrontmatter = match[1] ?? "";
|
|
204
|
+
const preservedFrontmatterLines = existingFrontmatter
|
|
205
|
+
.split(LINE_ENDING)
|
|
206
|
+
.map((line) => line.trimEnd())
|
|
207
|
+
.filter((line) => {
|
|
208
|
+
const pair = line.match(KEY_VALUE_PAIR);
|
|
209
|
+
if (!pair) {
|
|
210
|
+
return line.trim().length > 0;
|
|
211
|
+
}
|
|
212
|
+
return !META_KEY_SET.has(pair[1]!);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const body = match[2] ?? "";
|
|
216
|
+
const metaLines = serializeMeta(meta);
|
|
217
|
+
const nextFrontmatterLines = [...preservedFrontmatterLines, ...metaLines];
|
|
218
|
+
|
|
219
|
+
const nextContent = `---\n${nextFrontmatterLines.join("\n")}\n---\n${body}`;
|
|
220
|
+
await writeFile(agent.filePath, nextContent, "utf8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function deleteAgentFiles(name: string, cwd: string) {
|
|
224
|
+
const files = await getAgentFiles(cwd, name, true);
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
try {
|
|
227
|
+
const content = await readFile(file, "utf8");
|
|
228
|
+
const parsed = parseAgentConfig(content, file);
|
|
229
|
+
if (parsed?.name === name) await unlink(file);
|
|
230
|
+
} catch {} // skip
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isBuiltIn(filePath: string): boolean {
|
|
235
|
+
return filePath.startsWith(BUILT_IN_DIR);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function loadMainAgent(pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
239
|
+
const selected = ctx.sessionManager
|
|
240
|
+
.getEntries()
|
|
241
|
+
.find((entry) => entry.type === "custom" && entry.customType === "agent");
|
|
242
|
+
const name = selected?.type === "custom" ? (selected.data as string) : DEFAULT_AGENT;
|
|
243
|
+
|
|
244
|
+
const allMcpConfigs = await loadMcpConfigSet(ctx.cwd);
|
|
245
|
+
const agents = await loadAgents(ctx.cwd);
|
|
246
|
+
const main = agents.find((agent) => agent.name === name);
|
|
247
|
+
if (!main) {
|
|
248
|
+
throw new Error("Invalid agent name.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const { meta } = main;
|
|
252
|
+
const mcpConfigs = allMcpConfigs.filter(
|
|
253
|
+
(cfg) => cfg.enabled === true && (meta.mcp_tools ?? [cfg.name]).includes(cfg.name),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
pi.setActiveTools(
|
|
257
|
+
pi
|
|
258
|
+
.getAllTools()
|
|
259
|
+
.map((tool) => tool.name)
|
|
260
|
+
.filter((name) => (meta.tools ?? [name]).includes(name)),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (meta.model) {
|
|
264
|
+
const existing = ctx.modelRegistry.find(
|
|
265
|
+
meta.model.slice(0, meta.model.indexOf("/")),
|
|
266
|
+
meta.model.slice(meta.model.indexOf("/") + 1),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (existing) {
|
|
270
|
+
const ok = await pi.setModel(existing);
|
|
271
|
+
if (!ok) ctx.ui.notify("Agent model unavailable", "warning");
|
|
272
|
+
} else {
|
|
273
|
+
ctx.ui.notify(`Unknown model "${meta.model}" in agent config`, "warning");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (meta.thinking_level) {
|
|
277
|
+
pi.setThinkingLevel(meta.thinking_level);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await appendToolDetails(pi.getActiveTools(), {
|
|
281
|
+
mcp: `## Available MCP servers\n${mcpConfigs
|
|
282
|
+
.map((cfg) => (cfg.description ? `- ${cfg.name}: ${cfg.description}` : `- ${cfg.name}`))
|
|
283
|
+
.join("\n")}`,
|
|
284
|
+
subagent: `## Available agents for \`subagent\` tool\n${agents.map((profile) => `- ${profile.name}: ${profile.meta.description}`).join("\n")}`,
|
|
285
|
+
});
|
|
286
|
+
return main;
|
|
287
|
+
}
|