codemini-cli 0.6.3 → 0.6.5

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 (49) hide show
  1. package/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-BUp8EzDg.js} +2 -2
  2. package/codemini-web/dist/assets/CodeWikiPanel-Fp0VKdzo.js +1 -0
  3. package/codemini-web/dist/assets/ConfigDialog-DIpj779O.js +1 -0
  4. package/codemini-web/dist/assets/GitDiffDialog-ZLEuX8Qm.js +3 -0
  5. package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-D2YbENVd.js} +3 -3
  6. package/codemini-web/dist/assets/MessageBubble-BIgpZsLn.js +12 -0
  7. package/codemini-web/dist/assets/PatchDiff-CvKNaHsw.js +230 -0
  8. package/codemini-web/dist/assets/ProjectSelector-DXIep3lE.js +1 -0
  9. package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-DjPF-XBx.js} +4 -4
  10. package/codemini-web/dist/assets/SoulDialog-BfIoKETs.js +1 -0
  11. package/codemini-web/dist/assets/chevron-right-CfNZHlyU.js +1 -0
  12. package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-DMUdjM9q.js} +6 -6
  13. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-8ch0jz7Z.js} +1 -1
  14. package/codemini-web/dist/assets/index-BhMtCC8_.js +65 -0
  15. package/codemini-web/dist/assets/index-DRXwJ-n_.css +2 -0
  16. package/codemini-web/dist/assets/input-CYpdNDlR.js +1 -0
  17. package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
  18. package/codemini-web/dist/assets/mermaid-GHXKKRXX-KBEtMEB9.js +1 -0
  19. package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BdA2cEeE.js} +1 -1
  20. package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-CJGgUGiS.js} +1 -1
  21. package/codemini-web/dist/assets/select-BLOccU1M.js +1 -0
  22. package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-CQzNOch5.js} +1 -1
  23. package/codemini-web/dist/index.html +2 -2
  24. package/codemini-web/lib/runtime-bridge.js +332 -296
  25. package/codemini-web/server.js +319 -243
  26. package/package.json +1 -1
  27. package/src/core/agent-loop.js +188 -100
  28. package/src/core/chat-runtime.js +676 -571
  29. package/src/core/config-store.js +9 -3
  30. package/src/core/git-oplog-change-tracker.js +468 -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 +555 -434
  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/input-CNQgbKe6.js +0 -1
  46. package/codemini-web/dist/assets/lib-BOngVP_M.js +0 -11
  47. package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
  48. package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
  49. 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.5",
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 });
@@ -576,10 +613,10 @@ export async function runAgentLoop({
576
613
  let lastAutoDreamCheckStep = 0;
577
614
 
578
615
  // Mutable tool list — grows as tool_search loads deferred tools
579
- const activeTools = [...toolDefinitions];
580
-
581
- async function maybeRunAutoDream(stepNumber = 0, { force = false } = {}) {
582
- if (executionMode === 'plan') return;
616
+ const activeTools = [...toolDefinitions];
617
+
618
+ async function maybeRunAutoDream(stepNumber = 0, { force = false } = {}) {
619
+ if (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,11 @@ export async function runAgentLoop({
687
724
 
688
725
  pendingSummaryNudges = 0;
689
726
 
690
- if (executionMode === 'plan') {
727
+ const normalizedApprovalMode = ['review', 'auto', 'full_access'].includes(String(approvalMode || '').toLowerCase())
728
+ ? String(approvalMode || '').toLowerCase()
729
+ : 'review';
730
+
731
+ if (executionMode === 'plan') {
691
732
  const plannedLines = callsToPlanSummary(toolCalls);
692
733
  finalText = [
693
734
  assistantText || '',
@@ -713,17 +754,21 @@ export async function runAgentLoop({
713
754
  });
714
755
 
715
756
  // 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) {
757
+ const approvalResults = new Map();
758
+ for (const { call, toolName, displayName, args } of callsWithMeta) {
759
+ let approved = true;
760
+ let approvalArgs = args;
761
+ let preflightErrorContent = '';
762
+ const isSafeModeRun = toolName === 'run'
763
+ && config?.policy?.safe_mode !== false
764
+ && requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
765
+ const isFileWriteTool = toolName === 'edit' || toolName === 'write' || toolName === 'delete';
766
+ const needsApproval = normalizedApprovalMode === 'full_access'
767
+ ? false
768
+ : normalizedApprovalMode === 'auto'
769
+ ? ((!projectIsGit && isFileWriteTool) || isSafeModeRun)
770
+ : (toolName === 'delete' || isSafeModeRun || !alwaysAllowSet.has(toolName));
771
+ if (needsApproval) {
727
772
  approved = false;
728
773
  const handler = toolHandlers[toolName];
729
774
  if (toolName === 'delete' && typeof handler?.prepareApproval === 'function') {
@@ -749,10 +794,10 @@ export async function runAgentLoop({
749
794
  });
750
795
  approvalArgs = { ...args, _risk: evaluation.risk, _evaluation: evaluation };
751
796
  /* 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
- }
797
+ if (normalizedApprovalMode !== 'review' && evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
798
+ approvalResults.set(call.id, { approved: true, args: approvalArgs });
799
+ continue;
800
+ }
756
801
  } catch (_) {
757
802
  approvalArgs = { ...args, _risk: 'high', _evaluation: null };
758
803
  }
@@ -862,9 +907,16 @@ export async function runAgentLoop({
862
907
  };
863
908
  }
864
909
 
865
- let toolResult;
866
- try {
867
- toolResult = await handler(effectiveArgs);
910
+ let captureScope = null;
911
+ if (!isReadOnly && changeTracker && typeof changeTracker.begin === 'function') {
912
+ try {
913
+ captureScope = await changeTracker.begin({ toolName, args: effectiveArgs });
914
+ } catch {}
915
+ }
916
+
917
+ let toolResult;
918
+ try {
919
+ toolResult = await handler(effectiveArgs);
868
920
  } catch (error) {
869
921
  const durationMs = Date.now() - startedAt;
870
922
  const message = error instanceof Error ? error.message : String(error);
@@ -885,13 +937,42 @@ export async function runAgentLoop({
885
937
  };
886
938
  }
887
939
 
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
- }
940
+ const durationMs = Date.now() - startedAt;
941
+ const summary = summarizeToolResult(toolResult);
942
+ const resultMeta = extractToolResultMeta(toolName, toolResult);
943
+ /* 提取文件改动统计 */
944
+ const declaredFileChange = extractFileChange(toolName, toolResult);
945
+ let fileChanges = [];
946
+ let fileChange = null;
947
+ if (!isReadOnly && changeTracker && typeof changeTracker.capture === 'function' && captureScope) {
948
+ try {
949
+ const captured = await changeTracker.capture(captureScope, {
950
+ toolName,
951
+ toolCallId: call.id,
952
+ summary,
953
+ args: effectiveArgs,
954
+ declaredFileChanges: normalizeFileChanges(declaredFileChange)
955
+ });
956
+ const capturedChanges = normalizeFileChanges(captured);
957
+ if (capturedChanges.length) {
958
+ fileChanges = capturedChanges;
959
+ fileChange = fileChanges[0] || null;
960
+ }
961
+ } catch (error) {
962
+ const message = error instanceof Error ? error.message : String(error);
963
+ if (onEvent) {
964
+ onEvent({
965
+ type: 'system_tool:error',
966
+ id: `change-oplog-${call.id}`,
967
+ name: 'change_oplog',
968
+ summary: message
969
+ });
970
+ }
971
+ }
972
+ }
973
+ if (onEvent) {
974
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary, fileChange, fileChanges, resultMeta });
975
+ }
895
976
 
896
977
  // Auto-capture non-throwing tool failures (e.g. shell non-zero exit)
897
978
  if (toolResult && typeof toolResult === 'object') {
@@ -928,7 +1009,7 @@ export async function runAgentLoop({
928
1009
  // P0: Persist to disk if still large
929
1010
  formatted = await storeResultIfNeeded(call.id, formatted, toolResult);
930
1011
 
931
- return { callId: call.id, content: formatted, durationMs, summary, status: 'done', fileChange };
1012
+ return { callId: call.id, content: formatted, durationMs, summary, status: 'done', fileChange, fileChanges, resultMeta };
932
1013
  }
933
1014
 
934
1015
  // Separate read-only and write calls, preserving order
@@ -972,15 +1053,17 @@ export async function runAgentLoop({
972
1053
  continue;
973
1054
  }
974
1055
 
975
- attachToolCallSessionMeta(assistantMessage, call.id, { durationMs: entry.durationMs, summary: entry.summary || '', status: entry.status || 'done', fileChange: entry.fileChange });
1056
+ attachToolCallSessionMeta(assistantMessage, call.id, { durationMs: entry.durationMs, summary: entry.summary || '', status: entry.status || 'done', fileChange: entry.fileChange, fileChanges: entry.fileChanges, resultMeta: entry.resultMeta });
976
1057
  messages.push({
977
- role: 'tool',
978
- tool_call_id: call.id,
979
- content: entry.content,
980
- tool_duration_ms: entry.durationMs,
1058
+ role: 'tool',
1059
+ tool_call_id: call.id,
1060
+ content: entry.content,
1061
+ tool_duration_ms: entry.durationMs,
981
1062
  tool_summary: entry.summary || '',
982
1063
  tool_status: entry.status || 'done',
983
- ...(entry.fileChange ? { tool_file_change: entry.fileChange } : {})
1064
+ ...(entry.resultMeta ? { tool_result_meta: entry.resultMeta } : {}),
1065
+ ...(entry.fileChange ? { tool_file_change: entry.fileChange } : {}),
1066
+ ...(Array.isArray(entry.fileChanges) && entry.fileChanges.length > 0 ? { tool_file_changes: entry.fileChanges } : {})
984
1067
  });
985
1068
  if (onEvent) {
986
1069
  onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content });
@@ -1017,16 +1100,21 @@ function callsToPlanSummary(toolCalls = []) {
1017
1100
  });
1018
1101
  }
1019
1102
 
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);
1103
+ function attachToolCallSessionMeta(assistantMessage, callId, meta = {}) {
1104
+ if (!assistantMessage || !Array.isArray(assistantMessage.tool_calls)) return;
1105
+ const call = assistantMessage.tool_calls.find((tc) => String(tc?.id || '') === String(callId || ''));
1106
+ if (!call) return;
1107
+ if (Number.isFinite(Number(meta.durationMs))) call.durationMs = Number(meta.durationMs);
1025
1108
  if (typeof meta.summary === 'string' && meta.summary.trim()) call.summary = meta.summary.trim();
1026
1109
  if (typeof meta.status === 'string' && meta.status.trim()) call.status = meta.status.trim();
1110
+ if (meta.resultMeta && typeof meta.resultMeta === 'object') call.resultMeta = meta.resultMeta;
1027
1111
  const fileChange = normalizeFileChange(meta.fileChange);
1028
1112
  if (fileChange) {
1029
1113
  call.fileChange = fileChange;
1030
- appendUniqueFileChange(assistantMessage, fileChange);
1114
+ }
1115
+ const fileChanges = normalizeFileChanges(meta.fileChanges && meta.fileChanges.length ? meta.fileChanges : fileChange);
1116
+ if (fileChanges.length) {
1117
+ call.fileChanges = fileChanges;
1118
+ for (const change of fileChanges) appendUniqueFileChange(assistantMessage, change);
1031
1119
  }
1032
1120
  }