use-agentz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +308 -0
  2. package/dist/authoring/define.js +132 -0
  3. package/dist/cli.js +26581 -0
  4. package/dist/config/define.js +36 -0
  5. package/dist/ui/web/index.html +16 -0
  6. package/dist/ui/web/logo.png +0 -0
  7. package/dist/ui/web/main.tsx +2406 -0
  8. package/dist/ui/web/styles.css +2338 -0
  9. package/package.json +54 -0
  10. package/registry/r/agent.analyst.json +44 -0
  11. package/registry/r/agent.designer.json +34 -0
  12. package/registry/r/agent.developer.json +31 -0
  13. package/registry/r/agent.expert-orchestrator.json +36 -0
  14. package/registry/r/agent.expert.json +33 -0
  15. package/registry/r/agent.orchestrator.json +39 -0
  16. package/registry/r/agent.quick.json +28 -0
  17. package/registry/r/agent.reviewer-anthropic.json +34 -0
  18. package/registry/r/agent.reviewer-gpt.json +34 -0
  19. package/registry/r/agent.scout.json +34 -0
  20. package/registry/r/command.caveman.json +20 -0
  21. package/registry/r/command.grill-me.json +20 -0
  22. package/registry/r/group.cli-bun.json +13 -0
  23. package/registry/r/group.cli-go.json +13 -0
  24. package/registry/r/group.database-design.json +12 -0
  25. package/registry/r/group.design.json +14 -0
  26. package/registry/r/group.documents.json +13 -0
  27. package/registry/r/group.engineering-process.json +18 -0
  28. package/registry/r/group.go.json +21 -0
  29. package/registry/r/group.python.json +10 -0
  30. package/registry/r/group.react.json +11 -0
  31. package/registry/r/group.typescript.json +10 -0
  32. package/registry/r/lsp.proto.json +41 -0
  33. package/registry/r/mcp.context7.json +13 -0
  34. package/registry/r/mcp.playwright.json +50 -0
  35. package/registry/r/pack.basic.json +78 -0
  36. package/registry/r/pack.gpt-5-6.json +78 -0
  37. package/registry/r/pack.optimized.json +78 -0
  38. package/registry/r/pack.opus-fable.json +78 -0
  39. package/registry/r/plugin.caveman-compress.json +20 -0
  40. package/registry/r/preset.caveman-only.json +13 -0
  41. package/registry/r/preset.cost-aware-trio.json +41 -0
  42. package/registry/r/preset.cross-model-review.json +34 -0
  43. package/registry/r/preset.design.json +39 -0
  44. package/registry/r/preset.fable-pack.json +41 -0
  45. package/registry/r/preset.full.json +67 -0
  46. package/registry/r/rule.always-cross-review.json +25 -0
  47. package/registry/r/rule.always-design.json +25 -0
  48. package/registry/r/rule.always-qa.json +25 -0
  49. package/registry/r/rule.always-test.json +25 -0
  50. package/registry/r/rule.caveman-ultra.json +23 -0
  51. package/registry/r/rule.never-cross-review.json +25 -0
  52. package/registry/r/rule.never-design.json +25 -0
  53. package/registry/r/rule.never-qa.json +25 -0
  54. package/registry/r/rule.never-test.json +25 -0
  55. package/registry/r/rule.routing.json +26 -0
  56. package/registry/r/rule.use-question-tool.json +20 -0
  57. package/registry/r/skill.bun-cli.json +20 -0
  58. package/registry/r/skill.bun-tui.json +20 -0
  59. package/registry/r/skill.caveman-compress.json +17 -0
  60. package/registry/r/skill.caveman.json +20 -0
  61. package/registry/r/skill.cli-design.json +15 -0
  62. package/registry/r/skill.code-review.json +18 -0
  63. package/registry/r/skill.cost-aware-orchestration.json +14 -0
  64. package/registry/r/skill.cross-model-review.json +14 -0
  65. package/registry/r/skill.db-schema-design.json +15 -0
  66. package/registry/r/skill.design-system.json +19 -0
  67. package/registry/r/skill.design-taste.json +116 -0
  68. package/registry/r/skill.diagnosing-bugs.json +23 -0
  69. package/registry/r/skill.diagrams.json +21 -0
  70. package/registry/r/skill.docx-create.json +20 -0
  71. package/registry/r/skill.domain-modeling.json +18 -0
  72. package/registry/r/skill.frontend-qa.json +19 -0
  73. package/registry/r/skill.go-architecture.json +21 -0
  74. package/registry/r/skill.go-ci.json +21 -0
  75. package/registry/r/skill.go-cli.json +20 -0
  76. package/registry/r/skill.go-data-structures.json +21 -0
  77. package/registry/r/skill.go-dependencies.json +21 -0
  78. package/registry/r/skill.go-dev.json +20 -0
  79. package/registry/r/skill.go-documentation.json +21 -0
  80. package/registry/r/skill.go-ecosystem.json +21 -0
  81. package/registry/r/skill.go-qa.json +21 -0
  82. package/registry/r/skill.go-quality.json +21 -0
  83. package/registry/r/skill.go-refactoring.json +21 -0
  84. package/registry/r/skill.go-tooling.json +21 -0
  85. package/registry/r/skill.go-tui.json +20 -0
  86. package/registry/r/skill.go-types.json +21 -0
  87. package/registry/r/skill.grilling.json +18 -0
  88. package/registry/r/skill.improve-architecture.json +18 -0
  89. package/registry/r/skill.pdf-create.json +23 -0
  90. package/registry/r/skill.pptx-create.json +23 -0
  91. package/registry/r/skill.product-ui.json +22 -0
  92. package/registry/r/skill.python-dev.json +20 -0
  93. package/registry/r/skill.react-dev.json +20 -0
  94. package/registry/r/skill.resolving-merge-conflicts.json +18 -0
  95. package/registry/r/skill.scratchpad.json +17 -0
  96. package/registry/r/skill.skill-authoring.json +17 -0
  97. package/registry/r/skill.software-design.json +28 -0
  98. package/registry/r/skill.sql-migrations.json +15 -0
  99. package/registry/r/skill.tdd.json +28 -0
  100. package/registry/r/skill.typescript-dev.json +20 -0
  101. package/registry/r/skill.visual-assets.json +17 -0
  102. package/registry/r/skill.wayfinder.json +18 -0
  103. package/registry/r/skill.xlsx-create.json +20 -0
  104. package/registry/r/tool.a11y-quick.json +24 -0
  105. package/registry/r/tool.contrast-check.json +24 -0
  106. package/registry/r/tool.design-lint.json +24 -0
  107. package/registry/r/tool.design-search.json +23 -0
  108. package/registry/r/tool.pdf-create.json +23 -0
  109. package/registry/r/tool.pptx-create.json +23 -0
  110. package/registry/r/tool.scratchpad.json +68 -0
  111. package/registry/r/tool.token-validate.json +24 -0
  112. package/registry/registry.json +676 -0
  113. package/schema/agentz.schema.json +330 -0
