oh-pi 0.1.0 โ 0.1.2
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/README.md +132 -118
- package/dist/index.js +30 -0
- package/dist/tui/confirm-apply.js +1 -0
- package/dist/tui/provider-setup.js +194 -23
- package/dist/types.d.ts +14 -0
- package/dist/types.js +20 -0
- package/dist/utils/install.js +36 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,179 +1,193 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="./logo.svg" width="180" alt="oh-pi logo"/>
|
|
4
|
+
|
|
5
|
+
# ๐ oh-pi
|
|
6
|
+
|
|
7
|
+
**One command to supercharge [pi-coding-agent](https://github.com/badlogic/pi-mono).**
|
|
8
|
+
|
|
9
|
+
Like oh-my-zsh for pi โ but with an autonomous ant colony.
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/oh-pi)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://nodejs.org)
|
|
4
14
|
|
|
5
15
|
```bash
|
|
6
16
|
npx oh-pi
|
|
7
17
|
```
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
</div>
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
---
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
- **Prompt Templates** โ 10 ready-to-use templates (/review, /fix, /commit, /test, /security, etc.)
|
|
17
|
-
- **Extensions** โ Safety guards, git checkpoints, auto session naming, ant colony swarm
|
|
18
|
-
- **Skills** โ Debug helper, git workflow, quick project setup, ant colony orchestration
|
|
19
|
-
- **Keybindings** โ Default, Vim, or Emacs schemes
|
|
20
|
-
- **AGENTS.md** โ Role-specific project guidelines
|
|
21
|
-
- **๐ Ant Colony** โ Autonomous multi-agent swarm with adaptive concurrency
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
pi-coding-agent is powerful out of the box. But configuring providers, themes, extensions, skills, and prompts by hand is tedious. oh-pi gives you a modern TUI that does it all in under a minute โ and ships an **ant colony swarm** that turns pi into a multi-agent system.
|
|
22
26
|
|
|
23
27
|
## Quick Start
|
|
24
28
|
|
|
25
29
|
```bash
|
|
26
|
-
#
|
|
27
|
-
|
|
30
|
+
npx oh-pi # configure everything
|
|
31
|
+
pi # start coding
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. oh-pi detects your environment, walks you through setup, and writes `~/.pi/agent/` for you.
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
Already have a config? oh-pi detects it and offers **backup before overwriting**.
|
|
37
|
+
|
|
38
|
+
## What You Get
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
~/.pi/agent/
|
|
42
|
+
โโโ auth.json API keys (0600 permissions)
|
|
43
|
+
โโโ settings.json Model, theme, thinking level
|
|
44
|
+
โโโ keybindings.json Vim/Emacs shortcuts (optional)
|
|
45
|
+
โโโ AGENTS.md Role-specific AI guidelines
|
|
46
|
+
โโโ extensions/ 4 extensions
|
|
47
|
+
โ โโโ safe-guard Dangerous command confirmation + path protection
|
|
48
|
+
โ โโโ git-guard Auto stash checkpoints + dirty repo warning
|
|
49
|
+
โ โโโ auto-session Session naming from first message
|
|
50
|
+
โ โโโ ant-colony/ ๐ Autonomous multi-agent swarm
|
|
51
|
+
โโโ prompts/ 10 templates (/review /fix /commit /test ...)
|
|
52
|
+
โโโ skills/ 4 skills (debug, git, setup, colony)
|
|
53
|
+
โโโ themes/ 6 custom themes
|
|
31
54
|
```
|
|
32
55
|
|
|
33
56
|
## Setup Modes
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
| Mode | Steps | For |
|
|
59
|
+
|------|-------|-----|
|
|
60
|
+
| ๐ **Quick** | 3 | Pick provider โ enter key โ done |
|
|
61
|
+
| ๐ฆ **Preset** | 2 | Choose a role profile โ enter key |
|
|
62
|
+
| ๐๏ธ **Custom** | 6 | Pick everything yourself |
|
|
39
63
|
|
|
40
|
-
###
|
|
41
|
-
Choose a pre-made profile:
|
|
64
|
+
### Presets
|
|
42
65
|
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
| ๐ข Starter | oh-pi Dark | medium |
|
|
66
|
+
| | Theme | Thinking | Includes |
|
|
67
|
+
|---|-------|----------|----------|
|
|
68
|
+
| ๐ข Starter | oh-pi Dark | medium | Safety + git basics |
|
|
46
69
|
| ๐ต Pro Developer | Catppuccin | high | Full toolchain |
|
|
47
70
|
| ๐ฃ Security Researcher | Cyberpunk | high | Audit + pentesting |
|
|
48
|
-
| ๐ Data & AI
|
|
49
|
-
| ๐ด Minimal |
|
|
71
|
+
| ๐ Data & AI | Tokyo Night | medium | MLOps + pipelines |
|
|
72
|
+
| ๐ด Minimal | Default | off | Core only |
|
|
50
73
|
| โซ Full Power | oh-pi Dark | high | Everything + ant colony |
|
|
51
74
|
|
|
52
|
-
###
|
|
53
|
-
Pick every option yourself: providers, theme, keybindings, extensions, skills, AGENTS.md template.
|
|
75
|
+
### Providers
|
|
54
76
|
|
|
55
|
-
|
|
77
|
+
Anthropic ยท OpenAI ยท Google Gemini ยท Groq ยท OpenRouter ยท xAI ยท Mistral
|
|
78
|
+
|
|
79
|
+
Auto-detects API keys from environment variables.
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
## ๐ Ant Colony
|
|
58
82
|
|
|
59
|
-
|
|
83
|
+
The headline feature. A multi-agent swarm modeled after real ant ecology.
|
|
60
84
|
|
|
61
85
|
```
|
|
62
|
-
|
|
86
|
+
You: "Refactor auth from sessions to JWT"
|
|
87
|
+
|
|
88
|
+
oh-pi:
|
|
89
|
+
๐ Scout ants explore codebase (haiku โ fast, cheap)
|
|
90
|
+
๐ Task pool generated from discoveries
|
|
91
|
+
โ๏ธ Worker ants execute in parallel (sonnet โ capable)
|
|
92
|
+
๐ก๏ธ Soldier ants review all changes (sonnet โ thorough)
|
|
93
|
+
โ
Done โ summary report with metrics
|
|
63
94
|
```
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
- **Workers** (sonnet) โ Execute tasks, can spawn sub-tasks
|
|
67
|
-
- **Soldiers** (sonnet) โ Review quality, request fixes if needed
|
|
96
|
+
### Why ants?
|
|
68
97
|
|
|
69
|
-
|
|
98
|
+
Real ant colonies solve complex problems without central control. Each ant follows simple rules, communicates through **pheromone trails**, and the colony self-organizes. oh-pi maps this directly:
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
| Real Ants | oh-pi |
|
|
101
|
+
|-----------|-------|
|
|
102
|
+
| Scout finds food | Scout scans codebase, identifies targets |
|
|
103
|
+
| Pheromone trail | `.ant-colony/pheromone.jsonl` โ shared discoveries |
|
|
104
|
+
| Worker carries food | Worker executes task on assigned files |
|
|
105
|
+
| Soldier guards nest | Soldier reviews changes, requests fixes |
|
|
106
|
+
| More food โ more ants | More tasks โ higher concurrency (auto-adapted) |
|
|
107
|
+
| Pheromone evaporates | 10-minute half-life โ stale info fades |
|
|
76
108
|
|
|
77
|
-
###
|
|
109
|
+
### Auto-trigger
|
|
78
110
|
|
|
79
|
-
|
|
80
|
-
# LLM auto-triggers for complex tasks
|
|
81
|
-
"Refactor the auth system from sessions to JWT"
|
|
111
|
+
The LLM decides when to deploy the colony. You don't have to think about it:
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
- **โฅ3 files** need changes โ colony
|
|
114
|
+
- **Parallel workstreams** possible โ colony
|
|
115
|
+
- **Single file** change โ direct execution (no colony overhead)
|
|
116
|
+
|
|
117
|
+
Or trigger manually:
|
|
118
|
+
|
|
119
|
+
```
|
|
84
120
|
/colony migrate the entire project from CJS to ESM
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Adaptive Concurrency
|
|
124
|
+
|
|
125
|
+
The colony automatically finds the optimal parallelism for your machine:
|
|
85
126
|
|
|
86
|
-
# Shortcut
|
|
87
|
-
Ctrl+Alt+A
|
|
88
127
|
```
|
|
128
|
+
Cold start โ 1-2 ants (conservative)
|
|
129
|
+
Exploration โ +1 each wave, monitoring throughput
|
|
130
|
+
Throughput โ โ lock optimal, stabilize
|
|
131
|
+
CPU > 85% โ reduce immediately
|
|
132
|
+
429 rate limit โ halve concurrency + exponential backoff (15sโ30sโ60s)
|
|
133
|
+
Tasks done โ scale down to minimum
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### File Safety
|
|
89
137
|
|
|
90
|
-
|
|
138
|
+
One ant per file. Always. Conflicting tasks are automatically blocked and resume when locks release.
|
|
139
|
+
|
|
140
|
+
## Themes
|
|
141
|
+
|
|
142
|
+
| | |
|
|
143
|
+
|---|---|
|
|
144
|
+
| ๐ **oh-pi Dark** | Cyan + purple, high contrast |
|
|
145
|
+
| ๐ **Cyberpunk** | Neon magenta + electric cyan |
|
|
146
|
+
| ๐ **Nord** | Arctic blue palette |
|
|
147
|
+
| ๐ **Catppuccin Mocha** | Pastel on dark |
|
|
148
|
+
| ๐ **Tokyo Night** | Blue + purple twilight |
|
|
149
|
+
| ๐ **Gruvbox Dark** | Warm retro tones |
|
|
150
|
+
|
|
151
|
+
## Prompt Templates
|
|
91
152
|
|
|
92
153
|
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
/review Code review: bugs, security, performance
|
|
155
|
+
/fix Fix errors with minimal changes
|
|
156
|
+
/explain Explain code, simple to detailed
|
|
157
|
+
/refactor Refactor preserving behavior
|
|
158
|
+
/test Generate tests
|
|
159
|
+
/commit Conventional Commit message
|
|
160
|
+
/pr Pull request description
|
|
161
|
+
/security OWASP security audit
|
|
162
|
+
/optimize Performance optimization
|
|
163
|
+
/document Generate documentation
|
|
102
164
|
```
|
|
103
165
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
### Themes
|
|
109
|
-
|
|
110
|
-
| Theme | Style |
|
|
111
|
-
|-------|-------|
|
|
112
|
-
| oh-pi Dark | Cyan + Purple, high contrast |
|
|
113
|
-
| Cyberpunk | Neon magenta + electric cyan |
|
|
114
|
-
| Nord | Arctic blue palette |
|
|
115
|
-
| Catppuccin Mocha | Pastel colors on dark |
|
|
116
|
-
| Tokyo Night | Blue + purple twilight |
|
|
117
|
-
| Gruvbox Dark | Warm retro tones |
|
|
118
|
-
|
|
119
|
-
### Prompt Templates
|
|
120
|
-
|
|
121
|
-
| Command | Description |
|
|
122
|
-
|---------|-------------|
|
|
123
|
-
| `/review` | Code review: bugs, security, performance |
|
|
124
|
-
| `/fix` | Fix errors with minimal changes |
|
|
125
|
-
| `/explain` | Explain code from simple to detailed |
|
|
126
|
-
| `/refactor` | Refactor while preserving behavior |
|
|
127
|
-
| `/test` | Generate tests for code |
|
|
128
|
-
| `/commit` | Conventional Commit message |
|
|
129
|
-
| `/pr` | Pull request description |
|
|
130
|
-
| `/security` | OWASP security audit |
|
|
131
|
-
| `/optimize` | Performance optimization |
|
|
132
|
-
| `/document` | Generate documentation |
|
|
133
|
-
|
|
134
|
-
### Extensions
|
|
135
|
-
|
|
136
|
-
| Extension | Description |
|
|
137
|
-
|-----------|-------------|
|
|
138
|
-
| Safe Guard | Confirms dangerous commands (rm -rf, DROP, etc.) + protects .env, .git/ |
|
|
139
|
-
| Git Guard | Auto stash checkpoints + dirty repo warning + completion notification |
|
|
140
|
-
| Auto Session Name | Names sessions from first message |
|
|
141
|
-
| ๐ Ant Colony | Autonomous multi-agent swarm with adaptive concurrency |
|
|
142
|
-
|
|
143
|
-
### Skills
|
|
144
|
-
|
|
145
|
-
| Skill | Description |
|
|
146
|
-
|-------|-------------|
|
|
147
|
-
| `/skill:quick-setup` | Detect project type, generate .pi/ config |
|
|
148
|
-
| `/skill:debug-helper` | Error analysis, log interpretation, profiling |
|
|
149
|
-
| `/skill:git-workflow` | Branch strategy, PR workflow, conflict resolution |
|
|
150
|
-
| `/skill:ant-colony` | Colony orchestration strategies and tuning |
|
|
151
|
-
|
|
152
|
-
### AGENTS.md Templates
|
|
153
|
-
|
|
154
|
-
| Template | Description |
|
|
155
|
-
|----------|-------------|
|
|
166
|
+
## AGENTS.md Templates
|
|
167
|
+
|
|
168
|
+
| Template | Focus |
|
|
169
|
+
|----------|-------|
|
|
156
170
|
| General Developer | Universal coding guidelines |
|
|
157
|
-
| Full-Stack Developer | Frontend +
|
|
171
|
+
| Full-Stack Developer | Frontend + backend + DB |
|
|
158
172
|
| Security Researcher | Pentesting & audit |
|
|
159
173
|
| Data & AI Engineer | MLOps & pipelines |
|
|
160
|
-
| ๐ Colony Operator |
|
|
174
|
+
| ๐ Colony Operator | Multi-agent orchestration |
|
|
161
175
|
|
|
162
176
|
## Also a Pi Package
|
|
163
177
|
|
|
164
|
-
|
|
178
|
+
Skip the configurator, just install the resources:
|
|
165
179
|
|
|
166
180
|
```bash
|
|
167
181
|
pi install npm:oh-pi
|
|
168
182
|
```
|
|
169
183
|
|
|
170
|
-
|
|
184
|
+
Adds all themes, prompts, skills, and extensions to your existing pi setup.
|
|
171
185
|
|
|
172
186
|
## Requirements
|
|
173
187
|
|
|
174
|
-
- Node.js
|
|
175
|
-
- pi-coding-agent (installed automatically if missing)
|
|
188
|
+
- Node.js โฅ 20
|
|
176
189
|
- At least one LLM API key
|
|
190
|
+
- pi-coding-agent (installed automatically if missing)
|
|
177
191
|
|
|
178
192
|
## License
|
|
179
193
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
1
2
|
import { welcome } from "./tui/welcome.js";
|
|
2
3
|
import { selectMode } from "./tui/mode-select.js";
|
|
3
4
|
import { setupProviders } from "./tui/provider-setup.js";
|
|
@@ -49,6 +50,34 @@ async function customFlow(env) {
|
|
|
49
50
|
const keybindings = await selectKeybindings();
|
|
50
51
|
const extensions = await selectExtensions();
|
|
51
52
|
const agents = await selectAgents();
|
|
53
|
+
// Advanced: auto-compaction threshold
|
|
54
|
+
const wantAdvanced = await p.confirm({
|
|
55
|
+
message: "Configure advanced settings? (compaction threshold, etc.)",
|
|
56
|
+
initialValue: false,
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(wantAdvanced)) {
|
|
59
|
+
p.cancel("Cancelled.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
let compactThreshold = 0.75;
|
|
63
|
+
if (wantAdvanced) {
|
|
64
|
+
const threshold = await p.text({
|
|
65
|
+
message: "Auto-compact when context reaches % of window (0-100):",
|
|
66
|
+
placeholder: "75",
|
|
67
|
+
initialValue: "75",
|
|
68
|
+
validate: (v) => {
|
|
69
|
+
const n = Number(v);
|
|
70
|
+
if (isNaN(n) || n < 10 || n > 100)
|
|
71
|
+
return "Must be a number between 10 and 100";
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (p.isCancel(threshold)) {
|
|
76
|
+
p.cancel("Cancelled.");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
compactThreshold = Number(threshold) / 100;
|
|
80
|
+
}
|
|
52
81
|
return {
|
|
53
82
|
providers,
|
|
54
83
|
theme,
|
|
@@ -58,5 +87,6 @@ async function customFlow(env) {
|
|
|
58
87
|
prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "security", "document", "pr"],
|
|
59
88
|
agents,
|
|
60
89
|
thinking: "medium",
|
|
90
|
+
compactThreshold,
|
|
61
91
|
};
|
|
62
92
|
}
|
|
@@ -12,6 +12,7 @@ export async function confirmApply(config, env) {
|
|
|
12
12
|
`Theme: ${chalk.cyan(config.theme)}`,
|
|
13
13
|
`Keybindings: ${chalk.cyan(config.keybindings)}`,
|
|
14
14
|
`Thinking: ${chalk.cyan(config.thinking)}`,
|
|
15
|
+
`Compaction: ${chalk.cyan(`${Math.round((config.compactThreshold ?? 0.75) * 100)}% of context`)}`,
|
|
15
16
|
`Extensions: ${chalk.cyan(config.extensions.join(", ") || "none")}`,
|
|
16
17
|
`Skills: ${chalk.cyan(config.skills.join(", ") || "none")}`,
|
|
17
18
|
`Prompts: ${chalk.cyan(`${config.prompts.length} templates`)}`,
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { PROVIDERS } from "../types.js";
|
|
4
|
+
/** Fetch models from OpenAI-compatible /v1/models endpoint */
|
|
5
|
+
async function fetchModels(baseUrl, apiKey) {
|
|
6
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
10
|
+
signal: AbortSignal.timeout(8000),
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
return [];
|
|
14
|
+
const json = await res.json();
|
|
15
|
+
return (json.data ?? []).map(m => m.id).sort();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
4
21
|
export async function setupProviders() {
|
|
5
22
|
const entries = Object.entries(PROVIDERS);
|
|
6
23
|
const selected = await p.multiselect({
|
|
7
24
|
message: "Select API providers",
|
|
8
|
-
options:
|
|
9
|
-
value: key,
|
|
10
|
-
label:
|
|
11
|
-
|
|
12
|
-
})),
|
|
25
|
+
options: [
|
|
26
|
+
...entries.map(([key, info]) => ({ value: key, label: info.label, hint: info.env })),
|
|
27
|
+
{ value: "_custom", label: "๐ง Custom endpoint", hint: "Ollama, vLLM, LiteLLM, any OpenAI-compatible" },
|
|
28
|
+
],
|
|
13
29
|
initialValues: ["anthropic"],
|
|
14
30
|
required: true,
|
|
15
31
|
});
|
|
@@ -19,45 +35,200 @@ export async function setupProviders() {
|
|
|
19
35
|
}
|
|
20
36
|
const configs = [];
|
|
21
37
|
for (const name of selected) {
|
|
38
|
+
if (name === "_custom") {
|
|
39
|
+
const custom = await setupCustomProvider();
|
|
40
|
+
if (custom)
|
|
41
|
+
configs.push(custom);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
22
44
|
const info = PROVIDERS[name];
|
|
23
45
|
const envVal = process.env[info.env];
|
|
24
46
|
let apiKey;
|
|
25
47
|
if (envVal) {
|
|
26
|
-
const useEnv = await p.confirm({
|
|
27
|
-
message: `Found ${chalk.cyan(info.env)} in environment. Use it?`,
|
|
28
|
-
});
|
|
48
|
+
const useEnv = await p.confirm({ message: `Found ${chalk.cyan(info.env)} in environment. Use it?` });
|
|
29
49
|
if (p.isCancel(useEnv)) {
|
|
30
50
|
p.cancel("Cancelled.");
|
|
31
51
|
process.exit(0);
|
|
32
52
|
}
|
|
33
|
-
apiKey = useEnv ? info.env : await promptKey(info);
|
|
53
|
+
apiKey = useEnv ? info.env : await promptKey(info.label);
|
|
34
54
|
}
|
|
35
55
|
else {
|
|
36
|
-
apiKey = await promptKey(info);
|
|
56
|
+
apiKey = await promptKey(info.label);
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
// Ask for custom base URL (optional)
|
|
59
|
+
const wantCustomUrl = await p.confirm({
|
|
60
|
+
message: `Custom endpoint for ${info.label}? (proxy, Azure, etc.)`,
|
|
61
|
+
initialValue: false,
|
|
62
|
+
});
|
|
63
|
+
if (p.isCancel(wantCustomUrl)) {
|
|
64
|
+
p.cancel("Cancelled.");
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
let baseUrl;
|
|
68
|
+
if (wantCustomUrl) {
|
|
69
|
+
const url = await p.text({
|
|
70
|
+
message: `Base URL for ${info.label}:`,
|
|
71
|
+
placeholder: "https://your-proxy.example.com",
|
|
72
|
+
validate: (v) => (!v || !v.startsWith("http")) ? "Must be a valid URL" : undefined,
|
|
43
73
|
});
|
|
44
|
-
if (p.isCancel(
|
|
74
|
+
if (p.isCancel(url)) {
|
|
45
75
|
p.cancel("Cancelled.");
|
|
46
76
|
process.exit(0);
|
|
47
77
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
defaultModel = info.models[0];
|
|
78
|
+
baseUrl = url;
|
|
52
79
|
}
|
|
53
|
-
|
|
80
|
+
// Model selection โ try dynamic fetch, fall back to static list
|
|
81
|
+
const defaultModel = await selectModel(info.label, info.models, baseUrl, apiKey);
|
|
82
|
+
configs.push({ name, apiKey, defaultModel, ...(baseUrl ? { baseUrl } : {}) });
|
|
54
83
|
p.log.success(`${info.label} configured`);
|
|
55
84
|
}
|
|
56
85
|
return configs;
|
|
57
86
|
}
|
|
58
|
-
async function
|
|
87
|
+
async function setupCustomProvider() {
|
|
88
|
+
const name = await p.text({
|
|
89
|
+
message: "Provider name:",
|
|
90
|
+
placeholder: "ollama",
|
|
91
|
+
validate: (v) => (!v || v.trim().length === 0) ? "Name required" : undefined,
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(name)) {
|
|
94
|
+
p.cancel("Cancelled.");
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
const baseUrl = await p.text({
|
|
98
|
+
message: "Base URL:",
|
|
99
|
+
placeholder: "http://localhost:11434",
|
|
100
|
+
validate: (v) => (!v || !v.startsWith("http")) ? "Must be a valid URL" : undefined,
|
|
101
|
+
});
|
|
102
|
+
if (p.isCancel(baseUrl)) {
|
|
103
|
+
p.cancel("Cancelled.");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
const needsKey = await p.confirm({ message: "Requires API key?", initialValue: false });
|
|
107
|
+
if (p.isCancel(needsKey)) {
|
|
108
|
+
p.cancel("Cancelled.");
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
let apiKey = "none";
|
|
112
|
+
if (needsKey) {
|
|
113
|
+
apiKey = await promptKey(name);
|
|
114
|
+
}
|
|
115
|
+
// Dynamic model fetch
|
|
116
|
+
const s = p.spinner();
|
|
117
|
+
s.start(`Fetching models from ${baseUrl}`);
|
|
118
|
+
const models = await fetchModels(baseUrl, apiKey);
|
|
119
|
+
s.stop(models.length > 0 ? `Found ${models.length} models` : "No models found via API");
|
|
120
|
+
let defaultModel;
|
|
121
|
+
if (models.length > 0) {
|
|
122
|
+
const model = await p.select({
|
|
123
|
+
message: `Default model for ${name}:`,
|
|
124
|
+
options: models.slice(0, 30).map(m => ({ value: m, label: m })),
|
|
125
|
+
});
|
|
126
|
+
if (p.isCancel(model)) {
|
|
127
|
+
p.cancel("Cancelled.");
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
defaultModel = model;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const model = await p.text({
|
|
134
|
+
message: `Model name for ${name}:`,
|
|
135
|
+
placeholder: "llama3.1:8b",
|
|
136
|
+
validate: (v) => (!v || v.trim().length === 0) ? "Model name required" : undefined,
|
|
137
|
+
});
|
|
138
|
+
if (p.isCancel(model)) {
|
|
139
|
+
p.cancel("Cancelled.");
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
defaultModel = model;
|
|
143
|
+
}
|
|
144
|
+
p.log.success(`${name} configured (${baseUrl})`);
|
|
145
|
+
// Model capabilities (optional)
|
|
146
|
+
const wantCaps = await p.confirm({
|
|
147
|
+
message: "Configure model capabilities? (context window, multimodal, reasoning)",
|
|
148
|
+
initialValue: false,
|
|
149
|
+
});
|
|
150
|
+
if (p.isCancel(wantCaps)) {
|
|
151
|
+
p.cancel("Cancelled.");
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
let contextWindow;
|
|
155
|
+
let maxTokens;
|
|
156
|
+
let reasoning;
|
|
157
|
+
let multimodal;
|
|
158
|
+
if (wantCaps) {
|
|
159
|
+
const ctxInput = await p.text({
|
|
160
|
+
message: "Context window size (tokens):",
|
|
161
|
+
placeholder: "128000",
|
|
162
|
+
initialValue: "128000",
|
|
163
|
+
validate: (v) => {
|
|
164
|
+
const n = Number(v);
|
|
165
|
+
if (isNaN(n) || n < 1024)
|
|
166
|
+
return "Must be a number โฅ 1024";
|
|
167
|
+
return undefined;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
if (p.isCancel(ctxInput)) {
|
|
171
|
+
p.cancel("Cancelled.");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
contextWindow = Number(ctxInput);
|
|
175
|
+
const maxTokInput = await p.text({
|
|
176
|
+
message: "Max output tokens:",
|
|
177
|
+
placeholder: "8192",
|
|
178
|
+
initialValue: "8192",
|
|
179
|
+
validate: (v) => {
|
|
180
|
+
const n = Number(v);
|
|
181
|
+
if (isNaN(n) || n < 256)
|
|
182
|
+
return "Must be a number โฅ 256";
|
|
183
|
+
return undefined;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (p.isCancel(maxTokInput)) {
|
|
187
|
+
p.cancel("Cancelled.");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
maxTokens = Number(maxTokInput);
|
|
191
|
+
const isMultimodal = await p.confirm({ message: "Supports image input (multimodal)?", initialValue: false });
|
|
192
|
+
if (p.isCancel(isMultimodal)) {
|
|
193
|
+
p.cancel("Cancelled.");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
multimodal = isMultimodal;
|
|
197
|
+
const isReasoning = await p.confirm({ message: "Supports extended thinking (reasoning)?", initialValue: false });
|
|
198
|
+
if (p.isCancel(isReasoning)) {
|
|
199
|
+
p.cancel("Cancelled.");
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
reasoning = isReasoning;
|
|
203
|
+
}
|
|
204
|
+
return { name, apiKey, defaultModel, baseUrl, contextWindow, maxTokens, reasoning, multimodal };
|
|
205
|
+
}
|
|
206
|
+
async function selectModel(label, staticModels, baseUrl, apiKey) {
|
|
207
|
+
let models = staticModels;
|
|
208
|
+
// Try dynamic fetch if custom URL or known provider
|
|
209
|
+
if (baseUrl && apiKey) {
|
|
210
|
+
const s = p.spinner();
|
|
211
|
+
s.start(`Fetching models from ${label}`);
|
|
212
|
+
const fetched = await fetchModels(baseUrl, apiKey);
|
|
213
|
+
s.stop(fetched.length > 0 ? `Found ${fetched.length} models` : "Using default model list");
|
|
214
|
+
if (fetched.length > 0)
|
|
215
|
+
models = fetched;
|
|
216
|
+
}
|
|
217
|
+
if (models.length === 1)
|
|
218
|
+
return models[0];
|
|
219
|
+
const model = await p.select({
|
|
220
|
+
message: `Default model for ${label}:`,
|
|
221
|
+
options: models.slice(0, 30).map(m => ({ value: m, label: m })),
|
|
222
|
+
});
|
|
223
|
+
if (p.isCancel(model)) {
|
|
224
|
+
p.cancel("Cancelled.");
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
return model;
|
|
228
|
+
}
|
|
229
|
+
async function promptKey(label) {
|
|
59
230
|
const key = await p.password({
|
|
60
|
-
message: `API key for ${
|
|
231
|
+
message: `API key for ${label}:`,
|
|
61
232
|
validate: (v) => (!v || v.trim().length === 0) ? "API key cannot be empty" : undefined,
|
|
62
233
|
});
|
|
63
234
|
if (p.isCancel(key)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export interface ProviderConfig {
|
|
|
2
2
|
name: string;
|
|
3
3
|
apiKey: string;
|
|
4
4
|
defaultModel?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
contextWindow?: number;
|
|
7
|
+
maxTokens?: number;
|
|
8
|
+
reasoning?: boolean;
|
|
9
|
+
multimodal?: boolean;
|
|
5
10
|
}
|
|
6
11
|
export interface OhPConfig {
|
|
7
12
|
providers: ProviderConfig[];
|
|
@@ -12,7 +17,16 @@ export interface OhPConfig {
|
|
|
12
17
|
prompts: string[];
|
|
13
18
|
agents: string;
|
|
14
19
|
thinking: string;
|
|
20
|
+
compactThreshold?: number;
|
|
15
21
|
}
|
|
22
|
+
/** Official model capabilities for known providers */
|
|
23
|
+
export interface ModelCapabilities {
|
|
24
|
+
contextWindow: number;
|
|
25
|
+
maxTokens: number;
|
|
26
|
+
reasoning: boolean;
|
|
27
|
+
input: ("text" | "image")[];
|
|
28
|
+
}
|
|
29
|
+
export declare const MODEL_CAPABILITIES: Record<string, ModelCapabilities>;
|
|
16
30
|
export declare const PROVIDERS: Record<string, {
|
|
17
31
|
env: string;
|
|
18
32
|
label: string;
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
export const MODEL_CAPABILITIES = {
|
|
2
|
+
// Anthropic
|
|
3
|
+
"claude-sonnet-4-20250514": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
4
|
+
"claude-opus-4-0520": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
5
|
+
// OpenAI
|
|
6
|
+
"gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
7
|
+
"o3-mini": { contextWindow: 128000, maxTokens: 65536, reasoning: true, input: ["text"] },
|
|
8
|
+
// Google
|
|
9
|
+
"gemini-2.5-pro": { contextWindow: 1048576, maxTokens: 65536, reasoning: true, input: ["text", "image"] },
|
|
10
|
+
"gemini-2.5-flash": { contextWindow: 1048576, maxTokens: 65536, reasoning: true, input: ["text", "image"] },
|
|
11
|
+
// Groq
|
|
12
|
+
"llama-3.3-70b-versatile": { contextWindow: 128000, maxTokens: 32768, reasoning: false, input: ["text"] },
|
|
13
|
+
// OpenRouter
|
|
14
|
+
"anthropic/claude-sonnet-4": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
15
|
+
"openai/gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
16
|
+
// xAI
|
|
17
|
+
"grok-3": { contextWindow: 131072, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
18
|
+
// Mistral
|
|
19
|
+
"mistral-large-latest": { contextWindow: 128000, maxTokens: 8192, reasoning: false, input: ["text"] },
|
|
20
|
+
};
|
|
1
21
|
export const PROVIDERS = {
|
|
2
22
|
anthropic: { env: "ANTHROPIC_API_KEY", label: "Anthropic (Claude)", models: ["claude-sonnet-4-20250514", "claude-opus-4-0520"] },
|
|
3
23
|
openai: { env: "OPENAI_API_KEY", label: "OpenAI (GPT)", models: ["gpt-4o", "o3-mini"] },
|
package/dist/utils/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
-
import { KEYBINDING_SCHEMES, PROVIDERS } from "../types.js";
|
|
6
|
+
import { KEYBINDING_SCHEMES, MODEL_CAPABILITIES, PROVIDERS } from "../types.js";
|
|
7
7
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
8
|
function ensureDir(dir) {
|
|
9
9
|
mkdirSync(dir, { recursive: true });
|
|
@@ -35,13 +35,18 @@ export function applyConfig(config) {
|
|
|
35
35
|
// 2. settings.json
|
|
36
36
|
const primary = config.providers[0];
|
|
37
37
|
const providerInfo = primary ? PROVIDERS[primary.name] : undefined;
|
|
38
|
+
const compactThreshold = config.compactThreshold ?? 0.75;
|
|
39
|
+
const primaryModel = primary?.defaultModel ?? providerInfo?.models[0];
|
|
40
|
+
const primaryCaps = primaryModel ? MODEL_CAPABILITIES[primaryModel] : undefined;
|
|
41
|
+
const contextWindow = primary?.contextWindow ?? primaryCaps?.contextWindow ?? 128000;
|
|
42
|
+
const reserveTokens = Math.round(contextWindow * (1 - compactThreshold));
|
|
38
43
|
const settings = {
|
|
39
44
|
defaultProvider: primary?.name,
|
|
40
|
-
defaultModel:
|
|
45
|
+
defaultModel: primaryModel,
|
|
41
46
|
defaultThinkingLevel: config.thinking,
|
|
42
47
|
theme: config.theme,
|
|
43
48
|
enableSkillCommands: true,
|
|
44
|
-
compaction: { enabled: true, reserveTokens
|
|
49
|
+
compaction: { enabled: true, reserveTokens, keepRecentTokens: 20000 },
|
|
45
50
|
retry: { enabled: true, maxRetries: 3 },
|
|
46
51
|
};
|
|
47
52
|
if (config.providers.length > 1) {
|
|
@@ -51,19 +56,41 @@ export function applyConfig(config) {
|
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
writeFileSync(join(agentDir, "settings.json"), JSON.stringify(settings, null, 2));
|
|
54
|
-
// 3.
|
|
59
|
+
// 3. models.json (custom endpoints / providers)
|
|
60
|
+
const customProviders = config.providers.filter(p => p.baseUrl);
|
|
61
|
+
if (customProviders.length > 0) {
|
|
62
|
+
const models = {};
|
|
63
|
+
for (const cp of customProviders) {
|
|
64
|
+
const caps = cp.defaultModel ? MODEL_CAPABILITIES[cp.defaultModel] : undefined;
|
|
65
|
+
models[cp.name] = {
|
|
66
|
+
baseUrl: cp.baseUrl,
|
|
67
|
+
apiKey: cp.apiKey === "none" ? undefined : cp.apiKey,
|
|
68
|
+
api: "openai-completions",
|
|
69
|
+
models: cp.defaultModel ? [{
|
|
70
|
+
id: cp.defaultModel,
|
|
71
|
+
name: cp.defaultModel,
|
|
72
|
+
reasoning: cp.reasoning ?? caps?.reasoning ?? false,
|
|
73
|
+
input: cp.multimodal ? ["text", "image"] : (caps?.input ?? ["text"]),
|
|
74
|
+
contextWindow: cp.contextWindow ?? caps?.contextWindow ?? 128000,
|
|
75
|
+
maxTokens: cp.maxTokens ?? caps?.maxTokens ?? 8192,
|
|
76
|
+
}] : [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(join(agentDir, "models.json"), JSON.stringify(models, null, 2));
|
|
80
|
+
}
|
|
81
|
+
// 4. keybindings.json
|
|
55
82
|
const kb = KEYBINDING_SCHEMES[config.keybindings];
|
|
56
83
|
if (kb && Object.keys(kb).length > 0) {
|
|
57
84
|
writeFileSync(join(agentDir, "keybindings.json"), JSON.stringify(kb, null, 2));
|
|
58
85
|
}
|
|
59
|
-
//
|
|
86
|
+
// 5. AGENTS.md
|
|
60
87
|
const agentsSrc = join(PKG_ROOT, "pi-package", "agents", `${config.agents}.md`);
|
|
61
88
|
try {
|
|
62
89
|
const content = readFileSync(agentsSrc, "utf8");
|
|
63
90
|
writeFileSync(join(agentDir, "AGENTS.md"), content);
|
|
64
91
|
}
|
|
65
92
|
catch { /* template not found, skip */ }
|
|
66
|
-
//
|
|
93
|
+
// 6. Copy extensions (single file .ts or directory with index.ts)
|
|
67
94
|
const extDir = join(agentDir, "extensions");
|
|
68
95
|
ensureDir(extDir);
|
|
69
96
|
for (const ext of config.extensions) {
|
|
@@ -79,7 +106,7 @@ export function applyConfig(config) {
|
|
|
79
106
|
catch { /* skip */ }
|
|
80
107
|
}
|
|
81
108
|
}
|
|
82
|
-
//
|
|
109
|
+
// 7. Copy prompts
|
|
83
110
|
const promptDir = join(agentDir, "prompts");
|
|
84
111
|
ensureDir(promptDir);
|
|
85
112
|
for (const p of config.prompts) {
|
|
@@ -89,7 +116,7 @@ export function applyConfig(config) {
|
|
|
89
116
|
}
|
|
90
117
|
catch { /* skip */ }
|
|
91
118
|
}
|
|
92
|
-
//
|
|
119
|
+
// 8. Copy skills
|
|
93
120
|
const skillDir = join(agentDir, "skills");
|
|
94
121
|
ensureDir(skillDir);
|
|
95
122
|
for (const s of config.skills) {
|
|
@@ -101,7 +128,7 @@ export function applyConfig(config) {
|
|
|
101
128
|
}
|
|
102
129
|
catch { /* skip */ }
|
|
103
130
|
}
|
|
104
|
-
//
|
|
131
|
+
// 9. Copy themes (only custom ones)
|
|
105
132
|
const themeDir = join(agentDir, "themes");
|
|
106
133
|
ensureDir(themeDir);
|
|
107
134
|
const themeSrc = join(PKG_ROOT, "pi-package", "themes", `${config.theme}.json`);
|