diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..929d0b7 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "xurl", + "owner": { + "name": "Xuanwo" + }, + "metadata": { + "description": "Claude Code plugins for agents:// workflows built around xurl" + }, + "plugins": [ + { + "name": "xurl-statusline", + "source": "./plugins/xurl-statusline", + "description": "Show agents://claude URIs in the Claude Code status line", + "version": "0.1.0", + "author": { + "name": "Xuanwo" + } + } + ] +} diff --git a/plugins/xurl-statusline/.claude-plugin/plugin.json b/plugins/xurl-statusline/.claude-plugin/plugin.json new file mode 100644 index 0000000..f94a4bd --- /dev/null +++ b/plugins/xurl-statusline/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "xurl-statusline", + "version": "0.1.0", + "description": "Display agents:// Claude URIs in the status line.", + "author": { + "name": "Xuanwo" + } +} diff --git a/plugins/xurl-statusline/README.md b/plugins/xurl-statusline/README.md new file mode 100644 index 0000000..a75fca3 --- /dev/null +++ b/plugins/xurl-statusline/README.md @@ -0,0 +1,27 @@ +# xurl-statusline + +Claude Code plugin that shows the current Claude conversation as an `agents://` URI in the status line. + +## Install + +```text +/plugin marketplace add Xuanwo/xurl +/plugin install xurl-statusline@xurl +``` + +## Configure + +Ask Claude to use the `xurl-statusline-installer` agent to configure your status line. + +Copy and send: + +```text +Use the xurl-statusline-installer agent to configure Claude Code status line for the xurl-statusline plugin. +``` + +The agent updates `~/.claude/settings.json` for you. Run it again after plugin updates so the path stays current. + +The script prints: + +- `agents://claude/` for main threads +- `agents://claude//` for Claude sidechain transcripts diff --git a/plugins/xurl-statusline/agents/xurl-statusline-installer.md b/plugins/xurl-statusline/agents/xurl-statusline-installer.md new file mode 100644 index 0000000..7360747 --- /dev/null +++ b/plugins/xurl-statusline/agents/xurl-statusline-installer.md @@ -0,0 +1,23 @@ +--- +name: xurl-statusline-installer +description: Configure Claude Code statusLine.command for the xurl status line plugin. +--- + +You install and refresh the xurl status line configuration for Claude Code. + +When invoked: + +1. Read `~/.claude/settings.json` if it exists. +2. Update or create `statusLine.command` with: + `node ${CLAUDE_PLUGIN_ROOT}/scripts/agents_uri_statusline.js` +3. Preserve unrelated settings. +4. If `statusLine` exists, keep existing fields unless they conflict with the command-based status line setup. +5. Tell the user which file you changed and the final command value. + +Rules: + +- Use the current `${CLAUDE_PLUGIN_ROOT}` path exactly as provided. +- If the settings file is missing, create a minimal valid JSON object. +- Keep the file formatted as readable JSON. +- Do not modify any other Claude files. +- Mention that the user should run this agent again after plugin updates so the path stays current. diff --git a/plugins/xurl-statusline/scripts/agents_uri_statusline.js b/plugins/xurl-statusline/scripts/agents_uri_statusline.js new file mode 100644 index 0000000..3344e2e --- /dev/null +++ b/plugins/xurl-statusline/scripts/agents_uri_statusline.js @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); +const path = require("node:path"); +const readline = require("node:readline"); + +const AGENT_FILE_RE = /^agent-(?[^/]+)\.jsonl$/; + +async function main() { + const payload = readPayload(); + if (!payload) { + return; + } + + const sessionId = payload.session_id; + if (typeof sessionId !== "string" || sessionId.length === 0) { + return; + } + + const agentId = await inferAgentId(sessionId, payload.transcript_path); + if (agentId) { + process.stdout.write(`agents://claude/${sessionId}/${agentId}\n`); + return; + } + + process.stdout.write(`agents://claude/${sessionId}\n`); +} + +function readPayload() { + let raw; + try { + raw = fs.readFileSync(0, "utf8"); + } catch { + return null; + } + + if (raw.trim().length === 0) { + return null; + } + + try { + const payload = JSON.parse(raw); + return payload && typeof payload === "object" ? payload : null; + } catch { + return null; + } +} + +async function inferAgentId(sessionId, transcriptPath) { + if (typeof transcriptPath !== "string" || transcriptPath.length === 0) { + return null; + } + + const agentIdFromHeader = await inferAgentIdFromHeader(sessionId, transcriptPath); + if (agentIdFromHeader) { + return agentIdFromHeader; + } + + const match = AGENT_FILE_RE.exec(path.basename(transcriptPath)); + return match?.groups?.agentId ?? null; +} + +async function inferAgentIdFromHeader(sessionId, transcriptPath) { + let stream; + try { + stream = fs.createReadStream(transcriptPath, { encoding: "utf8" }); + } catch { + return null; + } + + const rl = readline.createInterface({ + input: stream, + crlfDelay: Infinity, + }); + + let lineCount = 0; + try { + for await (const line of rl) { + lineCount += 1; + if (lineCount > 30) { + break; + } + if (line.trim().length === 0) { + continue; + } + + let entry; + try { + entry = JSON.parse(line); + } catch { + continue; + } + + if ( + entry && + typeof entry === "object" && + typeof entry.agentId === "string" && + entry.agentId.length > 0 && + typeof entry.sessionId === "string" && + entry.sessionId === sessionId && + entry.isSidechain === true + ) { + return entry.agentId; + } + + return null; + } + } catch { + return null; + } finally { + rl.close(); + stream.destroy(); + } + + return null; +} + +main().catch(() => process.exit(0));