@miphamai/cli 0.85.5 → 0.85.7

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.
@@ -18,6 +18,23 @@ export interface ToolRule {
18
18
  enabled: boolean
19
19
  }
20
20
 
21
+ /**
22
+ * 一条**能真正生效**的规则必须自带 `match`/`fix` —— 而这两个是函数,落不了盘。
23
+ * 判据放在这里而不是内联,是为了让「载回来的规则必须是能用的规则」只有一处定义。
24
+ */
25
+ function isUsableRule(candidate: unknown): candidate is ToolRule {
26
+ const r = candidate as Partial<ToolRule> | null
27
+ return (
28
+ !!r &&
29
+ typeof r === 'object' &&
30
+ typeof r.id === 'string' &&
31
+ r.id.length > 0 &&
32
+ typeof r.toolName === 'string' &&
33
+ typeof r.match === 'function' &&
34
+ typeof r.fix === 'function'
35
+ )
36
+ }
37
+
21
38
  const BUILTIN_RULES: ToolRule[] = [
22
39
  {
23
40
  id: 'rule-timeout-bash-heavy',
@@ -168,9 +185,17 @@ export class ExperienceRuleEngine {
168
185
  load(): void {
169
186
  if (!existsSync(this.storePath)) return
170
187
  try {
171
- const raw = JSON.parse(readFileSync(this.storePath, 'utf-8')) as ToolRule[]
188
+ const parsed: unknown = JSON.parse(readFileSync(this.storePath, 'utf-8'))
189
+ // 形状门 —— 这一格的门比别处严,因为**这个 store 载不动一条可用的规则**:
190
+ // `ToolRule.match`/`fix` 是函数,`JSON.stringify` 必然丢,所以 `persist()` 写出去的
191
+ // 形状里不可能带回来它们。旧读法把这种条目照收,`getActiveRules()` 再把它报成
192
+ // active(`/rules` 面板跟着说它是活的),而 `intercept()` 里 `rule.match` 不存在
193
+ // → 抛 → 被那个 `try/catch` 吞成**静默惰性**。列出来是活的、实际永不触发 ——
194
+ // 正是本仓库反复收的那种账。收不进就是收不进:不校验形状 = 报一份假的活跃清单。
195
+ if (!Array.isArray(parsed)) return
172
196
  const reservedIds = new Set([...BUILTIN_RULES, ...MANAGED_RULES].map((r) => r.id))
173
- for (const rule of raw) {
197
+ for (const rule of parsed) {
198
+ if (!isUsableRule(rule)) continue
174
199
  // Reject if a builtin/managed rule with the same ID exists (source rules always win)
175
200
  if (reservedIds.has(rule.id)) continue
176
201
  this.rules.push(rule)
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * Inspired by Anthropic's RLAIF (Reinforcement Learning from AI Feedback):
5
5
  * instead of relying on human feedback loops, the AI critiques its own
6
- * tool calls before execution. A fast model (Flash / Qwen2.5-1.5B) performs
7
- * a lightweight safety & correctness check with <200ms latency.
6
+ * tool calls before execution. A fast model performs a lightweight safety &
7
+ * correctness check — but "lightweight" is about the *prompt*, not the clock:
8
+ * it is a full model round-trip, measured at a median ≈4s on the configured
9
+ * provider (see `DEFAULT_SELF_CRITIQUE_CONFIG.timeoutMs`), not the sub-200ms
10
+ * this comment used to claim.
8
11
  *
9
12
  * Architecture:
10
13
  * Model generates tool call
@@ -58,7 +61,16 @@ export const DEFAULT_SELF_CRITIQUE_CONFIG: SelfCritiqueConfig = {
58
61
  enabled: false, // Opt-in by default — user enables via /crsi critique on
59
62
  threshold: 0.6,
60
63
  targetTools: ['Bash', 'Write', 'Edit', 'Agent'],
61
- timeoutMs: 2000,
64
+ // Measured, not chosen: 30 real critiques against the configured provider
65
+ // (`findFastestModel` → `deepseek-v4-flash`; default model is the slower
66
+ // `-pro`) took min 1.85s / median 3.95s / max 17.9s, and **28 of 30 exceeded
67
+ // 2s** — the value this replaced. At 2s the budget would have skipped 93% of
68
+ // critiques, and since `critique()` fails *open* (null ⇒ the tool runs
69
+ // unreviewed) that failure is silent: `/crsi critique` would look enabled and
70
+ // do nothing. 15s keeps 90% (27/30) while still bounding a gated tool call to
71
+ // well under the provider's 90s stream-idle backstop. One model, one day —
72
+ // re-measure if the critique model changes.
73
+ timeoutMs: 15_000,
62
74
  }
63
75
 
64
76
  // ── Prompt Templates ──
@@ -23,6 +23,9 @@ export type SessionEvent =
23
23
  | { type: 'context/inject'; at: number; source: string; text: string }
24
24
  | { type: 'compaction/summary'; at: number; summary: string; replacedCount: number }
25
25
  | { type: 'compaction/rewrite'; at: number; messages: Message[] }
26
+ // 回退(`/rewind`)。投影语义与 `compaction/rewrite` 相同(整份快照替换),但**不是压缩**:
27
+ // 合用一个类型会让「这份日志里哪些轮被摘要过」的答案变成错的。
28
+ | { type: 'rewind'; at: number; messages: Message[] }
26
29
  | { type: 'checker/decision'; at: number; toolName: string; decision: CheckerDecision }
27
30
 
28
31
  export function messageToEvents(msg: Message, at = 0): SessionEvent[] {
@@ -110,6 +113,9 @@ export function deriveMessages(events: SessionEvent[]): Message[] {
110
113
  } else if (e.type === 'compaction/rewrite') {
111
114
  // 快照替换:整个投影重建(微压缩/截断等结构性编辑的字节级复现)
112
115
  out = structuredClone(e.messages)
116
+ } else if (e.type === 'rewind') {
117
+ // 回退同样是快照替换:整份投影重建为回退到的那一点
118
+ out = structuredClone(e.messages)
113
119
  }
114
120
  // 'session/start' / 'checker/decision' → 无消息(决策仅记录证据,不进投影,保字节级可逆)
115
121
  }
@@ -311,6 +317,9 @@ function isValidEvent(e: unknown): e is SessionEvent {
311
317
  case 'compaction/rewrite':
312
318
  // 快照替换:整份投影由它重建 ⇒ 元素形状与 message 事件同罪
313
319
  return Array.isArray(ev.messages) && ev.messages.every(isValidMessage)
320
+ case 'rewind':
321
+ // 同上:回退也是整份投影替换,坏元素同样会让投影崩
322
+ return Array.isArray(ev.messages) && ev.messages.every(isValidMessage)
314
323
  default:
315
324
  return true
316
325
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { spawn, type SpawnOptions } from 'node:child_process'
15
- import { closeSync, mkdirSync, openSync, readFileSync, statSync } from 'node:fs'
15
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, statSync } from 'node:fs'
16
16
  import { dirname, resolve } from 'node:path'
17
17
  import { miphamHome } from '../core/paths.ts'
18
18
 
@@ -141,6 +141,13 @@ const READY_TIMEOUT_MS = 10_000
141
141
  const POLL_INTERVAL_MS = 100
142
142
  /** How long `restart` waits for the *old* daemon to go before refusing. */
143
143
  const OLD_DAEMON_EXIT_TIMEOUT_MS = 10_000
144
+ /**
145
+ * Bound on the daemon log, enforced by `rotateLogIfLarge`. Two files at this size
146
+ * are invisible on any disk it will sit on, while the reader only ever wants a tail
147
+ * (`tailLog` takes the last 800 bytes) — so what the bound has to preserve is not
148
+ * volume but *the previous generation* of a start that keeps failing.
149
+ */
150
+ export const MAX_LOG_BYTES = 5 * 1024 * 1024
144
151
 
145
152
  async function defaultGetStatus(): Promise<DaemonStatusLike | null> {
146
153
  const { getDaemonStatus } = await import('./index')
@@ -157,6 +164,45 @@ function tailLog(logPath: string, maxBytes = 800): string {
157
164
  }
158
165
  }
159
166
 
167
+ /**
168
+ * Bound the log the daemon is about to append to.
169
+ *
170
+ * Called before the child is spawned, which is the only moment nothing holds it:
171
+ * `startDetachedDaemon` returns early when a daemon is already up, so by here its
172
+ * pid file was gone. (A daemon that died without unlinking the pid file could still
173
+ * hold the old inode — its lines then land in the renamed file, which loses nothing
174
+ * and corrupts nothing.)
175
+ *
176
+ * **Rename, not truncate.** Either bounds the file, and the reader only wants a tail
177
+ * — but the case this exists for is a start that keeps failing, and there the
178
+ * previous generation *is* the evidence. Keeping exactly one bounds the sink at two
179
+ * files; `renameSync` overwrites the target, so that is also the oldest generation's
180
+ * cleanup. There is never a `.2`.
181
+ *
182
+ * Failing to bound is never fatal: a daemon that refuses to start because its log
183
+ * could not be rotated trades a slow hazard for an immediate one. And an unreadable
184
+ * size means the append below is about to fail loudly anyway, so `return` is not a
185
+ * silent degradation of anything that was working.
186
+ */
187
+ export function rotateLogIfLarge(logPath: string, maxBytes: number = MAX_LOG_BYTES): void {
188
+ let size: number
189
+ try {
190
+ size = statSync(logPath).size
191
+ } catch {
192
+ return // no log yet (every first start)
193
+ }
194
+ if (size < maxBytes) return
195
+ try {
196
+ renameSync(logPath, `${logPath}.1`)
197
+ } catch (err) {
198
+ // Printed rather than thrown, but never swallowed: a bound that switched itself
199
+ // off in silence is the defect class this whole file is about.
200
+ process.stderr.write(
201
+ `⚠️ daemon 日志轮转失败,本次仍按无上限追加: ${err instanceof Error ? err.message : String(err)}\n`,
202
+ )
203
+ }
204
+ }
205
+
160
206
  /**
161
207
  * Start the daemon detached and *wait until it is actually up*.
162
208
  *
@@ -177,6 +223,7 @@ export async function startDetachedDaemon(
177
223
 
178
224
  const plan = planDaemonSpawn()
179
225
  mkdirSync(dirname(plan.logPath), { recursive: true, mode: 0o700 })
226
+ rotateLogIfLarge(plan.logPath)
180
227
 
181
228
  let spawnError: Error | null = null
182
229
  let exitCode: number | null = null
@@ -217,9 +264,29 @@ export async function startDetachedDaemon(
217
264
  }
218
265
  }
219
266
  }
267
+ // The deadline is a prediction, not a fact. Probe once more before acting on
268
+ // it: a child that became ready inside the last poll interval is up, and
269
+ // reporting failure for it — then killing it — would be this module's own
270
+ // "success with no daemon behind it" defect wearing the opposite sign.
271
+ const late = await getStatus()
272
+ if (late) return { ok: true, pid: late.pid, port: late.port }
273
+
274
+ // Reclaim the child. Leaving it running makes a failed start a half-truth the
275
+ // caller cannot act on: `getStatus()` is a pid file plus `kill(pid, 0)`, so the
276
+ // next `daemon start` finds the abandoned process and reports *success* with
277
+ // its pid. SIGKILL, not SIGTERM: this process is by definition not ready, so
278
+ // there is no session to drain — and a child that has already ignored the
279
+ // deadline is exactly the one that may ignore a polite signal too.
280
+ const reclaimed = child.kill('SIGKILL')
220
281
  return {
221
282
  ok: false,
222
- reason: `daemon did not become ready within ${opts.timeoutMs ?? READY_TIMEOUT_MS}ms (log: ${plan.logPath})`,
283
+ reason:
284
+ `daemon did not become ready within ${opts.timeoutMs ?? READY_TIMEOUT_MS}ms` +
285
+ // Claim the kill only when it happened: `kill()` also returns false for a
286
+ // child that exited on its own between the probe and here, and that is not
287
+ // something to report as "reclaimed".
288
+ (reclaimed ? '; the child it spawned was killed so no daemon is left running' : '') +
289
+ ` (log: ${plan.logPath})`,
223
290
  }
224
291
  }
225
292
 
@@ -42,129 +42,11 @@
42
42
  "dismiss": "Dismiss",
43
43
  "terminalName": "Mipham Code"
44
44
  },
45
- "web": {
46
- "hero": {
47
- "title": "Mipham Code",
48
- "subtitle": "Multi-model open-core intelligent coding terminal. Built for developers who demand power, speed, and flexibility.",
49
- "company": "Powered by One Mipham Corporation | 北京华安麦逸科技有限公司",
50
- "domains": "International: mipham.ai  |  中国大陆: onemipham.com",
51
- "cta_start": "Get Started",
52
- "cta_docs": "Documentation"
53
- },
54
- "features": {
55
- "title": "Why Mipham Code?",
56
- "multi_model": {
57
- "title": "Multi-Model",
58
- "desc": "Connect to Claude, GPT, DeepSeek, Qwen, Kimi, and more through a unified interface. Switch models on the fly with Ctrl+P."
59
- },
60
- "slash_commands": {
61
- "title": "85 Slash Commands",
62
- "desc": "Full Claude Code parity: /commit, /pr, /simplify, /lint, /loop init, /browse-plugins — all 85 commands with zero learning curve."
63
- },
64
- "plugins": {
65
- "title": "Plugin Marketplace",
66
- "desc": "Install plugins from npm or local paths. Discover community plugins including NotebookLM MCP integration."
67
- },
68
- "loopkit": {
69
- "title": "LoopKit Vault",
70
- "desc": "/loop init scaffolds a complete project structure: .mipham/ config, hooks, agents, 9-domain skills, .mcp.json, and more."
71
- },
72
- "mcp": {
73
- "title": "MCP Protocol",
74
- "desc": "Full Model Context Protocol support with stdio transport. Connect to NotebookLM, filesystem, GitHub, and custom MCP servers."
75
- },
76
- "secure": {
77
- "title": "Open-Core & Secure",
78
- "desc": "Apache 2.0 license. TLS 1.3, AES-256-GCM encryption, comprehensive permission system. Enterprise security out of the box."
79
- }
80
- },
81
- "models": {
82
- "title": "Supported Models",
83
- "status_active": "Active",
84
- "status_upcoming": "Upcoming"
85
- },
86
- "install_section": {
87
- "title": "Install in Seconds",
88
- "subtitle": "Four ways to install — choose what works for your platform.",
89
- "npm_recommended": "npm (recommended)",
90
- "curl_label": "curl (macOS / Linux)",
91
- "powershell_label": "PowerShell (Windows)",
92
- "label_international": "International · 国际站",
93
- "label_china": "中国大陆 · China mainland",
94
- "requirements": "Requires Bun 1.2+ or Node.js 22+. Supports macOS, Linux, Windows.",
95
- "full_guide": "Full installation guide"
96
- },
97
- "footer": {
98
- "copyright": "© {year} One Mipham Corporation. All rights reserved.",
99
- "docs": "Docs",
100
- "dashboard": "Dashboard",
101
- "github": "GitHub"
102
- },
103
- "install_page": {
104
- "title": "Installation",
105
- "current_version": "Current version:",
106
- "choose_platform": "Choose your platform and preferred install method.",
107
- "all_methods_same": "All methods install the same Mipham Code.",
108
- "prerequisites": "Prerequisites",
109
- "prereq_bun": "Bun 1.2+ (recommended) or Node.js 22+",
110
- "prereq_os": "macOS, Linux, or Windows (PowerShell)",
111
- "prereq_api_key": "At least one AI provider API key (e.g., Anthropic, OpenAI)",
112
- "npm_title": "1. npm Install (Recommended)",
113
- "npm_desc": "Works on all platforms — macOS, Linux, Windows.",
114
- "curl_title": "2. curl Install (macOS / Linux)",
115
- "powershell_title": "3. PowerShell Install (Windows)",
116
- "direct_download": "4. Direct Download",
117
- "table_platform": "Platform",
118
- "table_download": "Download",
119
- "platform_mac_arm": "macOS (Apple Silicon)",
120
- "platform_mac_intel": "macOS (Intel)",
121
- "platform_linux": "Linux (x64)",
122
- "platform_windows": "Windows (x64)",
123
- "verify_title": "Verify Installation",
124
- "start_title": "Start Mipham Code",
125
- "first_launch": "On first launch, the interactive setup wizard (/setup) will guide you through provider configuration, model selection, skills installation, and permissions.",
126
- "api_keys_title": "Set API Keys",
127
- "api_keys_desc": "Export your provider API keys as environment variables:",
128
- "international": "International · 国际站",
129
- "china_mainland": "中国大陆 · China mainland"
130
- },
131
- "docs": {
132
- "title": "Documentation",
133
- "quick_start": "Quick Start",
134
- "configuration": "Configuration",
135
- "commands": "Commands",
136
- "help": "Show help",
137
- "model": "Show current model",
138
- "switch": "Switch model",
139
- "clear": "Clear conversation",
140
- "exit": "Exit Mipham Code"
141
- },
142
- "dashboard": {
143
- "title": "Dashboard",
144
- "coming_soon": "Coming Soon",
145
- "description": "The Mipham Code dashboard will show your usage analytics — session history, token consumption, most-used models and tools, and skill activity. We’re building this now.",
146
- "sessions": "Sessions",
147
- "sessions_desc": "Session history & stats",
148
- "tokens": "Tokens",
149
- "tokens_desc": "Usage across providers",
150
- "skills": "Skills",
151
- "skills_desc": "Active skill analytics"
152
- },
153
- "not_found": {
154
- "code": "404",
155
- "title": "Page not found",
156
- "description": "The page you’re looking for doesn’t exist.",
157
- "back": "Back to Mipham Code"
158
- },
159
- "error": {
160
- "title": "Something went wrong",
161
- "fallback_message": "An unexpected error occurred.",
162
- "try_again": "Try again"
163
- }
164
- },
165
45
  "greeting": "Hello {name}",
166
46
  "commands": {
167
- "clear": { "confirmed": "✓ Conversation cleared. Context reset." },
47
+ "clear": {
48
+ "confirmed": "✓ Conversation cleared. Context reset."
49
+ },
168
50
  "compact": {
169
51
  "confirmed": "✓ Context compacted.",
170
52
  "tokens": "Tokens: {before} → {after} (saved {saved}%)"
@@ -304,7 +186,6 @@
304
186
  },
305
187
  "task_list": {
306
188
  "title": "── Background Tasks ──",
307
- "detected": "{count} task operations detected in this session.\n\nUse the Task tool (action \"create\" / \"update\" / \"list\") to manage structured task tracking.",
308
189
  "no_tasks": "No tasks tracked yet. Use the Task tool with action \"create\", \"update\", or \"list\" to manage structured tasks.",
309
190
  "reference": "Quick reference:",
310
191
  "legacy_hint": "Type /todos for the legacy task interface."
@@ -333,7 +214,9 @@
333
214
  "not_found_content": "No session named \"{name}\".\n\nUse /resume without arguments to list all saved sessions.",
334
215
  "found_content": "Name: {name}\nMessages: {messages}\nProvider: {provider} / {model}\nUpdated: {updated}\n\nTo continue this session, start a new process with:\n mipham --resume \"{target}\""
335
216
  },
336
- "help": { "title": "── Mipham Code Help ──" },
217
+ "help": {
218
+ "title": "── Mipham Code Help ──"
219
+ },
337
220
  "lang": {
338
221
  "current": "Language: {locale}",
339
222
  "set": "Language set to {locale}. Restart Mipham Code to apply."
@@ -433,6 +316,8 @@
433
316
  "update_failed": "✗ Update failed: {reason}{rolledBack}\n Try manually: {command}\n Your config backup is at: {path}",
434
317
  "rolled_back": "\n Your previous install was restored — mipham still works.",
435
318
  "no_rollback": "\n ⚠ Your previous install could not be restored — reinstall it manually (see below).",
319
+ "install_untouched": "\n Your previous install was not touched — mipham still works.",
320
+ "install_unknown": "\n ⚠ Could not locate the global install path — unable to tell whether your install survived.",
436
321
  "unverified": "⚠ Installed, but could not verify the new version: {reason}"
437
322
  },
438
323
  "no_plan": {
@@ -849,9 +734,15 @@
849
734
  "finished": "finished",
850
735
  "status_label": "Agents"
851
736
  },
852
- "system": { "role_label": "System" },
853
- "assistant": { "role_label": "Mipham Code" },
854
- "user": { "role_label": "User" },
737
+ "system": {
738
+ "role_label": "System"
739
+ },
740
+ "assistant": {
741
+ "role_label": "Mipham Code"
742
+ },
743
+ "user": {
744
+ "role_label": "User"
745
+ },
855
746
  "input": {
856
747
  "placeholder": "Type a message (Esc to clear)..."
857
748
  },
@@ -976,22 +867,70 @@
976
867
  }
977
868
  },
978
869
  "tools": {
979
- "bash": { "name": "Bash", "description": "Execute shell commands" },
980
- "read": { "name": "Read", "description": "Read a file from the filesystem" },
981
- "write": { "name": "Write", "description": "Write content to a file" },
982
- "edit": { "name": "Edit", "description": "Edit a file with string replacement" },
983
- "glob": { "name": "Glob", "description": "Find files matching a pattern" },
984
- "grep": { "name": "Grep", "description": "Search file contents with regex" },
985
- "web_fetch": { "name": "WebFetch", "description": "Fetch a URL and parse content" },
986
- "web_search": { "name": "WebSearch", "description": "Search the web" },
987
- "agent": { "name": "Agent", "description": "Launch a sub-agent for complex tasks" },
988
- "task": { "name": "Task", "description": "Create and track tasks" },
989
- "skill": { "name": "Skill", "description": "Invoke a named skill" },
990
- "memory": { "name": "Memory", "description": "Read/write persistent memory" },
991
- "workflow": { "name": "Workflow", "description": "Execute a multi-agent workflow" },
992
- "plan": { "name": "Plan", "description": "Enter plan mode for structured thinking" },
993
- "config": { "name": "Config", "description": "View and modify configuration" },
994
- "mcp": { "name": "MCP", "description": "Manage MCP server connections" }
870
+ "bash": {
871
+ "name": "Bash",
872
+ "description": "Execute shell commands"
873
+ },
874
+ "read": {
875
+ "name": "Read",
876
+ "description": "Read a file from the filesystem"
877
+ },
878
+ "write": {
879
+ "name": "Write",
880
+ "description": "Write content to a file"
881
+ },
882
+ "edit": {
883
+ "name": "Edit",
884
+ "description": "Edit a file with string replacement"
885
+ },
886
+ "glob": {
887
+ "name": "Glob",
888
+ "description": "Find files matching a pattern"
889
+ },
890
+ "grep": {
891
+ "name": "Grep",
892
+ "description": "Search file contents with regex"
893
+ },
894
+ "web_fetch": {
895
+ "name": "WebFetch",
896
+ "description": "Fetch a URL and parse content"
897
+ },
898
+ "web_search": {
899
+ "name": "WebSearch",
900
+ "description": "Search the web"
901
+ },
902
+ "agent": {
903
+ "name": "Agent",
904
+ "description": "Launch a sub-agent for complex tasks"
905
+ },
906
+ "task": {
907
+ "name": "Task",
908
+ "description": "Create and track tasks"
909
+ },
910
+ "skill": {
911
+ "name": "Skill",
912
+ "description": "Invoke a named skill"
913
+ },
914
+ "memory": {
915
+ "name": "Memory",
916
+ "description": "Read/write persistent memory"
917
+ },
918
+ "workflow": {
919
+ "name": "Workflow",
920
+ "description": "Execute a multi-agent workflow"
921
+ },
922
+ "plan": {
923
+ "name": "Plan",
924
+ "description": "Enter plan mode for structured thinking"
925
+ },
926
+ "config": {
927
+ "name": "Config",
928
+ "description": "View and modify configuration"
929
+ },
930
+ "mcp": {
931
+ "name": "MCP",
932
+ "description": "Manage MCP server connections"
933
+ }
995
934
  },
996
935
  "errors": {
997
936
  "tool_denied_deny_rule": "Tool \"{name}\" blocked by a deny rule (\"{pattern}\"). Deny rules override permission mode — try a different approach, or drop the rule with: /permissions remove \"{pattern}\"",