pi-firecode 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/config.example.jsonc +100 -0
  4. package/config.ts +474 -0
  5. package/deliver.ts +32 -0
  6. package/flame-frames.ts +460 -0
  7. package/format.ts +80 -0
  8. package/header.ts +92 -0
  9. package/herdr-client.ts +60 -0
  10. package/index.ts +62 -0
  11. package/jsonc.ts +34 -0
  12. package/master/event-card.ts +106 -0
  13. package/master/event-format.ts +52 -0
  14. package/master/index.ts +1200 -0
  15. package/master/prompt.ts +26 -0
  16. package/master/prompts/master.zh.md +17 -0
  17. package/master/prompts/worker.zh.md +1 -0
  18. package/master/role.ts +13 -0
  19. package/master/spawn.ts +193 -0
  20. package/master/state.ts +201 -0
  21. package/package.json +59 -0
  22. package/provider/claude-sub.ts +129 -0
  23. package/provider/openai-native/index.ts +8 -0
  24. package/provider/openai-native/src/compact-client.ts +362 -0
  25. package/provider/openai-native/src/config.ts +297 -0
  26. package/provider/openai-native/src/extension.ts +93 -0
  27. package/provider/openai-native/src/native-compaction.ts +151 -0
  28. package/provider/openai-native/src/native-details.ts +157 -0
  29. package/provider/openai-native/src/native-replay.ts +253 -0
  30. package/provider/openai-native/src/native-runtime.ts +144 -0
  31. package/provider/openai-native/src/options.ts +85 -0
  32. package/provider/openai-native/src/request-pipeline.ts +21 -0
  33. package/provider/openai-native/src/responses-input.ts +433 -0
  34. package/review/advisor.ts +112 -0
  35. package/review/card.ts +385 -0
  36. package/review/checkpoint.ts +384 -0
  37. package/review/evidence.ts +232 -0
  38. package/review/index.ts +1246 -0
  39. package/review/outcome.ts +74 -0
  40. package/review/progress.ts +324 -0
  41. package/review/prompt.ts +210 -0
  42. package/review/prompts/advisor.en.md +41 -0
  43. package/review/prompts/advisor.zh.md +42 -0
  44. package/review/prompts/review.en.md +75 -0
  45. package/review/prompts/review.zh.md +75 -0
  46. package/review/reviewer.ts +356 -0
  47. package/review/session.ts +129 -0
  48. package/review/state.ts +905 -0
  49. package/review/ui.ts +528 -0
  50. package/session/bark.ts +157 -0
  51. package/session/herdr-display.ts +87 -0
  52. package/session/presets.ts +257 -0
  53. package/session/rename.ts +46 -0
  54. package/session/stats.ts +310 -0
  55. package/session/working-flame.ts +116 -0
  56. package/statusbar/index.ts +118 -0
  57. package/statusbar/quota-cache.ts +60 -0
  58. package/statusbar/quota-parse.ts +85 -0
  59. package/statusbar/quota.ts +203 -0
  60. package/statusbar/render.ts +181 -0
  61. package/statusbar/tps.ts +87 -0
  62. package/theme.ts +84 -0
  63. package/tools/grouping.ts +233 -0
  64. package/tools/index.ts +183 -0
  65. package/tools/line.ts +194 -0
  66. package/tools/parts.ts +143 -0
  67. package/tools/timing.ts +22 -0
  68. package/watcher/card.ts +85 -0
  69. package/watcher/index.ts +204 -0
  70. package/watcher/observer.ts +75 -0
  71. package/watcher/prompts/watch.zh.md +46 -0
  72. package/watcher/transcript.ts +64 -0
