codemini-cli 0.6.3 → 0.6.4

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 (48) hide show
  1. package/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-MRopwNIL.js} +2 -2
  2. package/codemini-web/dist/assets/CodeWikiPanel-UpK5xGE3.js +1 -0
  3. package/codemini-web/dist/assets/ConfigDialog-CNl28wsj.js +1 -0
  4. package/codemini-web/dist/assets/GitDiffDialog-gSysUg2J.js +3 -0
  5. package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-DFUmo3Kl.js} +3 -3
  6. package/codemini-web/dist/assets/MessageBubble-CGnnViv0.js +12 -0
  7. package/codemini-web/dist/assets/PatchDiff-B8rwvEg5.js +230 -0
  8. package/codemini-web/dist/assets/ProjectSelector-BF59M1zb.js +1 -0
  9. package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-CQTjbSiw.js} +4 -4
  10. package/codemini-web/dist/assets/SoulDialog-BLjUGqqB.js +1 -0
  11. package/codemini-web/dist/assets/chevron-right--85xg7qk.js +1 -0
  12. package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-6uELoidu.js} +6 -6
  13. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-gb1UMBZ5.js} +1 -1
  14. package/codemini-web/dist/assets/index-1xqD0R5t.css +2 -0
  15. package/codemini-web/dist/assets/index-CDXQGwPs.js +65 -0
  16. package/codemini-web/dist/assets/{input-CNQgbKe6.js → input-Ca8O_061.js} +1 -1
  17. package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
  18. package/codemini-web/dist/assets/mermaid-GHXKKRXX-ROliF8Yd.js +1 -0
  19. package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BhT11Ztp.js} +1 -1
  20. package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-D7R5Lth6.js} +1 -1
  21. package/codemini-web/dist/assets/select-DBvcHBzs.js +1 -0
  22. package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-BfNZcWfX.js} +1 -1
  23. package/codemini-web/dist/index.html +2 -2
  24. package/codemini-web/lib/runtime-bridge.js +325 -296
  25. package/codemini-web/server.js +310 -243
  26. package/package.json +1 -1
  27. package/src/core/agent-loop.js +188 -97
  28. package/src/core/chat-runtime.js +674 -571
  29. package/src/core/config-store.js +11 -3
  30. package/src/core/git-oplog-change-tracker.js +387 -0
  31. package/src/core/non-git-backup.js +116 -0
  32. package/src/core/paths.js +123 -123
  33. package/src/core/session-store.js +148 -99
  34. package/src/core/tools.js +499 -456
  35. package/src/tui/chat-app.js +196 -56
  36. package/codemini-web/dist/assets/CodeWikiPanel-EPuoerNv.js +0 -1
  37. package/codemini-web/dist/assets/ConfigDialog-B5IGZCc9.js +0 -1
  38. package/codemini-web/dist/assets/GitDiffDialog-Bb_Tw5ZK.js +0 -222
  39. package/codemini-web/dist/assets/MessageBubble-wUff4GP4.js +0 -6
  40. package/codemini-web/dist/assets/ProjectSelector-C0leTf6f.js +0 -1
  41. package/codemini-web/dist/assets/SoulDialog-XDTEGWvH.js +0 -1
  42. package/codemini-web/dist/assets/chevron-right-Dbzw7YzA.js +0 -1
  43. package/codemini-web/dist/assets/index-D0EGtNPr.js +0 -65
  44. package/codemini-web/dist/assets/index-wOUf3WkN.css +0 -2
  45. package/codemini-web/dist/assets/lib-BOngVP_M.js +0 -11
  46. package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
  47. package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
  48. package/codemini-web/dist/assets/select-BZXfigic.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "An extremely restrained coding + tasks CLI. Every platform. Every terminal. Minimal by design.",
