@geminilight/mindos 0.5.63 → 0.5.65

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 (104) 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/changes/route.ts +7 -1
  5. package/app/app/api/file/route.ts +9 -0
  6. package/app/app/api/mcp/agents/route.ts +27 -1
  7. package/app/app/api/mcp/install-skill/route.ts +9 -24
  8. package/app/app/api/skills/route.ts +18 -2
  9. package/app/app/api/tree-version/route.ts +8 -0
  10. package/app/app/layout.tsx +1 -0
  11. package/app/app/page.tsx +1 -2
  12. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  13. package/app/components/ActivityBar.tsx +2 -2
  14. package/app/components/Backlinks.tsx +5 -5
  15. package/app/components/CreateSpaceModal.tsx +3 -2
  16. package/app/components/DirPicker.tsx +1 -1
  17. package/app/components/DirView.tsx +2 -3
  18. package/app/components/EditorWrapper.tsx +3 -3
  19. package/app/components/FileTree.tsx +25 -10
  20. package/app/components/GuideCard.tsx +4 -4
  21. package/app/components/HomeContent.tsx +44 -14
  22. package/app/components/MarkdownView.tsx +2 -2
  23. package/app/components/OnboardingView.tsx +1 -1
  24. package/app/components/Panel.tsx +1 -1
  25. package/app/components/RightAgentDetailPanel.tsx +2 -1
  26. package/app/components/RightAskPanel.tsx +1 -1
  27. package/app/components/SearchModal.tsx +10 -2
  28. package/app/components/SidebarLayout.tsx +36 -10
  29. package/app/components/ThemeToggle.tsx +1 -1
  30. package/app/components/agents/AgentDetailContent.tsx +454 -59
  31. package/app/components/agents/AgentsContentPage.tsx +89 -20
  32. package/app/components/agents/AgentsMcpSection.tsx +513 -85
  33. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  34. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  35. package/app/components/agents/AgentsSkillsSection.tsx +746 -105
  36. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  37. package/app/components/agents/agents-content-model.ts +308 -10
  38. package/app/components/ask/AskContent.tsx +34 -5
  39. package/app/components/ask/FileChip.tsx +1 -0
  40. package/app/components/ask/MentionPopover.tsx +13 -1
  41. package/app/components/ask/MessageList.tsx +5 -7
  42. package/app/components/ask/ToolCallBlock.tsx +4 -4
  43. package/app/components/changes/ChangesBanner.tsx +89 -13
  44. package/app/components/changes/ChangesContentPage.tsx +134 -51
  45. package/app/components/echo/EchoHero.tsx +10 -24
  46. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  47. package/app/components/echo/EchoPageSections.tsx +13 -9
  48. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  49. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  50. package/app/components/explore/ExploreContent.tsx +3 -7
  51. package/app/components/explore/UseCaseCard.tsx +4 -15
  52. package/app/components/panels/AgentsPanel.tsx +22 -128
  53. package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
  54. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
  55. package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
  56. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  57. package/app/components/panels/EchoPanel.tsx +8 -10
  58. package/app/components/panels/PanelNavRow.tsx +9 -2
  59. package/app/components/panels/PluginsPanel.tsx +5 -5
  60. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  61. package/app/components/renderers/agent-inspector/manifest.ts +5 -3
  62. package/app/components/renderers/config/manifest.ts +1 -0
  63. package/app/components/renderers/csv/manifest.ts +1 -0
  64. package/app/components/renderers/todo/manifest.ts +1 -0
  65. package/app/components/settings/AiTab.tsx +3 -3
  66. package/app/components/settings/AppearanceTab.tsx +2 -2
  67. package/app/components/settings/KnowledgeTab.tsx +3 -3
  68. package/app/components/settings/McpAgentInstall.tsx +3 -6
  69. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  70. package/app/components/settings/McpSkillRow.tsx +2 -3
  71. package/app/components/settings/McpSkillsSection.tsx +2 -2
  72. package/app/components/settings/McpTab.tsx +12 -13
  73. package/app/components/settings/MonitoringTab.tsx +13 -13
  74. package/app/components/settings/PluginsTab.tsx +6 -5
  75. package/app/components/settings/Primitives.tsx +3 -4
  76. package/app/components/settings/SettingsContent.tsx +3 -3
  77. package/app/components/settings/SyncTab.tsx +11 -17
  78. package/app/components/settings/UpdateTab.tsx +18 -21
  79. package/app/components/settings/types.ts +14 -0
  80. package/app/components/setup/StepKB.tsx +1 -1
  81. package/app/hooks/useMcpData.tsx +7 -4
  82. package/app/hooks/useMention.ts +25 -8
  83. package/app/lib/agent/log.ts +15 -18
  84. package/app/lib/agent/stream-consumer.ts +3 -0
  85. package/app/lib/agent/to-agent-messages.ts +6 -4
  86. package/app/lib/core/agent-audit-log.ts +280 -0
  87. package/app/lib/core/content-changes.ts +148 -8
  88. package/app/lib/core/index.ts +11 -0
  89. package/app/lib/fs.ts +16 -1
  90. package/app/lib/i18n-en.ts +317 -36
  91. package/app/lib/i18n-zh.ts +316 -35
  92. package/app/lib/mcp-agents.ts +273 -2
  93. package/app/lib/renderers/index.ts +1 -2
  94. package/app/lib/renderers/registry.ts +10 -0
  95. package/app/lib/types.ts +2 -0
  96. package/app/next-env.d.ts +1 -1
  97. package/bin/lib/mcp-agents.js +38 -13
  98. package/package.json +1 -1
  99. package/scripts/migrate-agent-audit-log.js +170 -0
  100. package/scripts/migrate-agent-diff.js +146 -0
  101. package/scripts/setup.js +12 -17
  102. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  103. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  104. package/app/components/renderers/diff/manifest.ts +0 -14
