summaryrefslogtreecommitdiff
path: root/packages/cli/src/config/memoryUtils.ts
blob: d44b5ebfa8296db24105cc64b3657a18d68d1276 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as fs from 'fs/promises';
import * as path from 'path';
import { homedir } from 'os';
import { SETTINGS_DIRECTORY_NAME } from './settings.js';
import {
  getErrorMessage,
  MemoryTool,
  GEMINI_MD_FILENAME,
  MEMORY_SECTION_HEADER,
} from '@gemini-code/server';

/**
 * Gets the absolute path to the global GEMINI.md file.
 */
export function getGlobalMemoryFilePath(): string {
  return path.join(homedir(), SETTINGS_DIRECTORY_NAME, GEMINI_MD_FILENAME);
}

/**
 * Adds a new memory entry to the global GEMINI.md file under the specified header.
 */
export async function addMemoryEntry(text: string): Promise<void> {
  const filePath = getGlobalMemoryFilePath();
  // The performAddMemoryEntry method from MemoryTool will handle its own errors
  // and throw an appropriately formatted error if needed.
  await MemoryTool.performAddMemoryEntry(text, filePath, {
    readFile: fs.readFile,
    writeFile: fs.writeFile,
    mkdir: fs.mkdir,
  });
}

/**
 * Deletes the last added memory entry from the "Gemini Added Memories" section.
 */
export async function deleteLastMemoryEntry(): Promise<boolean> {
  const filePath = getGlobalMemoryFilePath();
  try {
    let content = await fs.readFile(filePath, 'utf-8');
    const headerIndex = content.indexOf(MEMORY_SECTION_HEADER);

    if (headerIndex === -1) return false; // Section not found

    const startOfSectionContent = headerIndex + MEMORY_SECTION_HEADER.length;
    let endOfSectionIndex = content.indexOf('\n## ', startOfSectionContent);
    if (endOfSectionIndex === -1) {
      endOfSectionIndex = content.length;
    }

    const sectionPart = content.substring(
      startOfSectionContent,
      endOfSectionIndex,
    );
    const lines = sectionPart.split(/\r?\n/).map((line) => line.trimEnd());

    let lastBulletLineIndex = -1;
    for (let i = lines.length - 1; i >= 0; i--) {
      if (lines[i].trim().startsWith('- ')) {
        lastBulletLineIndex = i;
        break;
      }
    }

    if (lastBulletLineIndex === -1) return false; // No bullets found in section

    lines.splice(lastBulletLineIndex, 1);

    const newSectionPart = lines
      .filter((line) => line.trim().length > 0)
      .join('\n');

    const beforeHeader = content.substring(0, headerIndex);
    const afterSection = content.substring(endOfSectionIndex);

    if (newSectionPart.trim().length === 0) {
      // If section is now empty (no bullets), remove header too or leave it clean
      // For simplicity, let's leave the header but ensure it has a newline after if content follows
      content = `${beforeHeader}${MEMORY_SECTION_HEADER}\n${afterSection}`
        .replace(/\n{3,}/g, '\n\n')
        .trimEnd();
      if (content.length > 0) content += '\n';
    } else {
      content =
        `${beforeHeader}${MEMORY_SECTION_HEADER}\n${newSectionPart}\n${afterSection}`
          .replace(/\n{3,}/g, '\n\n')
          .trimEnd();
      if (content.length > 0) content += '\n';
    }

    await fs.writeFile(filePath, content, 'utf-8');
    return true;
  } catch (error) {
    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
      return false;
    }
    console.error(`Error deleting last memory entry from ${filePath}:`, error);
    throw new Error(
      `Failed to delete last memory entry: ${getErrorMessage(error)}`,
    );
  }
}

/**
 * Deletes all added memory entries (the entire "Gemini Added Memories" section).
 */
export async function deleteAllAddedMemoryEntries(): Promise<number> {
  const filePath = getGlobalMemoryFilePath();
  try {
    let content = await fs.readFile(filePath, 'utf-8');
    const headerIndex = content.indexOf(MEMORY_SECTION_HEADER);

    if (headerIndex === -1) return 0; // Section not found

    let endOfSectionIndex = content.indexOf(
      '\n## ',
      headerIndex + MEMORY_SECTION_HEADER.length,
    );
    if (endOfSectionIndex === -1) {
      endOfSectionIndex = content.length; // Section goes to EOF
    }

    const sectionContent = content.substring(headerIndex, endOfSectionIndex);
    const bulletCount = (sectionContent.match(/\n- /g) || []).length;

    if (bulletCount === 0 && !sectionContent.includes('- ')) {
      // No bullets found
      // If we only remove if bullets exist, or remove header if no bullets.
      // For now, if header exists but no bullets, consider 0 deleted if we only count bullets.
      // If the goal is to remove the section if it exists, this logic changes.
      // Let's assume we only care about bulleted items for the count.
    }

    // Remove the section including the header
    const beforeHeader = content.substring(0, headerIndex);
    const afterSection = content.substring(endOfSectionIndex);

    content = (
      beforeHeader.trimEnd() +
      (afterSection.length > 0 ? '\n' + afterSection.trimStart() : '')
    ).trim();
    if (content.length > 0) content += '\n';

    await fs.writeFile(filePath, content, 'utf-8');
    return bulletCount; // This counts '\n- ' occurrences, might need refinement for exact bullet count
  } catch (error) {
    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
      return 0;
    }
    console.error(
      `Error deleting all added memory entries from ${filePath}:`,
      error,
    );
    throw new Error(
      `Failed to delete all added memory entries: ${getErrorMessage(error)}`,
    );
  }
}