SLOPSHOPPER

agents-feed

Writes each session's state, context fill and rate limits to ~/.cache/claude-agents for the Noctalia agents widget

guardprocess
★ 6v0.1.0no licenseupdated 2026-10-18avegancafe/Juliet/symlinked/home/.claude/mods/agents-feed
A shopper browsing a rack in a slop shop
README

Juliet

A comprehensive dotfiles repository for macOS development environment setup. Manages configurations for terminal applications, editors, system utilities, and development tools through symlinked dotfiles and automated setup scripts.

Setup

To set up Juliet, run:

$ git clone --recursive git@github.com:avegancafe/Juliet ~/.config/Juliet
$ ~/.config/Juliet/bin/juliet-bootstrap

Available Tools and Functions

System Setup & Management

Core Binaries:

  • juliet-bootstrap - Main setup and update script
  • Installs Homebrew and dependencies
  • Updates Juliet repository from git
  • Runs brew bundle to install packages
  • Sets up symlinks using GNU Stow
  • Usage: ~/.config/Juliet/bin/juliet-bootstrap

System Utilities:

  • reindex-spotlight - macOS Spotlight reindexing
  • update-kitty - Kitty terminal updater
  • envsource - Environment variable sourcing
  • sudo - Enhanced sudo wrapper

Git & Version Control

Git Binaries:

  • better-branch - Enhanced git branch viewer
  • Shows colorized branch list with commit counts
  • Displays ahead/behind status relative to main branch
  • Shows last commit info for each branch
  • delete-current-branch - Safe branch deletion
  • Switches to main branch and deletes current branch
  • Logs deleted commit SHA to ~/.delog for recovery
  • git-recreate - Branch recreation utility
  • Deletes current branch and recreates it from latest main
  • Useful for starting fresh while keeping branch name
  • git-hoard/ - Git repository organization tool (Node.js project)

Git Functions:

  • g - Short alias for git
  • current-branch - Get current git branch name
  • cb - Copy current branch name to clipboard
  • current-sha - Get current commit SHA
  • default-branch - Get repository's default branch
  • gdiff - Git diff wrapper
  • gut - Git status shortcut
  • clone - Enhanced git clone

Git Aliases:

  • git a - Add files to staging
  • git ape - Amend previous commit and force push with lease
  • git b / git bbranch - Show enhanced branch list (uses better-branch)
  • git clean-branches - Delete all branches except master/main
  • git clean-merged / git cm - Delete branches that have been merged
  • git co - Checkout branch/commit
  • git com - Commit changes
  • git cp - Cherry-pick commits
  • git del - Delete current branch safely (uses delete-current-branch)
  • git diff-branch - Show diff from fork point with master
  • git fixup - Create fixup commit for interactive rebase
  • git fo - Fetch from origin
  • git judge - Enhanced blame with whitespace/move detection
  • git main - Switch to main/master branch automatically
  • git o / git open - Open repository in GitHub
  • git pdiff - Pretty diff using bat with syntax highlighting
  • git pf - Push with force-with-lease (safer force push)
  • git pu - Pull changes
  • git rb - Rebase commits
  • git re - Restore files
  • git s - Switch branches
  • git squash - Interactive rebase to squash commits against main
  • git staash - Stash all changes including untracked files
  • git staged - Show staged changes
  • git su - Update submodules recursively
  • git t - Ticket (custom command)
  • git up - Push changes

Editor & Development Environment

Editor Tools:

  • nv - Launch Neovide GUI editor, auto-installs if missing
  • vim - Alias for nvim
  • cursor - Launch Cursor editor

Neovim Management:

  • nvim-refresh-plugins - Neovim plugin manager
  • Clears lazy.nvim plugin cache
  • Restores plugins from lock file

File Operations & Navigation

Enhanced File Tools:

  • cat - Enhanced cat using bat with syntax highlighting
  • ls - Enhanced ls using eza with better formatting

Workspace Management:

  • work - Interactive workspace/directory switcher
  • Reads from ~/.workspaces and ~/.workspaces__local
  • Supports @workspace (directories with subdirs) and @dir (specific dirs)
  • Uses gum for interactive selection
  • Setup example: `` @workspace ~/workspace @dir ~/.config/nvim ``

Development & DevOps Tools

GitHub Integration:

  • run - GitHub Actions workflow runner
  • Runs workflows with optional current branch
  • Watches workflow execution
  • Usage: run <workflow-name> [-c/--current] [-a/--args="args"]
  • gist-to-pdf - GitHub Gist converter
  • Clones gists and converts to PDF format
  • Usage: gist-to-pdf <gist-id>

Container & Cloud Tools:

  • dc - Docker Compose shortcut
  • kc - kubectl shortcut

Language-Specific Tools:

  • node - Node.js wrapper
  • has-pipfile - Check for Python Pipfile

Monitoring & Utilities

System Monitoring:

  • logs - System log viewer
  • watch - File watching utility
  • pv - Process viewer
  • st - Status checker