@@ -0,0 +1,68 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "scratchpad",
4
+ "type": "tool",
5
+ "title": "scratchpad (agent handoff)",
6
+ "description": "Shared note store: scout writes summaries, developer/expert read them instead of re-reading raw files.",
7
+ "dependencies": [
8
+ "skill:scratchpad"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "scratchpad.ts",
13
+ "content": "#!/usr/bin/env bun\n// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — MCP server for Claude Code, built on the official MCP\n * TypeScript SDK (@modelcontextprotocol/sdk). Registered in .mcp.json as\n * `bun --install=fallback <this file>`, so missing packages resolve from\n * bun's cache automatically — the user's project needs no dependencies.\n *\n * The tool logic itself is the shared scratchpad core (inlined on install).\n */\n\nconst server = new McpServer({ name: \"scratchpad\", version: \"1.0.0\" })\n\nserver.registerTool(\n \"scratchpad\",\n {\n description:\n \"Shared scratchpad between agents, namespaced per session. Actions: set (store note), \" +\n \"get (read; falls back to other sessions), list (scope=all for every session). \" +\n \"Use to hand summaries/context between agents instead of re-reading files.\",\n inputSchema: {\n action: z.enum([\"set\", \"get\", \"list\"]).describe(\"Operation\"),\n key: z.string().optional().describe(\"Note key, e.g. 'auth-flow-summary'\"),\n text: z.string().optional().describe(\"Note content (for 'set')\"),\n scope: z.enum([\"session\", \"all\"]).optional().describe(\"For 'list'\"),\n },\n },\n async (args) => {\n const sessionId = process.env.AGENT_SESSION_ID ?? process.env.CLAUDE_SESSION_ID ?? \"default\"\n const text = runScratchpad(\n { dir: process.cwd(), sessionId, agent: process.env.AGENT_NAME ?? \"claude\" },\n args,\n )\n return { content: [{ type: \"text\", text }] }\n },\n)\n\nawait server.connect(new StdioServerTransport())\n",
14
+ "kind": "raw",
15
+ "paths": {
16
+ "claude": "scratchpad.ts"
17
+ }
18
+ },
19
+ {
20
+ "path": "scratchpad.ts",
21
+ "content": "#!/usr/bin/env bun\n// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — CLI wrapper around the shared core, for frameworks without a\n * custom-tool SDK (Copilot, Codex, KiloCode, Hermes). Same store as the\n * opencode plugin tool and the Claude MCP server (.agents/scratchpad.json).\n *\n * Usage:\n * bun scratchpad.ts set <key> <text> [--session <id>]\n * bun scratchpad.ts get <key> [--session <id>]\n * bun scratchpad.ts list [--session <id>] [--all]\n *\n * Session id resolution: --session flag > AGENT_SESSION_ID env >\n * CLAUDE_SESSION_ID env > \"default\".\n */\n\n/**\n * Parse argv into positionals + flags. `--session <id>` consumes its value;\n * `--all` is a boolean. A `--session` with no value (end of argv or another\n * `--flag` next) is an error, never silently swallowing the following token.\n */\nfunction parse(argv: string[]): { positional: string[]; session?: string; all: boolean } | string {\n const positional: string[] = []\n let session: string | undefined\n let all = false\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === \"--all\") {\n all = true\n } else if (a === \"--session\") {\n const next = argv[i + 1]\n if (next === undefined || next.startsWith(\"--\")) {\n return \"error: --session needs a value, e.g. --session my-id\"\n }\n session = next\n i++ // consume the value\n } else if (a.startsWith(\"--\")) {\n // Unknown flag: ignore (forward-compatible) but don't treat as text.\n } else {\n positional.push(a)\n }\n }\n return { positional, session, all }\n}\n\nfunction main(argv: string[]): string {\n const parsed = parse(argv)\n if (typeof parsed === \"string\") return parsed\n const { positional, session, all } = parsed\n const action = positional[0]\n if (action !== \"set\" && action !== \"get\" && action !== \"list\") {\n return \"usage: scratchpad <set|get|list> [key] [text] [--session id] [--all]\"\n }\n const args: ScratchpadArgs = {\n action,\n key: positional[1],\n // Join every remaining positional so unquoted multi-word text works:\n // scratchpad set note hello world -> text = \"hello world\"\n text: positional.length > 2 ? positional.slice(2).join(\" \") : undefined,\n scope: all ? \"all\" : \"session\",\n }\n const sessionId =\n session ??\n process.env.AGENT_SESSION_ID ??\n process.env.CLAUDE_SESSION_ID ??\n \"default\"\n return runScratchpad(\n { dir: process.cwd(), sessionId, agent: process.env.AGENT_NAME ?? \"cli\" },\n args,\n )\n}\n\nconsole.log(main(process.argv.slice(2)))\n",
22
+ "kind": "raw",
23
+ "paths": {
24
+ "codex": "scratchpad.ts"
25
+ }
26
+ },
27
+ {
28
+ "path": "scratchpad.ts",
29
+ "content": "#!/usr/bin/env bun\n// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — CLI wrapper around the shared core, for frameworks without a\n * custom-tool SDK (Copilot, Codex, KiloCode, Hermes). Same store as the\n * opencode plugin tool and the Claude MCP server (.agents/scratchpad.json).\n *\n * Usage:\n * bun scratchpad.ts set <key> <text> [--session <id>]\n * bun scratchpad.ts get <key> [--session <id>]\n * bun scratchpad.ts list [--session <id>] [--all]\n *\n * Session id resolution: --session flag > AGENT_SESSION_ID env >\n * CLAUDE_SESSION_ID env > \"default\".\n */\n\n/**\n * Parse argv into positionals + flags. `--session <id>` consumes its value;\n * `--all` is a boolean. A `--session` with no value (end of argv or another\n * `--flag` next) is an error, never silently swallowing the following token.\n */\nfunction parse(argv: string[]): { positional: string[]; session?: string; all: boolean } | string {\n const positional: string[] = []\n let session: string | undefined\n let all = false\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === \"--all\") {\n all = true\n } else if (a === \"--session\") {\n const next = argv[i + 1]\n if (next === undefined || next.startsWith(\"--\")) {\n return \"error: --session needs a value, e.g. --session my-id\"\n }\n session = next\n i++ // consume the value\n } else if (a.startsWith(\"--\")) {\n // Unknown flag: ignore (forward-compatible) but don't treat as text.\n } else {\n positional.push(a)\n }\n }\n return { positional, session, all }\n}\n\nfunction main(argv: string[]): string {\n const parsed = parse(argv)\n if (typeof parsed === \"string\") return parsed\n const { positional, session, all } = parsed\n const action = positional[0]\n if (action !== \"set\" && action !== \"get\" && action !== \"list\") {\n return \"usage: scratchpad <set|get|list> [key] [text] [--session id] [--all]\"\n }\n const args: ScratchpadArgs = {\n action,\n key: positional[1],\n // Join every remaining positional so unquoted multi-word text works:\n // scratchpad set note hello world -> text = \"hello world\"\n text: positional.length > 2 ? positional.slice(2).join(\" \") : undefined,\n scope: all ? \"all\" : \"session\",\n }\n const sessionId =\n session ??\n process.env.AGENT_SESSION_ID ??\n process.env.CLAUDE_SESSION_ID ??\n \"default\"\n return runScratchpad(\n { dir: process.cwd(), sessionId, agent: process.env.AGENT_NAME ?? \"cli\" },\n args,\n )\n}\n\nconsole.log(main(process.argv.slice(2)))\n",
30
+ "kind": "raw",
31
+ "paths": {
32
+ "copilot": "scratchpad.ts"
33
+ }
34
+ },
35
+ {
36
+ "path": "scratchpad.ts",
37
+ "content": "#!/usr/bin/env bun\n// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — CLI wrapper around the shared core, for frameworks without a\n * custom-tool SDK (Copilot, Codex, KiloCode, Hermes). Same store as the\n * opencode plugin tool and the Claude MCP server (.agents/scratchpad.json).\n *\n * Usage:\n * bun scratchpad.ts set <key> <text> [--session <id>]\n * bun scratchpad.ts get <key> [--session <id>]\n * bun scratchpad.ts list [--session <id>] [--all]\n *\n * Session id resolution: --session flag > AGENT_SESSION_ID env >\n * CLAUDE_SESSION_ID env > \"default\".\n */\n\n/**\n * Parse argv into positionals + flags. `--session <id>` consumes its value;\n * `--all` is a boolean. A `--session` with no value (end of argv or another\n * `--flag` next) is an error, never silently swallowing the following token.\n */\nfunction parse(argv: string[]): { positional: string[]; session?: string; all: boolean } | string {\n const positional: string[] = []\n let session: string | undefined\n let all = false\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === \"--all\") {\n all = true\n } else if (a === \"--session\") {\n const next = argv[i + 1]\n if (next === undefined || next.startsWith(\"--\")) {\n return \"error: --session needs a value, e.g. --session my-id\"\n }\n session = next\n i++ // consume the value\n } else if (a.startsWith(\"--\")) {\n // Unknown flag: ignore (forward-compatible) but don't treat as text.\n } else {\n positional.push(a)\n }\n }\n return { positional, session, all }\n}\n\nfunction main(argv: string[]): string {\n const parsed = parse(argv)\n if (typeof parsed === \"string\") return parsed\n const { positional, session, all } = parsed\n const action = positional[0]\n if (action !== \"set\" && action !== \"get\" && action !== \"list\") {\n return \"usage: scratchpad <set|get|list> [key] [text] [--session id] [--all]\"\n }\n const args: ScratchpadArgs = {\n action,\n key: positional[1],\n // Join every remaining positional so unquoted multi-word text works:\n // scratchpad set note hello world -> text = \"hello world\"\n text: positional.length > 2 ? positional.slice(2).join(\" \") : undefined,\n scope: all ? \"all\" : \"session\",\n }\n const sessionId =\n session ??\n process.env.AGENT_SESSION_ID ??\n process.env.CLAUDE_SESSION_ID ??\n \"default\"\n return runScratchpad(\n { dir: process.cwd(), sessionId, agent: process.env.AGENT_NAME ?? \"cli\" },\n args,\n )\n}\n\nconsole.log(main(process.argv.slice(2)))\n",
38
+ "kind": "raw",
39
+ "paths": {
40
+ "hermes": "scratchpad.ts"
41
+ }
42
+ },
43
+ {
44
+ "path": "scratchpad.ts",
45
+ "content": "#!/usr/bin/env bun\n// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — CLI wrapper around the shared core, for frameworks without a\n * custom-tool SDK (Copilot, Codex, KiloCode, Hermes). Same store as the\n * opencode plugin tool and the Claude MCP server (.agents/scratchpad.json).\n *\n * Usage:\n * bun scratchpad.ts set <key> <text> [--session <id>]\n * bun scratchpad.ts get <key> [--session <id>]\n * bun scratchpad.ts list [--session <id>] [--all]\n *\n * Session id resolution: --session flag > AGENT_SESSION_ID env >\n * CLAUDE_SESSION_ID env > \"default\".\n */\n\n/**\n * Parse argv into positionals + flags. `--session <id>` consumes its value;\n * `--all` is a boolean. A `--session` with no value (end of argv or another\n * `--flag` next) is an error, never silently swallowing the following token.\n */\nfunction parse(argv: string[]): { positional: string[]; session?: string; all: boolean } | string {\n const positional: string[] = []\n let session: string | undefined\n let all = false\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === \"--all\") {\n all = true\n } else if (a === \"--session\") {\n const next = argv[i + 1]\n if (next === undefined || next.startsWith(\"--\")) {\n return \"error: --session needs a value, e.g. --session my-id\"\n }\n session = next\n i++ // consume the value\n } else if (a.startsWith(\"--\")) {\n // Unknown flag: ignore (forward-compatible) but don't treat as text.\n } else {\n positional.push(a)\n }\n }\n return { positional, session, all }\n}\n\nfunction main(argv: string[]): string {\n const parsed = parse(argv)\n if (typeof parsed === \"string\") return parsed\n const { positional, session, all } = parsed\n const action = positional[0]\n if (action !== \"set\" && action !== \"get\" && action !== \"list\") {\n return \"usage: scratchpad <set|get|list> [key] [text] [--session id] [--all]\"\n }\n const args: ScratchpadArgs = {\n action,\n key: positional[1],\n // Join every remaining positional so unquoted multi-word text works:\n // scratchpad set note hello world -> text = \"hello world\"\n text: positional.length > 2 ? positional.slice(2).join(\" \") : undefined,\n scope: all ? \"all\" : \"session\",\n }\n const sessionId =\n session ??\n process.env.AGENT_SESSION_ID ??\n process.env.CLAUDE_SESSION_ID ??\n \"default\"\n return runScratchpad(\n { dir: process.cwd(), sessionId, agent: process.env.AGENT_NAME ?? \"cli\" },\n args,\n )\n}\n\nconsole.log(main(process.argv.slice(2)))\n",
46
+ "kind": "raw",
47
+ "paths": {
48
+ "kilocode": "scratchpad.ts"
49
+ }
50
+ },
51
+ {
52
+ "path": "scratchpad.ts",
53
+ "content": "// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { tool } from \"@opencode-ai/plugin\"\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\"\nimport { dirname, join } from \"path\"\n\n// --- inlined from tools-core/scratchpad.ts (AgentZ) ---\n// managed by AgentZ — shared scratchpad core, inlined into every per-SDK wrapper at install time\n\ntype ScratchpadNote = { text: string; updatedAt: string; agent: string }\n/** sessionID -> key -> note */\ntype ScratchpadStore = Record<string, Record<string, ScratchpadNote>>\n\ninterface ScratchpadArgs {\n action: \"set\" | \"get\" | \"list\"\n key?: string\n text?: string\n scope?: \"session\" | \"all\"\n}\n\ninterface ScratchpadCtx {\n /** Project directory (store lives at <dir>/.agents/scratchpad.json). */\n dir: string\n sessionId: string\n agent: string\n}\n\nfunction scratchpadFile(dir: string): string {\n return join(dir, \".agents\", \"scratchpad.json\")\n}\n\n/**\n * Load the store from disk. On a corrupt/unparseable file, quarantine it\n * (rename to scratchpad.json.corrupt-<timestamp>) and start fresh, returning a\n * one-line note the caller prepends to its result.\n */\nfunction loadStore(file: string): { store: ScratchpadStore; note?: string } {\n if (!existsSync(file)) return { store: {} }\n let raw: string\n try {\n raw = readFileSync(file, \"utf8\")\n } catch {\n return { store: {} }\n }\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\") return { store: parsed as ScratchpadStore }\n } catch {\n /* fall through to quarantine */\n }\n const corrupt = `${file}.corrupt-${Date.now()}`\n try {\n renameSync(file, corrupt)\n } catch {\n /* best-effort; still start fresh in memory */\n }\n return { store: {}, note: `[scratchpad] corrupt store quarantined to ${corrupt}; started fresh` }\n}\n\n/**\n * Atomic write: serialize to a temp file in the same directory, then rename\n * over the target so a concurrent/failed write never truncates the store.\n */\nfunction saveStore(file: string, store: ScratchpadStore): void {\n mkdirSync(dirname(file), { recursive: true })\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n writeFileSync(tmp, JSON.stringify(store, null, 2))\n renameSync(tmp, file)\n}\n\n/**\n * The one scratchpad implementation every SDK wrapper delegates to:\n * session-namespaced set/get/list with cross-session get fallback.\n */\nfunction runScratchpad(ctx: ScratchpadCtx, args: ScratchpadArgs): string {\n const file = scratchpadFile(ctx.dir)\n const { store, note: loadNote } = loadStore(file)\n const prefix = loadNote ? `${loadNote}\\n` : \"\"\n const session = (store[ctx.sessionId] ??= {})\n\n switch (args.action) {\n case \"set\": {\n if (!args.key || args.text === undefined) return `${prefix}need key + text`\n session[args.key] = {\n text: args.text,\n updatedAt: new Date().toISOString(),\n agent: ctx.agent,\n }\n saveStore(file, store)\n return `${prefix}stored '${args.key}' (${args.text.length} chars, session ${ctx.sessionId})`\n }\n case \"get\": {\n if (!args.key) return `${prefix}need key`\n const own = session[args.key]\n if (own) return `${prefix}${own.text}`\n // Cross-session fallback: pick the newest note by updatedAt. Legacy notes\n // without a timestamp sort as oldest (\"\").\n let best: { sid: string; note: ScratchpadNote } | null = null\n for (const [sid, notes] of Object.entries(store)) {\n const note = notes[args.key]\n if (!note) continue\n const ts = note.updatedAt ?? \"\"\n const bestTs = best?.note.updatedAt ?? \"\"\n if (!best || ts > bestTs) best = { sid, note }\n }\n if (best) return `${prefix}[from session ${best.sid}] ${best.note.text}`\n return `${prefix}no note '${args.key}' in any session`\n }\n case \"list\": {\n const entries =\n args.scope === \"all\"\n ? Object.entries(store).flatMap(([sid, notes]) =>\n Object.entries(notes).map(\n ([k, n]) => `${k} (${n.agent}, ${n.updatedAt}, session ${sid})`,\n ),\n )\n : Object.entries(session).map(([k, n]) => `${k} (${n.agent}, ${n.updatedAt})`)\n return `${prefix}${entries.length ? entries.join(\"\\n\") : \"scratchpad empty\"}`\n }\n }\n}\n// --- end tools-core ---\n/**\n * Scratchpad — opencode plugin-SDK wrapper around the shared core.\n * Session id and project directory come from opencode's ToolContext.\n */\nexport default tool({\n description:\n \"Shared scratchpad between scout/developer/expert agents, namespaced per session. \" +\n \"Actions: set (store note under key), get (read one; falls back to other sessions), \" +\n \"list (keys in this session; scope=all for every session). \" +\n \"Use to hand summaries/context between agents instead of re-reading files.\",\n args: {\n action: tool.schema.enum([\"set\", \"get\", \"list\"]).describe(\"Operation\"),\n key: tool.schema.string().optional().describe(\"Note key, e.g. 'auth-flow-summary' and SessionId\"),\n text: tool.schema.string().optional().describe(\"Note content (for 'set')\"),\n scope: tool.schema\n .enum([\"session\", \"all\"])\n .optional()\n .describe(\"For 'list': current session (default) or all sessions\"),\n },\n async execute(args, context) {\n return runScratchpad(\n { dir: context.directory, sessionId: context.sessionID, agent: context.agent },\n args,\n )\n },\n})\n",
54
+ "kind": "raw",
55
+ "paths": {
56
+ "opencode": "scratchpad.ts"
57
+ }
58
+ }
59
+ ],
60
+ "targets": [
61
+ "claude",
62
+ "codex",
63
+ "copilot",
64
+ "hermes",
65
+ "kilocode",
66
+ "opencode"
67
+ ]
68
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "token-validate",
4
+ "type": "tool",
5
+ "title": "token-validate (design)",
6
+ "description": "Scan files or a directory for hardcoded hex/rgb/hsl/font-family values outside var() (opencode only). Pairs with the design-system and design-taste skills.",
7
+ "dependencies": [
8
+ "skill:design-system",
9
+ "skill:design-taste"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "token-validate.ts",
14
+ "content": "// managed by AgentZ — edit freely; remove this line to stop AgentZ from updating this file\nimport { tool } from \"@opencode-ai/plugin\"\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\"\nimport { basename, isAbsolute, join } from \"node:path\"\n\n/**\n * token-validate — validate HTML for design-token compliance. Flags hardcoded\n * hex/rgb/rgba/hsl colors and fonts in <style> blocks / inline styles that\n * should be var(--token) references, and warns on low token usage.\n *\n * Generalized port of design-system/token_validator.ts: instead of scanning a\n * fixed assets/ layout, it takes explicit `files` and/or a `dir` to scan for\n * .html. The design-tokens import check is an ERROR only when the configured\n * `tokensCss` file actually exists in the project (otherwise a note is added,\n * so the tool stays useful in projects without a token file yet).\n */\n\n// Patterns that indicate hardcoded values (should use tokens).\nconst FORBIDDEN_PATTERNS: [RegExp, string][] = [\n [/#[0-9A-Fa-f]{3,8}\\b/g, \"hex color\"],\n [/rgb\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*\\)/g, \"rgb color\"],\n [/rgba\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*[\\d.]+\\s*\\)/g, \"rgba color\"],\n [/hsl\\([^)]+\\)/g, \"hsl color\"],\n [/font-family:\\s*'[^v][^a][^r][^']*',/g, \"hardcoded font\"],\n [/font-family:\\s*\"[^v][^a][^r][^\"]*\",/g, \"hardcoded font\"],\n]\n\n// Allowed rgba patterns (brand colors with transparency — CSS limitation).\nconst ALLOWED_RGBA_PATTERNS: RegExp[] = [\n /^rgba\\(\\s*59\\s*,\\s*130\\s*,\\s*246/,\n /^rgba\\(\\s*245\\s*,\\s*158\\s*,\\s*11/,\n /^rgba\\(\\s*16\\s*,\\s*185\\s*,\\s*129/,\n /^rgba\\(\\s*20\\s*,\\s*184\\s*,\\s*166/,\n /^rgba\\(\\s*0\\s*,\\s*0\\s*,\\s*0/,\n /^rgba\\(\\s*255\\s*,\\s*255\\s*,\\s*255/,\n /^rgba\\(\\s*15\\s*,\\s*23\\s*,\\s*42/,\n /^rgba\\(\\s*7\\s*,\\s*11\\s*,\\s*20/,\n]\n\nconst ALLOWED_EXCEPTIONS = [\n \"pexels.com\", \"unsplash.com\", \"youtube.com\", \"ytimg.com\",\n \"googlefonts\", \"fonts.googleapis.com\", \"fonts.gstatic.com\",\n]\n\nconst TOKEN_PATTERNS = [\n /var\\(--color-/g,\n /var\\(--primitive-/g,\n /var\\(--typography-/g,\n /var\\(--card-/g,\n /var\\(--button-/g,\n]\n\nclass ValidationResult {\n filePath: string\n errors: string[] = []\n warnings: string[] = []\n passed = true\n\n constructor(filePath: string) {\n this.filePath = filePath\n }\n\n get name(): string {\n return basename(this.filePath)\n }\n\n addError(msg: string): void {\n this.errors.push(msg)\n this.passed = false\n }\n\n addWarning(msg: string): void {\n this.warnings.push(msg)\n }\n}\n\nfunction isInsideBlock(content: string, matchPos: number, openTag: string, closeTag: string): boolean {\n const pre = content.slice(0, matchPos)\n const tagOpen = pre.lastIndexOf(openTag)\n const tagClose = pre.lastIndexOf(closeTag)\n return tagOpen > tagClose\n}\n\nfunction isAllowedException(context: string): boolean {\n const lower = context.toLowerCase()\n return ALLOWED_EXCEPTIONS.some((exc) => lower.includes(exc))\n}\n\nfunction isAllowedRgba(matchText: string): boolean {\n return ALLOWED_RGBA_PATTERNS.some((pattern) => pattern.test(matchText))\n}\n\nfunction getContext(content: string, pos: number, chars = 100): string {\n const start = Math.max(0, pos - chars)\n const end = Math.min(content.length, pos + chars)\n return content.slice(start, end)\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\ninterface ValidateOpts {\n verbose: boolean\n tokensImportName: string\n tokensCssExists: boolean\n}\n\nfunction validateHtml(content: string, filePath: string, opts: ValidateOpts): ValidationResult {\n const result = new ValidationResult(filePath)\n const { verbose, tokensImportName, tokensCssExists } = opts\n\n // 1. design tokens import present.\n // ERROR only when the token file actually exists in the project; otherwise a\n // note (project has no token file to import yet).\n if (!content.includes(tokensImportName)) {\n if (tokensCssExists) {\n result.addError(`Missing ${tokensImportName} import`)\n } else {\n result.addWarning(\n `note: no ${tokensImportName} import found and no token file exists at the configured path (import check skipped)`,\n )\n }\n }\n\n // 2. Forbidden patterns\n for (const [pattern, description] of FORBIDDEN_PATTERNS) {\n pattern.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = pattern.exec(content)) !== null) {\n const matchText = match[0]\n const matchPos = match.index\n const context = getContext(content, matchPos)\n\n if (match[0] === \"\") {\n pattern.lastIndex++\n continue\n }\n\n if (isInsideBlock(content, matchPos, \"<script\", \"</script>\")) {\n if (verbose) result.addWarning(`Allowed in <script>: ${matchText}`)\n continue\n }\n\n if (isAllowedException(context)) {\n if (verbose) result.addWarning(`Allowed external: ${matchText}`)\n continue\n }\n\n if (description === \"rgba color\" && isAllowedRgba(matchText)) {\n if (verbose) result.addWarning(`Allowed brand rgba: ${matchText}`)\n continue\n }\n\n // Skip if part of var() reference (false positive)\n if (context.includes(\"var(\") && context.includes(matchText)) {\n const varPattern = new RegExp(`var\\\\([^)]*${escapeRegExp(matchText)}[^)]*\\\\)`)\n if (varPattern.test(context)) continue\n }\n\n if (isInsideBlock(content, matchPos, \"<style\", \"</style>\")) {\n result.addError(`Hardcoded ${description} in <style>: ${matchText}`)\n } else if (context.includes('style=\"')) {\n result.addError(`Hardcoded ${description} in inline style: ${matchText}`)\n }\n }\n }\n\n // 3. Required var() usage indicators\n let tokenCount = 0\n for (const p of TOKEN_PATTERNS) {\n const matches = content.match(p)\n tokenCount += matches ? matches.length : 0\n }\n\n if (tokenCount < 5) {\n result.addWarning(\n `Low token usage (${tokenCount} var() references). Consider using more design tokens.`,\n )\n }\n\n return result\n}\n\nfunction validateFile(filePath: string, opts: ValidateOpts): ValidationResult {\n if (!existsSync(filePath)) {\n const result = new ValidationResult(filePath)\n result.addError(\"File not found\")\n return result\n }\n const content = readFileSync(filePath, \"utf-8\")\n return validateHtml(content, filePath, opts)\n}\n\n/** Recursively collect .html files, skipping vendored/build dirs. */\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"])\n\nfunction collectHtml(dirPath: string): string[] {\n const out: string[] = []\n const walk = (dir: string) => {\n let entries: string[]\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const entry of entries.sort()) {\n if (SKIP_DIRS.has(entry)) continue\n const full = join(dir, entry)\n let st\n try {\n st = statSync(full)\n } catch {\n continue\n }\n if (st.isDirectory()) walk(full)\n else if (st.isFile() && entry.endsWith(\".html\")) out.push(full)\n }\n }\n walk(dirPath)\n return out\n}\n\nfunction formatResultBlock(result: ValidationResult, verbose: boolean): string {\n const status = result.passed ? \"\\u2713\" : \"\\u2717\"\n const lines: string[] = [` ${status} ${result.name}`]\n\n if (result.errors.length > 0) {\n for (const error of result.errors.slice(0, 5)) {\n lines.push(` \\u251c\\u2500 ${error}`)\n }\n if (result.errors.length > 5) {\n lines.push(` \\u2514\\u2500 ... and ${result.errors.length - 5} more errors`)\n }\n }\n if (verbose) {\n for (const warn of result.warnings) {\n lines.push(` \\u00b7 ${warn}`)\n }\n }\n\n return lines.join(\"\\n\")\n}\n\nexport default tool({\n description:\n \"Validate HTML for design-token compliance: flags hardcoded hex/rgb/rgba/hsl colors and fonts \" +\n \"in <style> or inline styles that should use var(--token), and warns on low token usage. Pass \" +\n \"`files` (specific paths) and/or `dir` (scanned recursively for .html). `projectRoot` sets the \" +\n \"project (default cwd); `tokensCss` is the token file path relative to it (default \" +\n \"assets/design-tokens.css) — the import check is only an ERROR when that file exists. `verbose` \" +\n \"shows allowed/skipped notes. Use before shipping generated HTML to keep it on the design system.\",\n args: {\n files: tool.schema\n .array(tool.schema.string())\n .optional()\n .describe(\"Specific HTML file paths to validate.\"),\n dir: tool.schema\n .string()\n .optional()\n .describe(\"Directory scanned recursively for .html files to validate.\"),\n projectRoot: tool.schema\n .string()\n .optional()\n .describe(\"Target project root (default: current working directory).\"),\n tokensCss: tool.schema\n .string()\n .optional()\n .describe(\"Design-tokens CSS path relative to projectRoot (default 'assets/design-tokens.css').\"),\n verbose: tool.schema\n .boolean()\n .optional()\n .describe(\"Show allowed/skipped warnings and notes.\"),\n },\n async execute(args, context) {\n const projectRoot = args.projectRoot ?? context.directory\n const verbose = args.verbose ?? false\n const tokensCssRel = args.tokensCss ?? \"assets/design-tokens.css\"\n const tokensCssPath = isAbsolute(tokensCssRel) ? tokensCssRel : join(projectRoot, tokensCssRel)\n const tokensImportName = basename(tokensCssRel)\n const tokensCssExists = existsSync(tokensCssPath)\n\n const opts: ValidateOpts = { verbose, tokensImportName, tokensCssExists }\n\n // Collect target files.\n const targets: string[] = []\n if (args.files) {\n for (const f of args.files) targets.push(isAbsolute(f) ? f : join(projectRoot, f))\n }\n if (args.dir) {\n const dirPath = isAbsolute(args.dir) ? args.dir : join(projectRoot, args.dir)\n for (const f of collectHtml(dirPath)) targets.push(f)\n }\n\n if (targets.length === 0) {\n return \"No files to validate. Pass `files` (paths) and/or `dir` (scanned for .html).\"\n }\n\n const results = targets.map((f) => validateFile(f, opts))\n\n // Summary\n let totalPassed = 0\n let totalErrors = 0\n const lines: string[] = []\n lines.push(\"=\".repeat(60))\n lines.push(\"HTML DESIGN TOKEN VALIDATION SUMMARY\")\n lines.push(\"=\".repeat(60))\n if (!tokensCssExists) {\n lines.push(`(no token file at ${tokensCssRel}; import check reported as note only)`)\n }\n\n for (const result of results) {\n if (result.passed) totalPassed++\n totalErrors += result.errors.length\n // Print block for failing files, plus verbose passes.\n if (!result.passed || verbose) {\n lines.push(formatResultBlock(result, verbose))\n }\n }\n\n lines.push(\"-\".repeat(60))\n if (totalErrors === 0) {\n lines.push(`\\u2713 ALL PASSED: ${totalPassed}/${results.length} files valid`)\n } else {\n lines.push(\n `\\u2717 FAILED: ${results.length - totalPassed}/${results.length} files have issues ` +\n `(${totalErrors} total errors)`,\n )\n }\n lines.push(\"-\".repeat(60))\n\n return lines.join(\"\\n\")\n },\n})\n",
15
+ "kind": "raw",
16
+ "paths": {
17
+ "opencode": "token-validate.ts"
18
+ }
19
+ }
20
+ ],
21
+ "targets": [
22
+ "opencode"
23
+ ]
24
+ }