oh-pi 0.1.52 → 0.1.53

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/bin/oh-pi.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * @module oh-pi CLI 入口脚本
4
+ * 处理 Windows 终端 UTF-8 编码,然后启动主流程。
5
+ */
2
6
  import { execSync } from "node:child_process";
3
7
  // Windows terminals default to non-UTF-8 codepage (e.g. GBK/CP936),
4
8
  // causing garbled emoji and Unicode output. Force UTF-8 before any output.
@@ -1 +1,9 @@
1
+ /**
2
+ * Presents an interactive prompt for the user to select an agent template
3
+ * (e.g. general developer, fullstack, security, data/AI, colony operator).
4
+ *
5
+ * Exits the process if the user cancels the selection.
6
+ *
7
+ * @returns The selected agent template identifier string.
8
+ */
1
9
  export declare function selectAgents(): Promise<string>;
@@ -1,5 +1,13 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
+ /**
4
+ * Presents an interactive prompt for the user to select an agent template
5
+ * (e.g. general developer, fullstack, security, data/AI, colony operator).
6
+ *
7
+ * Exits the process if the user cancels the selection.
8
+ *
9
+ * @returns The selected agent template identifier string.
10
+ */
3
11
  export async function selectAgents() {
4
12
  const agent = await p.select({
5
13
  message: t("agent.select"),
@@ -1,3 +1,8 @@
1
1
  import type { OhPConfig } from "../types.js";
2
2
  import type { EnvInfo } from "../utils/detect.js";
3
+ /**
4
+ * 展示配置摘要,处理已有配置的备份/覆盖,安装 pi(如需),并应用最终配置。
5
+ * @param config - 用户选择的配置对象
6
+ * @param env - 当前环境信息
7
+ */
3
8
  export declare function confirmApply(config: OhPConfig, env: EnvInfo): Promise<void>;
@@ -2,9 +2,20 @@ import * as p from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { t } from "../i18n.js";
4
4
  import { applyConfig, installPi, backupConfig } from "../utils/install.js";
5
+ /**
6
+ * 统计已有配置中指定目录下的文件数量。
7
+ * @param env - 环境信息
8
+ * @param dir - 目录名称前缀
9
+ * @returns 匹配的文件数
10
+ */
5
11
  function countExisting(env, dir) {
6
12
  return env.existingFiles.filter(f => f.startsWith(dir + "/")).length;
7
13
  }
14
+ /**
15
+ * 展示配置摘要,处理已有配置的备份/覆盖,安装 pi(如需),并应用最终配置。
16
+ * @param config - 用户选择的配置对象
17
+ * @param env - 当前环境信息
18
+ */
8
19
  export async function confirmApply(config, env) {
9
20
  // ═══ Summary ═══
10
21
  const summary = [
@@ -1 +1,6 @@
1
+ /**
2
+ * Prompts the user to select enabled extensions from the available list via a multi-select TUI prompt.
3
+ * Exits the process if the user cancels the selection.
4
+ * @returns A promise that resolves to an array of selected extension names.
5
+ */
1
6
  export declare function selectExtensions(): Promise<string[]>;
@@ -1,6 +1,11 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
3
  import { EXTENSIONS } from "../types.js";
4
+ /**
5
+ * Prompts the user to select enabled extensions from the available list via a multi-select TUI prompt.
6
+ * Exits the process if the user cancels the selection.
7
+ * @returns A promise that resolves to an array of selected extension names.
8
+ */
4
9
  export async function selectExtensions() {
5
10
  const exts = await p.multiselect({
6
11
  message: t("ext.select"),
@@ -1 +1,8 @@
1
+ /**
2
+ * Prompts the user to select a keybinding scheme (default, Vim, or Emacs).
3
+ *
4
+ * Displays an interactive select prompt and exits the process if the user cancels.
5
+ *
6
+ * @returns The name of the selected keybinding scheme.
7
+ */
1
8
  export declare function selectKeybindings(): Promise<string>;
@@ -1,5 +1,12 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
+ /**
4
+ * Prompts the user to select a keybinding scheme (default, Vim, or Emacs).
5
+ *
6
+ * Displays an interactive select prompt and exits the process if the user cancels.
7
+ *
8
+ * @returns The name of the selected keybinding scheme.
9
+ */
3
10
  export async function selectKeybindings() {
4
11
  const kb = await p.select({
5
12
  message: t("kb.select"),
@@ -1,3 +1,8 @@
1
1
  import type { EnvInfo } from "../utils/detect.js";
2
2
  export type Mode = "quick" | "custom" | "preset";
3
+ /**
4
+ * Prompt the user to select a configuration mode: quick, preset, or custom.
5
+ * @param {EnvInfo} env - Detected environment information
6
+ * @returns {Promise<Mode>} The mode selected by the user
7
+ */
3
8
  export declare function selectMode(env: EnvInfo): Promise<Mode>;
@@ -1,5 +1,10 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
+ /**
4
+ * Prompt the user to select a configuration mode: quick, preset, or custom.
5
+ * @param {EnvInfo} env - Detected environment information
6
+ * @returns {Promise<Mode>} The mode selected by the user
7
+ */
3
8
  export async function selectMode(env) {
4
9
  const mode = await p.select({
5
10
  message: t("mode.select"),
@@ -1,5 +1,10 @@
1
1
  import type { OhPConfig } from "../types.js";
2
2
  interface Preset extends Omit<OhPConfig, "providers"> {
3
3
  }
4
+ /**
5
+ * Prompts the user to select a configuration preset via an interactive TUI menu.
6
+ * Exits the process if the user cancels the selection.
7
+ * @returns The {@link Preset} configuration object for the chosen preset.
8
+ */
4
9
  export declare function selectPreset(): Promise<Preset>;
5
10
  export {};
@@ -1,5 +1,9 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
+ /**
4
+ * Registry of built-in configuration presets (Full Power / Clean / Colony).
5
+ * Each entry maps a preset key to its i18n label/hint keys and a full {@link Preset} config object.
6
+ */
3
7
  const PRESETS = {
4
8
  full: {
5
9
  labelKey: "preset.full", hintKey: "preset.fullHint",
@@ -27,6 +31,11 @@ const PRESETS = {
27
31
  },
28
32
  },
29
33
  };
34
+ /**
35
+ * Prompts the user to select a configuration preset via an interactive TUI menu.
36
+ * Exits the process if the user cancels the selection.
37
+ * @returns The {@link Preset} configuration object for the chosen preset.
38
+ */
30
39
  export async function selectPreset() {
31
40
  const key = await p.select({
32
41
  message: t("preset.select"),
@@ -1,3 +1,8 @@
1
1
  import { type ProviderConfig } from "../types.js";
2
2
  import type { EnvInfo } from "../utils/detect.js";
3
+ /**
4
+ * Interactively configure API providers, detecting existing keys, allowing multi-select, and supporting custom endpoints.
5
+ * @param env - Current environment info with detected providers
6
+ * @returns Configured provider list
7
+ */
3
8
  export declare function setupProviders(env?: EnvInfo): Promise<ProviderConfig[]>;
@@ -12,7 +12,13 @@ const PROVIDER_API_URLS = {
12
12
  xai: "https://api.x.ai",
13
13
  mistral: "https://api.mistral.ai",
14
14
  };
15
- /** Fetch models dynamically — tries multiple API styles, returns metadata + detected API type */
15
+ /**
16
+ * 动态获取模型列表,依次尝试 Anthropic、Google、OpenAI 兼容 API 风格。
17
+ * @param provider - 提供商名称
18
+ * @param baseUrl - API 基础地址
19
+ * @param apiKey - API 密钥或环境变量名
20
+ * @returns 发现的模型列表及检测到的 API 类型
21
+ */
16
22
  async function fetchModels(provider, baseUrl, apiKey) {
17
23
  const base = baseUrl.replace(/\/+$/, "");
18
24
  const resolvedKey = process.env[apiKey] ?? apiKey;
@@ -91,6 +97,11 @@ async function fetchModels(provider, baseUrl, apiKey) {
91
97
  catch { /* fall through */ }
92
98
  return { models: [] };
93
99
  }
100
+ /**
101
+ * Interactively configure API providers, detecting existing keys, allowing multi-select, and supporting custom endpoints.
102
+ * @param env - Current environment info with detected providers
103
+ * @returns Configured provider list
104
+ */
94
105
  export async function setupProviders(env) {
95
106
  const entries = Object.entries(PROVIDERS);
96
107
  // Detect existing providers — offer skip or add new
@@ -175,6 +186,10 @@ export async function setupProviders(env) {
175
186
  }
176
187
  return configs;
177
188
  }
189
+ /**
190
+ * Interactively configure a custom provider (Ollama, vLLM, or other OpenAI-compatible endpoints).
191
+ * @returns Custom provider config, or null if cancelled
192
+ */
178
193
  async function setupCustomProvider() {
179
194
  const name = await p.text({
180
195
  message: t("provider.name"),
@@ -207,6 +222,15 @@ async function setupCustomProvider() {
207
222
  p.log.success(t("provider.customConfigured", { name, url: baseUrl }));
208
223
  return { name, apiKey, defaultModel, baseUrl, api, discoveredModels };
209
224
  }
225
+ /**
226
+ * Select a default model by dynamically fetching available models, falling back to a static list or manual input.
227
+ * @param provider - Provider name
228
+ * @param label - Provider display label
229
+ * @param staticModels - Static model list fallback
230
+ * @param baseUrl - API base URL
231
+ * @param apiKey - API key
232
+ * @returns Selected model and discovered model metadata
233
+ */
210
234
  async function selectModelWithMeta(provider, label, staticModels, baseUrl, apiKey) {
211
235
  let modelIds = staticModels;
212
236
  let discoveredModels;
@@ -246,6 +270,11 @@ async function selectModelWithMeta(provider, label, staticModels, baseUrl, apiKe
246
270
  }
247
271
  return { defaultModel: model, discoveredModels, api };
248
272
  }
273
+ /**
274
+ * Prompt the user to enter an API key.
275
+ * @param label - Provider display label
276
+ * @returns The entered API key
277
+ */
249
278
  async function promptKey(label) {
250
279
  const key = await p.password({
251
280
  message: t("provider.apiKey", { label }),
@@ -1 +1,6 @@
1
+ /**
2
+ * Prompts the user to select a theme from the available themes list.
3
+ * Exits the process if the user cancels the selection.
4
+ * @returns The name of the selected theme.
5
+ */
1
6
  export declare function selectTheme(): Promise<string>;
@@ -1,6 +1,11 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { t } from "../i18n.js";
3
3
  import { THEMES } from "../types.js";
4
+ /**
5
+ * Prompts the user to select a theme from the available themes list.
6
+ * Exits the process if the user cancels the selection.
7
+ * @returns The name of the selected theme.
8
+ */
4
9
  export async function selectTheme() {
5
10
  const theme = await p.select({
6
11
  message: t("theme.select"),
@@ -1,2 +1,6 @@
1
1
  import type { EnvInfo } from "../utils/detect.js";
2
+ /**
3
+ * 展示欢迎界面,显示 pi 安装状态、环境信息及已有配置概况。
4
+ * @param {EnvInfo} env - 当前检测到的环境信息
5
+ */
2
6
  export declare function welcome(env: EnvInfo): void;
@@ -1,6 +1,10 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { t } from "../i18n.js";
4
+ /**
5
+ * 展示欢迎界面,显示 pi 安装状态、环境信息及已有配置概况。
6
+ * @param {EnvInfo} env - 当前检测到的环境信息
7
+ */
4
8
  export function welcome(env) {
5
9
  console.clear();
6
10
  p.intro(chalk.cyan.bold(" oh-pi ") + chalk.dim(t("welcome.title")));
@@ -19,6 +23,11 @@ export function welcome(env) {
19
23
  categorize(env.existingFiles), t("welcome.existingConfig"));
20
24
  }
21
25
  }
26
+ /**
27
+ * 按顶层目录分类统计文件数量,返回格式化字符串。
28
+ * @param {string[]} files - 文件相对路径列表
29
+ * @returns {string} 分类统计字符串,如 "extensions (3) prompts (5)"
30
+ */
22
31
  function categorize(files) {
23
32
  const cats = {};
24
33
  for (const f of files) {
@@ -1,3 +1,4 @@
1
+ /** 资源路径映射对象 */
1
2
  export declare const resources: {
2
3
  agent: (name: string) => string;
3
4
  extension: (name: string) => string;
@@ -5,6 +5,7 @@ import { join, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
7
7
  const RESOURCES = join(PKG_ROOT, "pi-package");
8
+ /** 资源路径映射对象 */
8
9
  export const resources = {
9
10
  agent: (name) => join(RESOURCES, "agents", `${name}.md`),
10
11
  extension: (name) => join(RESOURCES, "extensions", name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,7 +67,7 @@ export function adapt(config: ConcurrencyConfig, pendingTasks: number): Concurre
67
67
  // 不超过待处理任务数
68
68
  const taskCap = Math.min(pendingTasks, config.max);
69
69
 
70
- if (samples.length < 3) {
70
+ if (samples.length < 2) {
71
71
  // 冷启动:直接给一半 max,快速利用并发
72
72
  next.current = Math.min(Math.ceil(config.max / 2), taskCap);
73
73
  return next;
@@ -111,5 +111,10 @@ export function adapt(config: ConcurrencyConfig, pendingTasks: number): Concurre
111
111
  }
112
112
  // 否则保持不变
113
113
 
114
+ // 429 recovery: restore to optimal when CPU is underutilized (e.g. after backoff)
115
+ if (latest.cpuLoad < 0.5 && next.current < config.optimal) {
116
+ next.current = config.optimal;
117
+ }
118
+
114
119
  return next;
115
120
  }
@@ -558,6 +558,75 @@ export default function antColonyExtension(pi: ExtensionAPI) {
558
558
  },
559
559
  });
560
560
 
561
+ // ═══ Helper: build status summary ═══
562
+
563
+ function buildStatusText(): string {
564
+ if (!activeColony) return "No colony is currently running.";
565
+ const c = activeColony;
566
+ const state = c.state;
567
+ const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
568
+ const m = state?.metrics;
569
+ const tasks = state?.tasks || [];
570
+ const ants = state?.ants || [];
571
+ const streams = Array.from(c.antStreams.values());
572
+
573
+ const lines: string[] = [
574
+ `## 🐜 Colony Status`,
575
+ `**Goal:** ${c.goal}`,
576
+ `**Phase:** ${c.phase}`,
577
+ `**Duration:** ${elapsed}`,
578
+ ];
579
+
580
+ if (m) {
581
+ lines.push(`**Tasks:** ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`);
582
+ lines.push(`**Ants spawned:** ${m.antsSpawned} | **Active:** ${streams.length}`);
583
+ lines.push(`**Cost:** ${formatCost(m.totalCost)} | **Tokens:** ${formatTokens(m.totalTokens)}`);
584
+ }
585
+
586
+ if (tasks.length > 0) {
587
+ lines.push("", "### Tasks");
588
+ for (const t of tasks) {
589
+ const icon = t.status === "done" ? "✓" : t.status === "failed" ? "✗" : t.status === "active" ? "●" : "○";
590
+ const dur = t.finishedAt && t.startedAt ? ` (${formatDuration(t.finishedAt - t.startedAt)})` : "";
591
+ lines.push(`- ${icon} [${t.caste}] ${t.title}${dur}`);
592
+ }
593
+ }
594
+
595
+ if (streams.length > 0) {
596
+ lines.push("", "### Active Ants");
597
+ for (const s of streams) {
598
+ lines.push(`- ${casteIcon(s.caste)} ${s.antId.slice(0, 14)} | ${s.tokens}tok | ${s.lastLine.slice(0, 60)}`);
599
+ }
600
+ }
601
+
602
+ return lines.join("\n");
603
+ }
604
+
605
+ // ═══ Tool: bg_colony_status ═══
606
+ pi.registerTool({
607
+ name: "bg_colony_status",
608
+ label: "Colony Status",
609
+ description: "Check the status of a running background ant colony. Use this instead of bg_status to monitor colony progress.",
610
+ parameters: Type.Object({}),
611
+ async execute() {
612
+ return {
613
+ content: [{ type: "text" as const, text: buildStatusText() }],
614
+ };
615
+ },
616
+ });
617
+
618
+ // ═══ Command: /colony-status ═══
619
+ pi.registerCommand("colony-status", {
620
+ description: "Show current colony progress",
621
+ async handler(_args, ctx) {
622
+ if (!activeColony) {
623
+ ctx.ui.notify("No colony is currently running.", "info");
624
+ return;
625
+ }
626
+ ctx.ui.notify(buildStatusText(), "info");
627
+ },
628
+ });
629
+
561
630
  // ═══ Command: /colony-stop ═══
562
631
  pi.registerCommand("colony-stop", {
563
632
  description: "Stop the running background colony",
@@ -226,8 +226,8 @@ export class Nest {
226
226
  // ═══ Internal ═══
227
227
 
228
228
  private withStateLock<T>(fn: () => T): T {
229
- const MAX_WAIT = 5000;
230
- const SPIN_MS = 15;
229
+ const MAX_WAIT = 3000;
230
+ const SPIN_MS = 1;
231
231
  const start = Date.now();
232
232
  while (true) {
233
233
  try {
@@ -378,7 +378,6 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
378
378
  nest.updateState({ status: "failed", finishedAt: Date.now() });
379
379
  const finalState = nest.getState();
380
380
  callbacks.onComplete(finalState);
381
- cleanup();
382
381
  return finalState;
383
382
  }
384
383
 
@@ -424,14 +423,14 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
424
423
  nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
425
424
  const finalState = nest.getState();
426
425
  callbacks.onComplete(finalState);
427
- cleanup();
428
426
  return finalState;
429
427
 
430
428
  } catch (e) {
431
429
  nest.updateState({ status: "failed", finishedAt: Date.now() });
432
430
  const failState = nest.getState();
433
431
  callbacks.onComplete(failState);
434
- cleanup();
435
432
  return failState;
433
+ } finally {
434
+ cleanup();
436
435
  }
437
436
  }
@@ -142,6 +142,9 @@ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string,
142
142
  if (task.files.length > 0) {
143
143
  prompt += `**Files scope:** ${task.files.join(", ")}\n`;
144
144
  }
145
+ if (/[\u4e00-\u9fff]/.test(task.description)) {
146
+ prompt += '\nIMPORTANT: Follow the language requirements specified in the task description. If the task says to write in Chinese, write in Chinese.\n';
147
+ }
145
148
  return prompt;
146
149
  }
147
150