<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules/ai-config.el, branch load-graph-classify-start</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-start</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-start'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-05-23T00:32:32+00:00</updated>
<entry>
<title>refactor(auth): consolidate the auth-source secret lookup into one helper</title>
<updated>2026-05-23T00:32:32+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-23T00:32:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=f6e5885b47e3ab244b293f4e478af7e520180710'/>
<id>urn:sha1:f6e5885b47e3ab244b293f4e478af7e520180710</id>
<content type='text'>
The auth-source-search + funcall-the-secret block was copied four times: calendar-sync--calendar-url, cj/auth-source-secret (ai-config), cj/--auth-source-password (transcription), and cj/slack--get-credential. Each searched authinfo, pulled :secret, and called it when the netrc backend returned a function.

I pulled that into cj/auth-source-secret-value in system-lib (a leaf, so calendar-sync doesn't have to depend on ai-config and drag in the gptel stack). It takes an optional user and returns the secret or nil. The four callers now delegate to it: ai-config layers its required-secret error on top, and the others keep their nil-on-miss behavior. With the direct auth-source-search calls gone, I dropped the now-unused (require 'auth-source) from transcription, slack, and calendar-sync. The helper's autoload covers it.

The transcription tests that exercise the delegated path stay green, and the primitive and the error wrapper get their own tests.
</content>
</entry>
<entry>
<title>fix(ai-config): require gptel backend libs so the fork's constructors load</title>
<updated>2026-05-22T20:34:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T20:34:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c835491b716e2c1243c124a65e01b22f641657c4'/>
<id>urn:sha1:c835491b716e2c1243c124a65e01b22f641657c4</id>
<content type='text'>
cj/toggle-gptel and gptel chat errored with "Symbol's function definition is void: gptel-make-anthropic". The local gptel fork on :load-path with :ensure nil ships no generated autoloads, so (require 'gptel) loads gptel.el but never gptel-anthropic.el or gptel-openai.el, where the gptel-make-* constructors live. cj/ensure-gptel-backends then reached gptel-make-anthropic before it was defined.

cj/ensure-gptel-backends now requires gptel-anthropic and gptel-openai first, through a small cj/--gptel-load-backend-libs helper. Verified end-to-end: with the fork on load-path, the constructors are fbound and both backends build.
</content>
</entry>
<entry>
<title>refactor(ai-config): switch gptel to local fork, drop tab-width advice</title>
<updated>2026-05-18T06:24:30+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-18T06:24:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=b3d41f9a0c63b13ad497a48677de933a3fb5a5cf'/>
<id>urn:sha1:b3d41f9a0c63b13ad497a48677de933a3fb5a5cf</id>
<content type='text'>
I switched the gptel use-package form to `:load-path "~/code/gptel"`
with `:ensure nil` so Emacs loads from the fork instead of the MELPA
release. The fork now carries the narrow `tab-width' copy in
`gptel-org--create-prompt' that karthink redirected the upstream PR
to, which replaces the local `:around' advice on
`gptel--with-buffer-copy-internal' I'd been carrying.

I also dropped the stale test file
`tests/test-ai-config-gptel-prompt-tab-width.el' and the matching
stub in `tests/testutil-ai-config.el'. Both existed only to test the
advice I removed.
</content>
</entry>
<entry>
<title>fix(ai-config): gptel-model must be a symbol, not a string</title>
<updated>2026-05-16T11:07:46+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T11:07:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=a3723120073f9486ac771ccf150ec32008d8c2ef'/>
<id>urn:sha1:a3723120073f9486ac771ccf150ec32008d8c2ef</id>
<content type='text'>
The default-backend swap to gpt-5.5 (commit 0f029ab5) set
`gptel-model' as the string "gpt-5.5".  gptel's modeline-display
code calls `symbolp' on the model value and signals
`wrong-type-argument symbolp "gpt-5.5"' on every render, which
manifested as Emacs freezing in the AI-Assistant buffer ("Querying
ChatGPT..." → error in process sentinel → repeated redisplay errors).

Both default-setting sites now use `'gpt-5.5' (interned symbol).
The Anthropic backend tolerated string model names so the original
"claude-opus-4-7" string worked, which is why this hadn't surfaced
before.
</content>
</entry>
<entry>
<title>chore(ai-config): switch default gptel backend to ChatGPT / gpt-5.5</title>
<updated>2026-05-16T10:58:24+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:58:24+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=0f029ab5f844c6399584aec4f5e1cc352e8ab726'/>
<id>urn:sha1:0f029ab5f844c6399584aec4f5e1cc352e8ab726</id>
<content type='text'>
Two places set the default backend + model on gptel initialization
-- `cj/ensure-gptel-backends' (the lazy-init fallback) and the
`use-package gptel :config' block (the eager-set after initialization).
Both now pick the ChatGPT backend with `gpt-5.5' instead of Claude
with `claude-opus-4-7'.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire web_fetch as a local tool</title>
<updated>2026-05-16T10:17:21+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:17:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=99d93203a867294addf4927ceec5644b9d3bf322'/>
<id>urn:sha1:99d93203a867294addf4927ceec5644b9d3bf322</id>
<content type='text'>
Fourth ADOPT entry from `docs/design/gptel-tools-shortlist.org'.
Lets gptel pull a URL into the conversation so the model can read
docs / current API shapes / etc. without me copy-pasting.

Shape:

- URL must be `http://' or `https://' (file://, ftp://, javascript:,
  scheme-less, etc. are rejected at the validator).
- HTML responses go through `pandoc -f html -t plain' so the model
  gets a reading shape that isn't full of markup; falls back to
  `w3m -dump -T text/html' if pandoc isn't on PATH; signals
  `user-error' if neither is.  Pass `raw=t' to skip stripping.
- Output capped at 200KB by default, hard cap 1MB; `max_bytes'
  argument lets the caller pick a lower cap.  Truncation reported
  inline.
- 4xx / 5xx response codes signal `error' with the code -- the
  alternative is returning an error page body, which the model
  would treat as content.

`:confirm t' on the tool because every call is a real outbound
network request.  The tool's description warns that URLs go
wherever the user-agent points, including internal networks if
that's what the URL names.

`tests/test-gptel-tools-web-fetch.el' -- 20 tests across Normal /
Boundary / Error.  URL validator covers http / https / non-string
/ empty / non-http schemes.  `--effective-max-bytes' covers default
/ low-clamp / hard-cap / passthrough.  Truncate helper covers
under-cap / at-cap / over-cap with the marker.  HTML stripper runs
against real pandoc / w3m (both installed in dev env, neither
should mangle simple markup).  Orchestrator stubs
`cj/gptel-web-fetch--retrieve' via `cl-letf' to cover normal /
raw / 4xx / 5xx / oversize / bad-scheme paths.

Wired into `cj/gptel-local-tool-features' so gptel exposes the
tool on next restart.

Note: `call-process-region' invocation flattened to a single
`with-temp-buffer' with DELETE=t -- the initial draft nested a
second temp buffer and routed output to the inner one, which got
killed before `buffer-string' on the outer ran.  Test caught it.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire git_status / git_log / git_diff as local tools</title>
<updated>2026-05-16T09:26:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T09:26:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=ceeae9b5e2625e23e6e3792d06a6c8122a36d18b'/>
<id>urn:sha1:ceeae9b5e2625e23e6e3792d06a6c8122a36d18b</id>
<content type='text'>
Three read-only git context tools so gptel can see what's changed
without me pasting `git status` / `git log` / `git diff` output into
every chat turn.  Builds the first batch from the ADOPT bucket in
`docs/design/gptel-tools-shortlist.org`.

Shape per tool:

- `gptel-tools/git_status.el` — `git status --short --branch` for a
  directory inside a git working tree under HOME.  Returns the
  porcelain output, or a "Clean working tree" marker when only the
  branch line is present.
- `gptel-tools/git_log.el` — `git log --oneline -nN` with an optional
  `--since` filter.  N defaults to 20, capped at 100; nil / non-
  integer / out-of-range N falls back to the default.
- `gptel-tools/git_diff.el` — `git diff [REF1 [REF2]] [-- FILE]`.
  Output capped at ~500KB so a runaway diff can't blow up context;
  truncation is reported inline.

Validation is uniform: path must resolve under HOME, must be a
directory, must be inside a git working tree (verified via
`git rev-parse --is-inside-work-tree`).  Color is disabled via
`-c color.ui=false` at the git level (`git status` doesn't accept
`--no-color` directly).

Tests run against real temp git repos created via `process-file`,
not mocked — there's nothing in gptel-tools/git_*.el that's
process-mockable in a meaningful way, and a real `git init` + a
couple of commits is cheaper than building a fake.  31 tests total:
7 for git_status, 11 for git_log, 13 for git_diff.

Wired into `cj/gptel-local-tool-features` so gptel exposes the
three tools on next restart.
</content>
</entry>
<entry>
<title>feat(ai-conversations-browser): dired-style browser for saved GPTel conversations</title>
<updated>2026-05-16T07:00:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T07:00:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=3f50f682053dd31d5fac96ecdf2b98aad1ce56d7'/>
<id>urn:sha1:3f50f682053dd31d5fac96ecdf2b98aad1ce56d7</id>
<content type='text'>
`cj/gptel-load-conversation` prompts via `completing-read`.  A
dedicated browser shows what each conversation is about at a
glance and supports single-key load / delete / rename without
having to scroll a minibuffer list.

New module `modules/ai-conversations-browser.el` +
`cj/gptel-browse-conversations` entry point bound to `C-; a b`
("browse conversations").  Opens `*GPTel-Conversations*` in
`cj/gptel-browser-mode` (a `special-mode` derivative).

Each row shows date, time, topic slug, and a preview of the most
recent message (length configurable via
`cj/gptel-browser-preview-length`, default 60 chars).  Rows sort
newest first.

In the browser:
- `RET` / `l`: load the conversation (delegates to
  `cj/gptel-load-conversation` with the file pre-selected via a
  `cl-letf` stub on `completing-read` so the user isn't prompted
  twice), then bury the window.
- `d`: delete the file under point after `y-or-n-p` confirmation,
  re-render.
- `r`: rename the file under point.  Preserves the timestamp,
  slugifies the new topic, refuses unchanged input and existing
  targets.
- `g`: refresh.
- `n` / `p`: next / previous row.
- `q`: quit-window.

21 tests cover the helpers (topic parsing, header stripping,
preview shaping for truncate / short / empty cases, row-for-file
with conversation + non-conversation filenames, rows enumeration,
render output for empty + populated cases, newest-first sort,
rename-target preservation of timestamp + slug, rename-target
error on missing timestamp) and the file-touching actions (delete
with y, cancel with n, rename, rename-on-empty-line error).
</content>
</entry>
<entry>
<title>feat(ai-rewrite): add directive-picker wrappers around gptel-rewrite</title>
<updated>2026-05-16T06:55:16+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T06:55:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=6ee37e0a68d31909861cf59684d3601bf40f5abe'/>
<id>urn:sha1:6ee37e0a68d31909861cf59684d3601bf40f5abe</id>
<content type='text'>
`gptel-rewrite` is the killer feature for the keep-gptel decision,
and it now lives behind two commands instead of the bare call:

- `cj/gptel-rewrite-with-directive` (`C-; a r`, replacing the
  former bare `gptel-rewrite` binding): completing-read on a
  directive name from `cj/gptel-rewrite-directives`, then rewrite
  the active region.
- `cj/gptel-rewrite-redo-with-different-directive` (`C-; a R`):
  replay the prior region with a different directive.  The region
  is preserved via markers stored buffer-local on the first call so
  it survives accept/reject of the prior rewrite.

I picked the hook injection approach over an `:after`-advice +
state-capture pattern.  `gptel-rewrite-directives-hook` is an
abnormal hook gptel-rewrite already consults for a per-call
system message.  Wrapping the call in a one-shot `let`-binding on
that hook gives the directive exactly the lifetime of the rewrite
and leaves nothing to clean up.  Mutating `gptel-directives`
globally would mean either restoring it afterward or living with
the change -- both worse than the hook.

Directives ship inline as a `defcustom` alist with the six names
called out in the task -- `terse`, `fix-grammar`,
`refactor-readability`, `add-docstring`, `explain-as-comment`,
`shorten`.  Customization is a `customize-variable` or `setq`
away.

9 tests cover the defcustom shape (default names present, bodies
non-empty strings), the wrapper (normal path, no-region error,
unknown-directive error, last-state recording), and the redo
(replays the prior region, errors when no previous, excludes the
current directive from the re-pick prompt).  `gptel-rewrite`
stubbed in tests so no rewrite UI fires.
</content>
</entry>
<entry>
<title>feat(ai-quick-ask): add cj/gptel-quick-ask one-shot command</title>
<updated>2026-05-16T06:48:59+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T06:48:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=670117cccdbae4706dfaa5e05144c256c3a657f0'/>
<id>urn:sha1:670117cccdbae4706dfaa5e05144c256c3a657f0</id>
<content type='text'>
New module `modules/ai-quick-ask.el`.  Bound to `C-; a q` via
`cj/ai-keymap` ("quick ask").

`cj/gptel-quick-ask` reads a prompt in the minibuffer, creates a
transient `*GPTel-Quick*` buffer in `cj/gptel-quick-mode` (a
special-mode derivative with `q` / `escape` / `c` bindings), inserts
"Q: &lt;prompt&gt;" plus a response marker, then calls `gptel-request`
with `:stream t` so the answer streams into the buffer.  Doesn't
touch `*AI-Assistant*`, doesn't autosave.

Two follow-up commands work in the buffer:

- `cj/gptel-quick-dismiss` (`q` / `escape`): delete the window and
  kill the buffer.  Idempotent when the buffer is absent.
- `cj/gptel-quick-continue` (`c`): extract the prompt + response,
  seed them into `*AI-Assistant*` under proper org headings (matching
  the `cj/gptel--fresh-org-prefix` shape), display the side window,
  then dismiss the quick buffer.

13 tests cover the pure helpers (initial-text shape, response
extraction across normal / multi-line / no-marker / empty inputs,
seed-text shape), the ask path (buffer created in right mode,
prompt recorded, gptel-request called, empty-prompt error), the
dismiss path (kills buffer / no-op when absent), and the continue
path (seeds `*AI-Assistant*`, dismisses quick buffer, errors
outside a quick buffer).  `gptel-request` is stubbed in tests so
nothing hits the network.
</content>
</entry>
</feed>
