SLOPSHOPPER

task-link

Shows the session's SpaceHub task as a link above the prompt; /taskurl sets or replaces it

newbandcommandtoastprompt
v0.1.0no licenseupdated 2026-10-06daanqq/agent-config/claude/skills/task-link
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · task-link
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ task-link │ ⏺ Read(src/auth.ts) │ Usage: /taskurl │ ⎿ Read 6 lines │ https://spacehub.esoft.tech/entity/EUTP-170658 │ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /taskurl ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Agent configuration platform

One repository for Pi, Claude Code, and portable Agent Skills. The clients keep their usual HOME paths; links.json connects selected configuration files and skill directories to this checkout.

Layout

  • common/skills/: portable skills with closed relative dependencies.
  • common/skills-maintenance/: one source lock, scoped inventory, checker, and semantic overlays.
  • pi/: Pi configuration, extensions, prompts, and client-specific skills.
  • claude/: Claude Code configuration, hooks, themes, local plugin source, and client-specific skills.
  • scripts/install.py: selective links, private backups, and conservative restore.

Client skill directories contain repository-relative links to the shared skills. HOME receives individual shared skill links in .agents/skills/ too; existing installer-owned skills such as Plannotator are not replaced.

retro stays client-specific even though its text matches: it depends on the client-specific writing-for-agents skill. mr-echat remains a Pi extension; the Claude skill is retained only in the Claude configuration.

Prepare and check

Requirements: Git, Python 3.10+, and the clients' existing dependencies. Node.js and npm are needed for Pi extensions. No new dependencies were introduced.

cd ~/agent-config
make check
python3 common/skills-maintenance/check.py --check-sources
python3 scripts/install.py --backup-existing

The last command is a dry run. --backup-existing lets it plan replacing occupied paths; no HOME paths are changed without --apply.

--check-sources reads locked Git revisions from ~/.cache/checkouts/. It does not fetch. Missing source caches are reported, not silently reconstructed.

Exact upstream copies retain their original formatting. .gitattributes permits upstream trailing whitespace only in the DnD and Vercel skill trees; source checks still require their complete contents to match the locked commits.

Connect HOME

This checkout is prepared separately; the original clients are not switched automatically. Review the dry run and stop Pi and Claude Code before connecting to avoid concurrent settings writes. Install the existing locked extension dependencies in the new checkout first:

make dep
python3 scripts/install.py --backup-existing --apply

Dependencies are not copied from the old clients, and make dep needs network access. The installer also links each declared Pi extension package's node_modules into HOME so the extension loader can resolve dependencies from the linked files. These directories stay local and untracked; existing HOME dependency directories are backed up and can be restored like configuration. Installing links before make dep is supported: dependency links remain dangling until npm creates the directories. Afterwards start new client sessions, or reload Pi's resources.

Existing files and skill directories are moved, not deleted, into a private run directory under ~/.local/state/agent-config/backups/. The installer prints its restore journal path. Correct links are left unchanged on repeated runs.

For an isolated HOME, use --home /absolute/path/to/existing/home. The same manifest is used; unknown files and runtime directories are left alone.

Restore

python3 scripts/install.py --restore <printed-run-directory>
python3 scripts/install.py --restore <printed-run-directory> --apply

Restore is also a dry run by default. It refuses to overwrite an installed link that another process replaced or changed. Backups and their journal remain available after restoration; they are not removed automatically.

Local data and credentials

The repository does not import authentication files, secrets, sessions, plugin caches, history, or the old repositories' .git directories. The original checkouts and their uncommitted changes remain available until you explicitly retire them. No history was rewritten and no remote was configured.

The existing Pi models.json contains a literal API key and is deliberately not linked or copied into this repository. pi/agent/models.example.json uses ${CLIPROXY_API_KEY} instead. Existing local ~/.pi/agent/models.json continues to work; on another machine, copy the example there and configure the variable privately. The installer does not perform that copy.

Claude and the Pi example use CLIProxyAPI through Tailscale at https://new.tail354056.ts.net; Pi's Codex provider retains the /backend-api suffix. Connect Tailscale with MagicDNS enabled before starting the clients. Existing private ~/.pi/agent/models.json files must be updated separately; changing the example does not change them. API keys stay unchanged. The SSH tunnel remains available as a fallback, with URLs changed back to localhost.

Claude's installed-plugin state and Pi's old checkout-specific .pi/settings.json are also excluded. Package/plugin declarations are retained in client settings; their installers own runtime installation state.

After activation, edit the files in this checkout. A client or third-party installer may replace a HOME symlink with a regular file. Re-run the installer in dry-run mode to detect it and reconcile those changes before replacing it.

