@jaybeeuu/agent-cortex 1.20.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/.beads/README.md +81 -0
- package/.beads/config.yaml +1 -0
- package/.beads/issues.jsonl +236 -0
- package/.beads/metadata.json +7 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.github/workflows/ci.test.ts +217 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +36 -0
- package/.junie/guidelines.md +87 -0
- package/.junie/mcp/mcp.json +10 -0
- package/AGENTS.md +102 -0
- package/CHANGELOG.md +705 -0
- package/README.md +209 -0
- package/agents/README.md +235 -0
- package/agents/plan.agent.md +56 -0
- package/agents/ralph-plan.agent.md +119 -0
- package/agents/ralph.agent.md +305 -0
- package/agents/strategy.agent.md +109 -0
- package/agents-native/ralph.md +193 -0
- package/bin/agent-cortex.mjs +27 -0
- package/claude/.claude-plugin/plugin.json +11 -0
- package/claude/.mcp.json +18 -0
- package/claude/agents/plan.md +56 -0
- package/claude/agents/ralph-plan.md +119 -0
- package/claude/agents/ralph.md +193 -0
- package/claude/agents/strategy.md +109 -0
- package/claude/hooks.json +23 -0
- package/claude/scripts/statusline-command.sh +124 -0
- package/docs/ideas/auto-model-selection.md +75 -0
- package/docs/ideas/improve-pi-system-prompt.md +34 -0
- package/docs/ideas/model-selector.md +40 -0
- package/docs/ideas/pi-task-memory.md +34 -0
- package/docs/ideas/qa-extension.md +43 -0
- package/docs/inspirations.md +33 -0
- package/docs/research/gas-city-research.md +256 -0
- package/docs/research/skill-design-research.md +286 -0
- package/docs/research/skill-improvements-analysis.md +117 -0
- package/docs/research/skill-patterns-research.md +24 -0
- package/docs/skills/skill-anatomy.md +149 -0
- package/docs/user-preferences.md +17 -0
- package/extensions/agent-modes/index.ts +507 -0
- package/extensions/auto-name.ts +107 -0
- package/extensions/context-loader.md +35 -0
- package/extensions/context-loader.ts +65 -0
- package/extensions/lib/tiny-model.ts +212 -0
- package/extensions/notify/index.ts +293 -0
- package/extensions/session-start.ts +44 -0
- package/extensions/skill-stats/README.md +74 -0
- package/extensions/skill-stats/context-loader.test.ts +107 -0
- package/extensions/skill-stats/discover.ts +119 -0
- package/extensions/skill-stats/format.ts +13 -0
- package/extensions/skill-stats/index.test.ts +452 -0
- package/extensions/skill-stats/index.ts +222 -0
- package/extensions/skill-stats/package.json +19 -0
- package/extensions/skill-stats/pnpm-lock.yaml +329 -0
- package/extensions/skill-stats/record.ts +48 -0
- package/extensions/skill-stats/store.ts +104 -0
- package/extensions/skill-stats/tsconfig.json +12 -0
- package/extensions/subagent/README.md +175 -0
- package/extensions/subagent/agents/planner.md +37 -0
- package/extensions/subagent/agents/reviewer.md +35 -0
- package/extensions/subagent/agents/scout.md +50 -0
- package/extensions/subagent/agents/worker.md +24 -0
- package/extensions/subagent/agents.ts +126 -0
- package/extensions/subagent/index.ts +1138 -0
- package/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/extensions/subagent/prompts/implement.md +10 -0
- package/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/hooks.json +10 -0
- package/lib/cli.mjs +37 -0
- package/package.json +39 -0
- package/pi/keybindings.json +3 -0
- package/pi/settings.json +29 -0
- package/plugin.json +35 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build-claude-agents.mjs +179 -0
- package/scripts/install-instructions.sh +82 -0
- package/skills/engineering/design-an-interface/SKILL.md +94 -0
- package/skills/engineering/grill-with-docs/SKILL.md +88 -0
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +78 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/engineering/tdd/SKILL.md +150 -0
- package/skills/engineering/tdd/deep-modules.md +33 -0
- package/skills/engineering/tdd/interface-design.md +31 -0
- package/skills/engineering/tdd/mocking.md +59 -0
- package/skills/engineering/tdd/refactoring.md +10 -0
- package/skills/engineering/tdd/tests.md +61 -0
- package/skills/engineering/technical-direction/SKILL.md +86 -0
- package/skills/planning/classify-bead/SKILL.md +67 -0
- package/skills/planning/create-task/SKILL.md +96 -0
- package/skills/planning/create-task/pipeline.json +33 -0
- package/skills/planning/create-task/scripts/create-chores.ts +112 -0
- package/skills/planning/create-task/scripts/package.json +14 -0
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
- package/skills/planning/create-task/scripts/tsconfig.json +12 -0
- package/skills/planning/create-task/templates/code.md +40 -0
- package/skills/planning/create-task/templates/document.md +40 -0
- package/skills/planning/create-task/templates/fix.md +27 -0
- package/skills/planning/create-task/templates/review.md +36 -0
- package/skills/planning/create-task/templates/verify.md +43 -0
- package/skills/planning/prd-to-tasks/SKILL.md +227 -0
- package/skills/planning/record-idea/SKILL.md +62 -0
- package/skills/planning/record-idea/scripts/new-idea.sh +89 -0
- package/skills/planning/request-refactor-plan/SKILL.md +73 -0
- package/skills/planning/write-a-prd/SKILL.md +74 -0
- package/skills/planning/write-a-ticket/SKILL.md +97 -0
- package/skills/productivity/bd-tool/SKILL.md +90 -0
- package/skills/productivity/grill-me/SKILL.md +10 -0
- package/skills/productivity/handoff/SKILL.md +15 -0
- package/skills/productivity/init-beads/SKILL.md +93 -0
- package/skills/productivity/teach/GLOSSARY-FORMAT.md +35 -0
- package/skills/productivity/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/skills/productivity/teach/MISSION-FORMAT.md +31 -0
- package/skills/productivity/teach/RESOURCES-FORMAT.md +32 -0
- package/skills/productivity/teach/SKILL.md +109 -0
- package/skills/productivity/validate-agent-dir/scripts/package.json +15 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +2 -0
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +19 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +317 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.ts +127 -0
- package/skills/productivity/write-a-skill/SKILL.md +74 -0
- package/skills/review/maintain-agent-docs/REFERENCE.md +246 -0
- package/skills/review/maintain-agent-docs/SKILL.md +83 -0
- package/skills/review/refactor-skill/EXAMPLES.md +20 -0
- package/skills/review/refactor-skill/SKILL.md +148 -0
- package/skills/review/review-security/SKILL.md +107 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +43 -0
- package/skills/style/style-code/REFERENCE.md +38 -0
- package/skills/style/style-code/SKILL.md +113 -0
- package/skills/style/style-comms/SKILL.md +93 -0
- package/skills/style/style-documentation/SKILL.md +112 -0
- package/skills/style/style-tests/SKILL.md +97 -0
- package/skills/workflow/git-workflow/SKILL.md +85 -0
- package/skills/workflow/hitl-collab/SKILL.md +29 -0
- package/skills/workflow/plan/SKILL.md +272 -0
- package/skills/workflow/ralph/REFERENCE.md +202 -0
- package/skills/workflow/ralph/SKILL.md +105 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +151 -0
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +18 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +19 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +14 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +20 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +783 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +586 -0
- package/skills/workflow/run-pipeline-stage/scripts/package.json +15 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +2 -0
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +12 -0
- package/skills/workflow/using-agent-skills/SKILL.md +109 -0
- package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +73 -0
- package/test/cli.test.mjs +149 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Modes Extension
|
|
3
|
+
*
|
|
4
|
+
* Makes the agent-cortex agents (ralph, ralph-plan, strategy) available as
|
|
5
|
+
* switchable PI modes. Each mode:
|
|
6
|
+
* - Injects the agent's full system prompt into the running context
|
|
7
|
+
* - Restricts tools based on the agent's declared tool set
|
|
8
|
+
* - Shows a status indicator with the active agent name
|
|
9
|
+
*
|
|
10
|
+
* Modeled on Pi's built-in preset.ts example and R-Dson/pi-modes.
|
|
11
|
+
*
|
|
12
|
+
* Commands:
|
|
13
|
+
* /agent — show mode selector
|
|
14
|
+
* /agent <name> — switch to agent mode directly
|
|
15
|
+
* Ctrl+Shift+A — cycle agents
|
|
16
|
+
* --agent <name> — start in a specific agent mode
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Key } from "@earendil-works/pi-tui";
|
|
24
|
+
|
|
25
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface AgentDef {
|
|
28
|
+
/** Lowercase id derived from filename (e.g. "ralph", "ralph-plan") */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Display name from frontmatter or prettified id */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Description from frontmatter */
|
|
33
|
+
description: string;
|
|
34
|
+
/** Colour for status indicator */
|
|
35
|
+
color: string;
|
|
36
|
+
/** PI tool names to enable (empty = all tools) */
|
|
37
|
+
tools: string[];
|
|
38
|
+
/** Full agent body (the system prompt to inject) */
|
|
39
|
+
prompt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OriginalState {
|
|
43
|
+
tools: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Discovery ───────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
// Agents dir is <extension-root>/../../agents/ — two levels up from extensions/agent-modes/
|
|
50
|
+
const AGENTS_DIR = join(__dirname, "..", "..", "agents");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Copilot CLI tool names to PI built-in tool names.
|
|
54
|
+
* Tools not in this map are left as-is (they may be custom tools).
|
|
55
|
+
*/
|
|
56
|
+
const COPILOT_TO_PI: Record<string, string[]> = {
|
|
57
|
+
bash: ["bash"],
|
|
58
|
+
view: ["read"],
|
|
59
|
+
read: ["read"],
|
|
60
|
+
edit: ["edit"],
|
|
61
|
+
create: ["write"],
|
|
62
|
+
write: ["write"],
|
|
63
|
+
grep: ["grep"],
|
|
64
|
+
rg: ["grep", "find"],
|
|
65
|
+
glob: ["find", "ls"],
|
|
66
|
+
find: ["find"],
|
|
67
|
+
ls: ["ls"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mapping from agent id → colour for the status indicator.
|
|
72
|
+
*/
|
|
73
|
+
const AGENT_COLORS: Record<string, string> = {
|
|
74
|
+
ralph: "accent",
|
|
75
|
+
"ralph-plan": "warning",
|
|
76
|
+
strategy: "accent",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const PERSIST_KEY = "agent-modes-state";
|
|
80
|
+
|
|
81
|
+
/** Modes with no tool restriction gets the full default set. */
|
|
82
|
+
const DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
83
|
+
|
|
84
|
+
// ─── Agent file parsing ──────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function parseFrontmatter(raw: string): {
|
|
87
|
+
name?: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
tools?: string[];
|
|
90
|
+
prompt: string;
|
|
91
|
+
} {
|
|
92
|
+
const lines = raw.split(/\r?\n/);
|
|
93
|
+
if (lines[0] !== "---") return { prompt: raw };
|
|
94
|
+
|
|
95
|
+
// Find closing ---
|
|
96
|
+
let sep = -1;
|
|
97
|
+
for (let i = 1; i < lines.length; i++) {
|
|
98
|
+
if (lines[i].trim() === "---") {
|
|
99
|
+
sep = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (sep === -1) return { prompt: raw };
|
|
104
|
+
|
|
105
|
+
const yamlLines = lines.slice(1, sep);
|
|
106
|
+
const prompt = lines.slice(sep + 1).join("\n").trim();
|
|
107
|
+
|
|
108
|
+
let name: string | undefined;
|
|
109
|
+
let description: string | undefined;
|
|
110
|
+
let tools: string[] | undefined;
|
|
111
|
+
|
|
112
|
+
for (const line of yamlLines) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
const colonIdx = trimmed.indexOf(":");
|
|
115
|
+
if (colonIdx === -1) continue;
|
|
116
|
+
|
|
117
|
+
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
118
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
119
|
+
|
|
120
|
+
if (key === "name") {
|
|
121
|
+
// Strip surrounding quotes
|
|
122
|
+
name = val.replace(/^["']|["']$/g, "");
|
|
123
|
+
} else if (key === "description") {
|
|
124
|
+
description = val.replace(/^["']|["']$/g, "");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse tools from the tools frontmatter.
|
|
129
|
+
// Supports two formats:
|
|
130
|
+
// tools: ["bash", "view"] (JSON array, single line)
|
|
131
|
+
// tools: (YAML list, subsequent lines)
|
|
132
|
+
// - bash
|
|
133
|
+
// - view
|
|
134
|
+
let inToolsBlock = false;
|
|
135
|
+
for (const line of yamlLines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
|
|
138
|
+
// Check for inline JSON array: tools: ["foo", "bar"]
|
|
139
|
+
const colonIdx = trimmed.indexOf(":");
|
|
140
|
+
if (colonIdx !== -1) {
|
|
141
|
+
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
|
142
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
143
|
+
if (key === "tools") {
|
|
144
|
+
if (val.startsWith("[")) {
|
|
145
|
+
// Inline JSON array
|
|
146
|
+
try {
|
|
147
|
+
tools = JSON.parse(val);
|
|
148
|
+
} catch {
|
|
149
|
+
// Malformed JSON — fall through
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (val === "[]" || val === "") {
|
|
154
|
+
// Empty tools — explicit
|
|
155
|
+
tools = [];
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// YAML block format: "tools:" on its own line, items on subsequent lines
|
|
162
|
+
if (trimmed === "tools:") {
|
|
163
|
+
inToolsBlock = true;
|
|
164
|
+
if (!tools) tools = [];
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inToolsBlock) {
|
|
168
|
+
// Check if this line is still part of the tools block (indented or starts with -)
|
|
169
|
+
if (line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("- ")) {
|
|
170
|
+
const item = trimmed.replace(/^- /, "").trim().replace(/^["']|["']$/g, "");
|
|
171
|
+
if (item) {
|
|
172
|
+
if (!tools) tools = [];
|
|
173
|
+
tools.push(item);
|
|
174
|
+
}
|
|
175
|
+
} else if (trimmed !== "") {
|
|
176
|
+
// Non-empty, non-indented line → tools block ended
|
|
177
|
+
inToolsBlock = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { name, description, tools, prompt };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function mapCopilotToolsToPi(copilotTools: string[] | undefined): string[] | undefined {
|
|
186
|
+
if (!copilotTools || copilotTools.length === 0) return undefined;
|
|
187
|
+
|
|
188
|
+
const piTools = new Set<string>();
|
|
189
|
+
for (const ct of copilotTools) {
|
|
190
|
+
const mapped = COPILOT_TO_PI[ct];
|
|
191
|
+
if (mapped) {
|
|
192
|
+
for (const t of mapped) piTools.add(t);
|
|
193
|
+
} else {
|
|
194
|
+
// Pass through unknown tool names (could be custom tools)
|
|
195
|
+
piTools.add(ct);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [...piTools].sort();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function discoverAgents(): AgentDef[] {
|
|
202
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
203
|
+
console.warn(`[agent-modes] Agents directory not found: ${AGENTS_DIR}`);
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const files = readdirSync(AGENTS_DIR)
|
|
208
|
+
.filter((f) => f.endsWith(".agent.md"))
|
|
209
|
+
.sort();
|
|
210
|
+
|
|
211
|
+
const agents: AgentDef[] = [];
|
|
212
|
+
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
const filePath = join(AGENTS_DIR, file);
|
|
215
|
+
if (!statSync(filePath).isFile()) continue;
|
|
216
|
+
|
|
217
|
+
let raw: string;
|
|
218
|
+
try {
|
|
219
|
+
raw = readFileSync(filePath, "utf-8");
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.warn(`[agent-modes] Failed to read ${file}: ${err}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const parsed = parseFrontmatter(raw);
|
|
226
|
+
const id = file.replace(/\.agent\.md$/i, "").toLowerCase();
|
|
227
|
+
const name = parsed.name || prettifyId(id);
|
|
228
|
+
const tools = mapCopilotToolsToPi(parsed.tools);
|
|
229
|
+
const color = AGENT_COLORS[id] || "accent";
|
|
230
|
+
|
|
231
|
+
agents.push({
|
|
232
|
+
id,
|
|
233
|
+
name,
|
|
234
|
+
description: parsed.description || "",
|
|
235
|
+
color,
|
|
236
|
+
tools: tools || [...DEFAULT_TOOLS],
|
|
237
|
+
prompt: parsed.prompt || `You are ${name}. ${parsed.description || ""}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return agents;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function prettifyId(id: string): string {
|
|
245
|
+
return id
|
|
246
|
+
.split("-")
|
|
247
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
248
|
+
.join(" ");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Extension ───────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
export default function agentModesExtension(pi: ExtensionAPI): void {
|
|
254
|
+
const agents = discoverAgents();
|
|
255
|
+
|
|
256
|
+
if (agents.length === 0) {
|
|
257
|
+
console.warn("[agent-modes] No agents found. Extension disabled.");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let activeAgentIndex = agents.length; // default mode is beyond the last agent
|
|
262
|
+
let originalState: OriginalState | undefined;
|
|
263
|
+
let baselineTools: string[] = [];
|
|
264
|
+
let registeredCommands = false;
|
|
265
|
+
|
|
266
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function getActiveModeLabel(): string {
|
|
269
|
+
if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return "default";
|
|
270
|
+
return agents[activeAgentIndex].name;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getActiveColor(): string {
|
|
274
|
+
if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return "accent";
|
|
275
|
+
return agents[activeAgentIndex].color;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isDefaultMode(): boolean {
|
|
279
|
+
return activeAgentIndex >= agents.length || activeAgentIndex < 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getActiveAgent(): AgentDef | undefined {
|
|
283
|
+
if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return undefined;
|
|
284
|
+
return agents[activeAgentIndex];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function setAgentMode(ctx: ExtensionContext, index: number): boolean {
|
|
288
|
+
if (index < 0) index = agents.length; // default mode
|
|
289
|
+
if (index > agents.length) return false;
|
|
290
|
+
|
|
291
|
+
// Snapshot before first switch
|
|
292
|
+
if (originalState === undefined) {
|
|
293
|
+
originalState = { tools: [...baselineTools] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (index >= agents.length) {
|
|
297
|
+
// Default mode: restore baseline tools
|
|
298
|
+
activeAgentIndex = agents.length;
|
|
299
|
+
try {
|
|
300
|
+
if (originalState) {
|
|
301
|
+
pi.setActiveTools(originalState.tools);
|
|
302
|
+
} else if (baselineTools.length > 0) {
|
|
303
|
+
pi.setActiveTools(baselineTools);
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn(`[agent-modes] Failed to restore tools: ${err}`);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Agent mode: set agent-specific tools
|
|
311
|
+
const agent = agents[index];
|
|
312
|
+
activeAgentIndex = index;
|
|
313
|
+
try {
|
|
314
|
+
pi.setActiveTools(agent.tools);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.warn(`[agent-modes] Failed to set tools for ${agent.id}: ${err}`);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
updateUI(ctx);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function updateUI(ctx: ExtensionContext): void {
|
|
326
|
+
const label = getActiveModeLabel();
|
|
327
|
+
if (label === "default") {
|
|
328
|
+
ctx.ui.setStatus("agent-mode", undefined);
|
|
329
|
+
} else {
|
|
330
|
+
ctx.ui.setStatus("agent-mode", ctx.ui.theme.fg(getActiveColor(), `agent:${label}`));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function persistState(): void {
|
|
335
|
+
try {
|
|
336
|
+
pi.appendEntry(PERSIST_KEY, {
|
|
337
|
+
agentIndex: activeAgentIndex,
|
|
338
|
+
agentId: activeAgentIndex < agents.length ? agents[activeAgentIndex]?.id : undefined,
|
|
339
|
+
});
|
|
340
|
+
} catch {
|
|
341
|
+
// Non-critical
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Register commands & shortcuts (once) ────────────────────────────────
|
|
346
|
+
|
|
347
|
+
function registerCommands(): void {
|
|
348
|
+
if (registeredCommands) return;
|
|
349
|
+
registeredCommands = true;
|
|
350
|
+
|
|
351
|
+
// /agent command
|
|
352
|
+
pi.registerCommand("agent", {
|
|
353
|
+
description: `Switch agent mode (${agents.map((a) => a.id).join(", ")})`,
|
|
354
|
+
handler: async (args, ctx) => {
|
|
355
|
+
if (!args?.trim()) {
|
|
356
|
+
await showAgentSelector(ctx);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const input = args.trim().toLowerCase();
|
|
361
|
+
|
|
362
|
+
// "default" returns to default mode
|
|
363
|
+
if (input === "default" || input === "none") {
|
|
364
|
+
if (setAgentMode(ctx, agents.length)) {
|
|
365
|
+
persistState();
|
|
366
|
+
ctx.ui.notify("Switched to default mode (full access)", "info");
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const index = agents.findIndex((a) => a.id === input);
|
|
372
|
+
if (index === -1) {
|
|
373
|
+
ctx.ui.notify(
|
|
374
|
+
`Unknown agent "${input}". Available: ${agents.map((a) => a.id).join(", ")}`,
|
|
375
|
+
"error",
|
|
376
|
+
);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (setAgentMode(ctx, index)) {
|
|
381
|
+
persistState();
|
|
382
|
+
ctx.ui.notify(`Switched to agent: ${agents[index].name}`, "info");
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Ctrl+Shift+A to cycle agents (A = Agent)
|
|
388
|
+
pi.registerShortcut(Key.ctrlShift("a"), {
|
|
389
|
+
description: "Next agent mode",
|
|
390
|
+
handler: async (ctx) => {
|
|
391
|
+
const next = (activeAgentIndex + 1) % (agents.length + 1);
|
|
392
|
+
if (setAgentMode(ctx, next)) {
|
|
393
|
+
persistState();
|
|
394
|
+
const label = getActiveModeLabel();
|
|
395
|
+
ctx.ui.notify(`Mode: ${label}`, "info");
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// --agent CLI flag
|
|
401
|
+
pi.registerFlag("agent", {
|
|
402
|
+
description: `Start in a specific agent mode (${agents.map((a) => a.id).join(", ")})`,
|
|
403
|
+
type: "string",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// inject agent prompt before each agent start
|
|
407
|
+
pi.on("before_agent_start", async (event) => {
|
|
408
|
+
const agent = getActiveAgent();
|
|
409
|
+
if (!agent || !agent.prompt) return;
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
systemPrompt: `${event.systemPrompt}\n\n---\n${agent.prompt}`,
|
|
413
|
+
message: {
|
|
414
|
+
customType: "agent-mode-context",
|
|
415
|
+
content: `[AGENT MODE: ${agent.name.toUpperCase()}]\n${agent.description || "No description"}`,
|
|
416
|
+
display: false,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Agent selector UI ────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
async function showAgentSelector(ctx: ExtensionContext): Promise<void> {
|
|
425
|
+
const choices: string[] = [
|
|
426
|
+
"(default) — Full tool access, no agent prompt",
|
|
427
|
+
...agents.map((agent) => {
|
|
428
|
+
const active = activeAgentIndex >= 0 && agents[activeAgentIndex]?.id === agent.id
|
|
429
|
+
? " (active)" : "";
|
|
430
|
+
return `${agent.name}${active} — ${agent.description.slice(0, 60)}`;
|
|
431
|
+
}),
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const result = await ctx.ui.select("Select agent mode:", choices);
|
|
435
|
+
if (!result) return;
|
|
436
|
+
|
|
437
|
+
if (result.startsWith("(default)")) {
|
|
438
|
+
if (setAgentMode(ctx, agents.length)) {
|
|
439
|
+
persistState();
|
|
440
|
+
ctx.ui.notify("Switched to default mode (full access)", "info");
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Match the agent name from the choice label
|
|
446
|
+
const matchedAgent = agents.find((a) => result.startsWith(a.name));
|
|
447
|
+
if (matchedAgent) {
|
|
448
|
+
const index = agents.indexOf(matchedAgent);
|
|
449
|
+
if (setAgentMode(ctx, index)) {
|
|
450
|
+
persistState();
|
|
451
|
+
ctx.ui.notify(`Switched to agent: ${matchedAgent.name}`, "info");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Bootstrap ────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
459
|
+
// Capture baseline tools once all extensions are loaded
|
|
460
|
+
try {
|
|
461
|
+
baselineTools = pi.getActiveTools();
|
|
462
|
+
} catch {
|
|
463
|
+
baselineTools = [...DEFAULT_TOOLS];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
registerCommands();
|
|
467
|
+
|
|
468
|
+
// Restore persisted state
|
|
469
|
+
const entries = ctx.sessionManager.getEntries();
|
|
470
|
+
let restoredIndex: number | undefined;
|
|
471
|
+
|
|
472
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
473
|
+
const entry = entries[i] as { type: string; customType?: string; data?: { agentIndex?: number } };
|
|
474
|
+
if (entry.type === "custom" && entry.customType === PERSIST_KEY) {
|
|
475
|
+
if (typeof entry.data?.agentIndex === "number") {
|
|
476
|
+
restoredIndex = entry.data.agentIndex;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// --agent CLI flag takes priority
|
|
483
|
+
const agentFlag = pi.getFlag("agent");
|
|
484
|
+
let targetIndex: number;
|
|
485
|
+
|
|
486
|
+
if (typeof agentFlag === "string" && agentFlag) {
|
|
487
|
+
const flagIdx = agents.findIndex((a) => a.id === agentFlag.toLowerCase());
|
|
488
|
+
if (flagIdx !== -1) {
|
|
489
|
+
targetIndex = flagIdx;
|
|
490
|
+
} else {
|
|
491
|
+
console.warn(`[agent-modes] Unknown --agent "${agentFlag}".`);
|
|
492
|
+
targetIndex = restoredIndex ?? agents.length;
|
|
493
|
+
}
|
|
494
|
+
} else if (restoredIndex !== undefined) {
|
|
495
|
+
targetIndex = restoredIndex;
|
|
496
|
+
} else {
|
|
497
|
+
targetIndex = agents.length; // default mode
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (setAgentMode(ctx, targetIndex)) {
|
|
501
|
+
const label = getActiveModeLabel();
|
|
502
|
+
if (label !== "default") {
|
|
503
|
+
ctx.ui.notify(`Agent: ${label}`, "info");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-name sessions using a tiny side-agent LLM.
|
|
3
|
+
*
|
|
4
|
+
* On the first user prompt of a new session, calls a cheap configured model
|
|
5
|
+
* (default: opencode/mimo-v2.5-free) to generate a concise session title,
|
|
6
|
+
* then sets it via pi.setSessionName(). Falls back to simple truncation if
|
|
7
|
+
* the LLM call fails.
|
|
8
|
+
*
|
|
9
|
+
* Config (in settings.json under `tinyModel`):
|
|
10
|
+
* ```json
|
|
11
|
+
* {
|
|
12
|
+
* "tinyModel": {
|
|
13
|
+
* "model": "opencode/mimo-v2.5-free",
|
|
14
|
+
* "maxNameLength": 50
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Existing names (set via --name or /name) are never overwritten.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import {
|
|
24
|
+
loadTinyModelConfig,
|
|
25
|
+
parseModelString,
|
|
26
|
+
generateNameFromPrompt,
|
|
27
|
+
} from "./lib/tiny-model.js";
|
|
28
|
+
import type { TinyModelConfig } from "./lib/tiny-model.js";
|
|
29
|
+
|
|
30
|
+
export default function (pi: ExtensionAPI) {
|
|
31
|
+
let config: TinyModelConfig;
|
|
32
|
+
let hasNamed = false;
|
|
33
|
+
|
|
34
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
35
|
+
config = loadTinyModelConfig(ctx.cwd);
|
|
36
|
+
hasNamed = false;
|
|
37
|
+
|
|
38
|
+
// If session already has a name (resume, fork, or --name), skip
|
|
39
|
+
if (pi.getSessionName()) {
|
|
40
|
+
hasNamed = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
pi.on("input", async (event, ctx) => {
|
|
45
|
+
if (!config.enabled || hasNamed) return;
|
|
46
|
+
if (event.source === "rpc") return;
|
|
47
|
+
if (pi.getSessionName()) {
|
|
48
|
+
hasNamed = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!event.text?.trim()) return;
|
|
52
|
+
|
|
53
|
+
hasNamed = true;
|
|
54
|
+
|
|
55
|
+
// Fire the LLM call asynchronously — don't block the agent
|
|
56
|
+
(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const { provider, id } = parseModelString(config.model);
|
|
59
|
+
const model = ctx.modelRegistry.find(provider, id);
|
|
60
|
+
if (!model) {
|
|
61
|
+
// Model not found; fall back to truncation
|
|
62
|
+
fallbackName(event.text, config.maxNameLength, pi);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { apiKey, headers } =
|
|
67
|
+
await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
68
|
+
|
|
69
|
+
const name = await generateNameFromPrompt(
|
|
70
|
+
model,
|
|
71
|
+
apiKey,
|
|
72
|
+
headers,
|
|
73
|
+
event.text,
|
|
74
|
+
config.maxNameLength,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (name) {
|
|
78
|
+
pi.setSessionName(name);
|
|
79
|
+
} else {
|
|
80
|
+
fallbackName(event.text, config.maxNameLength, pi);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.warn("[auto-name] LLM call failed, falling back:", err);
|
|
84
|
+
fallbackName(event.text, config.maxNameLength, pi);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fallback: truncate the first line of the prompt to use as a session name.
|
|
92
|
+
*/
|
|
93
|
+
function fallbackName(
|
|
94
|
+
text: string,
|
|
95
|
+
maxLength: number,
|
|
96
|
+
pi: ExtensionAPI,
|
|
97
|
+
): void {
|
|
98
|
+
const firstLine = text.split("\n")[0]?.trim() ?? "";
|
|
99
|
+
if (!firstLine) return;
|
|
100
|
+
|
|
101
|
+
const name =
|
|
102
|
+
firstLine.length > maxLength
|
|
103
|
+
? firstLine.slice(0, maxLength - 1) + "\u2026"
|
|
104
|
+
: firstLine;
|
|
105
|
+
|
|
106
|
+
pi.setSessionName(name);
|
|
107
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# context-loader
|
|
2
|
+
|
|
3
|
+
Utility module for loading project context files at session start.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Reads context files (AGENTS.md, user-preferences.md) from the project root and
|
|
8
|
+
formats them for injection into the agent session. This gives the agent baseline
|
|
9
|
+
project context without requiring manual file reads.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { loadSessionContext } from "./context-loader.js";
|
|
15
|
+
|
|
16
|
+
// Load default context files from project root
|
|
17
|
+
const context = loadSessionContext(cwd);
|
|
18
|
+
|
|
19
|
+
// Load custom file list
|
|
20
|
+
const context = loadSessionContext(cwd, ["AGENTS.md", "docs/custom.md"]);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Default Context Files
|
|
24
|
+
|
|
25
|
+
- `AGENTS.md` — Project instructions for agents
|
|
26
|
+
- `docs/user-preferences.md` — User tooling preferences
|
|
27
|
+
|
|
28
|
+
## Testing
|
|
29
|
+
|
|
30
|
+
Tests are in `extensions/skill-stats/context-loader.test.ts`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd extensions/skill-stats
|
|
34
|
+
node --test --import tsx/esm context-loader.test.ts
|
|
35
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context file loader for session-start extension.
|
|
3
|
+
*
|
|
4
|
+
* Reads project context files (AGENTS.md, user-preferences.md, etc.) and
|
|
5
|
+
* formats them for injection into the agent session at startup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
export interface ContextFile {
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default context files to load at session start.
|
|
18
|
+
* Paths are relative to the project root (cwd).
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_CONTEXT_FILES = [
|
|
21
|
+
"AGENTS.md",
|
|
22
|
+
"docs/user-preferences.md",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load a single context file. Returns null if the file doesn't exist or is empty.
|
|
27
|
+
*/
|
|
28
|
+
export function loadContextFile(filePath: string): ContextFile | null {
|
|
29
|
+
if (!filePath) return null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
33
|
+
if (!content) return null;
|
|
34
|
+
return { path: filePath, content };
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load all context files from the given paths (relative to cwd).
|
|
42
|
+
* Returns a formatted string ready for injection into the session.
|
|
43
|
+
*/
|
|
44
|
+
export function loadSessionContext(
|
|
45
|
+
cwd: string,
|
|
46
|
+
filePaths?: string[],
|
|
47
|
+
): string {
|
|
48
|
+
const files = filePaths ?? DEFAULT_CONTEXT_FILES;
|
|
49
|
+
const loaded: ContextFile[] = [];
|
|
50
|
+
|
|
51
|
+
for (const relPath of files) {
|
|
52
|
+
const absPath = path.resolve(cwd, relPath);
|
|
53
|
+
const file = loadContextFile(absPath);
|
|
54
|
+
if (file) loaded.push(file);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (loaded.length === 0) return "";
|
|
58
|
+
|
|
59
|
+
return loaded
|
|
60
|
+
.map((f) => {
|
|
61
|
+
const name = path.basename(f.path);
|
|
62
|
+
return `--- ${name} ---\n${f.content}`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n\n");
|
|
65
|
+
}
|