[emacs] Claude integration?

Written by Claude itself, mostly!
This commit is contained in:
John Doty 2025-03-11 11:53:37 -07:00
parent 1e292de616
commit 3ac73bb3e2
2 changed files with 728 additions and 13 deletions

View file

@ -1380,25 +1380,52 @@ Do this when you edit your project view."
;; =================================================================
(use-package jsonnet-mode :ensure t)
;; =================================================================
;; Copilot (ugh)
;; =================================================================
(use-package editorconfig :ensure)
(use-package jsonrpc :ensure)
;; (use-package copilot
;; :load-path (lambda () (expand-file-name "~/site-lisp/copilot"))
;; :after (editorconfig jsonrpc))
;; =================================================================
;; Fish shell
;; =================================================================
(use-package fish-mode :ensure)
;; =================================================================
;; gptel
;; AI Shit
;; =================================================================
(use-package gptel :ensure
:bind ("C-c RET" . gptel-send))
(use-package request :ensure)
(use-package claude
:load-path "~/site-lisp/"
:custom
(claude-model "claude-3-7-sonnet-20250219") ;; Current model as of March 2025
(claude-max-tokens 4000)
(claude-auto-display-results t)
:config
;; If you want to add any custom tools, add them here
;; (claude-register-tool
;; '(:name "my_custom_tool"
;; :description "A custom tool that does something specific"
;; :parameters ((properties
;; (param1 (title . "Parameter 1")
;; (type . "string")
;; (description . "Description of parameter 1"))))))
;; (claude-register-tool-handler
;; "my_custom_tool"
;; (lambda (parameters)
;; ;; Your implementation here
;; (format "Tool executed with param: %s" (cdr (assoc 'param1 parameters)))))
;; Enable the minor mode for keybindings
(claude-mode 1)
:bind (:map claude-mode-map
;; You can customize the keybindings if you prefer different ones
("C-c C-a a" . claude-prompt-and-send) ;; Add a custom binding
;; Default bindings included by claude-mode:
;; C-c C-a s - claude-send-region
;; C-c C-a b - claude-send-buffer
;; C-c C-a r - claude-code-review
;; C-c C-a e - claude-explain-code
;; C-c C-a c - claude-complete-code
;; C-c C-a t - claude-send-with-tools
;; C-c C-a l - claude-list-requested-tools
;; C-c C-a p - claude-prompt-and-send
))
;;; init.el ends here

688
site-lisp/claude.el Normal file
View file

@ -0,0 +1,688 @@
;;; 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
;; URL: https://git.d0ty.me/DeCarabas/claude-emacs
;;; 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.
;;; 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)
;;; Core functionality:
(defvar claude-buffer-name "*Claude*"
"Name of the buffer for Claude interactions.")
(defvar claude-conversation-history nil
"History of the current conversation with Claude.")
(defvar claude-response-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'quit-window)
(define-key map "r" 'claude-refresh-last-request) ;; We'll define this function later
map)
"Keymap for Claude response buffers.")
(define-derived-mode claude-response-mode markdown-mode "Claude"
"Major mode for viewing Claude AI responses."
(use-local-map claude-response-mode-map)
;; Enable visual line mode for word wrapping
(visual-line-mode 1)
;; Make buffer read-only by default
(setq buffer-read-only t))
(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-buffer ()
"Ensure the Claude buffer exists with proper formatting and keybindings."
(let ((buffer (get-buffer-create claude-buffer-name)))
(with-current-buffer buffer
(unless (eq major-mode 'claude-response-mode)
;; Use our custom mode that has the q key binding
(if (fboundp 'markdown-mode) ;; Check if markdown-mode is available
(claude-response-mode) ;; Use our derived mode if markdown is available
;; Fallback if markdown-mode isn't available
(special-mode) ;; special-mode also has the q key binding
(visual-line-mode 1))
;; Set word-wrap and margins regardless of mode
(setq word-wrap t)
(setq left-margin-width 2
right-margin-width 2)))
buffer))
(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)
;; (message "Claude: API response structure: %S" data)
(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-extract-text-content (message)
"Extract text content from MESSAGE returned by the API."
(let ((content-items (cdr (assoc 'content message)))
(result ""))
(dolist (item content-items)
(let ((type (cdr (assoc 'type item))))
(when (string= type "text")
(setq result (concat result (cdr (assoc 'text item)) "\n\n")))))
result))
(defun claude-display-response (data)
"Display the response DATA from Claude with nice formatting."
(let ((content (cdr (assoc 'content data)))
(buffer (claude-ensure-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
;; Clear the buffer
(erase-buffer)
;; Add a timestamp header
(insert (propertize
(format "Claude response at %s\n\n"
(format-time-string "%H:%M:%S"))
'face 'font-lock-comment-face))
;; Process content
(if (and content (arrayp content) (> (length content) 0))
(dotimes (i (length content))
(let* ((item (aref content i))
(type (cdr (assoc 'type item))))
(when (string= type "text")
(let ((text (cdr (assoc 'text item))))
(insert text "\n\n")))))
;; Handle unexpected response format
(insert (propertize "Unexpected response format from Claude API.\n"
'face 'font-lock-warning-face))
(insert "Response data: " (prin1-to-string data)))
;; Add helpful instructions at the bottom
(goto-char (point-max))
(insert (propertize "\n──────────────────────────────────────\n"
'face 'font-lock-comment-face))
(insert (propertize "Press q to close this window\n"
'face 'font-lock-comment-face))
;; Return to the start of the buffer
(goto-char (point-min))))
;; Only display the buffer if requested and not already visible
(when claude-auto-display-results
(unless (get-buffer-window buffer)
(display-buffer-other-window buffer)))))
;; (defun claude-display-response (data)
;; "Display the response DATA from Claude."
;; (let* ((message (aref (cdr (assoc 'messages data)) 0))
;; (content (claude-extract-text-content message))
;; (buffer (claude-ensure-buffer)))
;; (with-current-buffer buffer
;; (let ((inhibit-read-only t))
;; (erase-buffer)
;; (insert content)
;; (goto-char (point-min))))
;; (when claude-auto-display-results
;; (display-buffer buffer))))
(defun claude-send-message (prompt &optional system-prompt tools)
"Send PROMPT to Claude and display the response.
If SYSTEM-PROMPT is provided, include it in the request.
If TOOLS is provided, enable tool use."
(let ((data `((model . ,claude-model)
(max_tokens . ,claude-max-tokens)
(messages . [((role . "user")
(content . ,prompt))]))))
;; Add system prompt if provided
(when system-prompt
(setq data (append data `((system . ,system-prompt)))))
;; Add tools if provided
(when tools
(setq data (append data `((tools . ,tools)
(tool_choice . "auto")))))
;; Display the buffer with a loading message
(let ((buffer (claude-ensure-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert "Sending request to Claude...\n\n")))
(when claude-auto-display-results
(display-buffer buffer)))
;; Send request
(claude-send-request
data
(lambda (data)
(if tools
(claude-handle-tool-response data (claude-ensure-buffer))
(claude-display-response data)))
(lambda (error-thrown)
(let ((buffer (claude-ensure-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "Error: %s" error-thrown)))))))))
;;; Interactive commands:
(defun claude-send-region (start end)
"Send the region between START and END to Claude and display the response."
(interactive "r")
(let ((prompt (buffer-substring-no-properties start end)))
(claude-send-message prompt)))
(defun claude-send-buffer ()
"Send the entire buffer to Claude."
(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))))
(claude-send-message
(concat "Please review the following code and suggest improvements:\n\n```\n"
code "\n```"))))
(defun claude-explain-code ()
"Ask Claude to explain the selected code."
(interactive)
(if (use-region-p)
(let ((code (buffer-substring-no-properties (region-beginning) (region-end))))
(claude-send-message
(concat "Please explain what this code does in detail:\n\n```\n"
code "\n```")))
(message "No region selected")))
(defun claude-complete-code ()
"Ask Claude to complete the code at point."
(interactive)
(let* ((buffer-text (buffer-substring-no-properties (point-min) (point-max)))
(cursor-pos (point))
(text-before (buffer-substring-no-properties (point-min) cursor-pos))
(text-after (buffer-substring-no-properties cursor-pos (point-max))))
(claude-send-message
(concat "I'm writing code and need you to continue it from where the cursor is marked with [CURSOR]. Only provide the code that should replace [CURSOR], nothing else.\n\n```\n"
text-before "[CURSOR]" text-after "\n```"))))
(defun claude-prompt-and-send ()
"Prompt for input and send to Claude."
(interactive)
(let ((prompt (read-string "Ask Claude: ")))
(claude-send-message prompt)))
(defun claude-refresh-last-request ()
"Refresh the last Claude request."
(interactive)
(message "Refresh functionality not yet implemented."))
;;; 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-send-with-tools (prompt)
"Send PROMPT to Claude with tools enabled and handle the response."
(interactive "sPrompt: ")
(let ((tools-json (mapcar (lambda (tool)
`((name . ,(plist-get tool :name))
(description . ,(plist-get tool :description))
(parameters . ,(plist-get tool :parameters))))
claude-tools)))
(claude-send-message prompt nil tools-json)))
(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)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(let ((content-items (cdr (assoc 'content message))))
(dolist (item content-items)
(let ((type (cdr (assoc 'type item))))
(cond
((string= type "text")
(insert (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)))
(insert (format "Tool call: %s\n" tool-name))
(insert (format "Parameters: %s\n" (json-encode parameters)))
(insert (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)
;; Notify Claude about missing tool
(insert (format "Tool requested: %s\n" tool-name))
(insert (format "This tool is not currently available.\n"))
(insert "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))))))))))))))
(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 (cdr (assoc 'id (cdr (assoc 'tool_use (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0))))))
(api-key (claude-get-api-key))
(buffer (claude-ensure-buffer)))
(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)
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert (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)
;; (claude-register-shell-tools)
)
;;; Keybindings:
(defvar claude-mode-map
(let ((map (make-sparse-keymap)))
(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 c") 'claude-complete-code)
(define-key map (kbd "C-c C-a t") 'claude-send-with-tools)
(define-key map (kbd "C-c C-a l") 'claude-list-requested-tools)
(define-key map (kbd "C-c C-a p") 'claude-prompt-and-send)
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