@geminilight/mindos 0.5.64 → 0.5.66

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 (86) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/file/route.ts +9 -0
  5. package/app/app/api/mcp/agents/route.ts +27 -1
  6. package/app/app/api/skills/route.ts +18 -2
  7. package/app/app/api/tree-version/route.ts +8 -0
  8. package/app/components/ActivityBar.tsx +2 -2
  9. package/app/components/Backlinks.tsx +5 -5
  10. package/app/components/CreateSpaceModal.tsx +3 -2
  11. package/app/components/DirPicker.tsx +1 -1
  12. package/app/components/DirView.tsx +2 -3
  13. package/app/components/EditorWrapper.tsx +3 -3
  14. package/app/components/FileTree.tsx +25 -10
  15. package/app/components/GuideCard.tsx +4 -4
  16. package/app/components/HomeContent.tsx +6 -11
  17. package/app/components/MarkdownView.tsx +2 -2
  18. package/app/components/OnboardingView.tsx +1 -1
  19. package/app/components/Panel.tsx +1 -1
  20. package/app/components/RightAgentDetailPanel.tsx +1 -1
  21. package/app/components/RightAskPanel.tsx +1 -1
  22. package/app/components/SearchModal.tsx +10 -2
  23. package/app/components/SidebarLayout.tsx +35 -10
  24. package/app/components/ThemeToggle.tsx +1 -1
  25. package/app/components/agents/AgentDetailContent.tsx +454 -59
  26. package/app/components/agents/AgentsContentPage.tsx +70 -5
  27. package/app/components/agents/AgentsMcpSection.tsx +474 -159
  28. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  29. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  30. package/app/components/agents/AgentsSkillsSection.tsx +739 -121
  31. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  32. package/app/components/agents/agents-content-model.ts +292 -10
  33. package/app/components/ask/AskContent.tsx +34 -5
  34. package/app/components/ask/FileChip.tsx +1 -0
  35. package/app/components/ask/MentionPopover.tsx +13 -1
  36. package/app/components/ask/MessageList.tsx +5 -7
  37. package/app/components/ask/ToolCallBlock.tsx +4 -4
  38. package/app/components/changes/ChangesBanner.tsx +1 -2
  39. package/app/components/echo/EchoHero.tsx +10 -24
  40. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  41. package/app/components/echo/EchoPageSections.tsx +13 -9
  42. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  43. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  44. package/app/components/explore/ExploreContent.tsx +3 -7
  45. package/app/components/explore/UseCaseCard.tsx +4 -15
  46. package/app/components/panels/AgentsPanel.tsx +12 -104
  47. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  48. package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
  49. package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
  50. package/app/components/panels/EchoPanel.tsx +8 -10
  51. package/app/components/panels/PanelNavRow.tsx +9 -2
  52. package/app/components/panels/PluginsPanel.tsx +2 -2
  53. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  54. package/app/components/renderers/agent-inspector/manifest.ts +3 -3
  55. package/app/components/renderers/todo/manifest.ts +1 -0
  56. package/app/components/settings/AiTab.tsx +3 -3
  57. package/app/components/settings/AppearanceTab.tsx +2 -2
  58. package/app/components/settings/KnowledgeTab.tsx +3 -3
  59. package/app/components/settings/McpAgentInstall.tsx +3 -6
  60. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  61. package/app/components/settings/McpSkillRow.tsx +2 -3
  62. package/app/components/settings/McpSkillsSection.tsx +2 -2
  63. package/app/components/settings/McpTab.tsx +12 -13
  64. package/app/components/settings/MonitoringTab.tsx +13 -13
  65. package/app/components/settings/PluginsTab.tsx +2 -2
  66. package/app/components/settings/Primitives.tsx +3 -4
  67. package/app/components/settings/SettingsContent.tsx +3 -3
  68. package/app/components/settings/SyncTab.tsx +11 -17
  69. package/app/components/settings/UpdateTab.tsx +18 -21
  70. package/app/components/settings/types.ts +14 -0
  71. package/app/components/setup/StepKB.tsx +1 -1
  72. package/app/hooks/useMcpData.tsx +4 -2
  73. package/app/hooks/useMention.ts +25 -8
  74. package/app/lib/agent/log.ts +15 -18
  75. package/app/lib/agent/prompt.ts +17 -29
  76. package/app/lib/agent/stream-consumer.ts +3 -0
  77. package/app/lib/agent/to-agent-messages.ts +6 -4
  78. package/app/lib/core/agent-audit-log.ts +280 -0
  79. package/app/lib/core/index.ts +11 -0
  80. package/app/lib/fs.ts +9 -0
  81. package/app/lib/i18n-en.ts +259 -33
  82. package/app/lib/i18n-zh.ts +258 -32
  83. package/app/lib/mcp-agents.ts +231 -2
  84. package/app/lib/types.ts +2 -0
  85. package/package.json +1 -1
  86. package/scripts/migrate-agent-audit-log.js +170 -0
