@foolsecret/pi-prompt 0.4.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.
package/src/config.ts ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * 配置管理器:~/.pi/pi-prompt.json 的读写 + 三轴档位解析。
3
+ *
4
+ * 配置存 `~/.pi/` 根目录(与 pi-usager.json 同层);写入前 mkdirSync 防御
5
+ * (对齐 pi-usager 踩坑 7)。
6
+ *
7
+ * 档位解析优先级(会话显式设档 > 自动解析):
8
+ * show 自动 = perProvider[provider].show → env PI_PROMPT_SHOW(别名 PI_PROMPT_MODE)
9
+ * → defaultShow → 'full'
10
+ * write 自动 = perProvider[provider].write → env PI_PROMPT_WRITE → defaultWrite → 'normal'
11
+ * do 自动 = perProvider[provider].do → env PI_PROMPT_DO → defaultDo → 'normal'
12
+ *
13
+ * v0.2 迁移:旧 v0.1 配置 defaultMode / perProvider 字符串 会被读成 show 轴值。
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { dirname, join } from "node:path";
19
+ import {
20
+ normalizeDoMode,
21
+ normalizeShowMode,
22
+ normalizeWriteMode,
23
+ DO_DEFAULT,
24
+ SHOW_DEFAULT,
25
+ WRITE_DEFAULT,
26
+ type DoMode,
27
+ type ShowMode,
28
+ type WriteMode,
29
+ } from "./modes.ts";
30
+
31
+ /** 配置文件路径(~/.pi/pi-prompt.json) */
32
+ export const CONFIG_PATH: string = join(homedir(), ".pi", "pi-prompt.json");
33
+
34
+ /** 单厂商的三轴覆盖 */
35
+ export interface ProviderAxes {
36
+ show?: string;
37
+ write?: string;
38
+ do?: string;
39
+ }
40
+
41
+ /** 配置文件结构(defaultShow 可含 auto;perProvider 兼容旧字符串写法) */
42
+ export interface PromptConfigFile {
43
+ defaultShow?: string;
44
+ defaultWrite?: string;
45
+ defaultDo?: string;
46
+ quietStartup?: boolean;
47
+ hideStatus?: boolean;
48
+ /** DeepSeek 高峰升档(默认开;false 关闭) */
49
+ peakUpgrade?: boolean;
50
+ /** max_tokens 上限(正整数;缺省/非正数 = 不开) */
51
+ maxTokensCap?: number;
52
+ /** auto 低频自动抽样(默认关;true 开启) */
53
+ autoSample?: boolean;
54
+ /** 抽样间隔(多少轮后抽一次;默认 50) */
55
+ autoSampleInterval?: number;
56
+ /** 当日抽样上限(默认 3) */
57
+ autoSampleMaxPerDay?: number;
58
+ perProvider?: Record<string, string | ProviderAxes>;
59
+ }
60
+
61
+ /** env 布尔开关解析:值存在且非空/0/false/no 视为真;未设置返回 undefined */
62
+ function envTruthy(env: NodeJS.ProcessEnv | undefined, name: string): boolean | undefined {
63
+ const value = env?.[name];
64
+ if (value === undefined) return undefined;
65
+ const v = value.trim().toLowerCase();
66
+ return !(v === "" || v === "0" || v === "false" || v === "no");
67
+ }
68
+
69
+ /** 三轴默认值合计(status/重置用) */
70
+ export interface AxesDefault {
71
+ show: ShowMode;
72
+ write: WriteMode;
73
+ do: DoMode;
74
+ }
75
+
76
+ /** 配置管理器类:惰性读档(含 v0.1 迁移)、三轴解析、持久化 */
77
+ export class PromptConfigManager {
78
+ /** 当前配置(惰性加载后的缓存) */
79
+ private config: PromptConfigFile;
80
+ /** 是否已从磁盘读档 */
81
+ private loaded: boolean;
82
+
83
+ constructor(
84
+ private readonly env: NodeJS.ProcessEnv | undefined = process.env,
85
+ private readonly configPath: string = CONFIG_PATH,
86
+ config?: PromptConfigFile,
87
+ ) {
88
+ // 注入测试用途的初始配置;未注入则首次 get 时惰性读档
89
+ this.config = config ?? {};
90
+ this.loaded = config !== undefined;
91
+ }
92
+
93
+ /** 惰性读档(strip BOM;损坏回退空配置;v0.1 defaultMode → defaultShow) */
94
+ private load(): PromptConfigFile {
95
+ if (this.loaded) return this.config;
96
+ this.loaded = true;
97
+ try {
98
+ const raw = readFileSync(this.configPath, "utf8").replace(/^\uFEFF/, "");
99
+ const parsed = JSON.parse(raw) as (Partial<PromptConfigFile> & { defaultMode?: unknown }) | null;
100
+ this.config = {
101
+ defaultShow:
102
+ typeof parsed?.defaultShow === "string"
103
+ ? parsed.defaultShow
104
+ : typeof parsed?.defaultMode === "string"
105
+ ? parsed.defaultMode
106
+ : undefined,
107
+ defaultWrite: typeof parsed?.defaultWrite === "string" ? parsed.defaultWrite : undefined,
108
+ defaultDo: typeof parsed?.defaultDo === "string" ? parsed.defaultDo : undefined,
109
+ quietStartup: parsed?.quietStartup === true,
110
+ hideStatus: parsed?.hideStatus === true,
111
+ perProvider: parsed?.perProvider ?? {},
112
+ };
113
+ } catch {
114
+ this.config = {};
115
+ }
116
+ return this.config;
117
+ }
118
+
119
+ /** show 轴默认档(含 auto;env → defaultShow → 内置) */
120
+ getShowDefault(): ShowMode {
121
+ const envValue = this.env?.PI_PROMPT_SHOW ?? this.env?.PI_PROMPT_MODE;
122
+ const envMode = normalizeShowMode(envValue);
123
+ if (envMode !== null) return envMode;
124
+ const configMode = normalizeShowMode(this.load().defaultShow);
125
+ if (configMode !== null) return configMode;
126
+ return SHOW_DEFAULT;
127
+ }
128
+
129
+ /** write 轴默认档(env → defaultWrite → 内置 normal) */
130
+ getWriteDefault(): WriteMode {
131
+ return normalizeWriteMode(this.env?.PI_PROMPT_WRITE) ?? normalizeWriteMode(this.load().defaultWrite) ?? WRITE_DEFAULT;
132
+ }
133
+
134
+ /** do 轴默认档(env → defaultDo → 内置 normal) */
135
+ getDoDefault(): DoMode {
136
+ return normalizeDoMode(this.env?.PI_PROMPT_DO) ?? normalizeDoMode(this.load().defaultDo) ?? DO_DEFAULT;
137
+ }
138
+
139
+ /** 三轴默认合计(status / /prompt 无参恢复用) */
140
+ getDefaults(): AxesDefault {
141
+ return { show: this.getShowDefault(), write: this.getWriteDefault(), do: this.getDoDefault() };
142
+ }
143
+
144
+ /** 解析某厂商 show 轴:perProvider.show → getShowDefault() */
145
+ showForProvider(provider: string | undefined): ShowMode {
146
+ if (provider !== undefined) {
147
+ const overridden = this.providerAxes(provider).show;
148
+ const normalized = normalizeShowMode(overridden);
149
+ if (normalized !== null) return normalized;
150
+ }
151
+ return this.getShowDefault();
152
+ }
153
+
154
+ /** 解析某厂商 write 轴 */
155
+ writeForProvider(provider: string | undefined): WriteMode {
156
+ if (provider !== undefined) {
157
+ const overridden = this.providerAxes(provider).write;
158
+ const normalized = normalizeWriteMode(overridden);
159
+ if (normalized !== null) return normalized;
160
+ }
161
+ return this.getWriteDefault();
162
+ }
163
+
164
+ /** 解析某厂商 do 轴 */
165
+ doForProvider(provider: string | undefined): DoMode {
166
+ if (provider !== undefined) {
167
+ const overridden = this.providerAxes(provider).do;
168
+ const normalized = normalizeDoMode(overridden);
169
+ if (normalized !== null) return normalized;
170
+ }
171
+ return this.getDoDefault();
172
+ }
173
+
174
+ /** 读取厂商覆盖并归一化为三轴对象(兼容旧字符串写法 → show) */
175
+ private providerAxes(provider: string): ProviderAxes {
176
+ const raw = this.load().perProvider?.[provider];
177
+ if (typeof raw === "string") return { show: raw };
178
+ return raw ?? {};
179
+ }
180
+
181
+ /** 懒查询厂商覆盖映射(已归一化,status 展示用) */
182
+ perProviderConfig(): Record<string, ProviderAxes> {
183
+ const result: Record<string, ProviderAxes> = {};
184
+ const config = this.load();
185
+ for (const [provider, raw] of Object.entries(config.perProvider ?? {})) {
186
+ result[provider] = typeof raw === "string" ? { show: raw } : raw;
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /** 启动是否静默(env 优先,其次配置文件) */
192
+ isQuietStartup(): boolean {
193
+ return envTruthy(this.env, "PI_PROMPT_QUIET_STARTUP") ?? this.load().quietStartup === true;
194
+ }
195
+
196
+ /** 状态栏是否隐藏(env 优先,其次配置文件) */
197
+ isHideStatus(): boolean {
198
+ return envTruthy(this.env, "PI_PROMPT_HIDE_STATUS") ?? this.load().hideStatus === true;
199
+ }
200
+
201
+ /**
202
+ * DeepSeek 高峰升档是否开启:默认开(纯成本优化);env PI_PROMPT_PEAK=
203
+ * 可显式关闭(0/false/no),其次配置文件 peakUpgrade:false。
204
+ */
205
+ isPeakUpgrade(): boolean {
206
+ const env = envTruthy(this.env, "PI_PROMPT_PEAK");
207
+ if (env !== undefined) return env;
208
+ return this.load().peakUpgrade !== false;
209
+ }
210
+
211
+ /** max_tokens 上限:env PI_PROMPT_MAX_TOKENS 优先,其次配置文件 maxTokensCap;非正整数/缺省 → null 不开 */
212
+ maxTokensCap(): number | null {
213
+ const envRaw = this.env?.PI_PROMPT_MAX_TOKENS?.trim();
214
+ const envCap = envRaw !== undefined && envRaw !== "" && /^\d+$/.test(envRaw) ? Number(envRaw) : null;
215
+ const fileRaw = this.load().maxTokensCap;
216
+ const fileCap = typeof fileRaw === "number" ? fileRaw : null;
217
+ const cap = envCap ?? fileCap;
218
+ return cap !== null && cap > 0 ? cap : null;
219
+ }
220
+
221
+ /** auto 低频抽样是否开启(默认关;env PI_PROMPT_AUTO_SAMPLE=true/false 或配置 autoSample) */
222
+ isAutoSample(): boolean {
223
+ const env = envTruthy(this.env, "PI_PROMPT_AUTO_SAMPLE");
224
+ if (env !== undefined) return env;
225
+ return this.load().autoSample === true;
226
+ }
227
+
228
+ /** 抽样间隔(多少轮后触发一次;配置 autoSampleInterval,兜底 50) */
229
+ autoSampleInterval(): number {
230
+ const v = this.load().autoSampleInterval;
231
+ return typeof v === "number" && v >= 2 ? v : 50;
232
+ }
233
+
234
+ /** 当日抽样上限(配置 autoSampleMaxPerDay,兜底 3) */
235
+ autoSampleMaxPerDay(): number {
236
+ const v = this.load().autoSampleMaxPerDay;
237
+ return typeof v === "number" && v >= 1 ? v : 3;
238
+ }
239
+
240
+ /** 持久化某轴默认档(按轴校验;非法拒绝返回 false) */
241
+ writeDefaultAxis(axis: "show" | "write" | "do", value: string): boolean {
242
+ switch (axis) {
243
+ case "show": {
244
+ if (normalizeShowMode(value) === null) return false;
245
+ return this.persist({ defaultShow: value.toLowerCase() });
246
+ }
247
+ case "write": {
248
+ if (normalizeWriteMode(value) === null) return false;
249
+ return this.persist({ defaultWrite: value.toLowerCase() });
250
+ }
251
+ case "do": {
252
+ if (normalizeDoMode(value) === null) return false;
253
+ return this.persist({ defaultDo: value.toLowerCase() });
254
+ }
255
+ default:
256
+ // 运行时防御:类型约束外仍可能传入任意字符串
257
+ return false;
258
+ }
259
+ }
260
+
261
+ /** 写盘(mkdirSync 防御目录缺失):合并当前磁盘内容(防多写互相覆盖);失败返回 false */
262
+ private persist(patch: Partial<PromptConfigFile>): boolean {
263
+ try {
264
+ mkdirSync(dirname(this.configPath), { recursive: true });
265
+ let merged: PromptConfigFile = {};
266
+ try {
267
+ const raw = readFileSync(this.configPath, "utf8").replace(/^\uFEFF/, "");
268
+ const parsed = JSON.parse(raw) as Partial<PromptConfigFile> | null;
269
+ if (parsed && typeof parsed === "object") merged = parsed as PromptConfigFile;
270
+ } catch {
271
+ // 文件不存在或损坏:以空白为底继续合并
272
+ }
273
+ this.config = { ...merged, ...patch };
274
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), "utf8");
275
+ return true;
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ }
package/src/format.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 展示格式化工具:把 token 计数转成易读的紧凑文本。
3
+ */
4
+
5
+ /** 千分位/百万缩写(1_234_567 → "1.2M"),<1k 原样输出 */
6
+ export function formatTokens(n: number): string {
7
+ const abs = Math.abs(n);
8
+ if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
9
+ if (abs >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
10
+ return String(n);
11
+ }
package/src/modes.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 三轴档位定义与规范化。
3
+ *
4
+ * pi-prompt v0.2 由三根正交轴组成(每轴独立注入、独立配置/会话覆盖):
5
+ * - show(输出风格):normal / lite / full / ultra / auto
6
+ * normal = 不注入(同 value full);auto 是元档位:运行时由选档器
7
+ * (auto.ts)按成本函数解析为四档之一,可作默认配置,但不是直接注入的档
8
+ * (四档写死文案才保证字节稳定)。
9
+ * - write(代码写量):minimal / normal / complete
10
+ * - do(行为力度):direct / normal / deep
11
+ * review 不再是一个档位,改为独立审计技能 /prompt-review(会话级)。
12
+ *
13
+ * v0.2.2:show 轴旧档名 "off" 更名为 "normal"(与 write/do 的"不注入基线"统一)。
14
+ * 历史配置/env/台账里的 "off" 由下述 normalize 别名层归一为 normal,注入字节
15
+ * 零变化(off/normal 本就不注入),不会重置前缀 KV 缓存。
16
+ */
17
+
18
+ /** show 轴全部取值(auto 为元档位) */
19
+ export type ShowMode = "auto" | "normal" | "lite" | "full" | "ultra";
20
+
21
+ /** show 轴可直接注入的运行时档位(auto 已解析后的结果) */
22
+ export type RuntimeShowMode = Exclude<ShowMode, "auto">;
23
+
24
+ /** show 轴全部取值数组(校验/补全用) */
25
+ export const SHOW_MODES: readonly ShowMode[] = ["auto", "normal", "lite", "full", "ultra"];
26
+
27
+ /** show 轴运行时档位数组(选档器输出域) */
28
+ export const RUNTIME_SHOW_MODES: readonly RuntimeShowMode[] = ["normal", "lite", "full", "ultra"];
29
+
30
+ /** show 轴内置默认档位(v0.2 起默认 auto:运行时按成本函数选档) */
31
+ export const SHOW_DEFAULT: ShowMode = "auto";
32
+
33
+ /** write 轴全部取值 */
34
+ export type WriteMode = "minimal" | "normal" | "complete";
35
+
36
+ /** write 轴默认档位(normal = 不注入) */
37
+ export const WRITE_DEFAULT: WriteMode = "normal";
38
+
39
+ /** write 轴全部取值数组 */
40
+ export const WRITE_MODES: readonly WriteMode[] = ["minimal", "normal", "complete"];
41
+
42
+ /** do 轴全部取值 */
43
+ export type DoMode = "direct" | "normal" | "deep";
44
+
45
+ /** do 轴默认档位(normal = 不注入) */
46
+ export const DO_DEFAULT: DoMode = "normal";
47
+
48
+ /** do 轴全部取值数组 */
49
+ export const DO_MODES: readonly DoMode[] = ["direct", "normal", "deep"];
50
+
51
+ /** show 轴档位 → 展示图标(保留供 /prompt status 文案用;footer 已改纯文字) */
52
+ export const SHOW_ICONS: Record<ShowMode, string> = {
53
+ auto: "🔄",
54
+ normal: "",
55
+ lite: "🌿",
56
+ full: "⚡",
57
+ ultra: "🔥",
58
+ };
59
+
60
+ /**
61
+ * 严格归一化 show 轴取值(小写、去首尾空白);非法返回 null。
62
+ * 接受 auto(元档位),调用方决定后续解析。
63
+ * v0.2.2 兼容别名:历史取值 "off" 归一为 "normal"。
64
+ */
65
+ export function normalizeShowMode(value: unknown): ShowMode | null {
66
+ if (typeof value !== "string") return null;
67
+ const normalized = value.trim().toLowerCase();
68
+ if (normalized === "off") return "normal";
69
+ return (SHOW_MODES as readonly string[]).includes(normalized) ? (normalized as ShowMode) : null;
70
+ }
71
+
72
+ /** 严格归一化为 show 轴运行时档位(拒绝 auto);非法返回 null;兼容别名 off→normal */
73
+ export function normalizeRuntimeShow(value: unknown): RuntimeShowMode | null {
74
+ if (typeof value !== "string") return null;
75
+ const normalized = value.trim().toLowerCase();
76
+ if (normalized === "off") return "normal";
77
+ return (RUNTIME_SHOW_MODES as readonly string[]).includes(normalized)
78
+ ? (normalized as RuntimeShowMode)
79
+ : null;
80
+ }
81
+
82
+ /** 严格归一化 write 轴取值;非法返回 null */
83
+ export function normalizeWriteMode(value: unknown): WriteMode | null {
84
+ if (typeof value !== "string") return null;
85
+ const normalized = value.trim().toLowerCase();
86
+ return (WRITE_MODES as readonly string[]).includes(normalized) ? (normalized as WriteMode) : null;
87
+ }
88
+
89
+ /** 严格归一化 do 轴取值;非法返回 null */
90
+ export function normalizeDoMode(value: unknown): DoMode | null {
91
+ if (typeof value !== "string") return null;
92
+ const normalized = value.trim().toLowerCase();
93
+ return (DO_MODES as readonly string[]).includes(normalized) ? (normalized as DoMode) : null;
94
+ }
95
+
96
+ /**
97
+ * 全词关停指令匹配:"stop pi-prompt" / "normal mode"。
98
+ * 必须整条消息匹配(去尾标点、忽略大小写),防止任务中句子含词就误关
99
+ * (对齐 ponytail 踩坑:普通请求 "add a normal mode toggle" 也会被误伤)。
100
+ * 语义 = 全局回到三轴默认(show/默认档、write normal、do normal)。
101
+ */
102
+ export function isDeactivationCommand(text: unknown): boolean {
103
+ const t = String(text ?? "")
104
+ .trim()
105
+ .toLowerCase()
106
+ .replace(/[.!?。!?\s]+$/, "");
107
+ return t === "stop pi-prompt" || t === "normal mode";
108
+ }
109
+
110
+ /** 三轴默认值汇总(重置到默认用) */
111
+ export function defaultAxes(): { show: ShowMode; write: WriteMode; do: DoMode } {
112
+ return { show: SHOW_DEFAULT, write: WRITE_DEFAULT, do: DO_DEFAULT };
113
+ }