SLOPSHOPPER

agent4everything

Maps .agents/skills (global and per project) into .claude/skills so every skill is active, without duplicating existing ones

newtoastprocess
A shopper browsing a rack in a slop shop
README

Claude Configuration

This repository contains Claude Code configuration files and automatically syncs with the upstream repository.

Automated Sync

This repository automatically syncs from Melvynx/aiblueprint-cli-premium once per day at midnight UTC.

How It Works

Manual Sync

To trigger a sync manually:

  1. Go to the Actions tab in this repository
  2. Select "Sync from Upstream Repository" workflow
  3. Click "Run workflow" button
  4. Select the main branch and click "Run workflow"

Monitoring

  • Check the Actions tab to view sync history and status
  • Each sync run shows which commits were synchronized
  • You'll receive notifications for failed sync attempts (configure in Settings → Notifications)

Notes

  • This repository should be treated as read-only to avoid merge conflicts
  • The upstream repository always wins in case of conflicts
  • Sync logs are available in the Actions tab for 90 days
Source 1 files
hooks/register.ts 97 lines
1import type { Register } from 'claude-code'
2
3// `$` as a hook receives it.
4type Engine = Parameters<Parameters<Parameters<Register>[0]>[2]>[0]
5
6type Linked = { root: string; name: string }
7
8const SKILL_FILES = ['SKILL.md', 'skill.md']
9
10const realPath = async ($: Engine, path: string) =>
11  (await $.fs.stat(path, { resolve: true }).catch(() => undefined))?.realPath
12
13const homeDir = async ($: Engine) =>
14  (await $.process.run(['printenv', 'HOME'])).stdout.trim()
15
16// The session's cwd and every ancestor below $HOME, then $HOME itself (global).
17const rootsFor = (cwd: string, home: string) => {
18  const roots: string[] = []
19  let dir = cwd.replace(/\/+$/, '')
20  while (dir.startsWith(`${home}/`)) {
21    roots.push(dir)
22    dir = dir.slice(0, dir.lastIndexOf('/'))
23  }
24  if (!cwd.startsWith(`${home}/`) && cwd !== home) roots.push(cwd)
25  roots.push(home)
26  return roots
27}
28
29const isSkillDir = async ($: Engine, dir: string) => {
30  const stat = await $.fs.stat(dir).catch(() => undefined)
31  if (stat?.kind !== 'dir') return false
32  for (const file of SKILL_FILES) {
33    if (await $.fs.exists(`${dir}/${file}`)) return true
34  }
35  return false
36}
37
38// Links each <root>/.agents/skills/<name> missing from <root>/.claude/skills.
39const syncRoot = async ($: Engine, root: string): Promise<Linked[]> => {
40  const source = `${root}/.agents/skills`
41  const target = `${root}/.claude/skills`
42  if (!(await $.fs.exists(source))) return []
43
44  // .claude/skills already is .agents/skills (a symlinked folder): nothing to map.
45  const sourceReal = await realPath($, source)
46  if (sourceReal !== undefined && sourceReal === (await realPath($, target))) return []
47
48  // Every name already in .claude/skills, dangling links included, case-insensitive.
49  const existing = new Set(
50    (await $.fs.list(target).catch(() => [])).map(entry => entry.name.toLowerCase()),
51  )
52
53  const linked: Linked[] = []
54  for (const entry of await $.fs.list(source)) {
55    const { name } = entry
56    if (name.startsWith('.') || name.startsWith('__')) continue
57    if (existing.has(name.toLowerCase())) continue
58    if (!(await isSkillDir($, `${source}/${name}`))) continue
59
60    if (linked.length === 0) await $.process.run(['mkdir', '-p', target])
61    const ran = await $.process.run(['ln', '-s', `../../.agents/skills/${name}`, `${target}/${name}`])
62    if (ran.exitCode === 0) linked.push({ root, name })
63    else $.ui.log(`agent4everything: ln failed for ${name}: ${ran.stderr.trim()}`, { to: 'debug' })
64  }
65  return linked
66}
67
68const syncAll = async ($: Engine, cwd: string) => {
69  const home = await homeDir($)
70  const linked: Linked[] = []
71  for (const root of rootsFor(cwd, home)) linked.push(...(await syncRoot($, root)))
72  if (linked.length > 0) {
73    $.ui.toast(`agent4everything: linked ${linked.map(l => l.name).join(', ')}`)
74  }
75  return linked
76}
77
78export const register: Register = on => {
79  // Awaited before the first prompt: links exist before the session reads skills again.
80  on('session.start', async ($, e, next) => {
81    await syncAll($, e.cwd).catch(error => $.ui.log(`agent4everything: ${error}`, { to: 'debug' }))
82    return next(e)
83  })
84
85  // Startup, resume, /clear, compact: sync again and have the engine re-read skills.
86  on('classic.SessionStart', async ($, e, next) => {
87    await syncAll($, e.cwd).catch(() => [])
88    const result = await next(e)
89    return { ...result, reloadSkills: true }
90  })
91
92  on('classic.CwdChanged', async ($, e, next) => {
93    await syncAll($, e.new_cwd).catch(() => [])
94    return next(e)
95  })
96}
97