Claude quota footer

The quota refresher uses ~/.pi/agent/secrets/cliproxy-management.json when that file exists. It must contain managementUrl and managementKey. If the file is absent, it reads CLIPROXY_MANAGEMENT_URL and CLIPROXY_MANAGEMENT_KEY from the environment inherited by Claude Code. An invalid existing file is reported as an error; it does not fall back to environment credentials.

The key must be the plaintext CLIProxyAPI management key, not a model API key or the bcrypt hash in the proxy configuration. Keep it outside this repository. Use https://new.tail354056.ts.net for managementUrl or CLIPROXY_MANAGEMENT_URL, not the separate panel's HTTPS port 18443. The management endpoint must be reachable and expose an enabled Claude account. The scripts require jq, curl, and setsid for background refreshes.

Skills

python3 common/skills-maintenance/check.py
python3 common/skills-maintenance/check.py --scope pi
python3 common/skills-maintenance/check.py --scope claude

Use the shared update-skills skill for upstream reconciliation. The global source lock is shared; client-specific behavior is preserved in scoped overlays. Refresh hashes only after classifying accepted changes, never to hide unrelated drift. See common/skills-maintenance/README.md.

Source 2 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { TaskLink } from '../types'
5
6// Mirrors pi/agent/extensions/00-ui-editor.ts.
7export const TASK_URL_PREFIX = 'https://spacehub.esoft.tech/entity/'
8export const COMMAND = 'taskurl'
9
10const task = atom({ plugin: 'task-link', key: 'task' } as const, null)
11
12export function parseTaskUrl(value: unknown): TaskLink | undefined {
13  if (typeof value !== 'string' || !value.startsWith(TASK_URL_PREFIX)
14    || /[\s\x00-\x1f\x7f-\x9f]/.test(value)) return undefined
15  try {
16    const url = new URL(value)
17    const id = url.pathname.match(/^\/entity\/([A-Za-z0-9][A-Za-z0-9_-]*)\/?$/)?.[1]
18    if (id) return { id, url: url.href }
19  } catch {
20    return undefined
21  }
22  return undefined
23}
24
25export function firstTaskLink(text: string): TaskLink | undefined {
26  const links = text.matchAll(/https:\/\/spacehub\.esoft\.tech\/entity\/[^\s<>"'`\)\]\}]+/g)
27  for (const [link] of links) {
28    const found = parseTaskUrl(link.replace(/[.,;:!?]+$/, ''))
29    if (found) return found
30  }
31  return undefined
32}
33
34// The store outlives the session, so the task is kept per session id and
35// comes back on --resume.
36async function setTask($: EngineInterface, next: TaskLink): Promise<void> {
37  await $.store.set(await $.session.id(), next.url)
38  await update($, task, () => next)
39}
40
41export const register: Register = on => {
42  on('session.start', async ($, e, next) => {
43    await $.command.register({
44      name: COMMAND,
45      description: "Set or replace the session's SpaceHub task URL",
46      argumentHint: `${TASK_URL_PREFIX}EUTP-170658`,
47    })
48    const restored = parseTaskUrl(await $.store.get(await $.session.id()))
49    await update($, task, () => restored ?? null)
50
51    return next(e)
52  })
53
54  on('prompt.submit', async ($, e, next) => {
55    if (e.origin.kind === 'composer' && (await read($, task)) === null) {
56      const first = firstTaskLink(e.text)
57      if (first) await setTask($, first)
58    }
59
60    return next(e)
61  })
62
63  on('command.run', { command: COMMAND }, async ($, e) => {
64    const next = parseTaskUrl(e.args.trim())
65    if (!next) {
66      $.ui.toast(`Usage: /${COMMAND} ${TASK_URL_PREFIX}EUTP-170658`)
67      return {}
68    }
69    await setTask($, next)
70    $.ui.toast(`Session task: ${next.id}`)
71
72    return {}
73  })
74
75  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
76    const current = await read($, task)
77    if (current === null || e.props.hasSurvey) return next(e)
78
79    const { Box, Text } = $.ui.resolve(e)
80
81    // Plain text, not Link: without FORCE_HYPERLINK Claude Code does not emit
82    // OSC 8 under herdr, and herdr opens a clicked URL found in the text,
83    // trimming the unbalanced closing bracket.
84    return (
85      <Box width={e.props.bodyColumns} justifyContent="flex-end">
86        <Text>[{current.url}]</Text>
87      </Box>
88    )
89  })
90}
91
types/index.d.ts 8 lines
1export type TaskLink = { id: string; url: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'task-link': { task: TaskLink | null }
6  }
7}
8