@@ -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
  }
@@ -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
+
@@ -32,11 +32,18 @@ interface ChangeLogState {
32
32
  version: 1;
33
33
  lastSeenAt: string | null;
34
34
  events: ContentChangeEvent[];
35
+ legacy?: {
36
+ agentDiffImportedCount?: number;
37
+ lastImportedAt?: string | null;
38
+ };
35
39
  }
36
40
 
37
41
  interface ListOptions {
38
42
  path?: string;
39
43
  limit?: number;
44
+ source?: ContentChangeSource;
45
+ op?: string;
46
+ q?: string;
40
47
  }
41
48
 
42
49
  export interface ContentChangeSummary {
@@ -64,6 +71,10 @@ function defaultState(): ChangeLogState {
64
71
  version: 1,
65
72
  lastSeenAt: null,
66
73
  events: [],
74
+ legacy: {
75
+ agentDiffImportedCount: 0,
76
+ lastImportedAt: null,
77
+ },
67
78
  };
68
79
  }
69
80
 
@@ -87,6 +98,16 @@ function readState(mindRoot: string): ChangeLogState {
87
98
  version: 1,
88
99
  lastSeenAt: typeof parsed.lastSeenAt === 'string' ? parsed.lastSeenAt : null,
89
100
  events: parsed.events,
101
+ legacy: {
102
+ agentDiffImportedCount:
103
+ typeof parsed.legacy?.agentDiffImportedCount === 'number'
104
+ ? parsed.legacy.agentDiffImportedCount
105
+ : 0,
106
+ lastImportedAt:
107
+ typeof parsed.legacy?.lastImportedAt === 'string'
108
+ ? parsed.legacy.lastImportedAt
109
+ : null,
110
+ },
90
111
  };
