0101-agents 0.0.1 → 0.1.1
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.
- package/bin/cli.js +374 -1
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,2 +1,375 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// 0101-agents CLI — installs and updates curated Claude Code agents.
|
|
3
|
+
//
|
|
4
|
+
// Commands:
|
|
5
|
+
// 0101-agents login [<license-key>]
|
|
6
|
+
// 0101-agents logout
|
|
7
|
+
// 0101-agents whoami
|
|
8
|
+
// 0101-agents install <agent> [--force]
|
|
9
|
+
// 0101-agents update <agent>
|
|
10
|
+
// 0101-agents list
|
|
11
|
+
// 0101-agents help
|
|
12
|
+
|
|
13
|
+
import { execSync, spawn } from 'node:child_process'
|
|
14
|
+
import {
|
|
15
|
+
existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync,
|
|
16
|
+
rmSync, statSync, createWriteStream,
|
|
17
|
+
} from 'node:fs'
|
|
18
|
+
import { homedir, tmpdir } from 'node:os'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { Readable } from 'node:stream'
|
|
21
|
+
import { pipeline } from 'node:stream/promises'
|
|
22
|
+
import { createInterface } from 'node:readline/promises'
|
|
23
|
+
|
|
24
|
+
const HOME = homedir()
|
|
25
|
+
const CONFIG_DIR = join(HOME, '.0101')
|
|
26
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
|
|
27
|
+
const INSTALL_ROOT = join(HOME, '0101')
|
|
28
|
+
const BACKUPS_DIR = join(INSTALL_ROOT, '.backups')
|
|
29
|
+
|
|
30
|
+
const DEFAULT_API_BASE = 'https://0101.fyi'
|
|
31
|
+
|
|
32
|
+
// ─── pretty output ───────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const c = (code, s) => `\x1b[${code}m${s}\x1b[0m`
|
|
35
|
+
const green = s => c(32, s)
|
|
36
|
+
const red = s => c(31, s)
|
|
37
|
+
const dim = s => c(2, s)
|
|
38
|
+
const bold = s => c(1, s)
|
|
39
|
+
|
|
40
|
+
const ok = s => console.log(green('✓ ') + s)
|
|
41
|
+
const err = s => console.error(red('✗ ') + s)
|
|
42
|
+
const info = s => console.log(dim(' ' + s))
|
|
43
|
+
|
|
44
|
+
function die(msg, code = 1) {
|
|
45
|
+
err(msg)
|
|
46
|
+
process.exit(code)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── config ──────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function readConfig() {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'))
|
|
54
|
+
} catch {
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeConfig(next) {
|
|
60
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
61
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function apiBase() {
|
|
65
|
+
return readConfig().api_base || process.env['0101_API_BASE'] || DEFAULT_API_BASE
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function licenseKey() {
|
|
69
|
+
const k = readConfig().license_key
|
|
70
|
+
if (!k) die('No license key. Run: 0101-agents login')
|
|
71
|
+
return k
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── api ─────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async function fetchManifest(agent) {
|
|
77
|
+
const url = `${apiBase()}/api/releases/${encodeURIComponent(agent)}`
|
|
78
|
+
const res = await fetch(url, {
|
|
79
|
+
headers: { Authorization: `Bearer ${licenseKey()}` },
|
|
80
|
+
})
|
|
81
|
+
if (res.status === 401) die('License invalid. Run: 0101-agents login')
|
|
82
|
+
if (res.status === 403) {
|
|
83
|
+
const body = await res.json().catch(() => ({}))
|
|
84
|
+
die(body.error || 'License does not cover this agent.')
|
|
85
|
+
}
|
|
86
|
+
if (res.status === 404) die(`Agent not found: ${agent}`)
|
|
87
|
+
if (!res.ok) die(`Manifest fetch failed: ${res.status} ${res.statusText}`)
|
|
88
|
+
return res.json()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function downloadZip(downloadUrl, destPath) {
|
|
92
|
+
const res = await fetch(downloadUrl, {
|
|
93
|
+
headers: { Authorization: `Bearer ${licenseKey()}` },
|
|
94
|
+
})
|
|
95
|
+
if (!res.ok) die(`Download failed: ${res.status} ${res.statusText}`)
|
|
96
|
+
if (!res.body) die('Download returned empty body.')
|
|
97
|
+
|
|
98
|
+
// Node fetch returns a web ReadableStream; pipe to a file.
|
|
99
|
+
const nodeReadable = Readable.fromWeb(res.body)
|
|
100
|
+
await pipeline(nodeReadable, createWriteStream(destPath))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function agentInstallDir(agent) {
|
|
106
|
+
return join(INSTALL_ROOT, agent)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installedVersion(agent) {
|
|
110
|
+
const versionFile = join(agentInstallDir(agent), '.0101-version')
|
|
111
|
+
try { return readFileSync(versionFile, 'utf8').trim() } catch { return null }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeInstalledVersion(agent, version) {
|
|
115
|
+
writeFileSync(join(agentInstallDir(agent), '.0101-version'), version + '\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function listInstalled() {
|
|
119
|
+
if (!existsSync(INSTALL_ROOT)) return []
|
|
120
|
+
return readdirSync(INSTALL_ROOT)
|
|
121
|
+
.filter(name => !name.startsWith('.'))
|
|
122
|
+
.filter(name => statSync(join(INSTALL_ROOT, name)).isDirectory())
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ensureUnzipAvailable() {
|
|
126
|
+
try { execSync('command -v unzip', { stdio: 'ignore' }) } catch {
|
|
127
|
+
die('The `unzip` binary is required but not found.\n' +
|
|
128
|
+
' • macOS: comes pre-installed.\n' +
|
|
129
|
+
' • Linux: apt install unzip / dnf install unzip.\n' +
|
|
130
|
+
' • Windows: install via WSL or 7-Zip + PATH.')
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function unzipInto(zipPath, destDir, { skipPrefix } = {}) {
|
|
135
|
+
// Use the system unzip binary. Available everywhere except bare Windows.
|
|
136
|
+
mkdirSync(destDir, { recursive: true })
|
|
137
|
+
if (skipPrefix) {
|
|
138
|
+
// unzip with -x excludes paths matching glob patterns.
|
|
139
|
+
execSync(
|
|
140
|
+
`unzip -q -o "${zipPath}" -d "${destDir}" -x "${skipPrefix}/*" "${skipPrefix}"`,
|
|
141
|
+
{ stdio: 'inherit' },
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
execSync(`unzip -q -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function backupDataDir(agent) {
|
|
149
|
+
const dataDir = join(agentInstallDir(agent), 'data')
|
|
150
|
+
if (!existsSync(dataDir)) return null
|
|
151
|
+
mkdirSync(BACKUPS_DIR, { recursive: true })
|
|
152
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
153
|
+
const oldVersion = installedVersion(agent) ?? 'unknown'
|
|
154
|
+
const backupPath = join(BACKUPS_DIR, `${agent}-${oldVersion}-${ts}.tar.gz`)
|
|
155
|
+
execSync(
|
|
156
|
+
`tar -czf "${backupPath}" -C "${agentInstallDir(agent)}" data`,
|
|
157
|
+
{ stdio: 'inherit' },
|
|
158
|
+
)
|
|
159
|
+
return backupPath
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function prompt(question) {
|
|
163
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
164
|
+
try { return (await rl.question(question)).trim() } finally { rl.close() }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── commands ────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
async function cmdLogin(args) {
|
|
170
|
+
let key = args[0]
|
|
171
|
+
if (!key) {
|
|
172
|
+
console.log(dim('Get your license key at https://0101.fyi/account'))
|
|
173
|
+
key = await prompt('Paste your license key: ')
|
|
174
|
+
}
|
|
175
|
+
key = key.trim()
|
|
176
|
+
if (!key) die('No key provided. Get one at https://0101.fyi/account')
|
|
177
|
+
writeConfig({ ...readConfig(), license_key: key })
|
|
178
|
+
ok(`Saved license to ${CONFIG_PATH}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cmdLogout() {
|
|
182
|
+
const cfg = readConfig()
|
|
183
|
+
delete cfg.license_key
|
|
184
|
+
writeConfig(cfg)
|
|
185
|
+
ok('Logged out.')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function cmdWhoami() {
|
|
189
|
+
const key = readConfig().license_key
|
|
190
|
+
if (!key) die('Not logged in. Run: 0101-agents login')
|
|
191
|
+
// Don't validate against the server here — keep it offline. `install` /
|
|
192
|
+
// `update` will surface a real 401 if the key is bad.
|
|
193
|
+
console.log(dim('License: ') + key.slice(0, 6) + '…' + key.slice(-4))
|
|
194
|
+
console.log(dim('API: ') + apiBase())
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function cmdInstall(args) {
|
|
198
|
+
const agent = args[0]
|
|
199
|
+
if (!agent) die('Usage: 0101-agents install <agent> [--force]')
|
|
200
|
+
const force = args.includes('--force')
|
|
201
|
+
|
|
202
|
+
ensureUnzipAvailable()
|
|
203
|
+
const installDir = agentInstallDir(agent)
|
|
204
|
+
if (existsSync(installDir) && !force) {
|
|
205
|
+
die(`${installDir} already exists. Use --force to overwrite (will wipe data/).`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
info(`Fetching manifest for ${agent}…`)
|
|
209
|
+
const manifest = await fetchManifest(agent)
|
|
210
|
+
info(`Latest: ${manifest.latest}`)
|
|
211
|
+
|
|
212
|
+
const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
|
|
213
|
+
info(`Downloading…`)
|
|
214
|
+
await downloadZip(manifest.download_url, zipPath)
|
|
215
|
+
|
|
216
|
+
if (existsSync(installDir) && force) {
|
|
217
|
+
rmSync(installDir, { recursive: true, force: true })
|
|
218
|
+
}
|
|
219
|
+
mkdirSync(installDir, { recursive: true })
|
|
220
|
+
unzipInto(zipPath, installDir)
|
|
221
|
+
writeInstalledVersion(agent, manifest.latest)
|
|
222
|
+
rmSync(zipPath, { force: true })
|
|
223
|
+
|
|
224
|
+
console.log('')
|
|
225
|
+
ok(`Installed ${bold(agent)} ${manifest.latest} → ${installDir}`)
|
|
226
|
+
info(`Start: 0101-agents start ${agent}`)
|
|
227
|
+
info(` or: cd ${installDir} && claude`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function cmdUpdate(args) {
|
|
231
|
+
const agent = args[0]
|
|
232
|
+
if (!agent) die('Usage: 0101-agents update <agent>')
|
|
233
|
+
|
|
234
|
+
ensureUnzipAvailable()
|
|
235
|
+
const installDir = agentInstallDir(agent)
|
|
236
|
+
if (!existsSync(installDir)) die(`${agent} is not installed. Try: 0101-agents install ${agent}`)
|
|
237
|
+
|
|
238
|
+
const current = installedVersion(agent)
|
|
239
|
+
info(`Installed: ${current ?? 'unknown'}`)
|
|
240
|
+
|
|
241
|
+
info(`Fetching manifest…`)
|
|
242
|
+
const manifest = await fetchManifest(agent)
|
|
243
|
+
info(`Latest: ${manifest.latest}`)
|
|
244
|
+
|
|
245
|
+
if (current === manifest.latest) {
|
|
246
|
+
ok('Already up to date.')
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const ans = await prompt(`Update ${agent} ${current ?? '?'} → ${manifest.latest}? [Y/n] `)
|
|
251
|
+
if (ans && !/^y(es)?$/i.test(ans)) {
|
|
252
|
+
info('Cancelled.')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const backup = backupDataDir(agent)
|
|
257
|
+
if (backup) info(`Backed up data → ${backup}`)
|
|
258
|
+
|
|
259
|
+
const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
|
|
260
|
+
info(`Downloading…`)
|
|
261
|
+
await downloadZip(manifest.download_url, zipPath)
|
|
262
|
+
|
|
263
|
+
// Extract into the install dir, skipping the data/ subtree so user state
|
|
264
|
+
// survives. unzip's -x flag drops matching entries.
|
|
265
|
+
unzipInto(zipPath, installDir, { skipPrefix: 'data' })
|
|
266
|
+
writeInstalledVersion(agent, manifest.latest)
|
|
267
|
+
rmSync(zipPath, { force: true })
|
|
268
|
+
|
|
269
|
+
console.log('')
|
|
270
|
+
ok(`Updated ${bold(agent)} → ${manifest.latest}`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cmdList() {
|
|
274
|
+
const installed = listInstalled()
|
|
275
|
+
if (installed.length === 0) {
|
|
276
|
+
info('No agents installed.')
|
|
277
|
+
info(`Install one: 0101-agents install <name>`)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
for (const name of installed) {
|
|
281
|
+
const v = installedVersion(name) ?? dim('?')
|
|
282
|
+
console.log(` ${bold(name)} ${dim(v)}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function cmdStart(args) {
|
|
287
|
+
const agent = args[0]
|
|
288
|
+
if (!agent) die('Usage: 0101-agents start <agent> [-- <claude-args>]')
|
|
289
|
+
const dir = agentInstallDir(agent)
|
|
290
|
+
if (!existsSync(dir)) {
|
|
291
|
+
die(`${agent} is not installed. Try: 0101-agents install ${agent}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Best-effort update check. Silently skipped if offline or the server
|
|
295
|
+
// misbehaves — never blocks the launch.
|
|
296
|
+
try {
|
|
297
|
+
const manifest = await fetchManifest(agent)
|
|
298
|
+
const current = installedVersion(agent)
|
|
299
|
+
if (current && manifest.latest !== current) {
|
|
300
|
+
console.log(
|
|
301
|
+
dim(`⚠ ${agent} ${manifest.latest} available (run: 0101-agents update ${agent})`),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Everything after the agent name passes through to `claude`. Supports both:
|
|
309
|
+
// 0101-agents start marketer --resume
|
|
310
|
+
// 0101-agents start marketer -- --resume (explicit separator)
|
|
311
|
+
// The `--` is optional; if present, we drop it.
|
|
312
|
+
let claudeArgs = args.slice(1)
|
|
313
|
+
if (claudeArgs[0] === '--') claudeArgs = claudeArgs.slice(1)
|
|
314
|
+
|
|
315
|
+
// Hand the terminal over to `claude` with cwd set to the agent dir. When
|
|
316
|
+
// claude exits, the user returns to their original shell location.
|
|
317
|
+
const child = spawn('claude', claudeArgs, { cwd: dir, stdio: 'inherit' })
|
|
318
|
+
child.on('error', (e) => {
|
|
319
|
+
if (e.code === 'ENOENT') {
|
|
320
|
+
die('`claude` not found in PATH. Install Claude Code first: https://claude.com/code')
|
|
321
|
+
}
|
|
322
|
+
die(`Failed to start claude: ${e.message}`)
|
|
323
|
+
})
|
|
324
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cmdHelp() {
|
|
328
|
+
console.log(`0101-agents — install and update Claude Code agents.
|
|
329
|
+
|
|
330
|
+
Commands:
|
|
331
|
+
${bold('login')} [<key>] Save your license key
|
|
332
|
+
${bold('logout')} Forget your license key
|
|
333
|
+
${bold('whoami')} Show current license
|
|
334
|
+
${bold('install')} <agent> [--force] Install an agent
|
|
335
|
+
${bold('update')} <agent> Update an installed agent (preserves data/)
|
|
336
|
+
${bold('start')} <agent> [-- args] Open the agent (runs claude inside its dir)
|
|
337
|
+
${bold('list')} List installed agents
|
|
338
|
+
${bold('help')} Show this message
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
0101-agents start marketer
|
|
342
|
+
0101-agents start marketer --resume
|
|
343
|
+
0101-agents start marketer -- -p "audit https://example.com"
|
|
344
|
+
|
|
345
|
+
Install dir: ${INSTALL_ROOT}
|
|
346
|
+
Config: ${CONFIG_PATH}`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── dispatch ────────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
const [, , cmd, ...rest] = process.argv
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
switch (cmd) {
|
|
355
|
+
case 'login': await cmdLogin(rest); break
|
|
356
|
+
case 'logout': cmdLogout(); break
|
|
357
|
+
case 'whoami': await cmdWhoami(); break
|
|
358
|
+
case 'install': await cmdInstall(rest); break
|
|
359
|
+
case 'update': await cmdUpdate(rest); break
|
|
360
|
+
case 'start': await cmdStart(rest); break
|
|
361
|
+
case 'list': cmdList(); break
|
|
362
|
+
case 'help':
|
|
363
|
+
case '-h':
|
|
364
|
+
case '--help':
|
|
365
|
+
case undefined:
|
|
366
|
+
cmdHelp(); break
|
|
367
|
+
default:
|
|
368
|
+
err(`Unknown command: ${cmd}`)
|
|
369
|
+
cmdHelp()
|
|
370
|
+
process.exit(1)
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
err(e?.message ?? String(e))
|
|
374
|
+
process.exit(1)
|
|
375
|
+
}
|