#!/usr/bin/env -S bun --no-env-file --config=/dev/null /** * gstack-harness — register gstack's skills with the agent harness it is * installed into (OpenClaw, Hermes, dots, QM, any other), the harness's own * way, and take them back out. lib/harness-register.ts holds the targets and * the ledger; docs/hosts.md has the per-harness install, verify and uninstall * blocks. * * Usage: * gstack-harness detect [--json] * gstack-harness register [--skills router|all] [--skills-dir ] [--context ] [--print] [--yes] [--no-registry] * gstack-harness unregister [--remote] * gstack-harness status [--json] [--no-check] * gstack-harness refresh * gstack-harness auto * gstack-harness manifest [--skills-dir ] */ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { resolveStateRoot } from '../lib/state-root'; import { HarnessError, TARGETS, detect, getTarget, harnessSees, listLedgers, printPayload, refresh, register, sourceSkills, status, unregister, type RegisterResult, type SkillsMode, type TargetName, } from '../lib/harness-register'; import { registerQm, removeQmPack } from '../lib/harness-qm'; const argv = process.argv.slice(2); const flag = (name: string) => { const i = argv.indexOf(name); if (i < 0) return undefined; const v = argv[i + 1]; argv.splice(i, 2); return v; }; const bool = (name: string) => { const i = argv.indexOf(name); if (i < 0) return false; argv.splice(i, 1); return true; }; const checkout = path.resolve(fs.realpathSync(process.argv[1]), '..', '..'); const version = (() => { try { return fs.readFileSync(path.join(checkout, 'VERSION'), 'utf8').trim(); } catch { return 'unknown'; } })(); const stateRoot = resolveStateRoot(process.env); const env = process.env; const home = os.homedir(); const self = path.join(checkout, 'bin', 'gstack-harness'); function receipt(r: RegisterResult): void { const t = TARGETS[r.target]; console.log('gstack-harness: ' + t.displayName + ': ' + r.skills + ' gstack skills ' + (r.mode === 'router' ? 'reachable through one router skill' : 'registered') + ' at ' + r.dest + ' (' + r.status + (r.written ? ', ' + r.written + ' files written' : '') + ')'); if (r.router) console.log(' router: ' + r.router + (r.mode === 'router' ? ' (register with --skills all to list every skill natively)' : '')); console.log(' ledger: ' + r.ledger); console.log(' remove: ' + self + ' unregister ' + r.id); } function usage(code: number): never { const text = fs.readFileSync(process.argv[1], 'utf8').split('\n').slice(1).filter(l => l.startsWith(' *')).map(l => l.slice(3)).join('\n'); (code ? console.error : console.log)(text); process.exit(code); } async function main(): Promise { const json = bool('--json'); const sub = argv.shift(); const base = { checkout, stateRoot, env, home, version }; switch (sub) { case 'detect': { const d = detect(env, home); if (json) console.log(JSON.stringify(d)); else console.log('inside: ' + (d.inside.join(', ') || 'none') + '\npresent: ' + (d.present.join(', ') || 'none')); return 0; } case 'register': { const mode = flag('--skills') as SkillsMode | undefined; const skillsDir = flag('--skills-dir'); const context = flag('--context'); const print = bool('--print'); const yes = bool('--yes'); const noRegistry = bool('--no-registry'); const t = getTarget(argv.shift() ?? ''); if (print) { console.log(printPayload(checkout)); return 0; } if (t.name === 'qm') { const r = await registerQm({ ...base, yes }); console.log('gstack-harness: QM: pack ' + r.packId + ' at ' + r.ref + ' (' + r.status + '); imported: ' + JSON.stringify(r.imported).slice(0, 300)); console.log(' detach locally: ' + self + ' unregister ' + r.id + ' archive the org pack: ' + self + ' unregister ' + r.id + ' --remote'); return 0; } if (t.mechanism === 'verify-only') { const seen = harnessSees(t, '', env); if (seen.visible === 'yes') { console.log('gstack-harness: ' + t.displayName + ': discovered gstack skills through ~/.claude/skills (Claude-rendered, unverified in a live session); checked: ' + seen.checked); return 0; } console.log('gstack-harness: ' + t.displayName + ': unverified (' + (seen.checked ? seen.checked + ' did not list gstack' : t.name + ' is not on PATH') + '). Install gstack for Claude Code (./setup), or paste the block from: ' + self + ' register generic --print'); return 3; } receipt(register({ ...base, target: t, mode, skillsDir, context, registry: !noRegistry })); return 0; } case 'unregister': { const remote = bool('--remote'); const name = argv.shift() ?? ''; const ids = listLedgers(stateRoot).filter(l => l.ledger.meta.id === name || l.ledger.meta.target === name); if (remote) for (const l of ids.filter(l => l.ledger.meta.target === 'qm')) console.log('gstack-harness: archived QM pack ' + await removeQmPack(stateRoot, env, l.ledger.meta.id)); for (const id of unregister(stateRoot, checkout, name, env)) console.log('gstack-harness: removed ' + id); return 0; } case 'status': { const rows = status(stateRoot, version, env, !bool('--no-check')); const extra = (['muse', 'grok'] as TargetName[]).filter(n => TARGETS[n].present(env, home)).map(n => ({ target: n, ...harnessSees(TARGETS[n], '', env) })); if (json) { console.log(JSON.stringify({ registrations: rows, detected: extra })); return 0; } if (!rows.length && !extra.length) console.log('gstack-harness: no harness registrations on this machine'); for (const r of rows) console.log(r.target + '\t' + r.mode + '\t' + r.dest + '\t' + r.files + ' files\t' + (r.stale ? 'stale (' + r.version + '; refresh: ' + self + ' refresh)' : r.version) + '\tvisible to harness: ' + r.visible + (r.checked ? ' (' + r.checked + ')' : '') + '\t' + r.id); for (const x of extra) console.log(x.target + '\tdetect\t~/.claude/skills\t' + (x.visible === 'yes' ? 'discovered (Claude-rendered, unverified)' : 'unverified') + (x.checked ? ' (' + x.checked + ')' : '')); return 0; } case 'refresh': { for (const r of refresh(base)) if (r.status !== 'unchanged') receipt(r); return 0; } case 'auto': { const d = detect(env, home); for (const n of d.inside.filter(n => TARGETS[n].mechanism === 'skill-folder')) { try { receipt(register({ ...base, target: TARGETS[n], registry: true })); } catch (e) { if (e instanceof HarnessError) console.error(e.line()); else throw e; } } for (const n of d.present.filter(n => !d.inside.includes(n) && TARGETS[n].mechanism === 'skill-folder' && !listLedgers(stateRoot).some(l => l.ledger.meta.target === n))) { console.log('gstack-harness: ' + TARGETS[n].displayName + ' is installed here; to give it gstack skills: ' + self + ' register ' + n); } return 0; } case 'manifest': { const t = getTarget(argv.shift() ?? 'generic'); const skills = sourceSkills(t.render ? t : TARGETS.generic, checkout, fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-harness-'))); console.log(JSON.stringify({ schema: 1, gstack_version: version, target: t.name, skills: skills.map(s => ({ name: s.name, use: s.use, path: path.join(s.dir, 'SKILL.md') })) }, null, 2)); return 0; } case '--help': case '-h': case 'help': return usage(0); default: return usage(2); } } main().then(code => process.exit(code), (e) => { if (e instanceof HarnessError) { console.error(e.line()); process.exit(1); } throw e; });