91
112
  } catch {
92
113
  return defaultState();
@@ -99,8 +120,115 @@ function writeState(mindRoot: string, state: ChangeLogState): void {
99
120
  fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8');
100
121
  }
101
122
 
102
- export function appendContentChange(mindRoot: string, input: ContentChangeInput): ContentChangeEvent {
123
+ interface LegacyAgentDiffEntry {
124
+ ts?: string;
125
+ path?: string;
126
+ tool?: string;
127
+ before?: string;
128
+ after?: string;
129
+ }
130
+
131
+ function parseLegacyAgentDiffBlocks(content: string): LegacyAgentDiffEntry[] {
132
+ const blocks: LegacyAgentDiffEntry[] = [];
133
+ const re = /```agent-diff\s*\n([\s\S]*?)```/g;
134
+ let m: RegExpExecArray | null;
135
+ while ((m = re.exec(content)) !== null) {
136
+ try {
137
+ const parsed = JSON.parse(m[1].trim()) as LegacyAgentDiffEntry;
138
+ blocks.push(parsed);
139
+ } catch {
140
+ // Skip malformed block, keep import best-effort.
141
+ }
142
+ }
143
+ return blocks;
144
+ }
145
+
146
+ function toValidIso(ts: string | undefined): string {
147
+ if (!ts) return nowIso();
148
+ const ms = new Date(ts).getTime();
149
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : nowIso();
150
+ }
151
+
152
+ function removeLegacyFile(filePath: string): void {
153
+ try {
154
+ fs.rmSync(filePath, { force: true });
155
+ } catch {
156
+ // keep best-effort; migration should not fail main flow.
157
+ }
158
+ }
159
+
160
+ function importLegacyAgentDiffIfNeeded(mindRoot: string, state: ChangeLogState): ChangeLogState {
161
+ const legacyPath = path.join(mindRoot, 'Agent-Diff.md');
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 = parseLegacyAgentDiffBlocks(raw);
172
+ const importedCount = state.legacy?.agentDiffImportedCount ?? 0;
173
+ if (blocks.length <= importedCount) {
174
+ // Already migrated before: remove legacy file to avoid stale duplicate source.
175
+ if (blocks.length > 0) removeLegacyFile(legacyPath);
176
+ return state;
177
+ }
178
+
179
+ const incoming = blocks.slice(importedCount);
180
+ const importedEvents: ContentChangeEvent[] = incoming.map((entry, idx) => {
181
+ const before = normalizeText(entry.before);
182
+ const after = normalizeText(entry.after);
183
+ const toolName = typeof entry.tool === 'string' && entry.tool.trim()
184
+ ? entry.tool.trim()
185
+ : 'unknown-tool';
186
+ const targetPath = typeof entry.path === 'string' && entry.path.trim()
187
+ ? entry.path
188
+ : 'Agent-Diff.md';
189
+ return {
190
+ id: `legacy-${Date.now().toString(36)}-${idx.toString(36)}`,
191
+ ts: toValidIso(entry.ts),
192
+ op: 'legacy_agent_diff_import',
193
+ path: targetPath,
194
+ source: 'agent',
195
+ summary: `Imported legacy agent diff (${toolName})`,
196
+ before: before.value,
197
+ after: after.value,
198
+ truncated: before.truncated || after.truncated || undefined,
199
+ };
200
+ });
201
+
202
+ const merged = [...state.events, ...importedEvents].sort(
203
+ (a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime(),
204
+ );
205
+
206
+ const nextState = {
207
+ ...state,
208
+ events: merged.slice(0, MAX_EVENTS),
209
+ legacy: {
210
+ agentDiffImportedCount: blocks.length,
211
+ lastImportedAt: nowIso(),
212
+ },
213
+ };
214
+ removeLegacyFile(legacyPath);
215
+ return nextState;
216
+ }
217
+
218
+ function loadState(mindRoot: string): ChangeLogState {
103
219
  const state = readState(mindRoot);
220
+ const migrated = importLegacyAgentDiffIfNeeded(mindRoot, state);
221
+ const changed =
222
+ (state.legacy?.agentDiffImportedCount ?? 0) !== (migrated.legacy?.agentDiffImportedCount ?? 0) ||
223
+ state.events.length !== migrated.events.length;
224
+ if (changed) {
225
+ writeState(mindRoot, migrated);
226
+ }
227
+ return migrated;
228
+ }
229
+
230
+ export function appendContentChange(mindRoot: string, input: ContentChangeInput): ContentChangeEvent {
231
+ const state = loadState(mindRoot);
104
232
  const before = normalizeText(input.before);
105
233
  const after = normalizeText(input.after);
106
234
  const event: ContentChangeEvent = {
@@ -125,23 +253,35 @@ export function appendContentChange(mindRoot: string, input: ContentChangeInput)
125
253
  }
126
254
 
127
255
  export function listContentChanges(mindRoot: string, options: ListOptions = {}): ContentChangeEvent[] {
128
- const state = readState(mindRoot);
256
+ const state = loadState(mindRoot);
129
257
  const limit = Math.max(1, Math.min(options.limit ?? 50, 200));
130
- const pathFilter = options.path;
131
- const events = pathFilter
132
- ? state.events.filter((event) => event.path === pathFilter || event.beforePath === pathFilter || event.afterPath === pathFilter)
133
- : state.events;
258
+ const pathFilter = options.path?.trim();
259
+ const sourceFilter = options.source;
260
+ const opFilter = options.op?.trim();
261
+ const q = options.q?.trim().toLowerCase();
262
+ const events = state.events.filter((event) => {
263
+ if (pathFilter && event.path !== pathFilter && event.beforePath !== pathFilter && event.afterPath !== pathFilter) {
264
+ return false;
265
+ }
266
+ if (sourceFilter && event.source !== sourceFilter) return false;
267
+ if (opFilter && event.op !== opFilter) return false;
268
+ if (q) {
269
+ const haystack = `${event.path} ${event.beforePath ?? ''} ${event.afterPath ?? ''} ${event.summary} ${event.op} ${event.source}`.toLowerCase();
270
+ if (!haystack.includes(q)) return false;
271
+ }
272
+ return true;
273
+ });
134
274
  return events.slice(0, limit);
135
275
  }
136
276
 
137
277
  export function markContentChangesSeen(mindRoot: string): void {
138
- const state = readState(mindRoot);
278
+ const state = loadState(mindRoot);
139
279
  state.lastSeenAt = nowIso();
140
280
  writeState(mindRoot, state);
141
281
  }
142
282
 
143
283
  export function getContentChangeSummary(mindRoot: string): ContentChangeSummary {
144
- const state = readState(mindRoot);
284
+ const state = loadState(mindRoot);
145
285
  const lastSeenAtMs = state.lastSeenAt ? new Date(state.lastSeenAt).getTime() : 0;
146
286
  const unreadCount = state.events.filter((event) => new Date(event.ts).getTime() > lastSeenAtMs).length;
147
287
  return {
@@ -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
 
@@ -199,7 +208,13 @@ export function appendContentChange(input: ContentChangeInput): ContentChangeEve
199
208
  return coreAppendContentChange(getMindRoot(), input);
200
209
  }
201
210
 
202
- export function listContentChanges(options: { path?: string; limit?: number } = {}): ContentChangeEvent[] {
211
+ export function listContentChanges(options: {
212
+ path?: string;
213
+ limit?: number;
214
+ source?: 'user' | 'agent' | 'system';
215
+ op?: string;
216
+ q?: string;
217
+ } = {}): ContentChangeEvent[] {
203
218
  return coreListContentChanges(getMindRoot(), options);
204
219
  }
205
220