General Utilities:

  • note - Note-taking utility

Downloads

Architecture

  • symlinked/ - Contains all dotfiles organized by target location
  • symlinked/config/ - Files that go in ~/.config/
  • symlinked/home/ - Files that go in ~/
  • bin/ - Custom utility scripts and tools
  • etc/ - Assets (fonts, themes, wallpapers, keyboard layouts)
  • Brewfile - Homebrew package definitions

All Fish shell functions are auto-loaded alphabetically from symlinked/config/fish/functions/aliases/ and integrate with the broader Juliet ecosystem for a cohesive development environment.

Source 2 files
hooks/register.ts 154 lines
1// agents-feed: every session writes what it is doing to
2// ~/.cache/claude-agents/<sessionId>.json for the Noctalia claude_agents widget,
3// plus the account-wide rate limits to _usage.json. Observe only: every hook
4// passes its event through unchanged, and a failure here never reaches the session.
5
6import type { EngineInterface, Register } from 'claude-code'
7
8import { clip, feedDir, parseMarkers, summarize } from './feed'
9import type { Feed, FeedRateLimit, FeedState } from './feed'
10
11let feed: Feed | null = null
12let dir = ''
13// Set when a tool call's permission check came back `ask` (a prompt is up);
14// the call itself starting means you answered it.
15let isAwaitingPermission = false
16
17const IGNORED_NOTIFICATIONS = new Set(['idle_prompt', 'auth_success'])
18
19// The session's file as last written, so a reload of this module (which runs
20// session.start again with fresh variables) picks up where it left off.
21async function previous($: EngineInterface, path: string, sessionId: string): Promise<Partial<Feed>> {
22  try {
23    const old = JSON.parse(await $.fs.read(path)) as Partial<Feed>
24    return old.sessionId === sessionId && old.state !== 'ended' ? old : {}
25  } catch {
26    return {}
27  }
28}
29
30async function ensure($: EngineInterface): Promise<Feed> {
31  if (feed) return feed
32  const home = (await $.env.get('HOME')) ?? ''
33  const jobDir = await $.env.get('CLAUDE_JOB_DIR')
34  const sessionId = await $.session.id()
35  dir = feedDir(home)
36  const old = await previous($, `${dir}/${sessionId}.json`, sessionId)
37  feed = {
38    v: 1,
39    state: old.state ?? 'idle',
40    reason: old.reason ?? '',
41    summary: old.summary ?? '',
42    context: old.context ?? {},
43    rateLimits: old.rateLimits ?? [],
44    updatedAt: old.updatedAt ?? 0,
45    sessionId,
46    jobId: jobDir ? (jobDir.replace(/\/+$/, '').split('/').pop() ?? null) : null,
47    kind: jobDir ? 'background' : 'interactive',
48    cwd: await $.session.cwd(),
49    model: await $.session.model(),
50  }
51  return feed
52}
53
54// Tell a running widget to re-read now; missing noctalia (macOS) just fails quietly.
55function ping($: EngineInterface) {
56  $.process.run(['noctalia', 'msg', 'plugin', 'kyle/claude_agents:status', 'all', 'refresh']).catch(() => {})
57}
58
59async function save($: EngineInterface, patch: Partial<Feed>) {
60  try {
61    const f = await ensure($)
62    Object.assign(f, patch, { updatedAt: await $.clock.now() })
63    await $.fs.write(`${dir}/${f.sessionId}.json`, JSON.stringify(f))
64    ping($)
65  } catch {
66    // never let the feed break the session
67  }
68}
69
70function setState($: EngineInterface, state: FeedState, reason = '') {
71  const text = clip(reason)
72  return save($, { state, reason: text, summary: summarize(text) })
73}
74
75export const register: Register = (on) => {
76  on('session.start', async ($, e, next) => {
77    await save($, { cwd: e.cwd })
78    return next(e)
79  })
80
81  on('turn.start', async ($, e, next) => {
82    if (e.agentId === undefined) await setState($, 'working')
83    return next(e)
84  })
85
86  on('turn.complete', async ($, e, next) => {
87    if (e.agentId === undefined) {
88      const f = await ensure($).catch(() => null)
89      const isError = e.reason === 'error' || e.reason === 'refusal'
90      const marked = f?.kind === 'background' ? parseMarkers(e.answer) : null
91      if (marked) await setState($, marked.state, marked.reason)
92      else if (isError) await setState($, 'failed', e.reason === 'refusal' ? (e.refusal.explanation ?? 'refused') : 'API error')
93      else if (e.isAborted) await setState($, 'idle')
94      else if (f?.kind === 'interactive') await setState($, 'done', e.answer)
95      else await setState($, 'idle', e.answer)
96    }
97    return next(e)
98  })
99
100  // The model is asking you something: blocked on the question until it's answered.
101  on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
102    if (e.agentId === undefined) await setState($, 'blocked', e.questions[0]?.question ?? 'question')
103    const result = await next(e)
104    if (e.agentId === undefined) await setState($, 'working')
105    return result
106  }).catch(($, e, next) => next(e))
107
108  // A permission prompt: the check answered `ask`. Read the verdict, never change it.
109  on('tool.check', async ($, e, next) => {
110    const verdict = await next(e)
111    if (e.agentId === undefined && verdict.decision === 'ask') {
112      isAwaitingPermission = true
113      await setState($, 'blocked', `permission: ${e.tool}`)
114    }
115    return verdict
116  }).catch(($, e, next) => next(e))
117
118  // The call running means the prompt was answered: back to work.
119  on('tool.call', async ($, e, next) => {
120    if (isAwaitingPermission && e.agentId === undefined) {
121      isAwaitingPermission = false
122      await setState($, 'working')
123    }
124    return next(e)
125  }).catch(($, e, next) => next(e))
126
127  // Permission prompts and the like, where classic hooks reach installed plugins (an
128  // organization's managed settings can withhold them). The idle "waiting for your
129  // input" notice is skipped: turn.complete already said why the turn ended, and
130  // that reason is the useful one.
131  on('classic.Notification', async ($, e, next) => {
132    if (!IGNORED_NOTIFICATIONS.has(e.notification_type)) await setState($, 'blocked', e.message)
133    return next(e)
134  }).catch(($, e, next) => next(e))
135
136  on('session.measure', async ($, e, next) => {
137    const rateLimits: FeedRateLimit[] = e.rateLimits.map((r) => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt }))
138    await save($, { context: { percent: e.context.percent, tokens: e.context.tokens, window: e.context.window }, rateLimits })
139    if (rateLimits.length > 0) {
140      try {
141        await $.fs.write(`${dir}/_usage.json`, JSON.stringify({ at: await $.clock.now(), rateLimits }))
142      } catch {
143        // best-effort
144      }
145    }
146    return next(e)
147  })
148
149  on('session.end', async ($, e, next) => {
150    await setState($, 'ended')
151    return next(e)
152  })
153}
154
hooks/feed.ts 64 lines
1// Pure pieces of the feed: the snapshot shape, the background-job marker lines and
2// the <=3-word summary. No `$` here, so the tests can hold them directly.
3
4export type FeedState = 'idle' | 'working' | 'blocked' | 'failed' | 'done' | 'ended'
5
6export type FeedRateLimit = { kind: string; percentUsed: number; resetsAt?: string }
7
8// One file per session at ~/.cache/claude-agents/<sessionId>.json, read by the
9// Noctalia claude_agents widget (symlinked/home/.local/share/noctalia/plugins/claude_agents).
10export type Feed = {
11  v: 1
12  sessionId: string
13  jobId: string | null
14  kind: 'background' | 'interactive'
15  cwd: string
16  model: string
17  state: FeedState
18  reason: string
19  summary: string
20  context: { percent?: number; tokens?: number; window?: number }
21  rateLimits: FeedRateLimit[]
22  updatedAt: number
23}
24
25// Background sessions end a turn with one of these lines (see the background-job
26// conventions): `result:` delivered, `needs input:` blocked on you, `failed:` stuck.
27const MARKERS: ReadonlyArray<[RegExp, FeedState]> = [
28  [/^\s*result:\s*(.*)$/i, 'done'],
29  [/^\s*needs input:\s*(.*)$/i, 'blocked'],
30  [/^\s*failed:\s*(.*)$/i, 'failed'],
31]
32
33// The last marker line in the answer wins; null when there is none.
34export function parseMarkers(answer: string): { state: FeedState; reason: string } | null {
35  let found: { state: FeedState; reason: string } | null = null
36  for (const line of answer.split('\n')) {
37    for (const [rx, state] of MARKERS) {
38      const m = rx.exec(line)
39      if (m) found = { state, reason: (m[1] ?? '').trim() }
40    }
41  }
42  return found
43}
44
45const FILLER = new Set(['a', 'an', 'the', 'and', 'of', 'to', 'for', 'with', 'in', 'on', 'that', 'this', 'it', 'is', 'as'])
46
47// "delivered agent roster, commands, and run workflow" -> "agent roster commands"
48export function summarize(reason: string): string {
49  const words = (reason.toLowerCase().match(/[a-z][a-z0-9_]*/g) ?? []).filter((w) => !FILLER.has(w))
50  // Past-tense summaries ("delivered ...", "fixed ..."): drop the verb.
51  if (words.length > 3 && words[0]?.endsWith('ed')) words.shift()
52  return words.slice(0, 3).join(' ')
53}
54
55// First line, trimmed to what fits a panel row.
56export function clip(text: string, max = 160): string {
57  const line = text.trim().split('\n')[0] ?? ''
58  return line.length > max ? `${line.slice(0, max - 1)}…` : line
59}
60
61export function feedDir(home: string): string {
62  return `${home}/.cache/claude-agents`
63}
64