package/header.ts ADDED
@@ -0,0 +1,92 @@
1
+ /** 会话启动横幅:终端够宽显示火焰 + FireCode 字标,否则退化成一行。 */
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { visibleWidth } from "@earendil-works/pi-tui";
4
+ import { clip } from "./format.js";
5
+ import { ANSI, FLAME } from "./theme.js";
6
+
7
+ const GAP = " ";
8
+ const EXTRA_RIGHT_PAD = " ";
9
+ /** 字标相对火焰顶部下沉的行数 */
10
+ const TEXT_TOP_PAD = 4;
11
+
12
+ const FLAME_ART = [
13
+ "⠀⠀⠀⠀⠀⠀⢱⣆⠀⠀⠀⠀⠀⠀",
14
+ "⠀⠀⠀⠀⠀⠀⠈⣿⣷⡀⠀⠀⠀⠀",
15
+ "⠀⠀⠀⠀⠀⠀⢸⣿⣿⣷⣧⠀⠀⠀",
16
+ "⠀⠀⠀⠀⡀⢠⣿⡟⣿⣿⣿⡇⠀⠀",
17
+ "⠀⠀⠀⠀⣳⣼⣿⡏⢸⣿⣿⣿⢀⠀",
18
+ "⠀⠀⠀⣰⣿⣿⡿⠁⢸⣿⣿⡟⣼⡆",
19
+ "⢰⢀⣾⣿⣿⠟⠀⠀⣾⢿⣿⣿⣿⣿",
20
+ "⢸⣿⣿⣿⡏⠀⠀⠀⠃⠸⣿⣿⣿⡿",
21
+ "⢳⣿⣿⣿⠀⠀⠀⠀⠀⠀⢹⣿⡿⡁",
22
+ "⠀⠹⣿⣿⡄⠀⠀⠀⠀⠀⢠⣿⡞⠁",
23
+ "⠀⠀⠈⠛⢿⣄⠀⠀⠀⣠⠞⠋⠀⠀",
24
+ ];
25
+ const WORDMARK = [
26
+ "___________.__ _________ .___ ",
27
+ "\\_ _____/|__|______ ____ \\_ ___ \\ ____ __| _/____ ",
28
+ " | __) | \\_ __ \\_/ __ \\ / \\ \\/ / _ \\ / __ |/ __ \\ ",
29
+ " | \\ | || | \\/\\ ___/ \\ \\___( <_> ) /_/ \\ ___/ ",
30
+ " \\___ / |__||__| \\___ > \\______ /\\____/\\____ |\\___ >",
31
+ " \\/ \\/ \\/ \\/ \\/ ",
32
+ ];
33
+ const FLAME_COLORS = [
34
+ FLAME.red,
35
+ FLAME.red,
36
+ FLAME.orange,
37
+ FLAME.orange,
38
+ FLAME.gold,
39
+ FLAME.white,
40
+ FLAME.white,
41
+ FLAME.gold,
42
+ FLAME.orange,
43
+ FLAME.red,
44
+ FLAME.red,
45
+ ];
46
+ const TEXT_COLORS = [
47
+ FLAME.textTop,
48
+ FLAME.textTop,
49
+ FLAME.textMid,
50
+ FLAME.textCore,
51
+ FLAME.gold,
52
+ FLAME.gold,
53
+ ];
54
+
55
+ function padToWidth(line: string, width: number): string {
56
+ return line + " ".repeat(Math.max(0, width - visibleWidth(line)));
57
+ }
58
+
59
+ const FLAME_WIDTH = Math.max(...FLAME_ART.map(visibleWidth));
60
+ const FULL_WIDTH = Math.max(
61
+ ...FLAME_ART.map((flame, index) => {
62
+ const text = WORDMARK[index - TEXT_TOP_PAD] ?? "";
63
+ return visibleWidth(`${padToWidth(flame, FLAME_WIDTH)}${GAP}${text}${EXTRA_RIGHT_PAD}`);
64
+ }),
65
+ );
66
+ const FULL = FLAME_ART.map((art, index) => {
67
+ const textIndex = index - TEXT_TOP_PAD;
68
+ const wordmark = WORDMARK[textIndex] ?? "";
69
+ const flame = `${FLAME_COLORS[index]}${padToWidth(art, FLAME_WIDTH)}${ANSI.reset}`;
70
+ const title = wordmark
71
+ ? `${ANSI.bold}${TEXT_COLORS[textIndex]}${wordmark}${ANSI.reset}`
72
+ : "";
73
+ return padToWidth(`${flame}${GAP}${title}`, FULL_WIDTH);
74
+ });
75
+ const TINY = [`${FLAME.orange}🔥 Fire${ANSI.reset} ${FLAME.textCore}Code${ANSI.reset}`];
76
+
77
+ function center(line: string, width: number): string {
78
+ const clipped = clip(line, width, "end", "");
79
+ return " ".repeat(Math.max(0, Math.floor((width - visibleWidth(clipped)) / 2))) + clipped;
80
+ }
81
+
82
+ export function registerHeader(pi: ExtensionAPI): void {
83
+ pi.on("session_start", (_event, ctx) => {
84
+ ctx.ui.setHeader(() => ({
85
+ invalidate() {},
86
+ render(width: number): string[] {
87
+ const lines = width >= FULL_WIDTH ? FULL : TINY;
88
+ return ["", ...lines.map((line) => center(line, width)), ""];
89
+ },
90
+ }));
91
+ });
92
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * herdr socket 短连接客户端:单请求单连接,只有 herdr 返回 result 才算送达。
3
+ * herdr-display(身份投影)与 review(占用 state_label)共用;herdr 之外返回未送达。
4
+ */
5
+ import net from "node:net";
6
+
7
+ const REQUEST_TIMEOUT_MS = 500;
8
+
9
+ export interface HerdrPaneEnv {
10
+ paneId: string;
11
+ socketPath: string;
12
+ }
13
+
14
+ export function herdrPaneEnv(): HerdrPaneEnv | undefined {
15
+ const paneId = process.env.HERDR_PANE_ID;
16
+ const socketPath = process.env.HERDR_SOCKET_PATH;
17
+ if (process.env.HERDR_ENV !== "1" || !paneId || !socketPath) return undefined;
18
+ return { paneId, socketPath };
19
+ }
20
+
21
+ export function herdrRequest(
22
+ source: string,
23
+ method: string,
24
+ params: Record<string, unknown>,
25
+ ): Promise<boolean> {
26
+ const env = herdrPaneEnv();
27
+ if (!env) return Promise.resolve(false);
28
+ const endpoint =
29
+ process.platform === "win32" ? `\\\\.\\pipe\\${env.socketPath}` : env.socketPath;
30
+ return new Promise((resolve) => {
31
+ const socket = net.createConnection(endpoint);
32
+ let buffer = "";
33
+ const finish = (delivered: boolean) => {
34
+ clearTimeout(timer);
35
+ socket.destroy();
36
+ resolve(delivered);
37
+ };
38
+ const timer = setTimeout(() => finish(false), REQUEST_TIMEOUT_MS);
39
+ timer.unref?.();
40
+ socket.on("error", () => finish(false));
41
+ socket.on("end", () => finish(false));
42
+ socket.on("data", (chunk) => {
43
+ buffer += chunk;
44
+ const end = buffer.indexOf("\n");
45
+ if (end >= 0) finish(hasResult(buffer.slice(0, end)));
46
+ });
47
+ socket.on("connect", () => {
48
+ socket.write(`${JSON.stringify({ id: source, method, params })}\n`);
49
+ });
50
+ });
51
+ }
52
+
53
+ function hasResult(line: string): boolean {
54
+ try {
55
+ const response = JSON.parse(line) as { result?: unknown; error?: unknown };
56
+ return !response.error && typeof response.result === "object" && response.result !== null;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
package/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * FireCode:个人 pi 定制层——启动横幅、状态栏、工具行渲染、预设、会话命名,
3
+ * Claude 归因、OpenAI 请求层、对抗审查与按需 Master。各功能可在 config.jsonc 的 features 里单独关闭。
4
+ */
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { type Feature, loadConfig } from "./config.js";
7
+ import { registerHeader } from "./header.js";
8
+ import { registerClaudeSub } from "./provider/claude-sub.js";
9
+ import { registerOpenAINative } from "./provider/openai-native/index.js";
10
+ import { registerPresets } from "./session/presets.js";
11
+ import { registerHerdrDisplay } from "./session/herdr-display.js";
12
+ import { registerSessionName } from "./session/rename.js";
13
+ import { registerBark } from "./session/bark.js";
14
+ import { registerStats } from "./session/stats.js";
15
+ import { registerWorkingFlame } from "./session/working-flame.js";
16
+ import { registerStatusBar } from "./statusbar/index.js";
17
+ import { registerToolRendering } from "./tools/index.js";
18
+ import { registerReview } from "./review/index.js";
19
+ import { registerMaster } from "./master/index.js";
20
+ import { currentSubsessionRole } from "./master/role.js";
21
+ import { registerWatcher } from "./watcher/index.js";
22
+
23
+ const REGISTRARS: Record<Exclude<Feature, "review" | "master" | "watcher" | "statusbar" | "bark">, (pi: ExtensionAPI) => void> = {
24
+ header: registerHeader,
25
+ tools: registerToolRendering,
26
+ presets: registerPresets,
27
+ rename: registerSessionName,
28
+ stats: registerStats,
29
+ claudeSub: registerClaudeSub,
30
+ openaiNative: registerOpenAINative,
31
+ workingFlame: registerWorkingFlame,
32
+ };
33
+
34
+ type FirecodeSessionRole = "main" | "worker" | "observer" | "reviewer" | "advisor";
35
+
36
+ export function registerFirecode(pi: ExtensionAPI, role: FirecodeSessionRole = "main"): void {
37
+ const { config, problems } = loadConfig();
38
+ const subsession = role !== "main";
39
+ const reviewEnabled = config.features.review !== false;
40
+ for (const [feature, register] of Object.entries(REGISTRARS)) {
41
+ const enabled = config.features[feature as Exclude<Feature, "review" | "master" | "watcher" | "statusbar" | "bark">] !== false;
42
+ if (enabled) register(pi);
43
+ }
44
+ if (config.features.statusbar !== false) registerStatusBar(pi, subsession);
45
+ if (config.features.bark !== false) registerBark(pi, subsession);
46
+ if (config.features.watcher !== false) registerWatcher(pi, {}, subsession);
47
+ if (config.features.master !== false) registerMaster(pi, {}, subsession);
48
+ // herdr 显示投影没有开关:herdr 之外自我禁用,只写显示层。
49
+ registerHerdrDisplay(pi, subsession);
50
+ // 历史卡渲染与 checkpoint 收口不受 feature 开关控制;开关只控制命令和执行循环。
51
+ // features 整节类型错误会被安全回退成全关,但那是配置坏而非用户关闭:不封存 checkpoint。
52
+ registerReview(pi, reviewEnabled, problems.includes("features 必须是对象"));
53
+
54
+ if (problems.length === 0) return;
55
+ pi.on("session_start", (_event, ctx) => {
56
+ ctx.ui.notify(`FireCode 配置有问题:${problems.join(";")}`, "warning");
57
+ });
58
+ }
59
+
60
+ export default function firecode(pi: ExtensionAPI): void {
61
+ registerFirecode(pi, currentSubsessionRole() ?? "main");
62
+ }
package/jsonc.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** 去掉行注释和块注释,字符串内的斜杠不动。不支持尾逗号。 */
2
+ export function parseJsonc(text: string): unknown {
3
+ let output = "";
4
+ let inString = false;
5
+ let escape = false;
6
+ for (let index = 0; index < text.length; index++) {
7
+ const char = text[index];
8
+ if (inString) {
9
+ output += char;
10
+ if (escape) escape = false;
11
+ else if (char === "\\") escape = true;
12
+ else if (char === '"') inString = false;
13
+ continue;
14
+ }
15
+ if (char === '"') {
16
+ inString = true;
17
+ output += char;
18
+ continue;
19
+ }
20
+ if (char === "/" && text[index + 1] === "/") {
21
+ index += 1;
22
+ while (index + 1 < text.length && text[index + 1] !== "\n") index += 1;
23
+ continue;
24
+ }
25
+ if (char === "/" && text[index + 1] === "*") {
26
+ index += 1;
27
+ while (index + 1 < text.length && !(text[index] === "*" && text[index + 1] === "/")) index += 1;
28
+ index += 1;
29
+ continue;
30
+ }
31
+ output += char;
32
+ }
33
+ return JSON.parse(output);
34
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Master 事件卡:默认紧凑(每事件一行标题行),ctrl+o 展开完整内容。
3
+ * content 给模型(完整事实),details 给渲染——两者刻意不对齐(先例:review/card.ts)。
4
+ * 提取与校验的格式契约在 event-format.ts;旧会话无 details 的消息与校验失败一律降级
5
+ * 完整内容;渲染器永不抛异常。
6
+ */
7
+ import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
8
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
9
+ import { Box, type Component, Markdown, Text } from "@earendil-works/pi-tui";
10
+ import { clip, oneLine } from "../format.js";
11
+ import {
12
+ MASTER_EVENT_TYPE,
13
+ isValidMasterEventDetails,
14
+ type MasterEventDetails,
15
+ } from "./event-format.js";
16
+
17
+ export function registerMasterEventRenderer(pi: ExtensionAPI): void {
18
+ pi.registerMessageRenderer<MasterEventDetails>(
19
+ MASTER_EVENT_TYPE,
20
+ (message, options, theme) =>
21
+ new MasterEventCard(message.details, message.content, options.expanded, theme),
22
+ );
23
+ }
24
+
25
+ class MasterEventCard implements Component {
26
+ private readonly card: Component;
27
+ private readonly fallback: Component;
28
+
29
+ constructor(
30
+ details: MasterEventDetails | undefined,
31
+ content: string | (string | unknown)[],
32
+ expanded: boolean,
33
+ theme: Theme,
34
+ ) {
35
+ const text = plainContent(content);
36
+ this.fallback = new Text(text, 0, 0);
37
+ let card: Component | undefined;
38
+ try {
39
+ card = !expanded && isValidMasterEventDetails(details)
40
+ ? compactCard(details, theme)
41
+ : fullCard(text, theme);
42
+ } catch {
43
+ card = undefined;
44
+ }
45
+ this.card = card ?? this.fallback;
46
+ }
47
+
48
+ render(width: number): string[] {
49
+ try {
50
+ return this.card.render(Math.max(1, width));
51
+ } catch {
52
+ try {
53
+ return this.fallback.render(Math.max(1, width));
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+ }
59
+
60
+ invalidate(): void {
61
+ this.card.invalidate?.();
62
+ this.fallback.invalidate?.();
63
+ }
64
+ }
65
+
66
+ function compactCard(details: MasterEventDetails, theme: Theme): Component {
67
+ return card(theme, details.titles.map((title) => new CompactTitle(`◆ ${title}`)));
68
+ }
69
+
70
+ /**
71
+ * 单行截断:不用 pi-tui 的 TruncatedText——它的省略号带 `\x1b[0m` 全量重置,
72
+ * 会在截断点把外层 Box 的背景色掠断(行尾露底色、右缘参差);
73
+ * clip 是纯文本截断,Box 补位后背景整行连续。
74
+ */
75
+ class CompactTitle implements Component {
76
+ constructor(private readonly text: string) {}
77
+
78
+ invalidate(): void {}
79
+
80
+ render(width: number): string[] {
81
+ return [clip(oneLine(this.text), Math.max(1, width))];
82
+ }
83
+ }
84
+
85
+ function fullCard(text: string, theme: Theme): Component {
86
+ return card(theme, [new Markdown(text, 0, 0, getMarkdownTheme())]);
87
+ }
88
+
89
+ function card(theme: Theme, children: Component[]): Component {
90
+ // 宿主 CustomMessageComponent 已自带一行消息间距,这里不再叠加;垂直内边距也为 0。
91
+ const box = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
92
+ for (const child of children) box.addChild(child);
93
+ return box;
94
+ }
95
+
96
+ function plainContent(content: string | (string | unknown)[]): string {
97
+ if (typeof content === "string") return content;
98
+ if (!Array.isArray(content)) return "";
99
+ return content
100
+ .map((part) =>
101
+ typeof part === "object" && part !== null && "text" in part
102
+ ? String((part as { text: unknown }).text)
103
+ : "")
104
+ .filter(Boolean)
105
+ .join("\n");
106
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Master 事件文本与紧凑行的共享格式契约(纯函数、零宿主依赖,可被测试直接导入)。
3
+ * 分节标记是事件产文与紧凑卡提取的单一事实源:改词两侧编译期同步,杜绝静默退化
4
+ * (曾经的隐式跨文件耦合让"改文案→预览悄悄消失"且测试全绿)。
5
+ */
6
+ export const MASTER_EVENT_TYPE = "firecode-master-event";
7
+
8
+ /** 正文分节标记:事件产文用它拼标记行,紧凑行提取用它识别正文首句。 */
9
+ export const BODY_SECTIONS = {
10
+ reply: "回复",
11
+ error: "错误",
12
+ question: "问题",
13
+ finalReply: "最终回复",
14
+ lastOutput: "中断前最后输出",
15
+ } as const;
16
+
17
+ /** 产文侧唯一入口:正文段一律以 `<标记>:` 独占一行开头。 */
18
+ export function sectionLine(section: keyof typeof BODY_SECTIONS): string {
19
+ return `${BODY_SECTIONS[section]}:`;
20
+ }
21
+
22
+ const BODY_MARKER = new RegExp(`^(?:${Object.values(BODY_SECTIONS).join("|")}):$`, "u");
23
+
24
+ const VERSION = 1;
25
+
26
+ export interface MasterEventDetails {
27
+ version: typeof VERSION;
28
+ titles: string[];
29
+ }
30
+
31
+ /** 每个事件一行:首行标题句 + 正文首句预览(渲染时按宽度截断)。 */
32
+ export function masterEventDetails(contents: string[]): MasterEventDetails {
33
+ return { version: VERSION, titles: contents.map(compactLine) };
34
+ }
35
+
36
+ function compactLine(content: string): string {
37
+ const lines = content.split("\n");
38
+ const title = lines[0] ?? "";
39
+ const marker = lines.findIndex((line) => BODY_MARKER.test(line.trim()));
40
+ if (marker < 0) return title;
41
+ const preview = lines.slice(marker + 1).find((line) => line.trim());
42
+ return preview ? `${title} — ${preview.trim()}` : title;
43
+ }
44
+
45
+ export function isValidMasterEventDetails(value: unknown): value is MasterEventDetails {
46
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
47
+ const record = value as Record<string, unknown>;
48
+ if (Object.keys(record).length !== 2 || record.version !== VERSION) return false;
49
+ return Array.isArray(record.titles) &&
50
+ record.titles.length > 0 &&
51
+ record.titles.every((title) => typeof title === "string");
52
+ }