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.
- package/README.md +56 -0
- package/autark.mjs +326 -0
- 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
|
+
}
|