oh-pi 0.1.1 → 0.1.3

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/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`)}`,
@@ -142,7 +142,66 @@ async function setupCustomProvider() {
142
142
  defaultModel = model;
143
143
  }
144
144
  p.log.success(`${name} configured (${baseUrl})`);
145
- return { name, apiKey, defaultModel, 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 };
146
205
  }
147
206
  async function selectModel(label, staticModels, baseUrl, apiKey) {
148
207
  let models = staticModels;
package/dist/types.d.ts CHANGED
@@ -3,6 +3,10 @@ export interface ProviderConfig {
3
3
  apiKey: string;
4
4
  defaultModel?: string;
5
5
  baseUrl?: string;
6
+ contextWindow?: number;
7
+ maxTokens?: number;
8
+ reasoning?: boolean;
9
+ multimodal?: boolean;
6
10
  }
7
11
  export interface OhPConfig {
8
12
  providers: ProviderConfig[];
@@ -13,7 +17,16 @@ export interface OhPConfig {
13
17
  prompts: string[];
14
18
  agents: string;
15
19
  thinking: string;
20
+ compactThreshold?: number;
16
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>;
17
30
  export declare const PROVIDERS: Record<string, {
18
31
  env: string;
19
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"] },
@@ -21,6 +41,7 @@ export const EXTENSIONS = [
21
41
  { name: "safe-guard", label: "🛡️ Safe Guard — Dangerous command confirm + path protection", default: true },
22
42
  { name: "git-guard", label: "📦 Git Guard — Auto stash checkpoint + dirty repo warning + notify", default: true },
23
43
  { name: "auto-session-name", label: "📝 Auto Session Name — Name sessions from first message", default: true },
44
+ { name: "custom-footer", label: "📊 Custom Footer — Enhanced status bar with tokens, cost, time, git, cwd", default: false },
24
45
  { name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
25
46
  ];
26
47
  export const KEYBINDING_SCHEMES = {
@@ -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) {
@@ -56,6 +61,7 @@ export function applyConfig(config) {
56
61
  if (customProviders.length > 0) {
57
62
  const models = {};
58
63
  for (const cp of customProviders) {
64
+ const caps = cp.defaultModel ? MODEL_CAPABILITIES[cp.defaultModel] : undefined;
59
65
  models[cp.name] = {
60
66
  baseUrl: cp.baseUrl,
61
67
  apiKey: cp.apiKey === "none" ? undefined : cp.apiKey,
@@ -63,10 +69,10 @@ export function applyConfig(config) {
63
69
  models: cp.defaultModel ? [{
64
70
  id: cp.defaultModel,
65
71
  name: cp.defaultModel,
66
- reasoning: false,
67
- input: ["text"],
68
- contextWindow: 128000,
69
- maxTokens: 8192,
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,
70
76
  }] : [],
71
77
  };
72
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Custom Footer Extension — Enhanced status bar
3
+ *
4
+ * Displays: ↑input ↓output Rremaining $cost percent/contextWindow (auto) | ⏱ elapsed | 📂 cwd | 🌿 branch | model • thinking
5
+ * Color-coded context usage: green <50%, yellow 50-75%, red >75%
6
+ */
7
+
8
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
9
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ let sessionStart = Date.now();
14
+
15
+ function formatElapsed(ms: number): string {
16
+ const s = Math.floor(ms / 1000);
17
+ if (s < 60) return `${s}s`;
18
+ const m = Math.floor(s / 60);
19
+ const rs = s % 60;
20
+ if (m < 60) return `${m}m${rs > 0 ? rs + "s" : ""}`;
21
+ const h = Math.floor(m / 60);
22
+ const rm = m % 60;
23
+ return `${h}h${rm > 0 ? rm + "m" : ""}`;
24
+ }
25
+
26
+ function fmt(n: number): string {
27
+ if (n < 1000) return `${n}`;
28
+ return `${(n / 1000).toFixed(1)}k`;
29
+ }
30
+
31
+ pi.on("session_start", async (_event, ctx) => {
32
+ sessionStart = Date.now();
33
+
34
+ ctx.ui.setFooter((tui, theme, footerData) => {
35
+ const unsub = footerData.onBranchChange(() => tui.requestRender());
36
+ const timer = setInterval(() => tui.requestRender(), 30000);
37
+
38
+ return {
39
+ dispose() { unsub(); clearInterval(timer); },
40
+ invalidate() {},
41
+ render(width: number): string[] {
42
+ // --- Tokens & Cost ---
43
+ let input = 0, output = 0, cost = 0;
44
+ for (const e of ctx.sessionManager.getBranch()) {
45
+ if (e.type === "message" && e.message.role === "assistant") {
46
+ const m = e.message as AssistantMessage;
47
+ input += m.usage.input;
48
+ output += m.usage.output;
49
+ cost += m.usage.cost.total;
50
+ }
51
+ }
52
+
53
+ // --- Context usage ---
54
+ const usage = ctx.getContextUsage();
55
+ const tokens = usage?.tokens ?? 0;
56
+ const ctxWindow = usage?.contextWindow ?? 0;
57
+ const pct = usage?.percent ?? 0;
58
+ const remaining = Math.max(0, ctxWindow - tokens);
59
+
60
+ // Color by usage level
61
+ const pctColor = pct > 75 ? "error" : pct > 50 ? "warning" : "success";
62
+ const pctStr = `${pct.toFixed(1)}%/${fmt(ctxWindow)}`;
63
+
64
+ const tokenStats = [
65
+ theme.fg("accent", `↑${fmt(input)}`),
66
+ theme.fg("dim", ` ↓${fmt(output)}`),
67
+ theme.fg("muted", ` R${fmt(remaining)}`),
68
+ theme.fg("warning", ` $${cost.toFixed(3)}`),
69
+ " ",
70
+ theme.fg(pctColor, pctStr),
71
+ theme.fg("dim", " (auto)"),
72
+ ].join("");
73
+
74
+ // --- Elapsed ---
75
+ const elapsed = theme.fg("dim", `⏱ ${formatElapsed(Date.now() - sessionStart)}`);
76
+
77
+ // --- CWD (last 2 segments) ---
78
+ const cwd = process.cwd();
79
+ const parts = cwd.split("/");
80
+ const short = parts.length > 2 ? parts.slice(-2).join("/") : cwd;
81
+ const cwdStr = theme.fg("muted", `📂 ${short}`);
82
+
83
+ // --- Git branch ---
84
+ const branch = footerData.getGitBranch();
85
+ const branchStr = branch ? theme.fg("accent", `🌿 ${branch}`) : "";
86
+
87
+ // --- Right: model + thinking ---
88
+ const thinking = pi.getThinkingLevel();
89
+ const modelId = ctx.model?.id || "no-model";
90
+ const right = theme.fg("dim", `${modelId} • ${thinking}`);
91
+
92
+ // --- Layout ---
93
+ const sep = theme.fg("dim", " │ ");
94
+ const leftParts = [tokenStats, elapsed, cwdStr];
95
+ if (branchStr) leftParts.push(branchStr);
96
+ const left = leftParts.join(sep);
97
+
98
+ const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(sep) - visibleWidth(right)));
99
+ return [truncateToWidth(left + pad + right, width)];
100
+ },
101
+ };
102
+ });
103
+ });
104
+
105
+ pi.on("session_switch", async (event, _ctx) => {
106
+ if (event.reason === "new") {
107
+ sessionStart = Date.now();
108
+ }
109
+ });
110
+ }