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.
Files changed (2) hide show
  1. package/bin/cli.js +374 -1
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,2 +1,375 @@
1
1
  #!/usr/bin/env node
2
- console.log('0101 — coming soon. See https://0101.fyi')
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0101-agents",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "0101-agents": "./bin/cli.js"