/ src / tools / MemoryReadTool / MemoryReadTool.tsx
MemoryReadTool.tsx
  1  import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync } from 'fs'
  2  import { Box, Text } from 'ink'
  3  import { join } from 'path'
  4  import * as React from 'react'
  5  import { z } from 'zod'
  6  import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js'
  7  import { Tool } from '../../Tool.js'
  8  import { MEMORY_DIR } from '../../utils/env.js'
  9  import { DESCRIPTION, PROMPT } from './prompt.js'
 10  
 11  const inputSchema = z.strictObject({
 12    file_path: z
 13      .string()
 14      .optional()
 15      .describe('Optional path to a specific memory file to read'),
 16  })
 17  
 18  export const MemoryReadTool = {
 19    name: 'MemoryRead',
 20    async description() {
 21      return DESCRIPTION
 22    },
 23    async prompt() {
 24      return PROMPT
 25    },
 26    inputSchema,
 27    userFacingName() {
 28      return 'Read Memory'
 29    },
 30    async isEnabled() {
 31      // TODO: Use a statsig gate
 32      // TODO: Figure out how to do that without regressing app startup perf
 33      return false
 34    },
 35    isReadOnly() {
 36      return true
 37    },
 38    needsPermissions() {
 39      return false
 40    },
 41    renderResultForAssistant({ content }) {
 42      return content
 43    },
 44    renderToolUseMessage(input) {
 45      return Object.entries(input)
 46        .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
 47        .join(', ')
 48    },
 49    renderToolUseRejectedMessage() {
 50      return <FallbackToolUseRejectedMessage />
 51    },
 52    renderToolResultMessage(output) {
 53      return (
 54        <Box justifyContent="space-between" overflowX="hidden" width="100%">
 55          <Box flexDirection="row">
 56            <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
 57            <Text>{output.content}</Text>
 58          </Box>
 59        </Box>
 60      )
 61    },
 62    async validateInput({ file_path }) {
 63      if (file_path) {
 64        const fullPath = join(MEMORY_DIR, file_path)
 65        if (!fullPath.startsWith(MEMORY_DIR)) {
 66          return { result: false, message: 'Invalid memory file path' }
 67        }
 68        if (!existsSync(fullPath)) {
 69          return { result: false, message: 'Memory file does not exist' }
 70        }
 71      }
 72      return { result: true }
 73    },
 74    async *call({ file_path }) {
 75      mkdirSync(MEMORY_DIR, { recursive: true })
 76  
 77      // If a specific file is requested, return its contents
 78      if (file_path) {
 79        const fullPath = join(MEMORY_DIR, file_path)
 80        if (!existsSync(fullPath)) {
 81          throw new Error('Memory file does not exist')
 82        }
 83        const content = readFileSync(fullPath, 'utf-8')
 84        yield {
 85          type: 'result',
 86          data: {
 87            content,
 88          },
 89          resultForAssistant: this.renderResultForAssistant({ content }),
 90        }
 91        return
 92      }
 93  
 94      // Otherwise return the index and file list
 95      const files = readdirSync(MEMORY_DIR, { recursive: true })
 96        .map(f => join(MEMORY_DIR, f.toString()))
 97        .filter(f => !lstatSync(f).isDirectory())
 98        .map(f => `- ${f}`)
 99        .join('\n')
100  
101      const indexPath = join(MEMORY_DIR, 'index.md')
102      const index = existsSync(indexPath) ? readFileSync(indexPath, 'utf-8') : ''
103  
104      const quotes = "'''"
105      const content = `Here are the contents of the root memory file, \`${indexPath}\`:
106  ${quotes}
107  ${index}
108  ${quotes}
109  
110  Files in the memory directory:
111  ${files}`
112      yield {
113        type: 'result',
114        data: { content },
115        resultForAssistant: this.renderResultForAssistant({ content }),
116      }
117    },
118  } satisfies Tool<typeof inputSchema, { content: string }>