5
5
  "keywords": [
6
6
  "cli",
@@ -236,7 +236,7 @@ async function checkAutoDreamThreshold(config) {
236
236
 
237
237
  // ─── Exported helpers ────────────────────────────────────────────────
238
238
 
239
- function extractFileChange(toolName, result) {
239
+ function extractFileChange(toolName, result) {
240
240
  if (!result || typeof result !== 'object') return null;
241
241
  const FILE_TOOLS = new Set(['edit', 'write', 'delete']);
242
242
  if (!FILE_TOOLS.has(toolName)) return null;
@@ -252,56 +252,90 @@ function extractFileChange(toolName, result) {
252
252
  const isCreate = action === 'create';
253
253
  const added = Number(result.lines_added || 0);
254
254
  const removed = Number(result.lines_removed || 0);
255
- return {
256
- path: String(result.path || ''),
257
- action: isCreate ? 'create' : 'edit',
258
- linesAdded: added,
259
- linesRemoved: removed,
260
- changedLine: Number(result.changed_line || 0),
261
- diffPreview: String(result.diff_preview || '')
262
- };
263
- }
255
+ return {
256
+ path: String(result.path || ''),
257
+ action: isCreate ? 'create' : 'edit',
258
+ linesAdded: added,
259
+ linesRemoved: removed,
260
+ changedLine: Number(result.changed_line || 0),
261
+ diffPreview: String(result.diff_preview || '')
262
+ };
263
+ }
264
+
265
+ return null;
266
+ }
264
267
 
265
- return null;
266
- }
267
-
268
268
  function normalizeFileChange(change) {
269
- if (!change || typeof change !== 'object') return null;
270
- const path = String(change.path || '').trim();
271
- if (!path) return null;
272
- const action = String(change.action || '').trim();
273
- return {
274
- path,
275
- action: action === 'create' || action === 'delete' ? action : 'edit',
269
+ if (!change || typeof change !== 'object') return null;
270
+ const path = String(change.path || '').trim();
271
+ if (!path) return null;
272
+ const action = String(change.action || '').trim();
273
+ return {
274
+ path,
275
+ action: action === 'create' || action === 'delete' ? action : 'edit',
276
276
  linesAdded: Number(change.linesAdded || 0),
277
277
  linesRemoved: Number(change.linesRemoved || 0),
278
278
  changedLine: Number(change.changedLine || 0),
279
- diffPreview: String(change.diffPreview || '')
279
+ diffPreview: String(change.diffPreview || ''),
280
+ changeSetId: String(change.changeSetId || ''),
281
+ patchRef: String(change.patchRef || '')
280
282
  };
281
283
  }
282
-
283
- function fileChangeFingerprint(change) {
284
- return JSON.stringify({
285
- path: change.path,
286
- action: change.action,
287
- linesAdded: Number(change.linesAdded || 0),
288
- linesRemoved: Number(change.linesRemoved || 0),
289
- changedLine: Number(change.changedLine || 0),
290
- diffPreview: String(change.diffPreview || '')
284
+
285
+ function fileChangeFingerprint(change) {
286
+ return JSON.stringify({
287
+ path: change.path,
288
+ action: change.action,
289
+ linesAdded: Number(change.linesAdded || 0),
290
+ linesRemoved: Number(change.linesRemoved || 0),
291
+ changedLine: Number(change.changedLine || 0),
292
+ diffPreview: String(change.diffPreview || ''),
293
+ changeSetId: String(change.changeSetId || ''),
294
+ patchRef: String(change.patchRef || '')
291
295
  });
292
296
  }
293
-
297
+
294
298
  function appendUniqueFileChange(message, fileChange) {
295
- const existing = Array.isArray(message.file_changes) ? message.file_changes : [];
296
- const nextKey = fileChangeFingerprint(fileChange);
297
- if (existing.some((change) => fileChangeFingerprint(normalizeFileChange(change) || {}) === nextKey)) {
298
- message.file_changes = existing;
299
- return;
300
- }
299
+ const existing = Array.isArray(message.file_changes) ? message.file_changes : [];
300
+ const nextKey = fileChangeFingerprint(fileChange);
301
+ if (existing.some((change) => fileChangeFingerprint(normalizeFileChange(change) || {}) === nextKey)) {
302
+ message.file_changes = existing;
303
+ return;
304
+ }
301
305
  message.file_changes = [...existing, fileChange];
302
306
  }
303
-
304
- export const trimInline = _trimInline;
307
+
308
+ function normalizeFileChanges(changes) {
309
+ return (Array.isArray(changes) ? changes : [changes])
310
+ .map(normalizeFileChange)
311
+ .filter(Boolean);
312
+ }
313
+
314
+ function extractToolResultMeta(toolName, result) {
315
+ if (!result || typeof result !== 'object') return null;
316
+ if (!['edit', 'write', 'delete'].includes(String(toolName || ''))) return null;
317
+ const meta = {};
318
+ for (const key of [
319
+ 'path',
320
+ 'action',
321
+ 'changed_line',
322
+ 'lines_added',
323
+ 'lines_removed',
324
+ 'backupPath',
325
+ 'backupRelativePath',
326
+ 'backupCreated',
327
+ 'backupReused',
328
+ 'backupSkipped',
329
+ 'backupError',
330
+ 'backupReason',
331
+ 'non_git_backup'
332
+ ]) {
333
+ if (result[key] !== undefined && result[key] !== null && result[key] !== '') meta[key] = result[key];
334
+ }
335
+ return Object.keys(meta).length ? meta : null;
336
+ }
337
+
338
+ export const trimInline = _trimInline;
305
339
 
306
340
  function normalizeAssistantText(value) {
307
341
  return String(value || '').trim();
@@ -537,7 +571,7 @@ function formatToolResult(toolResult, toolName, args, toolFormatters, toolResult
537
571
 
538
572
  // ─── Main agent loop ────────────────────────────────────────────────
539
573
 
540
- export async function runAgentLoop({
574
+ export async function runAgentLoop({
541
575
  systemPrompt,
542
576
  userPrompt,
543
577
  model,
@@ -547,16 +581,19 @@ export async function runAgentLoop({
547
581
  maxSteps = 8,
548
582
  initialMessages = [],
549
583
  onEvent,
550
- executionMode = 'auto',
584
+ executionMode = 'normal',
585
+ approvalMode = 'review',
586
+ projectIsGit = false,
551
587
  alwaysAllowTools = [],
552
588
  requestToolApproval,
553
589
  toolResultMaxChars = 12000,
554
590
  toolFormatters = {},
555
591
  deferredDefinitions = {},
556
- signal,
557
- skipAnalysisNudge = false,
558
- config = {}
559
- }) {
592
+ signal,
593
+ skipAnalysisNudge = false,
594
+ config = {},
595
+ changeTracker = null
596
+ }) {
560
597
  const messages = [];
561
598
  if (systemPrompt) {
562
599
  messages.push({ role: 'system', content: systemPrompt });
@@ -579,7 +616,7 @@ export async function runAgentLoop({
579
616
  const activeTools = [...toolDefinitions];
580
617
 
581
618
  async function maybeRunAutoDream(stepNumber = 0, { force = false } = {}) {
582
- if (executionMode === 'plan') return;
619
+ if ((executionMode === 'auto' ? 'normal' : executionMode) === 'plan') return;
583
620
  const interval = Math.max(1, Number(config?.memory?.auto_dream_check_interval_steps || 20));
584
621
  const normalizedStep = Math.max(1, Number(stepNumber || 1));
585
622
  if (!force && lastAutoDreamCheckStep > 0 && normalizedStep - lastAutoDreamCheckStep < interval) return;
@@ -635,13 +672,13 @@ export async function runAgentLoop({
635
672
  const assistantText = completion.text || '';
636
673
  lastAssistantText = assistantText || lastAssistantText;
637
674
 
638
- const assistantMessage = completion?.assistantMessage
639
- ? {
640
- ...completion.assistantMessage,
641
- role: 'assistant',
642
- content: completion.assistantMessage.content ?? completion?.content ?? assistantText
643
- }
644
- : { role: 'assistant', content: completion?.content ?? assistantText };
675
+ const assistantMessage = completion?.assistantMessage
676
+ ? {
677
+ ...completion.assistantMessage,
678
+ role: 'assistant',
679
+ content: completion.assistantMessage.content ?? completion?.content ?? assistantText
680
+ }
681
+ : { role: 'assistant', content: completion?.content ?? assistantText };
645
682
  if (!Array.isArray(assistantMessage.tool_calls) && toolCalls.length > 0) {
646
683
  assistantMessage.tool_calls = toolCalls.map((tc) => ({
647
684
  id: tc.id,
@@ -654,11 +691,11 @@ export async function runAgentLoop({
654
691
  onEvent({
655
692
  type: 'assistant:response',
656
693
  step: step + 1,
657
- text: assistantText,
658
- toolCalls: toolCalls.map((tc) => tc.name),
659
- usage: completion.usage || null,
660
- assistantMessage
661
- });
694
+ text: assistantText,
695
+ toolCalls: toolCalls.map((tc) => tc.name),
696
+ usage: completion.usage || null,
697
+ assistantMessage
698
+ });
662
699
  }
663
700
 
664
701
  if (toolCalls.length === 0) {
@@ -687,7 +724,14 @@ export async function runAgentLoop({
687
724
 
688
725
  pendingSummaryNudges = 0;
689
726
 
690
- if (executionMode === 'plan') {
727
+ const workMode = executionMode === 'auto' ? 'normal' : executionMode;
728
+ const normalizedApprovalMode = ['review', 'auto', 'full_access'].includes(String(approvalMode || '').toLowerCase())
729
+ ? String(approvalMode || '').toLowerCase()
730
+ : executionMode === 'auto'
731
+ ? 'auto'
732
+ : 'review';
733
+
734
+ if (workMode === 'plan') {
691
735
  const plannedLines = callsToPlanSummary(toolCalls);
692
736
  finalText = [
693
737
  assistantText || '',
@@ -713,17 +757,21 @@ export async function runAgentLoop({
713
757
  });
714
758
 
715
759
  // Approval checks first — must be done synchronously before any execution
716
- const approvalResults = new Map();
717
- for (const { call, toolName, displayName, args } of callsWithMeta) {
718
- let approved = true;
719
- let approvalArgs = args;
720
- let preflightErrorContent = '';
721
- const isSafeModeRun = toolName === 'run'
722
- && config?.policy?.safe_mode !== false
723
- && requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
724
- const needsApproval = toolName === 'delete' || isSafeModeRun
725
- || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
726
- if (needsApproval) {
760
+ const approvalResults = new Map();
761
+ for (const { call, toolName, displayName, args } of callsWithMeta) {
762
+ let approved = true;
763
+ let approvalArgs = args;
764
+ let preflightErrorContent = '';
765
+ const isSafeModeRun = toolName === 'run'
766
+ && config?.policy?.safe_mode !== false
767
+ && requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
768
+ const isFileWriteTool = toolName === 'edit' || toolName === 'write' || toolName === 'delete';
769
+ const needsApproval = normalizedApprovalMode === 'full_access'
770
+ ? false
771
+ : normalizedApprovalMode === 'auto'
772
+ ? ((!projectIsGit && isFileWriteTool) || isSafeModeRun)
773
+ : (toolName === 'delete' || isSafeModeRun || !alwaysAllowSet.has(toolName));
774
+ if (needsApproval) {
727
775
  approved = false;
728
776
  const handler = toolHandlers[toolName];
729
777
  if (toolName === 'delete' && typeof handler?.prepareApproval === 'function') {
@@ -749,10 +797,10 @@ export async function runAgentLoop({
749
797
  });
750
798
  approvalArgs = { ...args, _risk: evaluation.risk, _evaluation: evaluation };
751
799
  /* LLM says low-risk + allow → auto-approve, skip confirmation panel */
752
- if (executionMode !== 'normal' && evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
753
- approvalResults.set(call.id, { approved: true, args: approvalArgs });
754
- continue;
755
- }
800
+ if (normalizedApprovalMode !== 'review' && evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
801
+ approvalResults.set(call.id, { approved: true, args: approvalArgs });
802
+ continue;
803
+ }
756
804
  } catch (_) {
757
805
  approvalArgs = { ...args, _risk: 'high', _evaluation: null };
758
806
  }
@@ -862,9 +910,16 @@ export async function runAgentLoop({
862
910
  };
863
911
  }
864
912
 
865
- let toolResult;
866
- try {
867
- toolResult = await handler(effectiveArgs);
913
+ let captureScope = null;
914
+ if (!isReadOnly && changeTracker && typeof changeTracker.begin === 'function') {
915
+ try {
916
+ captureScope = await changeTracker.begin({ toolName, args: effectiveArgs });
917
+ } catch {}
918
+ }
919
+
920
+ let toolResult;
921
+ try {
922
+ toolResult = await handler(effectiveArgs);
868
923
  } catch (error) {
869
924
  const durationMs = Date.now() - startedAt;
870
925
  const message = error instanceof Error ? error.message : String(error);
@@ -885,13 +940,42 @@ export async function runAgentLoop({
885
940
  };
886
941
  }
887
942
 
888
- const durationMs = Date.now() - startedAt;
889
- const summary = summarizeToolResult(toolResult);
890
- /* 提取文件改动统计 */
891
- const fileChange = extractFileChange(toolName, toolResult);
892
- if (onEvent) {
893
- onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary, fileChange });
894
- }
943
+ const durationMs = Date.now() - startedAt;
944
+ const summary = summarizeToolResult(toolResult);
945
+ const resultMeta = extractToolResultMeta(toolName, toolResult);
946
+ /* 提取文件改动统计 */
947
+ const declaredFileChange = extractFileChange(toolName, toolResult);
948
+ let fileChanges = [];
949
+ let fileChange = null;
950
+ if (!isReadOnly && changeTracker && typeof changeTracker.capture === 'function' && captureScope) {
951
+ try {
952
+ const captured = await changeTracker.capture(captureScope, {
953
+ toolName,
954
+ toolCallId: call.id,
955
+ summary,
956
+ args: effectiveArgs,
957
+ declaredFileChanges: normalizeFileChanges(declaredFileChange)
958
+ });
959
+ const capturedChanges = normalizeFileChanges(captured);
960
+ if (capturedChanges.length) {
961
+ fileChanges = capturedChanges;
962
+ fileChange = fileChanges[0] || null;
963
+ }
964
+ } catch (error) {
965
+ const message = error instanceof Error ? error.message : String(error);
966
+ if (onEvent) {
967
+ onEvent({
968
+ type: 'system_tool:error',
969
+ id: `change-oplog-${call.id}`,
970
+ name: 'change_oplog',
971
+ summary: message
972
+ });
973
+ }
974
+ }
975
+ }
976
+ if (onEvent) {
977
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary, fileChange, fileChanges, resultMeta });
978
+ }
895
979
 
896
980
  // Auto-capture non-throwing tool failures (e.g. shell non-zero exit)
897
981
  if (toolResult && typeof toolResult === 'object') {
@@ -928,7 +1012,7 @@ export async function runAgentLoop({
928
1012
  // P0: Persist to disk if still large
929
1013
  formatted = await storeResultIfNeeded(call.id, formatted, toolResult);
930
1014
 
931
- return { callId: call.id, content: formatted, durationMs, summary, status: 'done', fileChange };
1015
+ return { callId: call.id, content: formatted, durationMs, summary, status: 'done', fileChange, fileChanges, resultMeta };
932
1016
  }
933
1017
 
934
1018
  // Separate read-only and write calls, preserving order
@@ -972,15 +1056,17 @@ export async function runAgentLoop({
972
1056
  continue;
973
1057
  }
974
1058
 
975
- attachToolCallSessionMeta(assistantMessage, call.id, { durationMs: entry.durationMs, summary: entry.summary || '', status: entry.status || 'done', fileChange: entry.fileChange });
1059
+ attachToolCallSessionMeta(assistantMessage, call.id, { durationMs: entry.durationMs, summary: entry.summary || '', status: entry.status || 'done', fileChange: entry.fileChange, fileChanges: entry.fileChanges, resultMeta: entry.resultMeta });
976
1060
  messages.push({
977
- role: 'tool',
978
- tool_call_id: call.id,
979
- content: entry.content,
980
- tool_duration_ms: entry.durationMs,
1061
+ role: 'tool',
1062
+ tool_call_id: call.id,
1063
+ content: entry.content,
1064
+ tool_duration_ms: entry.durationMs,
981
1065
  tool_summary: entry.summary || '',
982
1066
  tool_status: entry.status || 'done',
983
- ...(entry.fileChange ? { tool_file_change: entry.fileChange } : {})
1067
+ ...(entry.resultMeta ? { tool_result_meta: entry.resultMeta } : {}),
1068
+ ...(entry.fileChange ? { tool_file_change: entry.fileChange } : {}),
1069
+ ...(Array.isArray(entry.fileChanges) && entry.fileChanges.length > 0 ? { tool_file_changes: entry.fileChanges } : {})
984
1070
  });
985
1071
  if (onEvent) {
986
1072
  onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content });
@@ -1017,16 +1103,21 @@ function callsToPlanSummary(toolCalls = []) {
1017
1103
  });
1018
1104
  }
1019
1105
 
1020
- function attachToolCallSessionMeta(assistantMessage, callId, meta = {}) {
1021
- if (!assistantMessage || !Array.isArray(assistantMessage.tool_calls)) return;
1022
- const call = assistantMessage.tool_calls.find((tc) => String(tc?.id || '') === String(callId || ''));
1023
- if (!call) return;
1024
- if (Number.isFinite(Number(meta.durationMs))) call.durationMs = Number(meta.durationMs);
1106
+ function attachToolCallSessionMeta(assistantMessage, callId, meta = {}) {
1107
+ if (!assistantMessage || !Array.isArray(assistantMessage.tool_calls)) return;
1108
+ const call = assistantMessage.tool_calls.find((tc) => String(tc?.id || '') === String(callId || ''));
1109
+ if (!call) return;
1110
+ if (Number.isFinite(Number(meta.durationMs))) call.durationMs = Number(meta.durationMs);
1025
1111
  if (typeof meta.summary === 'string' && meta.summary.trim()) call.summary = meta.summary.trim();
1026
1112
  if (typeof meta.status === 'string' && meta.status.trim()) call.status = meta.status.trim();
1113
+ if (meta.resultMeta && typeof meta.resultMeta === 'object') call.resultMeta = meta.resultMeta;
1027
1114
  const fileChange = normalizeFileChange(meta.fileChange);
1028
1115
  if (fileChange) {
1029
1116
  call.fileChange = fileChange;
1030
- appendUniqueFileChange(assistantMessage, fileChange);
1117
+ }
1118
+ const fileChanges = normalizeFileChanges(meta.fileChanges && meta.fileChanges.length ? meta.fileChanges : fileChange);
1119
+ if (fileChanges.length) {
1120
+ call.fileChanges = fileChanges;
1121
+ for (const change of fileChanges) appendUniqueFileChange(assistantMessage, change);
1031
1122
  }
1032
1123
  }