@miphamai/cli 0.85.1 → 0.85.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.
Files changed (63) hide show
  1. package/bin/mipham.ts +42 -2
  2. package/package.json +1 -1
  3. package/skills/standard/mipham-code-setup.SKILL.md +9 -2
  4. package/src/agent/agent-experience.ts +3 -2
  5. package/src/agent/background-registry.ts +7 -3
  6. package/src/agent/cross-session/discovery.ts +5 -12
  7. package/src/agent/cross-session/file-inbox.ts +2 -2
  8. package/src/agent/effectiveness-tracker.ts +3 -2
  9. package/src/agent/sub-agent.ts +67 -6
  10. package/src/agent/types.ts +10 -0
  11. package/src/agent-view/agent-view-manager.ts +46 -0
  12. package/src/agent-view/dashboard.tsx +106 -16
  13. package/src/agent-view/session-view.tsx +128 -0
  14. package/src/commands/autoloop-journal.ts +6 -5
  15. package/src/commands/environment.ts +11 -8
  16. package/src/commands/project.ts +5 -3
  17. package/src/config/credential-crypto.ts +13 -1
  18. package/src/config/keys-manager.ts +7 -4
  19. package/src/config/loader.ts +144 -41
  20. package/src/config/preferences.ts +6 -3
  21. package/src/core/constitution-loader.ts +3 -2
  22. package/src/core/context.ts +35 -6
  23. package/src/core/crsi-producer.ts +4 -2
  24. package/src/core/crsi-sandbox.ts +2 -1
  25. package/src/core/dream-engine.ts +5 -11
  26. package/src/core/engine.ts +9 -4
  27. package/src/core/error-signature-db.ts +3 -2
  28. package/src/core/eval-harness.ts +3 -3
  29. package/src/core/hooks-executor.ts +60 -2
  30. package/src/core/instructions.ts +69 -48
  31. package/src/core/memory/memory-manager.ts +10 -6
  32. package/src/core/permission-audit.ts +13 -6
  33. package/src/core/permission-config.ts +28 -7
  34. package/src/core/permission-rules.ts +99 -5
  35. package/src/core/permission.ts +6 -1
  36. package/src/core/rule-engine.ts +3 -2
  37. package/src/core/session-log.ts +48 -11
  38. package/src/core/session-store.ts +64 -44
  39. package/src/daemon/attach-protocol.ts +30 -3
  40. package/src/daemon/auth.ts +4 -3
  41. package/src/daemon/index.ts +4 -3
  42. package/src/daemon/remote-engine.ts +173 -24
  43. package/src/daemon/server.ts +51 -1
  44. package/src/daemon/session-worker.ts +34 -1
  45. package/src/i18n-core/locales/en-US.json +1 -0
  46. package/src/i18n-core/locales/zh-CN.json +1 -0
  47. package/src/index.tsx +75 -17
  48. package/src/mcp/oauth.ts +47 -5
  49. package/src/mcp/token-store.ts +10 -11
  50. package/src/providers/openai-compat.ts +11 -0
  51. package/src/shared/arg-validation.ts +74 -2
  52. package/src/shared/package-info.ts +1 -1
  53. package/src/shared/regular-file.ts +63 -0
  54. package/src/shared/sanitize.ts +14 -2
  55. package/src/skills/bundled-skills.ts +1 -1
  56. package/src/tools/agent/memory.ts +4 -2
  57. package/src/tools/agent/workflow.ts +6 -3
  58. package/src/tools/exec/task.ts +82 -30
  59. package/src/tools/scheduling/cron.ts +3 -9
  60. package/src/ui/app.tsx +78 -14
  61. package/src/ui/commands.ts +73 -31
  62. package/src/ui/ctrl-c-confirm.ts +63 -0
  63. package/src/workflow/journal.ts +80 -26
