Init-Files/site-lisp/claude.el
2025-04-25 18:15:27 +00:00

907 lines
36 KiB
EmacsLisp

;;; claude.el --- Integration with Claude AI assistant -*- lexical-binding: t -*-
;; Author: John Doty <john@d0ty.me>; mostly generated by claude.ai
;; Version: 1.0.0
;; Package-Requires: ((emacs "27.1") (request "0.3.0") (json "1.5"))
;; Keywords: tools, ai, assistant
;;; Commentary:
;; This package provides integration with the Claude AI assistant from
;; Anthropic. It includes basic functionality like sending text to Claude, as
;; well as code-specific features and tool use capabilities.
;;
;; It now features a conversational interface with support for context buffers
;; and optional tool use.
;;; Code:
(require 'request)
(require 'json)
(require 'auth-source)
;;; Customization:
(defgroup claude nil
"Integration with Claude AI assistant."
:group 'tools)
(defcustom claude-model "claude-3-7-sonnet-20250219"
"Claude model to use."
:type 'string
:group 'claude)
(defcustom claude-max-tokens 4000
"Maximum number of tokens in Claude's response."
:type 'integer
:group 'claude)
(defcustom claude-auto-display-results t
"If non-nil, automatically display Claude's responses."
:type 'boolean
:group 'claude)
(defcustom claude-conversation-buffer-name "*Claude Conversation*"
"Name of the buffer for Claude conversations."
:type 'string
:group 'claude)
(defcustom claude-enable-markdown-fontification t
"If non-nil, enable markdown fontification in the conversation buffer."
:type 'boolean
:group 'claude)
(defcustom claude-user-message-face 'font-lock-keyword-face
"Face for user messages in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-assistant-message-face 'font-lock-comment-face
"Face for Claude's messages in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-timestamp-face 'font-lock-doc-face
"Face for timestamps in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-separator-string "────────────────────────────────────────────────────────────"
"Separator string used between messages in the conversation buffer."
:type 'string
:group 'claude)
;;; Core functionality:
(defvar claude-conversation-history nil
"History of conversation with Claude as a list of alists with keys 'role' and 'content'.")
(defvar claude-context-buffers nil
"List of buffers to provide as context for the conversation.")
(defvar claude-tools-enabled nil
"If non-nil, enable tool use in the conversation.")
(defvar claude-conversation-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") 'claude-send-input)
(define-key map (kbd "C-c C-c") 'claude-send-input)
(define-key map (kbd "C-c C-k") 'claude-cancel-input)
;; Change these keybindings to avoid conflict with the global prefix
(define-key map (kbd "C-c a") 'claude-add-context-buffer) ;; was C-c C-a
(define-key map (kbd "C-c r") 'claude-remove-context-buffer) ;; was C-c C-r
(define-key map (kbd "C-c l") 'claude-list-context-buffers) ;; was C-c C-l
(define-key map (kbd "C-c t") 'claude-toggle-tool-use) ;; was C-c C-t
(define-key map (kbd "C-c n") 'claude-new-conversation) ;; was C-c C-n
map)
"Keymap for Claude conversation buffers.")
(define-derived-mode claude-conversation-mode text-mode "Claude Conversation"
"Major mode for conversing with Claude AI assistant."
;; Enable visual line mode for word wrapping
(visual-line-mode 1)
(setq-local indent-tabs-mode nil)
(use-local-map claude-conversation-mode-map)
(when claude-enable-markdown-fontification
(when (fboundp 'markdown-mode)
;; We only want the font-lock features of markdown mode
(setq-local font-lock-defaults (cadr (assoc 'font-lock-defaults
(symbol-plist 'markdown-mode))))))
;; Make the bottom of the buffer input-only
(setq-local claude-input-marker (point-max-marker))
(set-marker-insertion-type claude-input-marker nil)) ;; ?
(defun claude-get-api-key ()
"Get Claude API key from auth-source."
(let ((auth-info (nth 0 (auth-source-search :host "anthropic.com"
:user "claude-api"
:require '(:secret)
:create t))))
(if auth-info
(let ((secret (plist-get auth-info :secret)))
(if (functionp secret)
(funcall secret)
secret))
(error "Claude API key not found in auth-source"))))
(defun claude-ensure-conversation-buffer ()
"Ensure the Claude conversation buffer exists and return it."
(let ((buffer (get-buffer-create claude-conversation-buffer-name)))
(with-current-buffer buffer
(unless (eq major-mode 'claude-conversation-mode)
(claude-conversation-mode)
(claude-insert-conversation-header)))
buffer))
(defun claude-insert-conversation-header ()
"Insert the header for a new conversation."
(let ((inhibit-read-only t))
(erase-buffer)
(insert (propertize "Claude Conversation" 'face 'bold) "\n\n")
(insert "Type your message below and press RET or C-c C-c to send.\n")
(insert "Commands:\n")
(insert " C-c a: Add a context buffer\n") ;; was C-c C-a
(insert " C-c r: Remove a context buffer\n") ;; was C-c C-r
(insert " C-c l: List context buffers\n") ;; was C-c C-l
(insert " C-c t: Toggle tool use\n") ;; was C-c C-t
(insert " C-c n: Start a new conversation\n") ;; was C-c C-n
(insert "\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")
;; Set the input marker at the end of the buffer
(setq-local claude-input-marker (point-marker))
(set-marker-insertion-type claude-input-marker nil)))
(defun claude-send-request (data &optional callback error-callback)
"Send request with DATA to Claude API.
If CALLBACK is provided, call it with the response data.
If ERROR-CALLBACK is provided, call it with any error."
(let ((api-key (claude-get-api-key)))
(request
"https://api.anthropic.com/v1/messages"
:type "POST"
:headers `(("Content-Type" . "application/json")
("x-api-key" . ,api-key)
("anthropic-version" . "2023-06-01"))
:data (json-encode data)
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(when callback
(funcall callback data))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(if error-callback
(funcall error-callback error-thrown)
(message "Claude API error: %s" error-thrown)))))))
(defun claude-append-to-conversation (role content)
"Append a message with ROLE and CONTENT to the conversation history."
(add-to-list 'claude-conversation-history
`((role . ,role)
(content . ,content))
t))
(defun claude-display-user-message (message)
"Display the user MESSAGE in the conversation buffer."
(let ((buffer (claude-ensure-conversation-buffer))
(timestamp (format-time-string "%H:%M:%S")))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[You - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize message 'face claude-user-message-face))
(insert "\n\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")))
(setq-local claude-input-marker (point-max-marker)))))
(defun claude-display-assistant-message (content)
"Display the assistant's CONTENT in the conversation buffer."
(let ((buffer (claude-ensure-conversation-buffer))
(timestamp (format-time-string "%H:%M:%S")))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[Claude - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize content 'face claude-assistant-message-face))
(insert "\n\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")))
(setq-local claude-input-marker (point-max-marker))
(goto-char (point-max)))))
(defun claude-display-loading-indicator ()
"Display a loading indicator in the conversation buffer."
(let ((buffer (claude-ensure-conversation-buffer))
(timestamp (format-time-string "%H:%M:%S")))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[Claude - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize "Thinking..." 'face 'italic))
;; Don't insert separator yet
(insert "\n\n")
(setq-local claude-loading-indicator-point (point-marker))
(set-marker-insertion-type claude-loading-indicator-point t))))))
(defun claude-remove-loading-indicator ()
"Remove the loading indicator from the conversation buffer."
(when (and (boundp 'claude-loading-indicator-point)
claude-loading-indicator-point)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-loading-indicator-point)
(forward-line -3) ;; Go to the start of the loading message
(delete-region (point) claude-loading-indicator-point)))))))
(defun claude-process-response (data)
"Process and display the response DATA from Claude."
(let ((content (cdr (assoc 'content data))))
(claude-remove-loading-indicator)
(if (and content (arrayp content) (> (length content) 0))
(let ((response-text ""))
;; Collect text content from all response parts
(dotimes (i (length content))
(let* ((item (aref content i))
(type (cdr (assoc 'type item))))
(when (string= type "text")
(let ((text (cdr (assoc 'text item))))
(setq response-text (concat response-text text))))))
;; Add to conversation history
(claude-append-to-conversation "assistant" response-text)
;; Display the response
(claude-display-assistant-message response-text))
;; Handle unexpected response format
(let ((error-message "Received an unexpected response format from Claude API."))
(claude-display-assistant-message
(concat error-message "\n\nResponse data: " (prin1-to-string data))))))
;; Move to input area
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char (point-max))))
(defun claude-get-context-content ()
"Get the content from all context buffers."
(let ((context-content ""))
(dolist (buffer claude-context-buffers)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(setq context-content
(concat context-content
(format "\n\nContent from buffer '%s':\n\n%s"
(buffer-name buffer)
(buffer-substring-no-properties (point-min) (point-max))))))))
context-content))
(defun claude-send-message (prompt)
"Send PROMPT to Claude and display the response in the conversation buffer."
;; Add user message to history
(claude-append-to-conversation "user" prompt)
;; Display the user's message
(claude-display-user-message prompt)
;; Show loading indicator
(claude-display-loading-indicator)
;; Prepare messages for the API
(let* ((context (claude-get-context-content))
(final-prompt (if (string-empty-p context)
prompt
(concat prompt "\n\n" context)))
(messages-array (vconcat
(apply #'vector
(mapcar (lambda (msg)
`((role . ,(cdr (assoc 'role msg)))
(content . ,(cdr (assoc 'content msg)))))
claude-conversation-history))))
(data `((model . ,claude-model)
(max_tokens . ,claude-max-tokens)
(messages . ,messages-array))))
;; Add tools if enabled
(when claude-tools-enabled
(setq data (append data `((tools . ,(mapcar (lambda (tool)
`((name . ,(plist-get tool :name))
(description . ,(plist-get tool :description))
(parameters . ,(plist-get tool :parameters))))
claude-tools))
(tool_choice . "auto")))))
;; Send the request
(claude-send-request
data
(lambda (data)
(if (and claude-tools-enabled
(or (assoc 'tool_use (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0))
(assoc 'tool_result (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0))))
(claude-handle-tool-response data (claude-ensure-conversation-buffer))
(claude-process-response data)))
(lambda (error-thrown)
(claude-remove-loading-indicator)
(claude-display-assistant-message (format "Error: %s" error-thrown))))))
(defun claude-send-input ()
"Send the current input to Claude."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(let ((input (buffer-substring-no-properties claude-input-marker (point-max))))
(when (not (string-empty-p (string-trim input)))
;; Delete the input text
(let ((inhibit-read-only t))
(delete-region claude-input-marker (point-max)))
;; Send the message
(claude-send-message (string-trim input)))))))
(defun claude-cancel-input ()
"Cancel the current input."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(delete-region claude-input-marker (point-max))))))
(defun claude-new-conversation ()
"Start a new conversation with Claude."
(interactive)
(setq claude-conversation-history nil)
(setq claude-context-buffers nil)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(claude-insert-conversation-header)))
(message "Started a new conversation with Claude"))
(defun claude-add-context-buffer (buffer)
"Add BUFFER to the list of context buffers."
(interactive (list (read-buffer "Add buffer as context: " (other-buffer (current-buffer) t))))
(let ((buf (get-buffer buffer)))
(when buf
(unless (memq buf claude-context-buffers)
(add-to-list 'claude-context-buffers buf)
(message "Added %s to context buffers" (buffer-name buf))))))
(defun claude-remove-context-buffer (buffer)
"Remove BUFFER from the list of context buffers."
(interactive
(list
(when claude-context-buffers
(let ((buffer-names (mapcar #'buffer-name claude-context-buffers)))
(completing-read "Remove buffer from context: " buffer-names nil t)))))
(when buffer
(let ((buf (get-buffer buffer)))
(when buf
(setq claude-context-buffers (delq buf claude-context-buffers))
(message "Removed %s from context buffers" (buffer-name buf))))))
(defun claude-list-context-buffers ()
"List all context buffers in the minibuffer."
(interactive)
(if claude-context-buffers
(let ((buffer-list (mapconcat #'buffer-name claude-context-buffers ", ")))
(message "Context buffers: %s" buffer-list))
(message "No context buffers set")))
(defun claude-toggle-tool-use ()
"Toggle the use of tools in the conversation."
(interactive)
(setq claude-tools-enabled (not claude-tools-enabled))
(message "Claude tool use %s" (if claude-tools-enabled "enabled" "disabled")))
;;; Tool use functionality:
(defvar claude-tools nil
"List of tool definitions available to Claude.")
(defvar claude-tool-handlers (make-hash-table :test 'equal)
"Hash table of tool handlers, keyed by tool name.")
(defvar claude-tool-requests nil
"List of tools that Claude has requested but aren't registered.")
(defun claude-register-tool (tool-def)
"Register a tool definition for Claude to use.
TOOL-DEF should be a plist with :name, :description, and :parameters."
(add-to-list 'claude-tools tool-def t))
(defun claude-clear-tools ()
"Clear all registered tools."
(setq claude-tools nil))
(defun claude-register-tool-handler (tool-name handler)
"Register a HANDLER function for TOOL-NAME.
The handler will be called with the parameters passed by Claude."
(puthash tool-name handler claude-tool-handlers))
(defun claude-execute-tool (tool-name parameters)
"Execute the tool with TOOL-NAME using PARAMETERS."
(let ((handler (gethash tool-name claude-tool-handlers)))
(if handler
(funcall handler parameters)
(format "Error: No handler registered for tool %s" tool-name))))
(defun claude-handle-tool-response (data buffer)
"Handle response DATA from Claude that may contain tool calls.
Display results in BUFFER."
(let ((message (aref (cdr (assoc 'messages data)) 0)))
(claude-remove-loading-indicator)
(with-current-buffer buffer
(let ((content-items (cdr (assoc 'content message)))
(tool-usage-text "")
(response-text ""))
;; Process all content items
(dotimes (i (length content-items))
(let* ((item (aref content-items i))
(type (cdr (assoc 'type item))))
(cond
((string= type "text")
(setq response-text
(concat response-text (cdr (assoc 'text item)) "\n\n")))
((string= type "tool_use")
(let* ((tool-use (cdr (assoc 'tool_use item)))
(tool-name (cdr (assoc 'name tool-use)))
(parameters (cdr (assoc 'parameters tool-use)))
(handler (gethash tool-name claude-tool-handlers)))
;; Check if we have this tool
(if handler
(let ((tool-result (claude-execute-tool tool-name parameters)))
(setq tool-usage-text
(concat tool-usage-text
(format "Tool call: %s\n" tool-name)
(format "Parameters: %s\n" (json-encode parameters))
(format "Result: %s\n\n" tool-result)))
;; Send the tool result back to Claude
(claude-send-tool-result data tool-name tool-result))
;; Tool not found - record the request and notify
(let ((description (format "Tool requested by Claude for task: %s"
(or (cdr (assoc 'description tool-use))
"No description provided")))
(param-structure (or (cdr (assoc 'parameter_structure tool-use))
parameters)))
;; Record the tool request
(claude-request-tool tool-name description param-structure)
;; Add to response text
(setq tool-usage-text
(concat tool-usage-text
(format "Tool requested: %s\n" tool-name)
"This tool is not currently available.\n"
"The request has been recorded. You can implement it with M-x claude-list-requested-tools.\n\n"))
;; Send error message back to Claude
(let ((error-msg (format "The requested tool '%s' is not currently available. Would you like me to suggest an alternative approach?" tool-name)))
(claude-send-tool-result data tool-name error-msg))))))))))
;; Display the combined message
(let ((full-response (concat response-text
(if (not (string-empty-p tool-usage-text))
(concat "\n## Tool Usage\n\n" tool-usage-text)
""))))
(claude-append-to-conversation "assistant" full-response)
(claude-display-assistant-message full-response)))))
(defun claude-send-tool-result (data tool-name tool-result)
"Send TOOL-RESULT for TOOL-NAME back to Claude based on the original DATA."
(let ((message-id (cdr (assoc 'id (aref (cdr (assoc 'messages data)) 0))))
(tool-call-id nil)
(api-key (claude-get-api-key))
(buffer (claude-ensure-conversation-buffer)))
;; Find the tool call ID
(let* ((content-items (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0)))))
(dotimes (i (length content-items))
(let* ((item (aref content-items i))
(type (cdr (assoc 'type item))))
(when (string= type "tool_use")
(setq tool-call-id (cdr (assoc 'id (cdr (assoc 'tool_use item)))))))))
(when tool-call-id
;; Show loading indicator
(claude-display-loading-indicator)
(request
"https://api.anthropic.com/v1/messages"
:type "POST"
:headers `(("Content-Type" . "application/json")
("x-api-key" . ,api-key)
("anthropic-version" . "2023-06-01"))
:data (json-encode
`((model . ,claude-model)
(max_tokens . ,claude-max-tokens)
(messages . ,(vconcat (cdr (assoc 'messages data))
`[((role . "assistant")
(content . [((type . "tool_result")
(tool_result . ((tool_call_id . ,tool-call-id)
(content . ,tool-result))))]))]))))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(claude-handle-tool-response data buffer)))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(claude-remove-loading-indicator)
(claude-display-assistant-message (format "Error: %s" error-thrown))))))))
(defun claude-request-tool (tool-name description parameters)
"Record a request from Claude for a tool that isn't available."
(add-to-list 'claude-tool-requests
(list :name tool-name
:description description
:parameters parameters)
t))
(defun claude-list-requested-tools ()
"Show all tools that Claude has requested but aren't registered."
(interactive)
(let ((buffer (get-buffer-create "*Claude Tool Requests*")))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert "# Tools Requested by Claude\n\n")
(if claude-tool-requests
(dolist (tool claude-tool-requests)
(insert (format "## %s\n\n" (plist-get tool :name)))
(insert (format "Description: %s\n\n" (plist-get tool :description)))
(insert "Parameters:\n")
(let ((params (plist-get tool :parameters)))
(dolist (param params)
(insert (format "- %s: %s\n"
(car param)
(cdr (assoc 'description (cdr param)))))))
(insert "\n"))
(insert "No tools have been requested yet.\n"))
(insert "\nUse M-x claude-implement-requested-tool to implement one of these tools.\n")))
(switch-to-buffer buffer)))
(defun claude-implement-requested-tool (tool-name)
"Provide a template to implement a requested tool."
(interactive
(list (completing-read "Select tool to implement: "
(mapcar (lambda (tool) (plist-get tool :name))
claude-tool-requests))))
(let* ((tool (car (cl-remove-if-not
(lambda (t) (string= (plist-get t :name) tool-name))
claude-tool-requests)))
(name (plist-get tool :name))
(description (plist-get tool :description))
(parameters (plist-get tool :parameters))
(buffer (get-buffer-create (format "*Implement Tool: %s*" name))))
(with-current-buffer buffer
(erase-buffer)
(emacs-lisp-mode)
(insert (format ";; Implementation template for tool: %s\n\n" name))
(insert "(claude-register-tool\n")
(insert (format " '(:name \"%s\"\n" name))
(insert (format " :description \"%s\"\n" description))
(insert " :parameters ((properties\n")
;; Format parameters
(dolist (param parameters)
(let ((param-name (car param))
(param-props (cdr param)))
(insert (format " (%s (title . \"%s\")\n"
param-name
(or (cdr (assoc 'title param-props)) param-name)))
(insert (format " (type . \"%s\")\n"
(or (cdr (assoc 'type param-props)) "string")))
(insert (format " (description . \"%s\")))\n"
(or (cdr (assoc 'description param-props)) "")))))
(insert " )))\n\n")
(insert "(claude-register-tool-handler\n")
(insert (format " \"%s\"\n" name))
(insert " (lambda (parameters)\n")
(insert " ;; Extract parameters\n")
;; Parameter extraction
(dolist (param parameters)
(let ((param-name (car param)))
(insert (format " (let ((%s (cdr (assoc '%s parameters))))\n"
param-name param-name))))
(insert " ;; Your implementation here\n")
(insert " ;; Return the result as a string or JSON-encodable object\n")
(insert " )))\n"))
(switch-to-buffer buffer)))
;;; Built-in tools:
(defun claude-register-filesystem-tools ()
"Register filesystem-related tools."
(interactive)
;; List directory contents
(claude-register-tool
'(:name "list_directory"
:description "List files and directories in a specified path"
:parameters ((properties
(path (title . "Path")
(type . "string")
(description . "Directory path to list"))))))
(claude-register-tool-handler
"list_directory"
(lambda (parameters)
(let ((path (cdr (assoc 'path parameters))))
(condition-case err
(let ((files (directory-files-and-attributes path t)))
(json-encode
(mapcar (lambda (file)
`((name . ,(file-name-nondirectory (car file)))
(type . ,(if (cadr file) "directory" "file"))
(size . ,(file-attribute-size (cdr file)))))
files)))
(error (format "Error listing directory: %s" (error-message-string err)))))))
;; Read file contents
(claude-register-tool
'(:name "read_file"
:description "Read the contents of a file"
:parameters ((properties
(path (title . "Path")
(type . "string")
(description . "Path to the file to read"))))))
(claude-register-tool-handler
"read_file"
(lambda (parameters)
(let ((path (cdr (assoc 'path parameters))))
(condition-case err
(with-temp-buffer
(insert-file-contents path)
(buffer-string))
(error (format "Error reading file: %s" (error-message-string err)))))))
;; Write to file
(claude-register-tool
'(:name "write_file"
:description "Write content to a file"
:parameters ((properties
(path (title . "Path")
(type . "string")
(description . "Path to the file to write"))
(content (title . "Content")
(type . "string")
(description . "Content to write to the file"))))))
(claude-register-tool-handler
"write_file"
(lambda (parameters)
(let ((path (cdr (assoc 'path parameters)))
(content (cdr (assoc 'content parameters))))
(condition-case err
(progn
(with-temp-file path
(insert content))
(format "Successfully wrote to %s" path))
(error (format "Error writing to file: %s" (error-message-string err))))))))
(defun claude-register-emacs-tools ()
"Register Emacs-specific tools."
(interactive)
;; List buffers
(claude-register-tool
'(:name "list_buffers"
:description "List all Emacs buffers"
:parameters ((properties ()))))
(claude-register-tool-handler
"list_buffers"
(lambda (parameters)
(json-encode
(mapcar (lambda (buffer)
`((name . ,(buffer-name buffer))
(file . ,(or (buffer-file-name buffer) ""))
(modified . ,(if (buffer-modified-p buffer) t :json-false))
(size . ,(buffer-size buffer))))
(buffer-list)))))
;; Get buffer content
(claude-register-tool
'(:name "get_buffer_content"
:description "Get the content of an Emacs buffer"
:parameters ((properties
(buffer_name (title . "Buffer Name")
(type . "string")
(description . "Name of the buffer to get content from"))))))
(claude-register-tool-handler
"get_buffer_content"
(lambda (parameters)
(let ((buffer-name (cdr (assoc 'buffer_name parameters))))
(if (get-buffer buffer-name)
(with-current-buffer buffer-name
(buffer-string))
(format "Buffer '%s' not found" buffer-name)))))
;; List installed packages
(claude-register-tool
'(:name "list_packages"
:description "List installed Emacs packages"
:parameters ((properties ()))))
(claude-register-tool-handler
"list_packages"
(lambda (parameters)
(require 'package)
(json-encode
(mapcar (lambda (pkg)
(let ((pkg-name (car pkg))
(pkg-desc (cadr pkg)))
`((name . ,(symbol-name pkg-name))
(version . ,(package-version-join
(package-desc-version pkg-desc)))
(status . ,(if (package-built-in-p pkg-name)
"built-in"
"installed")))))
package-alist)))))
(defun claude-register-shell-tools ()
"Register shell command tool."
(interactive)
;; Run shell command
(claude-register-tool
'(:name "run_shell_command"
:description "Run a shell command and return its output"
:parameters ((properties
(command (title . "Command")
(type . "string")
(description . "Shell command to execute"))))))
(claude-register-tool-handler
"run_shell_command"
(lambda (parameters)
(let ((command (cdr (assoc 'command parameters))))
(condition-case err
(shell-command-to-string command)
(error (format "Error running command: %s" (error-message-string err))))))))
(defun claude-init-tools ()
"Initialize all available tools."
(interactive)
(claude-clear-tools)
(claude-register-filesystem-tools)
(claude-register-emacs-tools)
;; Uncomment the line below if you want shell tools enabled by default
;; (claude-register-shell-tools)
)
;;; Interactive commands:
(defun claude-start-conversation ()
"Start or switch to a conversation with Claude."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(switch-to-buffer buffer)))
(defun claude-send-region (start end)
"Send the region between START and END to Claude in the conversation buffer."
(interactive "r")
(let ((prompt (buffer-substring-no-properties start end)))
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert prompt)
(claude-send-input))))
(defun claude-send-buffer ()
"Send the entire buffer to Claude in the conversation buffer."
(interactive)
(claude-send-region (point-min) (point-max)))
(defun claude-code-review ()
"Ask Claude to review the code in the current buffer."
(interactive)
(let ((code (buffer-substring-no-properties (point-min) (point-max)))
(buffer-name (buffer-name))
(buffer (current-buffer)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert (format "Please review the code in %s and suggest improvements." buffer-name))
(claude-send-input))))
(defun claude-explain-code ()
"Ask Claude to explain the selected code or current buffer."
(interactive)
(let ((buffer (current-buffer)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(if (use-region-p)
(insert "Please explain the selected code in detail.")
(insert (format "Please explain the code in %s in detail." (buffer-name buffer))))
(claude-send-input))))
(defun claude-complete-code ()
"Ask Claude to complete the code at point."
(interactive)
(let* ((buffer (current-buffer))
(cursor-pos (point))
(buffer-name (buffer-name)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert (format "Complete the code at the cursor position in %s. The cursor is at position %d."
buffer-name cursor-pos))
(claude-send-input))))
;;; Keybindings:
(defvar claude-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-a c") 'claude-start-conversation)
(define-key map (kbd "C-c C-a s") 'claude-send-region)
(define-key map (kbd "C-c C-a b") 'claude-send-buffer)
(define-key map (kbd "C-c C-a r") 'claude-code-review)
(define-key map (kbd "C-c C-a e") 'claude-explain-code)
(define-key map (kbd "C-c C-a o") 'claude-complete-code)
map)
"Keymap for Claude mode.")
;;;###autoload
(define-minor-mode claude-mode
"Toggle Claude mode.
With a prefix argument ARG, enable Claude mode if ARG is positive,
and disable it otherwise. If called from Lisp, enable the mode
if ARG is omitted or nil.
Claude mode provides key bindings for interacting with the Claude AI assistant."
:init-value nil
:lighter " Claude"
:keymap claude-mode-map
:global t
(if claude-mode
(progn
(message "Claude mode enabled")
(claude-init-tools))
(message "Claude mode disabled")))
;; Initialize tools on load
(claude-init-tools)
(provide 'claude)
;;; claude.el ends here