autark-cli 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 (3) hide show
  1. package/README.md +56 -0
  2. package/autark.mjs +326 -0
  3. package/package.json +22 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # autark-cli
2
+
3
+ CLI for [autark](https://autark.kushalsm.com) — hypothesis-driven product runbooks.
4
+
5
+ A product has hypotheses. Each hypothesis has runs. Each run logs actions (emails sent, posts made, replies received). The dashboard shows it all.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i -g autark-cli
11
+ ```
12
+
13
+ ## Login
14
+
15
+ ```sh
16
+ autark login send your@email.com
17
+ # check inbox
18
+ autark login verify your@email.com --code 123456
19
+ ```
20
+
21
+ ## Use
22
+
23
+ ```sh
24
+ # products
25
+ autark product upsert --slug chrome-relay --name "chrome-relay" --tagline "..."
26
+ autark product list
27
+
28
+ # hypotheses
29
+ autark hypothesis create --product chrome-relay --code H01 --md @./H01.md --title "..."
30
+ autark hypothesis status chrome-relay/H01 --status active # active|inactive|dead
31
+
32
+ # runs
33
+ autark run start --hypothesis chrome-relay/H01
34
+ autark run finish --run <run_id> --narrative @./narrative.md
35
+
36
+ # actions (anything the agent did during a run)
37
+ autark log action --run <run_id> --channel email --title "..." \
38
+ --recipient person@example.com --agentmail-thread-id <thread_id>
39
+ autark log action --run <run_id> --channel github --title "..." --url https://github.com/...
40
+
41
+ # context (full hypothesis state for an agent picking up work)
42
+ autark context chrome-relay/H01
43
+
44
+ autark me
45
+ autark logout
46
+ ```
47
+
48
+ ## ENV
49
+
50
+ - `AUTARK_API_URL` — override the default Worker URL (`https://autark-api.kushalsokke.workers.dev`)
51
+
52
+ ## Architecture
53
+
54
+ The CLI is a thin HTTP client over a Cloudflare Worker that holds the InstantDB admin token. Magic-link auth via InstantDB. Token saved to `~/.autark/credentials.json` after login.
55
+
56
+ The web at autark.kushalsm.com is read-only. All writes (product, hypothesis, run, action) come through this CLI or future MCP server.
package/autark.mjs ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ // autark CLI — thin HTTP client over the autark-api Worker.
3
+ //
4
+ // Auth: magic-link flow saves a refresh_token to ~/.autark/credentials.json.
5
+ // Every subsequent command sends it as `Authorization: Bearer <token>`.
6
+ //
7
+ // All business logic, schema knowledge, and InstantDB connectivity lives in
8
+ // the Worker. The CLI is intentionally thin — it parses args, makes HTTP
9
+ // calls, formats output. Replace this binary in any language tomorrow and
10
+ // the system still works.
11
+
12
+ import fs from 'node:fs'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import process from 'node:process'
16
+
17
+ const API = process.env.AUTARK_API_URL || 'https://autark-api.kushalsokke.workers.dev'
18
+ const CREDS_PATH = path.join(os.homedir(), '.autark', 'credentials.json')
19
+
20
+ const args = process.argv.slice(2)
21
+ main().catch(err => {
22
+ console.error(err.message)
23
+ process.exit(1)
24
+ })
25
+
26
+ async function main() {
27
+ const [group, command, ...rest] = args
28
+ if (!group || group === 'help' || group === '--help' || group === '-h') return usage()
29
+
30
+ if (group === 'login') {
31
+ if (command === 'send') return loginSend(rest)
32
+ if (command === 'verify') return loginVerify(rest)
33
+ if (!command || command === '--help') return loginUsage()
34
+ }
35
+ if (group === 'logout') return logout()
36
+ if (group === 'me') return me()
37
+
38
+ if (group === 'product') {
39
+ if (command === 'upsert') return productUpsert(rest)
40
+ if (command === 'list') return productList()
41
+ }
42
+ if (group === 'hypothesis') {
43
+ if (command === 'create') return hypothesisCreate(rest)
44
+ if (command === 'status') return hypothesisStatus(rest)
45
+ }
46
+ if (group === 'run') {
47
+ if (command === 'start') return runStart(rest)
48
+ if (command === 'finish') return runFinish(rest)
49
+ }
50
+ if (group === 'log' && command === 'action') return logAction(rest)
51
+ if (group === 'context') return context([command, ...rest].filter(Boolean))
52
+
53
+ usage()
54
+ process.exit(1)
55
+ }
56
+
57
+ // ============================================================ auth flow
58
+
59
+ async function loginSend(rest) {
60
+ const opts = parseArgs(rest)
61
+ const email = required(opts._[0] || opts.email, 'email')
62
+ await api('POST', '/v1/auth/login/send', { email }, { auth: false })
63
+ console.log(`code sent to ${email}`)
64
+ console.log(`now run: autark login verify ${email} --code <6-digit-code>`)
65
+ }
66
+
67
+ async function loginVerify(rest) {
68
+ const opts = parseArgs(rest)
69
+ const email = required(opts._[0] || opts.email, 'email')
70
+ const code = required(opts.code, '--code')
71
+ const result = await api('POST', '/v1/auth/login/verify', { email, code }, { auth: false })
72
+ if (!result.token) throw new Error('verify succeeded but no token returned')
73
+ saveCredentials({
74
+ user_id: result.user.id,
75
+ email: result.user.email,
76
+ token: result.token,
77
+ saved_at: new Date().toISOString(),
78
+ })
79
+ console.log(`logged in as ${result.user.email}`)
80
+ console.log(`user_id: ${result.user.id}`)
81
+ console.log(`saved to ${CREDS_PATH}`)
82
+ }
83
+
84
+ async function logout() {
85
+ if (fs.existsSync(CREDS_PATH)) fs.unlinkSync(CREDS_PATH)
86
+ console.log('logged out')
87
+ }
88
+
89
+ async function me() {
90
+ const result = await api('GET', '/v1/me')
91
+ console.log(`${result.email} (${result.id})`)
92
+ }
93
+
94
+ // =========================================================== products
95
+
96
+ async function productUpsert(rest) {
97
+ const opts = parseArgs(rest)
98
+ const slug = required(opts.slug, '--slug')
99
+ const name = required(opts.name, '--name')
100
+ const result = await api('POST', '/v1/products', {
101
+ slug,
102
+ name,
103
+ url: opts.url || '',
104
+ tagline: opts.tagline || '',
105
+ visibility: opts.visibility || 'public',
106
+ })
107
+ console.log(result.id)
108
+ }
109
+
110
+ async function productList() {
111
+ const result = await api('GET', '/v1/products')
112
+ if (!result.products?.length) {
113
+ console.log('(no products)')
114
+ return
115
+ }
116
+ for (const p of result.products) {
117
+ console.log(`${(p.slug || '').padEnd(20)} ${(p.visibility || '').padEnd(8)} ${p.name || ''}`)
118
+ }
119
+ }
120
+
121
+ // ========================================================= hypotheses
122
+
123
+ async function hypothesisCreate(rest) {
124
+ const opts = parseArgs(rest)
125
+ const product = required(opts.product, '--product')
126
+ const code = required(opts.code, '--code')
127
+ const md = readValue(required(opts.md, '--md'))
128
+ const title = opts.title
129
+ const result = await api('POST', '/v1/hypotheses', {
130
+ product, code, md,
131
+ title: title || undefined,
132
+ status: opts.status || undefined,
133
+ })
134
+ console.log(result.id)
135
+ }
136
+
137
+ async function hypothesisStatus(rest) {
138
+ const opts = parseArgs(rest)
139
+ const ref = required(opts._[0] || opts.hypothesis, 'hypothesis (e.g. chrome-relay/H01)')
140
+ const status = required(opts.status, '--status')
141
+ const [productSlug, code] = splitHypothesisRef(ref)
142
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
143
+ const hypId = ctx.hypothesis?.id
144
+ if (!hypId) throw new Error(`hypothesis not found: ${ref}`)
145
+ const result = await api('PATCH', `/v1/hypotheses/${hypId}/status`, { status })
146
+ console.log(`${ref} → ${result.status}`)
147
+ }
148
+
149
+ // =============================================================== runs
150
+
151
+ async function runStart(rest) {
152
+ const opts = parseArgs(rest)
153
+ const ref = required(opts.hypothesis || opts._[0], '--hypothesis (e.g. chrome-relay/H01)')
154
+ const [productSlug, code] = splitHypothesisRef(ref)
155
+ const result = await api('POST', '/v1/runs', { product: productSlug, hypothesis: code })
156
+ console.log(result.id)
157
+ }
158
+
159
+ async function runFinish(rest) {
160
+ const opts = parseArgs(rest)
161
+ const id = required(opts.run, '--run')
162
+ const narrative = readValue(required(opts.narrative, '--narrative'))
163
+ const result = await api('PATCH', `/v1/runs/${id}/finish`, { narrative_md: narrative })
164
+ console.log(`${result.id} finished_at=${result.finished_at}`)
165
+ }
166
+
167
+ // ============================================================ actions
168
+
169
+ async function logAction(rest) {
170
+ const opts = parseArgs(rest)
171
+ const run = required(opts.run, '--run')
172
+ const channel = required(opts.channel, '--channel')
173
+ const title = required(opts.title, '--title')
174
+ const result = await api('POST', '/v1/actions', {
175
+ run,
176
+ channel,
177
+ title,
178
+ url: opts.url || undefined,
179
+ agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
180
+ recipient: opts.recipient || undefined,
181
+ metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
182
+ occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
183
+ })
184
+ console.log(result.id)
185
+ }
186
+
187
+ // ============================================================ context
188
+
189
+ async function context(rest) {
190
+ const opts = parseArgs(rest)
191
+ const ref = required(opts._[0] || opts.hypothesis, 'hypothesis (e.g. chrome-relay/H01)')
192
+ const [productSlug, code] = splitHypothesisRef(ref)
193
+ const result = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
194
+
195
+ console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
196
+ console.log(`Status: ${result.hypothesis.status}\n`)
197
+ console.log(result.hypothesis.hypothesis_md)
198
+ for (const run of result.runs) {
199
+ console.log(`\n\n## Run ${run.run_number} (${run.id})`)
200
+ console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}`)
201
+ if (run.actions?.length) {
202
+ console.log(`\nActions:`)
203
+ for (const a of run.actions) {
204
+ const pointer = a.url || a.agentmail_thread_id || a.recipient || ''
205
+ console.log(` [${a.channel}] ${a.title}${pointer ? ` → ${pointer}` : ''}`)
206
+ }
207
+ }
208
+ if (run.narrative_md) {
209
+ console.log(`\n${run.narrative_md}`)
210
+ }
211
+ }
212
+ }
213
+
214
+ // ============================================================ HTTP layer
215
+
216
+ async function api(method, pathStr, body, { auth = true } = {}) {
217
+ const headers = { 'content-type': 'application/json' }
218
+ if (auth) {
219
+ const creds = loadCredentials()
220
+ if (!creds) throw new Error('not logged in. run: autark login send <email>')
221
+ headers.authorization = `Bearer ${creds.token}`
222
+ }
223
+ const init = { method, headers }
224
+ if (body !== undefined) init.body = JSON.stringify(body)
225
+ const res = await fetch(API + pathStr, init)
226
+ const text = await res.text()
227
+ let data = null
228
+ try { data = text ? JSON.parse(text) : null } catch { data = { raw: text } }
229
+ if (!res.ok) {
230
+ const msg = data?.error || `HTTP ${res.status}`
231
+ throw new Error(msg)
232
+ }
233
+ return data
234
+ }
235
+
236
+ // ============================================================ credentials
237
+
238
+ function saveCredentials(creds) {
239
+ fs.mkdirSync(path.dirname(CREDS_PATH), { recursive: true, mode: 0o700 })
240
+ fs.writeFileSync(CREDS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 })
241
+ }
242
+
243
+ function loadCredentials() {
244
+ if (!fs.existsSync(CREDS_PATH)) return null
245
+ try {
246
+ return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'))
247
+ } catch {
248
+ return null
249
+ }
250
+ }
251
+
252
+ // ============================================================ arg parsing
253
+
254
+ function parseArgs(list) {
255
+ const opts = { _: [] }
256
+ for (let i = 0; i < list.length; i++) {
257
+ const arg = list[i]
258
+ if (!arg.startsWith('--')) {
259
+ opts._.push(arg)
260
+ continue
261
+ }
262
+ const key = arg.slice(2)
263
+ const next = list[i + 1]
264
+ if (!next || next.startsWith('--')) {
265
+ opts[key] = true
266
+ } else {
267
+ opts[key] = next
268
+ i++
269
+ }
270
+ }
271
+ return opts
272
+ }
273
+
274
+ function required(value, label) {
275
+ if (value === undefined || value === null || value === '' || value === true) {
276
+ throw new Error(`${label} is required`)
277
+ }
278
+ return value
279
+ }
280
+
281
+ function readValue(value) {
282
+ if (typeof value === 'string' && value.startsWith('@')) {
283
+ return fs.readFileSync(value.slice(1), 'utf8')
284
+ }
285
+ return String(value)
286
+ }
287
+
288
+ function splitHypothesisRef(ref) {
289
+ const m = String(ref).match(/^([\w.-]+)\/(H\d{2})$/)
290
+ if (!m) throw new Error(`expected product/Hxx, got ${ref}`)
291
+ return [m[1], m[2]]
292
+ }
293
+
294
+ // ============================================================ usage
295
+
296
+ function loginUsage() {
297
+ console.log(`autark login send <email>
298
+ autark login verify <email> --code <6-digit-code>`)
299
+ }
300
+
301
+ function usage() {
302
+ console.log(`autark — agent runbook CLI
303
+
304
+ autark login send <email> send a magic code
305
+ autark login verify <email> --code <code> complete login
306
+ autark logout clear local credentials
307
+ autark me show signed-in user
308
+
309
+ autark product upsert --slug <slug> --name <name> [--url <url>] [--tagline <text>]
310
+ autark product list
311
+
312
+ autark hypothesis create --product <slug> --code H01 --md @./hypothesis.md [--title <t>]
313
+ autark hypothesis status <slug>/<H01> --status active|inactive|dead
314
+
315
+ autark run start --hypothesis <slug>/<H01>
316
+ autark run finish --run <id> --narrative @./run.md
317
+
318
+ autark log action --run <id> --channel <c> --title <t> [--url <u>]
319
+ [--agentmail-thread-id <uuid>] [--recipient <email>]
320
+ [--metadata @./meta.json]
321
+
322
+ autark context <slug>/<H01>
323
+
324
+ ENV:
325
+ AUTARK_API_URL override the default Worker URL`)
326
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "autark-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for autark — hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "autark": "autark.mjs"
9
+ },
10
+ "files": [
11
+ "autark.mjs",
12
+ "README.md"
13
+ ],
14
+ "homepage": "https://autark.kushalsm.com",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/kiluazen/autark.git"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ }
22
+ }