@@ -1,12 +1,7 @@
1
- import {
2
- mkdirSync,
3
- writeFileSync,
4
- readFileSync,
5
- existsSync,
6
- appendFileSync,
7
- readdirSync,
8
- } from 'node:fs'
1
+ import { mkdirSync, existsSync, readdirSync } from 'node:fs'
9
2
  import { join } from 'node:path'
3
+ import { atomicWriteFileSync } from '../shared/atomic-write'
4
+ import { appendRegularFileSync, readRegularFileSync } from '../shared/regular-file'
10
5
  import { miphamHome } from '../core/paths.ts'
11
6
 
12
7
  const WORKFLOW_DIR = miphamHome('workflows')
@@ -33,15 +28,23 @@ export function createJournal(runId: string, script: string): string {
33
28
  const dir = join(WORKFLOW_DIR, runId)
34
29
  mkdirSync(dir, { recursive: true })
35
30
 
36
- writeFileSync(join(dir, 'script.js'), script, 'utf-8')
37
- writeFileSync(join(dir, 'journal.jsonl'), '', 'utf-8')
38
- writeFileSync(join(dir, 'state.json'), JSON.stringify({ seq: 0, phases: [] }), 'utf-8')
31
+ // 三份都走助手:新建 run 目录里本来没有旧内容可丢,但**路径上可能被放了个 FIFO**
32
+ // (`writeFileSync` 会打开它写 ⇒ 干等到有读者为止)。`atomicWriteFileSync` 的 rename
33
+ // 与 `appendRegularFileSync` 的类型闸都不打开目标,于是在这里也一并关掉。
34
+ atomicWriteFileSync(join(dir, 'script.js'), script, { mode: 0o644 })
35
+ if (!appendRegularFileSync(join(dir, 'journal.jsonl'), '')) {
36
+ throw new Error(`workflow journal for run "${runId}" is not appendable`)
37
+ }
38
+ atomicWriteFileSync(join(dir, 'state.json'), JSON.stringify({ seq: 0, phases: [] }))
39
39
 
40
40
  return dir
41
41
  }
42
42
 
43
43
  /**
44
44
  * Append an agent call to the journal. Returns the new sequence number.
45
+ *
46
+ * 先 append journal(权威记录)、后写 state(它的影子)—— 顺序有意义:中途被打断时
47
+ * 唯一的后果是影子落后,而不是记录丢失。
45
48
  */
46
49
  export function appendJournal(runId: string, entry: Omit<JournalEntry, 'seq'>): number {
47
50
  const dir = join(WORKFLOW_DIR, runId)
@@ -50,32 +53,84 @@ export function appendJournal(runId: string, entry: Omit<JournalEntry, 'seq'>):
50
53
  const seq = state.seq + 1
51
54
  const fullEntry: JournalEntry = { seq, ...entry }
52
55
 
53
- appendFileSync(join(dir, 'journal.jsonl'), JSON.stringify(fullEntry) + '\n', 'utf-8')
56
+ // 权威记录写不进去 = 这个 run 没法记账(含路径上是个 FIFO):**不做影子**、当场说清,
57
+ // 好过把 state 推进到一个 journal 里不存在的 seq 上。
58
+ if (!appendRegularFileSync(join(dir, 'journal.jsonl'), JSON.stringify(fullEntry) + '\n')) {
59
+ throw new Error(`workflow journal for run "${runId}" is not appendable`)
60
+ }
54
61
 
55
62
  state.seq = seq
56
- writeFileSync(join(dir, 'state.json'), JSON.stringify(state), 'utf-8')
63
+ atomicWriteFileSync(join(dir, 'state.json'), JSON.stringify(state))
57
64
 
58
65
  return seq
59
66
  }
60
67
 
61
68
  /**
62
69
  * Load all journal entries for a run. Returns empty array if run not found.
70
+ *
71
+ * **逐行**解析:`appendFileSync` 被打断会在尾部留下半截行,而从前一个半截行让整份
72
+ * 读取抛 `SyntaxError`(`/workflow` 那条链上没有兜它)。坏行丢掉、其余全留 —— 一份
73
+ * 日志里少一行,好过整份读不出来。
63
74
  */
