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 CHANGED
@@ -1,179 +1,193 @@
1
- # oh-pi!
1
+ <div align="center">
2
2
 
3
- > One-click setup for [pi-coding-agent](https://github.com/badlogic/pi-mono). Like oh-my-zsh for pi.
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
+ [![npm](https://img.shields.io/npm/v/oh-pi)](https://www.npmjs.com/package/oh-pi)
12
+ [![license](https://img.shields.io/npm/l/oh-pi)](./LICENSE)
13
+ [![node](https://img.shields.io/node/v/oh-pi)](https://nodejs.org)
4
14
 
5
15
  ```bash
6
16
  npx oh-pi
7
17
  ```
8
18
 
9
- ## What it does
19
+ </div>
10
20
 
11
- oh-pi! is a modern interactive TUI that configures pi-coding-agent in minutes:
21
+ ---
12
22
 
13
- - **API Key Setup** โ€” Multi-provider configuration with validation (Anthropic, OpenAI, Google, Groq, OpenRouter, xAI, Mistral)
14
- - **Preset Profiles** โ€” Pre-made configs for different roles (Developer, Security, Data/AI, Colony Operator, Minimal)
15
- - **Custom Themes** โ€” 6 beautiful themes (oh-pi Dark, Cyberpunk, Nord, Catppuccin, Tokyo Night, Gruvbox)
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
- # Run the configurator
27
- npx oh-pi
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
- # Then start coding
30
- pi
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
- ### ๐Ÿš€ Quick Setup (3 steps)
36
- 1. Pick your API provider(s)
37
- 2. Enter API key(s)
38
- 3. Done โ€” sensible defaults applied
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
- ### ๐Ÿ“ฆ Preset
41
- Choose a pre-made profile:
64
+ ### Presets
42
65
 
43
- | Preset | Theme | Thinking | Focus |
44
- |--------|-------|----------|-------|
45
- | ๐ŸŸข Starter | oh-pi Dark | medium | Basic safety + git |
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 Engineer | Tokyo Night | medium | MLOps + pipelines |
49
- | ๐Ÿ”ด Minimal | Pi Default | off | Core only |
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
- ### ๐ŸŽ›๏ธ Custom
53
- Pick every option yourself: providers, theme, keybindings, extensions, skills, AGENTS.md template.
75
+ ### Providers
54
76
 
55
- ## ๐Ÿœ Ant Colony
77
+ Anthropic ยท OpenAI ยท Google Gemini ยท Groq ยท OpenRouter ยท xAI ยท Mistral
78
+
79
+ Auto-detects API keys from environment variables.
56
80
 
57
- Autonomous multi-agent swarm built as a pi extension. Modeled after real ant colony behavior.
81
+ ## ๐Ÿœ Ant Colony
58
82
 
59
- ### How it works
83
+ The headline feature. A multi-agent swarm modeled after real ant ecology.
60
84
 
61
85
  ```
62
- Goal โ†’ ๐Ÿ” Scouts explore โ†’ ๐Ÿ“‹ Task pool generated โ†’ โš’๏ธ Workers execute in parallel โ†’ ๐Ÿ›ก๏ธ Soldiers review โ†’ โœ… Done
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
- - **Scouts** (haiku) โ€” Fast codebase recon, identify targets
66
- - **Workers** (sonnet) โ€” Execute tasks, can spawn sub-tasks
67
- - **Soldiers** (sonnet) โ€” Review quality, request fixes if needed
96
+ ### Why ants?
68
97
 
69
- ### Key features
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
- - **Auto-trigger** โ€” LLM automatically deploys colony for complex multi-file tasks
72
- - **Adaptive concurrency** โ€” Starts at 1, explores throughput ceiling, stabilizes at optimal
73
- - **429 backoff** โ€” Rate limits trigger exponential backoff (15sโ†’30sโ†’60s) + concurrency halving
74
- - **Pheromone communication** โ€” Ants share discoveries via file-based pheromone trails (10min half-life)
75
- - **File locking** โ€” One ant per file, blocked tasks auto-resume when locks release
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
- ### Usage
109
+ ### Auto-trigger
78
110
 
79
- ```bash
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
- # Manual command
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
- ## What Gets Installed
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
- ~/.pi/agent/
94
- โ”œโ”€โ”€ auth.json # API keys (0600 permissions)
95
- โ”œโ”€โ”€ settings.json # Model, theme, thinking level
96
- โ”œโ”€โ”€ keybindings.json # Vim/Emacs shortcuts (if selected)
97
- โ”œโ”€โ”€ AGENTS.md # Project guidelines for the AI
98
- โ”œโ”€โ”€ extensions/ # Safety guards, git tools, ant colony
99
- โ”œโ”€โ”€ prompts/ # /review, /fix, /commit, /test, etc.
100
- โ”œโ”€โ”€ skills/ # debug-helper, git-workflow, ant-colony
101
- โ””โ”€โ”€ themes/ # Custom color themes
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
- Existing config? oh-pi! detects it and offers backup before overwriting.
105
-
106
- ## Included Resources
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 + Backend + DB |
171
+ | Full-Stack Developer | Frontend + backend + DB |
158
172
  | Security Researcher | Pentesting & audit |
159
173
  | Data & AI Engineer | MLOps & pipelines |
160
- | ๐Ÿœ Colony Operator | Ant swarm multi-agent orchestration |
174
+ | ๐Ÿœ Colony Operator | Multi-agent orchestration |
161
175
 
162
176
  ## Also a Pi Package
163
177
 
164
- oh-pi! is also a pi package. Install just the resources without the configurator:
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
- This adds all themes, prompts, skills, and extensions to your pi setup.
184
+ Adds all themes, prompts, skills, and extensions to your existing pi setup.
171
185
 
172
186
  ## Requirements
173
187
 
174
- - Node.js >= 20
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: entries.map(([key, info]) => ({
9
- value: key,
10
- label: info.label,
11
- hint: info.env,
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
- let defaultModel;
39
- if (info.models.length > 1) {
40
- const model = await p.select({
41
- message: `Default model for ${info.label}:`,
42
- options: info.models.map(m => ({ value: m, label: m })),
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(model)) {
74
+ if (p.isCancel(url)) {
45
75
  p.cancel("Cancelled.");
46
76
  process.exit(0);
47
77
  }
48
- defaultModel = model;
49
- }
50
- else {
51
- defaultModel = info.models[0];
78
+ baseUrl = url;
52
79
  }
53
- configs.push({ name, apiKey, defaultModel });
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 promptKey(info) {
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 ${info.label}:`,
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"] },
@@ -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: primary?.defaultModel ?? providerInfo?.models[0],
45
+ defaultModel: primaryModel,
41
46
  defaultThinkingLevel: config.thinking,
42
47
  theme: config.theme,
43
48
  enableSkillCommands: true,
44
- compaction: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
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. keybindings.json
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
- // 4. AGENTS.md
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
- // 5. Copy extensions (single file .ts or directory with index.ts)
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
- // 6. Copy prompts
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
- // 7. Copy skills
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
- // 8. Copy themes (only custom ones)
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {