aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* docs(todo): close external-open/media-utils coverageCraig Jennings2026-05-231-13/+2
|
* test(media-utils): cover player discovery and play/download commandsCraig Jennings2026-05-231-0/+105
| | | | media-utils.el had no tests. I added eight: cj/get-available-media-players filtering by executable-find, cj/media-play-it building a direct command versus the yt-dlp -g stream-URL wrap (plus the missing-player error), and cj/yt-dl-it erroring when yt-dlp or tsp is absent and queueing through tsp + yt-dlp when both are present. Every external boundary is mocked, so nothing launches.
* docs(todo): close title-case edge coverage and mail/system-commands coverageCraig Jennings2026-05-231-21/+4
|
* test(custom-case): cover leading-quote, paren, and RTL title-case edgesCraig Jennings2026-05-231-0/+16
| | | | The state machine in cj/title-case-region skips leading non-word characters and passes caseless letters through, but no test pinned that. I added three boundary cases: a leading double-quote and a leading paren each still capitalize the first real word, and a caseless RTL first word (Hebrew) passes through while consuming the is-first slot, so the following minor word stays lowercase.
* docs(todo): close host-env predicate cleanupCraig Jennings2026-05-231-16/+3
|
* refactor(host-env): fix env-desktop-p doc and normalize the X predicatesCraig Jennings2026-05-231-4/+7
| | | | | | env-desktop-p's docstring described a laptop, but the function returns t for the desktop case (no battery). env-x-p compared the window system against the string "x" while its sibling env-x11-p used `eq` against the symbol, so the two read differently for the same check. I corrected the docstring and switched env-x-p to the symbol comparison. I also spelled out the difference between env-x-p (any X display, including XWayland) and env-x11-p (a real X11 session, no Wayland). Behavior is unchanged, so the existing display-predicate tests stay green.
* docs(todo): close dirvish hardening and coverage tasksCraig Jennings2026-05-231-25/+4
|
* fix(dirvish): guard nil file and reject path-traversal playlist namesCraig Jennings2026-05-233-12/+46
| | | | | | cj/set-wallpaper passed `(dired-file-name-at-point)` straight to `expand-file-name`, so running it with no file at point raised a bare `wrong-type-argument` instead of a clear error. cj/dired-create-playlist-from-marked expanded the raw playlist name under `music-dir` without checking it, so a name like "../foo" or "/etc/foo" would write outside the music directory. I added a nil-file guard to set-wallpaper and a `cj/--playlist-name-safe-p` check that rejects any name carrying a directory separator before the path is built. Both paths now fail cleanly with a user-error. Regression tests went into the existing wrapper and playlist test files.
* docs(todo): add :solo: tag and mark Claude-doable hardening tasksCraig Jennings2026-05-231-22/+19
| | | | I added a :solo: tag to the legend for tasks Claude can take end to end with no input from me: bounded scope, no design or preference call, verifiable locally. Tagged the nine hardening findings I've already assessed that way. Also closed the dirvish runtime-require task, shipped in b63c4f83.
* fix(dirvish): declare runtime constant/util deps with plain requireCraig Jennings2026-05-232-2/+32
| | | | | | dirvish-config builds `dirvish-quick-access-entries` from `code-dir`, `music-dir`, `pix-dir`, and the recording dirs at load time, and binds keys to `cj/xdg-open` and `cj/open-file-with-command`. Those come from user-constants and system-utils, but the module only required them under `eval-when-compile`, so the compiled module carries no runtime require and leans on init order having loaded them first. I switched both to plain requires, matching host-environment, system-lib, and external-open-lib right below. Added a dependency-contract smoke test that fails if the requires are dropped.
* docs(todo): record TRAMP/dirvish "?" root cause, leave fix to verifyCraig Jennings2026-05-221-15/+15
| | | | | | I traced why remote dirvish shows "?" for dates: dirvish fetches remote attributes through an async `ls -1lahi` parser that only runs when the connection has direct-async and the remote has GNU ls. It falls back to skipping `file-attributes` (rendering "?") when either gate is shut. That's why the earlier dired-listing-switches attempts missed the real path, and why disabling direct-async made it worse. The config already enables direct-async, so the rest is per-host and needs a live remote. I left the three diagnostic evals and the likely fixes in the task body.
* docs(todo): close org-log-done reconcileCraig Jennings2026-05-221-2/+3
|
* refactor(org): give org-log-done a single homeCraig Jennings2026-05-223-2/+27
| | | | | | `org-log-done` was set in two places: `cj/org-todo-settings` in org-config.el set it nil, and org-roam-config.el's `:config` set it to 'time. Whichever module loaded last won, so the effective value was load-order-dependent and fragile. I set it once in `cj/org-todo-settings` and dropped the org-roam-config setter, leaving a comment at the old site so it doesn't creep back. The value is 'time rather than nil because the dated-completion workflow wants a CLOSED timestamp stamped on every TODO->DONE.
* docs(todo): close always-save-daily taskCraig Jennings2026-05-221-2/+3
|
* fix(org-roam): always save the daily after a journal task-copyCraig Jennings2026-05-222-5/+66
| | | | | | The save lived inside the `unless` branch that only ran when the completed task needed an `org-refile` into a different file. When the task was already in today's daily, the copy left the buffer modified but unsaved. A crash before the next manual save lost it, and shutdown prompted about the unsaved journal buffer. I pulled the save out of the refile branch into a `cj/--org-roam-save-daily` helper that runs on both paths and only writes when the buffer is modified. Extracting it also makes the save logic testable without driving the org-roam capture machinery.
* docs(todo): close auth-source consolidationCraig Jennings2026-05-221-1/+3
| | | | I closed the auth-source-idiom consolidation. The four copies now delegate to one system-lib helper.
* refactor(auth): consolidate the auth-source secret lookup into one helperCraig Jennings2026-05-227-33/+123
| | | | | | | | 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.
* docs(todo): close dashboard navigator/keymap dedupCraig Jennings2026-05-221-1/+3
| | | | I closed the navigator and keymap duplication task. Both now derive from one launcher table.
* refactor(dashboard): derive the navigator and keybindings from one launcher ↵Craig Jennings2026-05-222-81/+147
| | | | | | | | table The 12 dashboard launchers were inlined twice (once as navigator icon buttons, once as dashboard-mode-map keybindings), so adding or reordering one meant editing both lists, and the icon-row order could drift from the key order. I pulled them into a single cj/dashboard--launchers table of (KEY ICON-FN ICON-NAME LABEL TOOLTIP ACTION) tuples. cj/dashboard--navigator-rows chunks it four per row into the navigator buttons, and cj/dashboard--bind-launchers binds each key to its action. The icons and the keys now come from one place, with no behavior change: same icons, labels, order, and keys, locked by tests.
* docs(todo): close dashboard subtitle and color bugs, file navigator-color ↵Craig Jennings2026-05-221-4/+11
| | | | | | follow-up I closed the subtitle-centering and the navigator/item color bugs, and filed a follow-up to give the navigator its own color separate from the list items, which is blocked by the shared dashboard-items-face overlay.
* fix(dashboard): center the banner subtitle and color the navigator and itemsCraig Jennings2026-05-222-2/+2
| | | | | | The banner subtitle sat left of center because dashboard-banner-title-offset was 5, which over-shifts. I dropped it to 3, which lines the subtitle up under the banner image. The navigator and the recentf/project/bookmark list rendered in the default near-white. I set dashboard-items-face to steel+2 so they pick up a theme color, and the section headers stay blue via dashboard-heading. The navigator and the items share dashboard-items-face, because the navigator is drawn with a dashboard-items-face overlay that wins over its per-button dashboard-navigator face, so they take one color by design here.
* chore(themes): regenerate dupre palette preview from palette.elCraig Jennings2026-05-223-175/+156
| | | | | | The committed palette PNG had drifted from the theme. It labeled steel as "cyan", invented magenta colors the palette never had, and left out blue+2. There was no generator, so the preview and the source diverged silently. I added gen-palette-preview.py, which parses the (name "#hex") pairs straight out of dupre-palette.el and emits the preview HTML grouped one row per color family. I regenerated the HTML and the PNG from it, so all 32 colors show with square swatches at 2x, columns aligned, and nothing drifts from palette.el again.
* docs(todo): close test-name abort bugCraig Jennings2026-05-221-1/+4
| | | | The real cause wasn't gptel: the gptel-tools tests already stub the constructors. test-name aborted because a test file leaked default-directory at load. Fixed with absolute load paths plus containment.
* fix(test): make test-name resilient to load-time cwd changesCraig Jennings2026-05-222-2/+7
| | | | | | make test-name loads every test file into one Emacs, then selects by name. test-system-defaults-functions.el requires system-defaults at load, which runs (setq default-directory user-home-dir), an intentional config choice. That leaked the cwd into the shared session, so every relative -l tests/X.el load after it resolved against the wrong directory and aborted the whole run with Error 255. I made two changes. test-name now passes absolute paths to -l so loads survive any cwd change, and the test contains the leak by let-binding default-directory around the require. The production setq stays as is.
* docs(todo): close gptel backend-load bugCraig Jennings2026-05-221-1/+2
| | | | The fork's backend constructors load again, verified end-to-end.
* fix(ai-config): require gptel backend libs so the fork's constructors loadCraig Jennings2026-05-222-1/+69
| | | | | | 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.
* docs(todo): close finalize-task keybinding, file follow-upsCraig Jennings2026-05-221-29/+23
| | | | I closed the finalize-task keybinding work and recorded its design in the task body. Three follow-ups came out of it: reconcile the duplicate org-log-done setting, always save the daily after a journal task-copy, and manually verify the live journal copy that the unit tests mock out.
* feat(org-config): add cj/org-finalize-task with testsCraig Jennings2026-05-222-0/+210
| | | | | | | | | | I added a command on C-; O d that finalizes the task at point. It prompts for a finalized keyword from org-done-keywords, so the picker tracks org-todo-keywords automatically. Marking the task done fires the org-roam journal-copy hook, so the completed task lands in today's daily. Then the heading is reshaped by depth. A sub-task (level 3 or deeper, or a VERIFY at any depth) becomes a dated log entry: the keyword and priority cookie are stripped, a sortable timestamp is prepended, and the tags are kept. A top-level task keeps its keyword and gains a date-only CLOSED line. The command binds org-inhibit-logging around the org-todo call so it owns the CLOSED line rather than depending on org-log-done, which is set inconsistently across two modules. The journal hook keys off org-state, not org-log-done, so the copy still fires. Tests run in org temp-buffers with the journal hook bound to nil, exercise the real org primitives, and inject a fixed time so the stamp shape is deterministic.
* docs(todo): review all tasks, re-grade gptel, expand Corfu subtasksCraig Jennings2026-05-221-3/+133
| | | | | | | | | | | | I walked all 24 unreviewed top-level tasks in one pass and stamped each with :LAST_REVIEWED: 2026-05-22. That dropped the threshold-7 staleness count from 24 to 0. I re-graded "gptel fork not loading: gptel-make-anthropic void" from [#C] to [#B]. It breaks gptel chat entirely and is the root cause of the test-name batch-load abort, so it shouldn't sit at the bottom of the pile. I reworded the sentence-shaped keybinding task to a terse topic and tagged it :quick:. Then I answered its embedded request with a cj/org-heading-to-dated-log sketch that drops the keyword and priority cookie and prepends a sortable timestamp, plus three mnemonic key candidates. I switched the org-noter task from VERIFY to TODO. It's in-progress implementation work, not an open question waiting on input. I expanded the Company-to-Corfu task with the migration spec's nine implementation steps as sub-tasks, so the work is ready to pick up without re-reading the design doc.
* docs(todo): log launch + dashboard bugs, tag quick tasks, close shipped workCraig Jennings2026-05-211-25/+50
| | | | I filed the gptel-fork-load error, the org-contacts launch error, and two dashboard polish bugs (off-center banner subtitle, uncolored navigator icons + titles). I tagged the genuinely quick tasks :quick:, dropped all gptel work to #C, and closed the two tasks shipped this session: the ai-vterm graceful close and the org-contacts launch fix.
* fix(org-contacts): set org-contacts-files eagerly so launch doesn't errorCraig Jennings2026-05-212-4/+71
| | | | | | | | At startup the agenda-finalize hook ran cj/org-contacts-anniversaries-safe, which calls org-contacts-anniversaries, which calls org-contacts-files. That function messages "[org-contacts] ERROR: Your custom variable `org-contacts-files' is nil." when the variable is nil, and at that point it was nil. The value was set via the use-package :custom, which only applies when org-contacts loads, and that load is deferred behind :after (org mu4e) — later than the first agenda finalize. I set org-contacts-files eagerly at require time instead, so it's never nil by the time the hook fires. I also guarded the wrapper: org-contacts-files emits a message rather than signaling, so ignore-errors couldn't suppress it on its own. Now the call only runs when the variable is set. Three tests cover the eager set, the guard skipping when files are nil, and the wrapper running when they're set. Full suite green.
* feat(ai-vterm): add graceful agent close on M-f9 / C-S-f9Craig Jennings2026-05-213-48/+184
| | | | | | | | cj/ai-vterm-close tears an agent down cleanly: it kills the agent's tmux session (stopping the process), removes the vterm window when it isn't the only one in the frame, then kills the buffer. It targets the current agent buffer, the sole live agent, or prompts among several, and confirms before killing since that interrupts work in progress. I also folded the whole F9 family onto ai-vterm. M-f9 used to run cj/toggle-gptel, but gptel is broken right now (the local fork doesn't load, so gptel-make-anthropic is void), and grouping every ai-vterm command under F9 reads better anyway. M-f9 is the primary close binding. C-S-f9 is a second binding that the Wayland/PGTK layer may swallow on some machines. I covered it with 7 tests over the tmux-kill helper, the per-buffer teardown, and target selection, mocking process-file and the prompt at the boundary.
* docs(todo): archive shipped tasks, file calendar and harness follow-upsCraig Jennings2026-05-211-8/+79
| | | | Moved the ai-vterm sizing and dashboard fixes to Resolved now that they've shipped. Filed two follow-ups: make test-name aborting on gptel-dependent test files, and consolidating the duplicated auth-source secret-funcall idiom.
* feat(calendar-sync): resolve .ics feed URLs from auth-sourceCraig Jennings2026-05-213-10/+128
| | | | | | A calendar's .ics feed URL is a secret token, so I'd rather not keep it in a plaintext config file. A calendar can now name a :secret-host, and calendar-sync--calendar-url looks the URL up in auth-source (~/.authinfo.gpg) at sync time. Inline :url still works and wins when both are set, so existing configs are unaffected. I added 7 tests covering the explicit-url, string-secret, function-secret, precedence, and no-match paths, and switched the .example template to the :secret-host shape.
* fix(dashboard): trim padding newlines and reset window-start on openCraig Jennings2026-05-202-6/+67
| | | | | | The dashboard often opened already scrolled: content sat partly above the visible window with empty lines stranded at the bottom. There were two causes. The startupify list inserted five padding newlines that pushed the content past one screenful, and cj/dashboard-only moved point to point-min without resetting window-start, so a previously-scrolled view leaked into the next display. I trimmed the padding to one newline after the banner title and one before the items, and added a set-window-start to point-min in cj/dashboard-only so the view always starts at the top. A characterization test locks the window-start reset.
* feat(ai-vterm): default to bottom-75% on laptop, right-50% on desktopCraig Jennings2026-05-204-32/+145
| | | | | | | | The agent window's default placement was hardcoded to a right-side split at 50% width. That's wrong on a laptop, where the screen is shorter and a bottom split with more height fits better than a narrow side panel. Pick the default from the host: bottom at 75% height on a laptop, right at 50% width on a desktop, branching on env-laptop-p in cj/--ai-vterm-default-direction and cj/--ai-vterm-default-size. The defaults still feed the existing toggle-capture mechanism, so re-orienting the window mid-session sticks the same way it did before. Renamed cj/ai-vterm-window-width to cj/ai-vterm-desktop-width and added cj/ai-vterm-laptop-height so each axis has its own knob.
* feat(calendar-sync): dispatch Google calendars through API helperCraig Jennings2026-05-203-6/+290
| | | | | | | | | | The Python helper from d6a995b could fetch and render on its own, but nothing in Emacs called it. This wires it in. Each entry in calendar-sync-calendars now takes a :fetcher key. 'api routes through the helper, and the default 'ics keeps the existing curl + Elisp parser path. Proton and any plain .ics feed work unchanged because the key defaults to 'ics. The 'api path reads :account and :calendar-id off the calendar plist, builds the helper command (honoring the past/future window and the calendar-sync-skip-declined toggle), and runs it through make-process. The script writes the org file directly, so the sentinel only handles state bookkeeping and failure reporting, the same as the .ics worker. I split the old --sync-calendar body into --sync-calendar-ics and turned --sync-calendar into a dispatcher. The command builder and script-path resolution are pure functions, tested directly. The dispatch routing is tested with both leaf syncers stubbed, so no process runs. I added 14 tests across the two new files, and the full suite is green. Running the 'api path still needs the one-time OAuth bootstrap from docs/calendar-sync-api-setup.org.
* feat(calendar-sync): add Python helper for Google Calendar API syncCraig Jennings2026-05-198-0/+797
| | | | | | | | | | | | Google's .ics export drops per-occurrence response statuses on recurring events. When OOO auto-declines a meeting, the master event keeps PARTSTAT=ACCEPTED and declined instances inherit it. The .ics path can't filter the declines out. The API path expands recurrences server-side via singleEvents=True, and each occurrence carries its own attendees[].self.responseStatus. scripts/calendar_sync_api.py fetches events and renders them as org entries. OAuth is one-time per account. The refresh token lives at ~/.config/calendar-sync/token-<account>.json under 0600. Output matches the existing .ics shape: heading sanitization, LOCATION/ORGANIZER/STATUS/URL property drawer, HTML-stripped descriptions, org timestamps with weekday abbreviations. I wrote 30 stdlib-unittest tests against fixture JSON, covering rendering, filtering, timestamp formatting, and HTML cleanup. I left auth and HTTP uncovered — they're thin wrappers around the Google client libraries, best checked by running the script once after OAuth setup. docs/calendar-sync-api-setup.org walks through the Google Cloud OAuth client setup and the per-account auth bootstrap. .gitignore picks up Python bytecode now that the project has a Python helper. The Elisp dispatch (:fetcher 'api routing in calendar-sync.el) lands in a follow-up commit.
* fix(calendar-sync): drop declined events from synced outputCraig Jennings2026-05-192-0/+154
| | | | | | | | | | | | | | | | The sync parsed PARTSTAT into a :STATUS: declined property but kept the event. Meetings I'd declined still landed in dcal.org / gcal.org and showed on the agenda. I added a pure --filter-declined helper called inside --parse-ics after event collection, plus the calendar-sync-skip-declined defvar (default t) so it can be flipped off without code changes. The .ics feed and the Calendar API can disagree on PARTSTAT. OOO auto-declines sometimes only write API-side, so a few declined events may still slip through. I'm calling this out because the filter looks absolute from the agenda but isn't. Tests cover Normal/Boundary/Error (11 cases). Full suite is green.
* chore(todo): claim dashboard task + dated-log subtask rewritesCraig Jennings2026-05-181-57/+78
| | | | | | | | | | - Claim [#C] Dashboard buffer too long as DOING :bug: with expanded body. - File [#C] Collapse dashboard navigator + keymap duplication :refactor: surfaced by the audit of dashboard-config.el. - Mechanical: rewrote completed sub-tasks (level *** and deeper) from "DONE [#priority]" keyword form to "YYYY-MM-DD Day" dated-log form per the project's depth-based completion rule.
* fix(vterm): stop wheel/escape forwarders from blocking EmacsCraig Jennings2026-05-181-4/+23
| | | | | | | | | | | | | | | | vterm-send-string ends with (accept-process-output ... vterm-timer-delay ...). The global vterm-timer-delay is nil in this config, so the call blocks forever when the pty's program consumes the event without producing output -- a common pattern for TUIs like Claude Code reacting to mouse wheel or Escape. The symptom is a spinning cursor until C-g. cj/vterm--send-mouse-wheel and cj/vterm-send-escape now wrap the send in a let-binding that pins vterm-timer-delay to 0, so accept-process-output returns immediately. A top-level (defvar vterm-timer-delay) declaration goes alongside so the let is dynamic. Without it, lexical-binding-t in this file makes the binding lexical, invisible to vterm-send-string across files. The backtrace from the failing case confirmed the lookup was still receiving nil before the declaration.
* feat(vterm): forward <escape> to the pty in vterm-modeCraig Jennings2026-05-182-1/+28
| | | | | | | | `<escape>' is bound globally to `keyboard-escape-quit' in modules/keybindings.el, so Emacs swallows the key before it can reach the pty. Bind it in vterm-mode-map to cj/vterm-send-escape, which writes a literal ESC byte via vterm-send-string. tmux's copy-mode `cancel' binding then fires; vi-mode exits, fzf cancel, etc., also work as expected.
* chore(todo): shorten title to "Emacs Config"Craig Jennings2026-05-181-1/+1
|
* feat(vterm): forward wheel events and route C-; x c into tmux copy-modeCraig Jennings2026-05-182-12/+215
| | | | | | | | | | | | | | | vterm-mode-map binds only mouse-1 and mouse-yank-primary, so wheel events fall through to Emacs scrolling and never reach the pty. tmux's `set -g mouse on' never sees them. Bind wheel-up / wheel-down (and X11 mouse-4 / mouse-5) to send SGR mouse-wheel escapes via vterm-send-string. tmux's existing WheelUpPane / WheelDownPane bindings route into copy-mode from there. For keyboard parity, route C-; x c through cj/vterm-copy-mode-dwim, which sends C-b [ when a tmux client is attached and falls back to vterm-copy-mode otherwise. tmux's history-limit is now reachable from either entry point. The matching copy-mode keys (M-w stays, C-g / q / Escape exit, Enter unbound) land in the dotfiles repo alongside.
* refactor(ai-config): switch gptel to local fork, drop tab-width adviceCraig Jennings2026-05-183-143/+2
| | | | | | | | | | | | | | 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.
* docs(design): keep local gptel-magit design draft as .local.orgCraig Jennings2026-05-171-0/+592
| | | | Two drafts of `docs/design/gptel-git-tools-magit-backend.org` existed at the same path: a 592-line local copy and the 192-line upstream version that just landed in main. I renamed the local draft to `.local.org` so the upstream version can sit at the canonical path. I'll reconcile the two in a follow-up.
* chore(todo): close Phase 1 ai-mcp foundationCraig Jennings2026-05-171-13/+2
|
* feat(ai-mcp): add pure-helper foundation with testsCraig Jennings2026-05-172-0/+835
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | First of nine phases for wiring mcp.el into GPTel. I scoped this phase to sections 1 (constants and defcustoms) and 3 (pure helpers) of the seven-section outline in the design doc. The other five sections ship in later phases. The module loads only under the test harness for now. init.el wiring waits for Phase 4. What I added: - cj/mcp-server-specs defconst: secret-free description of the 9 servers (linear, notion, figma, slack-deepsat, drawio, google-calendar, google-docs-personal, google-docs-work, google-keep). - Seven defcustoms: claude-config path, enabled-servers list, start-on-entry-points scope, two timeouts, per-tool confirm overrides, audit-log toggle. - cj/mcp--read-claude-config with an mtime cache and structured (:ok t/nil :reason ...) returns. - cj/mcp--get-server-entry, get-env, and get-secret-arg for pulling server data from the parsed config (figma's API key lives in args, not env). - cj/mcp--build-server-alist: pure transformer from specs plus config to the alist mcp-hub-servers expects. - cj/mcp--confirm-p classifier with write-pattern, read-pattern, and unknown-fails-closed branches, plus a cj/mcp-tool-confirm-overrides alist override. - cj/mcp--normalize-description prefixing tool descriptions with [SERVER], [SERVER WRITE], or [SERVER ?]. - cj/mcp--redact masking --token, --secret, --password, and --figma-api-key flags, Authorization headers, and ?token= URL params. Tests in tests/test-ai-mcp-helpers.el (41 ERT tests, all green): fixtures via make-temp-file, no real ~/.claude.json reads, no subprocesses, no network. Sentinel REDACTED_TEST_SECRET never appears in any redactor output. Design doc: docs/design/mcp-el-gptel-integration.org
* docs(design): MCP-into-gptel + gh-as-gptel-tool specs + MCP phasesCraig Jennings2026-05-173-1/+2644
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two new design docs in docs/design/ covering the next two GPTel work items, plus matching task scaffolding in todo.org. mcp-el-gptel-integration.org wires mcp.el into the config so GPTel gets access to the nine MCP servers Claude Code already uses (linear, notion, figma, slack-deepsat, drawio, google-calendar, google-docs-personal, google-docs-work, google-keep). The design covers async startup, the write-confirmation policy, a server-enablement defcustom, a doctor with live-auth-check, the audit buffer, and the mcp.el compatibility layer. The spec is at revision 3 after two code-review passes flagged a critical confirmation gap (gptel-confirm-tool-calls nil at ai-config.el:386 silently ignored per-tool :confirm slots) and several incorrect mcp.el API assumptions. Both are addressed. gptel-gh-tool.org wraps the gh CLI as a hybrid surface: 14 typed read wrappers plus one general write tool gated by :confirm t. Host/repo resolution is command-aware: --repo HOST/OWNER/REPO for repo commands, --hostname only for api and auth status. The runner enforces an irreversible-command blocklist, a 64KB in-flight output cap, and a debug-record plus last-error-buffer story. The spec is at revision 2 after a code-review pass corrected gh flag assumptions and reframed the safety story around per-tool confirm. todo.org gains a link to the MCP spec under the parent task plus nine TODO sub-tasks (one per implementation phase), and a new gh-tool TODO with the same spec-link shape.
* feat(ai-conversations): autosave on a periodic timerCraig Jennings2026-05-162-10/+155
| | | | | | | | | | | | The existing autosave only fired after gptel-send returned, so a conversation paused mid-thought wasn't on disk if Emacs crashed. I added a buffer-local repeating timer that calls cj/gptel--save-buffer-to-file every cj/gptel-autosave-interval seconds (default 60) for as long as cj/gptel--autosave-active-p holds. Toggle-off and kill-buffer-hook cancel it cleanly. Tests cover start/stop idempotency, the active-p predicate, the kill-buffer cleanup hook, and the toggle integration.