64
75
  export function loadJournal(runId: string): JournalEntry[] {
65
- const journalPath = join(WORKFLOW_DIR, runId, 'journal.jsonl')
66
- if (!existsSync(journalPath)) return []
67
-
68
- const raw = readFileSync(journalPath, 'utf-8')
69
- return raw
70
- .split('\n')
71
- .filter((line) => line.trim())
72
- .map((line) => JSON.parse(line) as JournalEntry)
76
+ const raw = readRegularFileSync(join(WORKFLOW_DIR, runId, 'journal.jsonl'))
77
+ if (raw === null) return []
78
+
79
+ const entries: JournalEntry[] = []
80
+ for (const line of raw.split('\n')) {
81
+ if (!line.trim()) continue
82
+ try {
83
+ const parsed: unknown = JSON.parse(line)
84
+ if (isJournalEntry(parsed)) entries.push(parsed)
85
+ } catch {
86
+ // 半截行(append 被打断)/坏行 —— 只丢这一行
87
+ }
88
+ }
89
+ return entries
73
90
  }
74
91
 
92
+ function isJournalEntry(e: unknown): e is JournalEntry {
93
+ if (!e || typeof e !== 'object' || Array.isArray(e)) return false
94
+ const ev = e as Record<string, unknown>
95
+ return (
96
+ typeof ev.seq === 'number' && (ev.type === 'agent' || ev.type === 'phase' || ev.type === 'log')
97
+ )
98
+ }
99
+
100
+ /**
101
+ * 读 state.json。
102
+ *
103
+ * 从前这里是**裸**的 `JSON.parse(readFileSync(...))`:state.json 是非原子写的,半截写
104
+ * 会让 `appendJournal` 当场抛 `SyntaxError`,整个 run 停摆 —— 而 state 只是**派生**信息。
105
+ * 所以坏掉/缺字段时把 `seq` 从 journal 尾行**重新推出来**,而不是归零:归零会让后续
106
+ * append 复用已经在用的序号,而那正是这份文件存在的意义。
107
+ */
75
108
  function readState(runId: string): JournalState {
76
- const statePath = join(WORKFLOW_DIR, runId, 'state.json')
77
- if (!existsSync(statePath)) return { seq: 0, phases: [] }
78
- return JSON.parse(readFileSync(statePath, 'utf-8'))
109
+ const raw = readRegularFileSync(join(WORKFLOW_DIR, runId, 'state.json'))
110
+ if (raw !== null) {
111
+ try {
112
+ const parsed: unknown = JSON.parse(raw)
113
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
114
+ const { seq, phases } = parsed as { seq?: unknown; phases?: unknown }
115
+ if (typeof seq === 'number' && Number.isFinite(seq)) {
116
+ return {
117
+ seq,
118
+ phases: Array.isArray(phases)
119
+ ? phases.filter((p): p is string => typeof p === 'string')
120
+ : [],
121
+ }
122
+ }
123
+ }
124
+ } catch {
125
+ // 半截写/坏形状 —— 落到下面按 journal 推。
126
+ }
127
+ }
128
+ return { seq: lastJournalSeq(runId), phases: [] }
129
+ }
130
+
131
+ /** journal 尾行的 seq —— 权威记录在 journal.jsonl,state.json 只是它的影子。 */
132
+ function lastJournalSeq(runId: string): number {
133
+ return loadJournal(runId).reduce((max, e) => (e.seq > max ? e.seq : max), 0)
79
134
  }
80
135
 
81
136
  /**
@@ -83,8 +138,7 @@ function readState(runId: string): JournalState {
83
138
  */
84
139
  export function loadScript(runId: string): string {
85
140
  const scriptPath = join(WORKFLOW_DIR, runId, 'script.js')
86
- if (!existsSync(scriptPath)) return ''
87
- return readFileSync(scriptPath, 'utf-8')
141
+ return readRegularFileSync(scriptPath) ?? ''
88
142
  }
89
143
 
90
144
  /**