pi-soly 0.2.1 → 0.4.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/ask/README.md +135 -0
- package/ask/index.ts +218 -0
- package/ask/package.json +51 -0
- package/ask/picker.ts +686 -0
- package/ask/prompt.ts +37 -0
- package/ask/tests/picker.test.ts +588 -0
- package/ask/tests/prompt.test.ts +54 -0
- package/ask/tsconfig.json +28 -0
- package/commands.ts +2 -2
- package/docs.ts +2 -2
- package/index.ts +21 -13
- package/intent.ts +2 -2
- package/iteration.ts +1 -1
- package/package.json +6 -2
- package/switch/README.md +107 -0
- package/switch/core.ts +202 -0
- package/switch/index.ts +345 -0
- package/switch/package.json +52 -0
- package/switch/prompt.ts +134 -0
- package/switch/tests/core.test.ts +188 -0
- package/switch/tests/index.test.ts +47 -0
- package/switch/tests/prompt.test.ts +106 -0
- package/switch/tsconfig.json +28 -0
- package/tools.ts +6 -6
- package/workflows/execute.ts +1 -1
- package/workflows/index.ts +7 -7
- package/workflows/pause.ts +1 -1
- package/workflows/planning.ts +1 -1
- package/workflows/quick.ts +1 -1
- package/workflows/resume.ts +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"types": [
|
|
10
|
+
"node"
|
|
11
|
+
],
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"noUncheckedIndexedAccess": false,
|
|
20
|
+
"allowImportingTsExtensions": true
|
|
21
|
+
},
|
|
22
|
+
"include": [
|
|
23
|
+
"**/*.ts"
|
|
24
|
+
],
|
|
25
|
+
"exclude": [
|
|
26
|
+
"node_modules"
|
|
27
|
+
]
|
|
28
|
+
}
|
package/commands.ts
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
readIfExists,
|
|
29
29
|
type RuleFile,
|
|
30
30
|
type SolyState,
|
|
31
|
-
} from "./core.
|
|
32
|
-
import type { SolyConfig } from "./config.
|
|
31
|
+
} from "./core.ts";
|
|
32
|
+
import type { SolyConfig } from "./config.ts";
|
|
33
33
|
|
|
34
34
|
/** Minimum ui surface the command handlers actually need. */
|
|
35
35
|
export interface CommandUI {
|
package/docs.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { estimateTokens, findMarkdownFiles, readIfExists } from "./core.
|
|
14
|
-
import { extractTitleAndPreview, stripHtml } from "./html.
|
|
13
|
+
import { estimateTokens, findMarkdownFiles, readIfExists } from "./core.ts";
|
|
14
|
+
import { extractTitleAndPreview, stripHtml } from "./html.ts";
|
|
15
15
|
|
|
16
16
|
// Re-export the stripHtml helper so existing imports of `stripHtml from
|
|
17
17
|
// "./docs.js"` (used by tools.ts) continue to work without churn.
|
package/index.ts
CHANGED
|
@@ -43,24 +43,28 @@ import {
|
|
|
43
43
|
type RuleFile,
|
|
44
44
|
type SolyState,
|
|
45
45
|
type SourceSpec,
|
|
46
|
-
} from "./core.
|
|
47
|
-
import { buildIntegrationsSection } from "./integrations.
|
|
48
|
-
import { installSolyAgents } from "./agents-install.
|
|
46
|
+
} from "./core.ts";
|
|
47
|
+
import { buildIntegrationsSection } from "./integrations.ts";
|
|
48
|
+
import { installSolyAgents } from "./agents-install.ts";
|
|
49
49
|
import {
|
|
50
50
|
DEFAULT_CONFIG,
|
|
51
51
|
loadConfig,
|
|
52
52
|
pruneOldIterations,
|
|
53
53
|
type SolyConfig,
|
|
54
|
-
} from "./config.
|
|
55
|
-
import { classifyTaskHeuristics, buildNudgeSection } from "./nudge.
|
|
56
|
-
import { registerCommands, type CommandUI } from "./commands.
|
|
57
|
-
import { registerTools } from "./tools.
|
|
58
|
-
import { registerWorkflows } from "./workflows/index.
|
|
59
|
-
import { readGitContext, buildGitSection, type GitContext } from "./git.
|
|
60
|
-
import { startHotReload, type HotReloadHandle } from "./hotreload.
|
|
61
|
-
import { detectEnv, buildEnvSection, type EnvSummary } from "./env.
|
|
62
|
-
import { buildCodeMap, buildCodeMapSection, type CodeMap } from "./codemap.
|
|
63
|
-
import { loadIntentDocs, buildIntentSection, loadInlineIntentBodies, type IntentDoc } from "./intent.
|
|
54
|
+
} from "./config.ts";
|
|
55
|
+
import { classifyTaskHeuristics, buildNudgeSection } from "./nudge.ts";
|
|
56
|
+
import { registerCommands, type CommandUI } from "./commands.ts";
|
|
57
|
+
import { registerTools } from "./tools.ts";
|
|
58
|
+
import { registerWorkflows } from "./workflows/index.ts";
|
|
59
|
+
import { readGitContext, buildGitSection, type GitContext } from "./git.ts";
|
|
60
|
+
import { startHotReload, type HotReloadHandle } from "./hotreload.ts";
|
|
61
|
+
import { detectEnv, buildEnvSection, type EnvSummary } from "./env.ts";
|
|
62
|
+
import { buildCodeMap, buildCodeMapSection, type CodeMap } from "./codemap.ts";
|
|
63
|
+
import { loadIntentDocs, buildIntentSection, loadInlineIntentBodies, type IntentDoc } from "./intent.ts";
|
|
64
|
+
|
|
65
|
+
// Built-in sub-features (merged from former pi-asked, pi-agented packages):
|
|
66
|
+
import piAskExtension from "./ask/index.ts";
|
|
67
|
+
import piSwitchExtension from "./switch/index.ts";
|
|
64
68
|
|
|
65
69
|
export default function solyExtension(pi: ExtensionAPI) {
|
|
66
70
|
// ============================================================================
|
|
@@ -715,4 +719,8 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
715
719
|
updateStatus(ctx);
|
|
716
720
|
}
|
|
717
721
|
});
|
|
722
|
+
|
|
723
|
+
// Mount built-in sub-features
|
|
724
|
+
piAskExtension(pi);
|
|
725
|
+
piSwitchExtension(pi);
|
|
718
726
|
}
|
package/intent.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
|
-
import { formatTok, resolveImports } from "./core.
|
|
26
|
-
import { extractTitleAndPreview, stripHtml } from "./html.
|
|
25
|
+
import { formatTok, resolveImports } from "./core.ts";
|
|
26
|
+
import { extractTitleAndPreview, stripHtml } from "./html.ts";
|
|
27
27
|
|
|
28
28
|
const DOC_EXTS = new Set([".md", ".html", ".htm"]);
|
|
29
29
|
|
package/iteration.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import * as fs from "node:fs";
|
|
27
27
|
import * as path from "node:path";
|
|
28
|
-
import { atomicWriteFileSync, readIfExists } from "./core.
|
|
28
|
+
import { atomicWriteFileSync, readIfExists } from "./core.ts";
|
|
29
29
|
|
|
30
30
|
export type IterationKind = "exec" | "plan" | "discuss" | "pause";
|
|
31
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Project management for pi — plans, state, subagent-driven execution. Inspired by GSD.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"tools.ts",
|
|
39
39
|
"agents-install.ts",
|
|
40
40
|
"agents",
|
|
41
|
+
"ask",
|
|
42
|
+
"switch",
|
|
41
43
|
"workflows",
|
|
42
44
|
"workflows-data"
|
|
43
45
|
],
|
|
@@ -47,7 +49,9 @@
|
|
|
47
49
|
"pi-package",
|
|
48
50
|
"project-management",
|
|
49
51
|
"planning",
|
|
50
|
-
"subagents"
|
|
52
|
+
"subagents",
|
|
53
|
+
"multi-question",
|
|
54
|
+
"agent-switcher"
|
|
51
55
|
],
|
|
52
56
|
"license": "MIT",
|
|
53
57
|
"pi": {
|
package/switch/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# pi-switch — generic subagent switcher for pi
|
|
2
|
+
|
|
3
|
+
A tiny pi extension that gives you a **persistent indicator of the current subagent** (header bar above chat) and lets you **cycle / set / create** agents. Generic — works with any agent in `~/.pi/agent/agents/`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Header bar above chat** — shows current agent with emoji + description
|
|
8
|
+
- **Ctrl+Shift+S** to cycle to next agent (Shift+Tab is taken by pi's thinking-level cycler)
|
|
9
|
+
- **`/agent`** slash command to show current + available
|
|
10
|
+
- **`/agent <name>`** to set explicitly
|
|
11
|
+
- **`/agent create <name>`** to scaffold a new user agent
|
|
12
|
+
- **`/agent doctor`** to diagnose
|
|
13
|
+
- **`/agent recommend <task>`** to suggest the right agent for a task
|
|
14
|
+
- **Task → agent heuristics** baked into the system prompt so the LLM picks the right agent for the task
|
|
15
|
+
- Persists to `.soly/agent` (if soly project) or `~/.pi-switch/agent` (standalone)
|
|
16
|
+
- Reads user agents from `~/.pi/agent/agents/*.md` on every cycle — drop a file and Ctrl+Shift+S to see it
|
|
17
|
+
|
|
18
|
+
## How agents work
|
|
19
|
+
|
|
20
|
+
Agents are markdown files with YAML frontmatter. pi-subagents (and pi-switch) discover them from these locations:
|
|
21
|
+
|
|
22
|
+
| Path | Type | Editable |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `~/.pi/agent/npm/node_modules/pi-subagents/agents/*.md` | built-in (worker, oracle, scout, ...) | ❌ |
|
|
25
|
+
| `~/.pi/agent/agents/*.md` | user-defined | ✅ |
|
|
26
|
+
| `~/.pi/agent/extensions/soly/agents/*.md` (auto-installed if `useSolyWorkerSubagents: true` in `.soly/config.json`) | soly-augmented (soly-worker, soly-debugger, ...) | ✅ source |
|
|
27
|
+
|
|
28
|
+
### Frontmatter schema
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
---
|
|
32
|
+
name: my-reviewer # required, unique, [a-zA-Z0-9_-]{1,64}
|
|
33
|
+
description: One-liner shown in picker
|
|
34
|
+
thinking: medium # off | minimal | low | medium | high | xhigh
|
|
35
|
+
systemPromptMode: replace # replace | append
|
|
36
|
+
inheritProjectContext: true
|
|
37
|
+
inheritSkills: false
|
|
38
|
+
tools: read, grep, find, ls, bash, edit, write
|
|
39
|
+
defaultContext: fork # fresh | fork
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
You are `my-reviewer`. The system prompt goes here.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Create a new agent
|
|
46
|
+
|
|
47
|
+
### Option A: manually
|
|
48
|
+
Drop a markdown file in `~/.pi/agent/agents/<name>.md` (see schema above). Press `Ctrl+Shift+S` in pi — it joins the cycle.
|
|
49
|
+
|
|
50
|
+
### Option B: via slash command
|
|
51
|
+
```
|
|
52
|
+
/agent create my-debugger
|
|
53
|
+
```
|
|
54
|
+
You'll be prompted for a one-liner description. Then edit the file to specialize the system prompt.
|
|
55
|
+
|
|
56
|
+
## Test agents
|
|
57
|
+
|
|
58
|
+
| Action | How |
|
|
59
|
+
|---|---|
|
|
60
|
+
| See current + available | `/agent` |
|
|
61
|
+
| Cycle | `Ctrl+Shift+S` |
|
|
62
|
+
| Set explicitly | `/agent soly-debugger` |
|
|
63
|
+
| Diagnose | `/agent doctor` |
|
|
64
|
+
| Recommend for a task | `/agent recommend investigate React Server Components` |
|
|
65
|
+
|
|
66
|
+
The LLM can also auto-pick — see "Task → agent" below.
|
|
67
|
+
|
|
68
|
+
## Task → agent heuristics
|
|
69
|
+
|
|
70
|
+
The LLM's system prompt includes a table mapping task keywords to agents. When the user request matches, the LLM should call `/agent <name>` first, then `subagent({ agent: <name>, ... })`.
|
|
71
|
+
|
|
72
|
+
| Keywords | Agent | Why |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| research, investigate, look up, find out, explore, compare libraries | 📚 researcher | external docs, ecosystem behavior |
|
|
75
|
+
| scout, scan, map, where is, locate, skim | 🔍 scout | codebase recon |
|
|
76
|
+
| plan, design, architect, outline, structure | 📋 planner | decompose into steps |
|
|
77
|
+
| review, audit, check, adversarial, critique, qa | 👀 reviewer | adversarial review |
|
|
78
|
+
| oracle, decision, tradeoff, which approach, drift | 🔮 oracle | decision consistency |
|
|
79
|
+
| debug, bug, fix, crash, error, repro | 🐞 soly-debugger | bug investigation |
|
|
80
|
+
| test, tests, coverage, spec, assert | 🧪 soly-tester | test-only work |
|
|
81
|
+
| refactor, clean up, simplify, extract, rename | 🔄 soly-refactor | pure refactoring |
|
|
82
|
+
| document, docs, readme, jsdoc | 📝 soly-documenter | doc updates |
|
|
83
|
+
| implement, build, write code, add feature | ⚡ worker | implementation |
|
|
84
|
+
| orchestrate, coordinate, dispatch, chain | 🤝 delegate | multi-agent |
|
|
85
|
+
|
|
86
|
+
Same keywords in Russian work (изучи, баг, тест, etc.).
|
|
87
|
+
|
|
88
|
+
## Integration with other extensions
|
|
89
|
+
|
|
90
|
+
- **soly** reads `globalThis.__PI_SWITCH_AGENT__` to know which subagent to launch for `soly execute`. Falls back to `"worker"` if pi-switch isn't loaded.
|
|
91
|
+
- **Soly** also auto-installs soly-augmented agents (soly-worker, soly-debugger, etc.) to `~/.pi/agent/agents/` when `useSolyWorkerSubagents: true` in `.soly/config.json`.
|
|
92
|
+
|
|
93
|
+
## Files
|
|
94
|
+
|
|
95
|
+
- `core.ts` — agent metadata, discovery, cycling, persistence
|
|
96
|
+
- `prompt.ts` — system-prompt section + task→agent heuristics + `recommendAgent`
|
|
97
|
+
- `index.ts` — header bar, Ctrl+Shift+S, `/agent` slash command, `/agent create`/`/agent doctor`/`/agent recommend`
|
|
98
|
+
- `tests/core.test.ts` — 21 tests for core logic
|
|
99
|
+
- `tests/prompt.test.ts` — 20 tests for prompt + recommendAgent
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd ~/.pi/agent/extensions/pi-switch
|
|
105
|
+
bun test # 41 tests
|
|
106
|
+
bun run typecheck # tsc --noEmit
|
|
107
|
+
```
|
package/switch/core.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// core.ts — Generic subagent switcher for pi
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Lets the user pick which subagent the LLM uses (for `subagent(...)` calls
|
|
6
|
+
// in the pi-subagents system, and for any extension that reads the current
|
|
7
|
+
// agent). Generic — works with pi-subagents' built-ins (worker, oracle,
|
|
8
|
+
// scout, ...) AND any user-defined agent in `~/.pi/agent/agents/`.
|
|
9
|
+
//
|
|
10
|
+
// Cycle order (Shift+Tab in pi is taken by thinking-level, so we use
|
|
11
|
+
// Ctrl+Shift+S — mnemonic for "S"witch).
|
|
12
|
+
//
|
|
13
|
+
// Communication with other extensions:
|
|
14
|
+
// - Writes `globalThis.__PI_SWITCH_AGENT__` (in-process)
|
|
15
|
+
// - Reads/writes `.soly/agent` if it exists (cross-session persistence,
|
|
16
|
+
// shared with soly extension). If no soly project, persists to
|
|
17
|
+
// `~/.pi-switch/agent` instead.
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as os from "node:os";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
|
|
24
|
+
/** Default agent used when no override is set. */
|
|
25
|
+
export const DEFAULT_AGENT = "worker";
|
|
26
|
+
|
|
27
|
+
/** Built-in pi-subagents that we always offer in the cycle. */
|
|
28
|
+
export const BUILTIN_AGENTS: readonly string[] = [
|
|
29
|
+
"worker",
|
|
30
|
+
"oracle",
|
|
31
|
+
"scout",
|
|
32
|
+
"researcher",
|
|
33
|
+
"planner",
|
|
34
|
+
"context-builder",
|
|
35
|
+
"reviewer",
|
|
36
|
+
"delegate",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/** Visual metadata for every known agent. Used by the rich status badge,
|
|
40
|
+
* the header bar, and the multi-line switch notify. */
|
|
41
|
+
export interface AgentMeta {
|
|
42
|
+
emoji: string;
|
|
43
|
+
shortLabel: string;
|
|
44
|
+
description: string;
|
|
45
|
+
writesFiles: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const AGENT_META: Record<string, AgentMeta> = {
|
|
49
|
+
worker: { emoji: "\u26a1", shortLabel: "worker", description: "generic implementation, all tools", writesFiles: true },
|
|
50
|
+
oracle: { emoji: "\ud83d\udd2e", shortLabel: "oracle", description: "decision-consistency, no file edits", writesFiles: false },
|
|
51
|
+
scout: { emoji: "\ud83d\udd0d", shortLabel: "scout", description: "codebase recon, read-only", writesFiles: false },
|
|
52
|
+
researcher: { emoji: "\ud83d\udcda", shortLabel: "researcher", description: "external docs / libraries", writesFiles: false },
|
|
53
|
+
planner: { emoji: "\ud83d\udccb", shortLabel: "planner", description: "planning + ordering, no code", writesFiles: false },
|
|
54
|
+
"context-builder": { emoji: "\ud83c\udfd7", shortLabel: "ctx-builder", description: "context handoff for other agents", writesFiles: true },
|
|
55
|
+
reviewer: { emoji: "\ud83d\udc40", shortLabel: "reviewer", description: "adversarial code review", writesFiles: false },
|
|
56
|
+
delegate: { emoji: "\ud83e\udd1d", shortLabel: "delegate", description: "pure orchestration, dispatches others", writesFiles: false },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Get metadata for an agent. Falls back to a neutral entry for unknown. */
|
|
60
|
+
export function getAgentMeta(name: string): AgentMeta {
|
|
61
|
+
return AGENT_META[name] ?? {
|
|
62
|
+
emoji: "\u2753",
|
|
63
|
+
shortLabel: name.length > 12 ? name.slice(0, 11) + "\u2026" : name,
|
|
64
|
+
description: "user-defined agent",
|
|
65
|
+
writesFiles: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate an agent name. */
|
|
70
|
+
export function isValidAgentName(name: string): boolean {
|
|
71
|
+
return /^[a-zA-Z0-9_-]{1,64}$/.test(name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Discover agent `.md` files in user dir. */
|
|
75
|
+
export function discoverUserAgents(userDir: string = path.join(os.homedir(), ".pi", "agent", "agents")): string[] {
|
|
76
|
+
if (!fs.existsSync(userDir)) return [];
|
|
77
|
+
const names: string[] = [];
|
|
78
|
+
for (const file of fs.readdirSync(userDir)) {
|
|
79
|
+
if (!file.endsWith(".md")) continue;
|
|
80
|
+
try {
|
|
81
|
+
const raw = fs.readFileSync(path.join(userDir, file), "utf-8");
|
|
82
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
83
|
+
if (!m) continue;
|
|
84
|
+
const fm = m[1] ?? "";
|
|
85
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
86
|
+
if (nameMatch) {
|
|
87
|
+
const n = (nameMatch[1] ?? "").trim();
|
|
88
|
+
if (isValidAgentName(n)) names.push(n);
|
|
89
|
+
}
|
|
90
|
+
} catch { /* skip */ }
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build the full cycle of available agents. Built-ins first, then
|
|
96
|
+
* user-discovered. Dedupes while preserving first-occurrence order. */
|
|
97
|
+
export function availableAgents(userDir?: string): string[] {
|
|
98
|
+
const out: string[] = [];
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
const push = (n: string) => {
|
|
101
|
+
if (!seen.has(n)) {
|
|
102
|
+
seen.add(n);
|
|
103
|
+
out.push(n);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (const a of BUILTIN_AGENTS) push(a);
|
|
107
|
+
for (const a of discoverUserAgents(userDir)) push(a);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Cycle order. */
|
|
112
|
+
export function nextAgent(current: string, cycle: readonly string[]): string {
|
|
113
|
+
if (cycle.length === 0) return DEFAULT_AGENT;
|
|
114
|
+
const idx = cycle.indexOf(current);
|
|
115
|
+
if (idx < 0) return cycle[0]!;
|
|
116
|
+
return cycle[(idx + 1) % cycle.length]!;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Parse a user-supplied agent name. */
|
|
120
|
+
export function parseAgentName(raw: string): string | null {
|
|
121
|
+
const n = raw.trim();
|
|
122
|
+
if (!isValidAgentName(n)) return null;
|
|
123
|
+
return n;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Short badge: `<emoji> <name>`. Null for default (silent). */
|
|
127
|
+
export function formatAgentBadge(agent: string): string | null {
|
|
128
|
+
if (agent === DEFAULT_AGENT) return null;
|
|
129
|
+
const meta = getAgentMeta(agent);
|
|
130
|
+
return `${meta.emoji} ${agent}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Multi-line switch notify. */
|
|
134
|
+
export function formatAgentSwitchNotify(prev: string, next: string): string {
|
|
135
|
+
const prevMeta = getAgentMeta(prev);
|
|
136
|
+
const nextMeta = getAgentMeta(next);
|
|
137
|
+
const lines: string[] = [
|
|
138
|
+
"pi-switch agent changed",
|
|
139
|
+
"",
|
|
140
|
+
` ${prevMeta.emoji} ${prev.padEnd(16)} → ${nextMeta.emoji} ${next}`,
|
|
141
|
+
` ${"".padEnd(16)} ${nextMeta.description}`,
|
|
142
|
+
"",
|
|
143
|
+
` writes files: ${nextMeta.writesFiles ? "yes" : "no (read-only)"} · next subagent call uses: ${next}`,
|
|
144
|
+
];
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Group agents: built-ins + user-defined. */
|
|
149
|
+
export function groupedAvailableAgents(userDir?: string): Array<{ header: string; agents: string[] }> {
|
|
150
|
+
const all = availableAgents(userDir);
|
|
151
|
+
const groups: Array<{ header: string; agents: string[] }> = [];
|
|
152
|
+
const builtin = all.filter((a) => BUILTIN_AGENTS.includes(a));
|
|
153
|
+
if (builtin.length > 0) groups.push({ header: "built-in", agents: builtin });
|
|
154
|
+
const user = all.filter((a) => !BUILTIN_AGENTS.includes(a));
|
|
155
|
+
if (user.length > 0) groups.push({ header: "user-defined", agents: user });
|
|
156
|
+
return groups;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Header line shown above chat. Persistent, dim, single line. */
|
|
160
|
+
export function formatHeaderLine(agent: string): string {
|
|
161
|
+
const meta = getAgentMeta(agent);
|
|
162
|
+
const writeTag = meta.writesFiles ? "" : " \u00b7 read-only";
|
|
163
|
+
return `${meta.emoji} ${agent} \u00b7 ${meta.description}${writeTag} [Ctrl+Shift+S to cycle]`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Persistence
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/** Where to persist the current agent. Prefers `.soly/agent` if a soly
|
|
171
|
+
* project exists (shared with soly extension). Otherwise `~/.pi-switch/agent`. */
|
|
172
|
+
export function agentFilePath(cwd: string): string {
|
|
173
|
+
const solyAgent = path.join(cwd, ".soly", "agent");
|
|
174
|
+
if (fs.existsSync(path.join(cwd, ".soly"))) return solyAgent;
|
|
175
|
+
// Respect HOME/USERPROFILE for testability (otherwise os.homedir() ignores them on Windows)
|
|
176
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
177
|
+
const fallbackDir = path.join(home, ".pi-switch");
|
|
178
|
+
fs.mkdirSync(fallbackDir, { recursive: true });
|
|
179
|
+
return path.join(fallbackDir, "agent");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Read persisted agent from disk. Returns null if missing/invalid. */
|
|
183
|
+
export function loadAgent(cwd: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
const file = agentFilePath(cwd);
|
|
186
|
+
if (!fs.existsSync(file)) return null;
|
|
187
|
+
const raw = fs.readFileSync(file, "utf-8").trim();
|
|
188
|
+
if (!isValidAgentName(raw)) return null;
|
|
189
|
+
return raw;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Write current agent to disk. */
|
|
196
|
+
export function saveAgent(cwd: string, agent: string): void {
|
|
197
|
+
try {
|
|
198
|
+
const file = agentFilePath(cwd);
|
|
199
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
200
|
+
fs.writeFileSync(file, agent + "\n", "utf-8");
|
|
201
|
+
} catch { /* best-effort */ }
|
|
202
|
+
}
|