@@ -1,9 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getMindRoot } from '@/lib/fs';
4
+ import { appendAgentAuditEvent } from '@/lib/core/agent-audit-log';
4
5
 
5
- const LOG_FILE = '.agent-log.json';
6
- const MAX_SIZE = 500 * 1024; // 500KB
6
+ const LEGACY_LOG_FILE = '.agent-log.json';
7
7
 
8
8
  interface AgentOpEntry {
9
9
  ts: string;
@@ -22,23 +22,20 @@ interface AgentOpEntry {
22
22
  export function logAgentOp(entry: AgentOpEntry): void {
23
23
  try {
24
24
  const root = getMindRoot();
25
- const logPath = path.join(root, LOG_FILE);
26
-
27
- const line = JSON.stringify(entry) + '\n';
28
-
29
- // Check size and truncate if needed
30
- if (fs.existsSync(logPath)) {
31
- const stat = fs.statSync(logPath);
32
- if (stat.size > MAX_SIZE) {
33
- const content = fs.readFileSync(logPath, 'utf-8');
34
- const lines = content.trimEnd().split('\n');
35
- // Keep the newer half
36
- const kept = lines.slice(Math.floor(lines.length / 2));
37
- fs.writeFileSync(logPath, kept.join('\n') + '\n');
38
- }
25
+ appendAgentAuditEvent(root, {
26
+ ts: entry.ts,
27
+ tool: entry.tool,
28
+ params: entry.params,
29
+ result: entry.result,
30
+ message: entry.message,
31
+ durationMs: entry.durationMs,
32
+ });
33
+ // Best-effort cleanup of legacy JSONL path.
34
+ try {
35
+ fs.rmSync(path.join(root, LEGACY_LOG_FILE), { force: true });
36
+ } catch {
37
+ // ignore
39
38
  }
40
-
41
- fs.appendFileSync(logPath, line);
42
39
  } catch {
43
40
  // Logging should never break tool execution
44
41
  }
@@ -9,39 +9,27 @@
9
9
  * Token budget: ~600 tokens (down from ~900 in v2). Freed space = more room for
10
10
  * SKILL.md + bootstrap context within the same context window.
11
11
  */
12
- export const AGENT_SYSTEM_PROMPT = `You are MindOS Agent — a personal knowledge-base operator that reads, writes, and organizes a user's second brain.
12
+ export const AGENT_SYSTEM_PROMPT = `You are MindOS Agent — the operator of the user's second brain.
13
13
 
14
- Persona: methodical, concise, execution-oriented. You surface what you found (or didn't find) and act on it — no filler, no caveats that add no information.
14
+ Persona: Methodical, strictly objective, execution-oriented. Zero fluff. Never use preambles like "Here is the result" or "I found...".
15
15
 
16
- ## What is already loaded
16
+ ## Core Directives
17
17
 
18
- The server auto-loads before each request:
19
- - Bootstrap context: INSTRUCTION.md, README.md, CONFIG files, and directory-local guidance.
20
- - Skill guidance (SKILL.md): detailed knowledge-base rules, tool selection, execution patterns.
21
- - Tool definitions with per-tool usage instructions.
18
+ 1. **Anti-Hallucination**: Strictly separate your training data from the user's local knowledge. If asked about the user's notes/life/projects, rely EXCLUSIVELY on tool outputs. If a search yields nothing, state "Not found in knowledge base." NEVER fabricate or infer missing data.
19
+ 2. **Think Before Acting**: For any non-trivial task, use a brief \`<thinking>\` block to outline your plan or analyze an error BEFORE calling tools.
20
+ 3. **Read Before Write**: You MUST read a file before modifying it. Prefer precise section/line edits over full overwrites. Verify edits by reading again.
21
+ 4. **Cite Sources**: Always include the exact file path when answering from local knowledge so the user can verify.
22
+ 5. **Smart Recovery**: If a tool fails (e.g., File Not Found), do NOT retry identical arguments. Use \`search\` or \`list_files\` to find the correct path first.
23
+ 6. **Token Efficiency**: Batch parallel independent tool calls in a single turn. Do not waste rounds.
24
+ 7. **Language Alignment**: Match the language of the file when writing, and match the user's language when replying.
22
25
 
23
- Treat these as your initialization baseline. If the task needs fresher or broader evidence, call tools proactively before concluding.
26
+ ## Context Mechanics
24
27
 
25
- ## Behavioral rules
28
+ - **Auto-loaded**: Configs, instructions, and SKILL.md are already in your context. Do not search for them unless explicitly asked.
29
+ - **Uploaded Files**: Local files attached by the user appear in the "⚠️ USER-UPLOADED FILES" section below. Use this content directly. Do NOT use tools to read/search them.
26
30
 
27
- 1. **Read before write.** Never modify a file you haven't read in this request.
28
- 2. **Minimal edits.** Prefer section/heading/line-level tools over full file overwrites.
29
- 3. **Verify after edit.** Re-read the changed file to confirm correctness.
30
- 4. **Cite sources.** When answering from stored knowledge, state the file path so the user can verify.
31
- 5. **Fail fast.** If a tool call returns an error or unexpected result, try a different approach or ask the user — do not retry identical arguments.
32
- 6. **Be token-aware.** You have a limited step budget (typically 10-30). Batch parallel reads/searches when possible. Do not waste steps on redundant tool calls.
33
- 7. **Multilingual content, user-language replies.** Write file content in whatever language the file already uses. Reply to the user in the language they used.
31
+ ## Output
34
32
 
35
- ## Uploaded files
36
-
37
- Users may upload local files (PDF, txt, csv, etc.) via the chat interface.
38
- - Their content appears in a "⚠️ USER-UPLOADED FILES" section near the end of this prompt.
39
- - Use that content directly — do NOT call read_file or search tools for uploaded files; they are not in the knowledge base.
40
- - If the section is empty or missing, tell the user the upload may have failed.
41
-
42
- ## Output format
43
-
44
- - Answer in the user's language.
45
- - Use Markdown when it improves clarity (headings, lists, tables, code blocks).
46
- - For multi-step tasks: output a brief numbered plan, execute, then summarize outcomes.
47
- - End with concrete next actions when applicable.`;
33
+ - Reply in the user's language.
34
+ - Use clean Markdown (tables, lists, bold).
35
+ - End with concrete next actions if the task is incomplete.`;
@@ -31,6 +31,8 @@ export async function consumeUIMessageStream(
31
31
  let currentTextId: string | null = null;
32
32
  let currentReasoningPart: ReasoningPart | null = null;
33
33
 
34
+ const startedAt = Date.now();
35
+
34
36
  /** Build an immutable Message snapshot from current parts */
35
37
  function buildMessage(): Message {
36
38
  const clonedParts: MessagePart[] = parts.map(p => {
@@ -47,6 +49,7 @@ export async function consumeUIMessageStream(
47
49
  role: 'assistant',
48
50
  content: textContent,
49
51
  parts: clonedParts,
52
+ timestamp: startedAt,
50
53
  };
51
54
  }
52
55
 
@@ -24,11 +24,13 @@ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
24
24
  const result: AgentMessage[] = [];
25
25
 
26
26
  for (const msg of messages) {
27
+ const timestamp = msg.timestamp ?? Date.now();
28
+
27
29
  if (msg.role === 'user') {
28
30
  result.push({
29
31
  role: 'user',
30
32
  content: msg.content,
31
- timestamp: Date.now(),
33
+ timestamp,
32
34
  } satisfies UserMessage as AgentMessage);
33
35
  continue;
34
36
  }
@@ -49,7 +51,7 @@ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
49
51
  model: '',
50
52
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
51
53
  stopReason: 'stop',
52
- timestamp: Date.now(),
54
+ timestamp,
53
55
  } satisfies AssistantMessage as AgentMessage);
54
56
  }
55
57
  continue;
@@ -85,7 +87,7 @@ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
85
87
  model: '',
86
88
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
87
89
  stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',
88
- timestamp: Date.now(),
90
+ timestamp,
89
91
  } satisfies AssistantMessage as AgentMessage);
90
92
  }
91
93
 
@@ -97,7 +99,7 @@ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
97
99
  toolName: tc.toolName,
98
100
  content: [{ type: 'text', text: tc.output ?? '' }],
99
101
  isError: tc.state === 'error',
100
- timestamp: Date.now(),
102
+ timestamp,
101
103
  } satisfies ToolResultMessage as AgentMessage);
102
104
  }
103
105
  }
@@ -0,0 +1,280 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export interface AgentAuditEvent {
5
+ id: string;
6
+ ts: string;
7
+ tool: string;
8
+ params: Record<string, unknown>;
9
+ result: 'ok' | 'error';
10
+ message?: string;
11
+ durationMs?: number;
12
+ op?: 'append' | 'legacy_agent_audit_md_import' | 'legacy_agent_log_jsonl_import';
13
+ }
14
+
15
+ export interface AgentAuditInput {
16
+ ts: string;
17
+ tool: string;
18
+ params: Record<string, unknown>;
19
+ result: 'ok' | 'error';
20
+ message?: string;
21
+ durationMs?: number;
22
+ }
23
+
24
+ interface AgentAuditState {
25
+ version: 1;
26
+ events: AgentAuditEvent[];
27
+ legacy?: {
28
+ mdImportedCount?: number;
29
+ jsonlImportedCount?: number;
30
+ lastImportedAt?: string | null;
31
+ };
32
+ }
33
+
34
+ const LOG_DIR_NAME = '.mindos';
35
+ const LOG_FILE_NAME = 'agent-audit-log.json';
36
+ const LEGACY_MD_FILE = 'Agent-Audit.md';
37
+ const LEGACY_JSONL_FILE = '.agent-log.json';
38
+ const MAX_EVENTS = 1000;
39
+ const MAX_MESSAGE_CHARS = 2000;
40
+
41
+ function nowIso() {
42
+ return new Date().toISOString();
43
+ }
44
+
45
+ function validIso(ts: string | undefined): string {
46
+ if (!ts) return nowIso();
47
+ const ms = new Date(ts).getTime();
48
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : nowIso();
49
+ }
50
+
51
+ function normalizeMessage(message: string | undefined): string | undefined {
52
+ if (typeof message !== 'string') return undefined;
53
+ if (message.length <= MAX_MESSAGE_CHARS) return message;
54
+ return message.slice(0, MAX_MESSAGE_CHARS);
55
+ }
56
+
57
+ function defaultState(): AgentAuditState {
58
+ return {
59
+ version: 1,
60
+ events: [],
61
+ legacy: {
62
+ mdImportedCount: 0,
63
+ jsonlImportedCount: 0,
64
+ lastImportedAt: null,
65
+ },
66
+ };
67
+ }
68
+
69
+ function logPath(mindRoot: string) {
70
+ return path.join(mindRoot, LOG_DIR_NAME, LOG_FILE_NAME);
71
+ }
72
+
73
+ function readState(mindRoot: string): AgentAuditState {
74
+ const file = logPath(mindRoot);
75
+ try {
76
+ if (!fs.existsSync(file)) return defaultState();
77
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as Partial<AgentAuditState>;
78
+ if (!Array.isArray(parsed.events)) return defaultState();
79
+ return {
80
+ version: 1,
81
+ events: parsed.events,
82
+ legacy: {
83
+ mdImportedCount: typeof parsed.legacy?.mdImportedCount === 'number' ? parsed.legacy.mdImportedCount : 0,
84
+ jsonlImportedCount: typeof parsed.legacy?.jsonlImportedCount === 'number' ? parsed.legacy.jsonlImportedCount : 0,
85
+ lastImportedAt: typeof parsed.legacy?.lastImportedAt === 'string' ? parsed.legacy.lastImportedAt : null,
86
+ },
87
+ };
88
+ } catch {
89
+ return defaultState();
90
+ }
91
+ }
92
+
93
+ function writeState(mindRoot: string, state: AgentAuditState): void {
94
+ const file = logPath(mindRoot);
95
+ fs.mkdirSync(path.dirname(file), { recursive: true });
96
+ fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8');
97
+ }
98
+
99
+ function removeLegacyFile(filePath: string): void {
100
+ try {
101
+ fs.rmSync(filePath, { force: true });
102
+ } catch {
103
+ // Keep migration best-effort.
104
+ }
105
+ }
106
+
107
+ interface LegacyAgentOp {
108
+ ts?: string;
109
+ tool?: string;
110
+ params?: Record<string, unknown>;
111
+ result?: 'ok' | 'error';
112
+ message?: string;
113
+ durationMs?: number;
114
+ }
115
+
116
+ function parseLegacyMdBlocks(raw: string): LegacyAgentOp[] {
117
+ const blocks: LegacyAgentOp[] = [];
118
+ const re = /```agent-op\s*\n([\s\S]*?)```/g;
119
+ let match: RegExpExecArray | null;
120
+ while ((match = re.exec(raw)) !== null) {
121
+ try {
122
+ blocks.push(JSON.parse(match[1].trim()) as LegacyAgentOp);
123
+ } catch {
124
+ // Ignore malformed blocks.
125
+ }
126
+ }
127
+ return blocks;
128
+ }
129
+
130
+ function parseJsonLines(raw: string): LegacyAgentOp[] {
131
+ const entries: LegacyAgentOp[] = [];
132
+ for (const line of raw.split('\n')) {
133
+ const trimmed = line.trim();
134
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
135
+ try {
136
+ entries.push(JSON.parse(trimmed) as LegacyAgentOp);
137
+ } catch {
138
+ // Ignore malformed lines.
139
+ }
140
+ }
141
+ return entries;
142
+ }
143
+
144
+ function toEvent(entry: LegacyAgentOp, op: AgentAuditEvent['op'], idx: number): AgentAuditEvent {
145
+ const tool = typeof entry.tool === 'string' && entry.tool.trim() ? entry.tool.trim() : 'unknown-tool';
146
+ const result = entry.result === 'error' ? 'error' : 'ok';
147
+ const params = entry.params && typeof entry.params === 'object' ? entry.params : {};
148
+ return {
149
+ id: `legacy-${Date.now().toString(36)}-${idx.toString(36)}`,
150
+ ts: validIso(entry.ts),
151
+ tool,
152
+ params,
153
+ result,
154
+ message: normalizeMessage(entry.message),
155
+ durationMs: typeof entry.durationMs === 'number' ? entry.durationMs : undefined,
156
+ op,
157
+ };
158
+ }
159
+
160
+ function importLegacyMdIfNeeded(mindRoot: string, state: AgentAuditState): AgentAuditState {
161
+ const legacyPath = path.join(mindRoot, LEGACY_MD_FILE);
162
+ if (!fs.existsSync(legacyPath)) return state;
163
+
164
+ let raw = '';
165
+ try {
166
+ raw = fs.readFileSync(legacyPath, 'utf-8');
167
+ } catch {
168
+ return state;
169
+ }
170
+
171
+ const blocks = parseLegacyMdBlocks(raw);
172
+ const importedCount = state.legacy?.mdImportedCount ?? 0;
173
+ if (blocks.length <= importedCount) {
174
+ if (blocks.length > 0) removeLegacyFile(legacyPath);
175
+ return state;
176
+ }
177
+
178
+ const incoming = blocks.slice(importedCount);
179
+ const imported = incoming.map((entry, idx) => toEvent(entry, 'legacy_agent_audit_md_import', idx));
180
+ const merged = [...state.events, ...imported]
181
+ .sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
182
+ .slice(0, MAX_EVENTS);
183
+
184
+ const next = {
185
+ ...state,
186
+ events: merged,
187
+ legacy: {
188
+ mdImportedCount: blocks.length,
189
+ jsonlImportedCount: state.legacy?.jsonlImportedCount ?? 0,
190
+ lastImportedAt: nowIso(),
191
+ },
192
+ };
193
+ removeLegacyFile(legacyPath);
194
+ return next;
195
+ }
196
+
197
+ function importLegacyJsonlIfNeeded(mindRoot: string, state: AgentAuditState): AgentAuditState {
198
+ const legacyPath = path.join(mindRoot, LEGACY_JSONL_FILE);
199
+ if (!fs.existsSync(legacyPath)) return state;
200
+
201
+ let raw = '';
202
+ try {
203
+ raw = fs.readFileSync(legacyPath, 'utf-8');
204
+ } catch {
205
+ return state;
206
+ }
207
+
208
+ const lines = parseJsonLines(raw);
209
+ const importedCount = state.legacy?.jsonlImportedCount ?? 0;
210
+ if (lines.length <= importedCount) {
211
+ if (lines.length > 0) removeLegacyFile(legacyPath);
212
+ return state;
213
+ }
214
+
215
+ const incoming = lines.slice(importedCount);
216
+ const imported = incoming.map((entry, idx) => toEvent(entry, 'legacy_agent_log_jsonl_import', idx));
217
+ const merged = [...state.events, ...imported]
218
+ .sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
219
+ .slice(0, MAX_EVENTS);
220
+
221
+ const next = {
222
+ ...state,
223
+ events: merged,
224
+ legacy: {
225
+ mdImportedCount: state.legacy?.mdImportedCount ?? 0,
226
+ jsonlImportedCount: lines.length,
227
+ lastImportedAt: nowIso(),
228
+ },
229
+ };
230
+ removeLegacyFile(legacyPath);
231
+ return next;
232
+ }
233
+
234
+ function loadState(mindRoot: string): AgentAuditState {
235
+ const base = readState(mindRoot);
236
+ const mdMigrated = importLegacyMdIfNeeded(mindRoot, base);
237
+ const migrated = importLegacyJsonlIfNeeded(mindRoot, mdMigrated);
238
+ const changed =
239
+ base.events.length !== migrated.events.length ||
240
+ (base.legacy?.mdImportedCount ?? 0) !== (migrated.legacy?.mdImportedCount ?? 0) ||
241
+ (base.legacy?.jsonlImportedCount ?? 0) !== (migrated.legacy?.jsonlImportedCount ?? 0);
242
+ if (changed) writeState(mindRoot, migrated);
243
+ return migrated;
244
+ }
245
+
246
+ export function appendAgentAuditEvent(mindRoot: string, input: AgentAuditInput): AgentAuditEvent {
247
+ const state = loadState(mindRoot);
248
+ const event: AgentAuditEvent = {
249
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
250
+ ts: validIso(input.ts),
251
+ tool: input.tool,
252
+ params: input.params && typeof input.params === 'object' ? input.params : {},
253
+ result: input.result === 'error' ? 'error' : 'ok',
254
+ message: normalizeMessage(input.message),
255
+ durationMs: typeof input.durationMs === 'number' ? input.durationMs : undefined,
256
+ op: 'append',
257
+ };
258
+ state.events.unshift(event);
259
+ if (state.events.length > MAX_EVENTS) state.events = state.events.slice(0, MAX_EVENTS);
260
+ writeState(mindRoot, state);
261
+ return event;
262
+ }
263
+
264
+ export function listAgentAuditEvents(mindRoot: string, limit = 100): AgentAuditEvent[] {
265
+ const state = loadState(mindRoot);
266
+ const safeLimit = Math.max(1, Math.min(limit, 1000));
267
+ return state.events.slice(0, safeLimit);
268
+ }
269
+
270
+ export function parseAgentAuditJsonLines(raw: string): AgentAuditInput[] {
271
+ return parseJsonLines(raw).map((entry) => ({
272
+ ts: validIso(entry.ts),
273
+ tool: typeof entry.tool === 'string' && entry.tool.trim() ? entry.tool.trim() : 'unknown-tool',
274
+ params: entry.params && typeof entry.params === 'object' ? entry.params : {},
275
+ result: entry.result === 'error' ? 'error' : 'ok',
276
+ message: normalizeMessage(entry.message),
277
+ durationMs: typeof entry.durationMs === 'number' ? entry.durationMs : undefined,
278
+ }));
279
+ }
280
+
@@ -78,3 +78,14 @@ export type {
78
78
  ContentChangeSummary,
79
79
  ContentChangeSource,
80
80
  } from './content-changes';
81
+
82
+ // Agent audit log
83
+ export {
84
+ appendAgentAuditEvent,
85
+ listAgentAuditEvents,
86
+ parseAgentAuditJsonLines,
87
+ } from './agent-audit-log';
88
+ export type {
89
+ AgentAuditEvent,
90
+ AgentAuditInput,
91
+ } from './agent-audit-log';
package/app/lib/fs.ts CHANGED
@@ -57,6 +57,14 @@ interface FileTreeCache {
57
57
  let _cache: FileTreeCache | null = null;
58
58
  const CACHE_TTL_MS = 5_000; // 5 seconds
59
59
 
60
+ let _treeVersion = 0;
61
+
62
+ /** Monotonically increasing counter — bumped on every file mutation so the
63
+ * client can cheaply detect changes without rebuilding the full tree. */
64
+ export function getTreeVersion(): number {
65
+ return _treeVersion;
66
+ }
67
+
60
68
  function isCacheValid(): boolean {
61
69
  return _cache !== null && (Date.now() - _cache.timestamp) < CACHE_TTL_MS;
62
70
  }
@@ -65,6 +73,7 @@ function isCacheValid(): boolean {
65
73
  export function invalidateCache(): void {
66
74
  _cache = null;
67
75
  _searchIndex = null;
76
+ _treeVersion++;
68
77
  invalidateSearchIndex();
69
78
  }
70
79