autark-cli 0.1.3 → 0.1.5

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 +33 -19
  2. package/autark.mjs +384 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,39 +18,53 @@ autark login send your@email.com
18
18
  autark login verify your@email.com --code 123456
19
19
  ```
20
20
 
21
- ## Use
21
+ ## Runbook use
22
+
23
+ Autark is ID-first. Use `product list` / `context` to discover IDs, then pass IDs for writes. `slug/H01` remains a convenience alias.
22
24
 
23
25
  ```sh
24
- # products
25
- autark product upsert --slug chrome-relay --name "chrome-relay" --tagline "..."
26
- autark product list
26
+ autark product upsert --slug chrome-relay --name "Chrome Relay" --tagline "..."
27
+ autark product list # prints slug, visibility, id, name
28
+ autark context chrome-relay
29
+
30
+ autark hypothesis create --product-id <product_id> --md @./H01.md --code H01 --title "..."
31
+ autark run start --hypothesis-id <hypothesis_id>
32
+ autark log action --run-id <run_id> --channel github --title "..." --url https://github.com/...
33
+ autark run finish --run-id <run_id> --narrative @./narrative.md
34
+ ```
27
35
 
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
36
+ ## Mail use
31
37
 
32
- # runs
33
- autark run start --hypothesis chrome-relay/H01
34
- autark run finish --run <run_id> --narrative @./narrative.md
38
+ `autark mail` covers the AgentMail surface Autark needs. It does not require a separate AgentMail CLI login.
35
39
 
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
+ ```sh
41
+ autark mail setup --prefix laksh
40
42
 
41
- # context (full hypothesis state for an agent picking up work)
42
- autark context chrome-relay/H01
43
+ autark mail send --run-id <run_id> \
44
+ --to person@example.com \
45
+ --subject "Subject" \
46
+ --text @./body.txt
43
47
 
44
- autark me
45
- autark logout
48
+ autark mail reply --run-id <run_id> --message-id <message_id> --text @./reply.txt
49
+
50
+ autark mail threads --limit 20
51
+ autark mail thread <thread_id>
52
+ autark mail messages --limit 20
53
+ autark mail message <message_id>
54
+ autark mail raw <message_id>
55
+ autark mail attachment --message-id <message_id> --attachment-id <attachment_id> --out file.bin
46
56
  ```
47
57
 
58
+ Mail sends/replies call AgentMail directly with the user's inbox-scoped key from `~/.autark/credentials.json`, then log an Autark `email` action containing `agentmail_thread_id` and `agentmail_inbox_id`.
59
+
48
60
  ## ENV
49
61
 
50
62
  - `AUTARK_API_URL` — override the default Worker URL (`https://autark-api.kushalsokke.workers.dev`)
63
+ - `AGENTMAIL_API_URL` — override AgentMail API base
64
+ - `AGENTMAIL_API_KEY`, `AGENTMAIL_EMAIL`, `AGENTMAIL_INBOX_ID` — override local mail credentials for debugging
51
65
 
52
66
  ## Architecture
53
67
 
54
68
  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
69
 
56
- The web at autark.kushalsm.com is read-only. All writes (product, hypothesis, run, action) come through this CLI or future MCP server.
70
+ The Worker uses the AgentMail org key only for provisioning and dashboard thread reads. Local mail send/reply uses the user's inbox-scoped key.
package/autark.mjs CHANGED
@@ -1,13 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // autark CLI — thin HTTP client over the autark-api Worker.
3
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.
4
+ // The runbook API is ID-first. slug/Hxx remains as a convenience alias.
5
+ // Mail commands call AgentMail directly with the user's inbox-scoped key;
6
+ // autark-api uses the org key only for provisioning and dashboard thread reads.
11
7
 
12
8
  import fs from 'node:fs'
13
9
  import os from 'node:os'
@@ -15,6 +11,7 @@ import path from 'node:path'
15
11
  import process from 'node:process'
16
12
 
17
13
  const API = process.env.AUTARK_API_URL || 'https://autark-api.kushalsokke.workers.dev'
14
+ const AGENTMAIL_API = process.env.AGENTMAIL_API_URL || 'https://api.agentmail.to/v0'
18
15
  const CREDS_PATH = path.join(os.homedir(), '.autark', 'credentials.json')
19
16
 
20
17
  const args = process.argv.slice(2)
@@ -49,6 +46,7 @@ async function main() {
49
46
  }
50
47
  if (group === 'log' && command === 'action') return logAction(rest)
51
48
  if (group === 'context') return context([command, ...rest].filter(Boolean))
49
+ if (group === 'mail') return mail(command, rest)
52
50
 
53
51
  usage()
54
52
  process.exit(1)
@@ -71,6 +69,7 @@ async function loginVerify(rest) {
71
69
  const result = await api('POST', '/v1/auth/login/verify', { email, code }, { auth: false })
72
70
  if (!result.token) throw new Error('verify succeeded but no token returned')
73
71
  saveCredentials({
72
+ ...loadCredentials(),
74
73
  user_id: result.user.id,
75
74
  email: result.user.email,
76
75
  token: result.token,
@@ -116,7 +115,7 @@ async function productList() {
116
115
  return
117
116
  }
118
117
  for (const p of result.products) {
119
- console.log(`${(p.slug || '').padEnd(20)} ${(p.visibility || '').padEnd(8)} ${p.name || ''}`)
118
+ console.log(`${(p.slug || '').padEnd(20)} ${(p.visibility || '').padEnd(8)} ${(p.id || '').padEnd(36)} ${p.name || ''}`)
120
119
  }
121
120
  }
122
121
 
@@ -124,45 +123,65 @@ async function productList() {
124
123
 
125
124
  async function hypothesisCreate(rest) {
126
125
  const opts = parseArgs(rest)
127
- const product = required(opts.product, '--product')
128
- const code = required(opts.code, '--code')
129
- const md = readValue(required(opts.md, '--md'))
130
- const title = opts.title
131
- const result = await api('POST', '/v1/hypotheses', {
132
- product, code, md,
133
- title: title || undefined,
126
+ const md = readValue(required(opts.md, '--md'))
127
+ const body = {
128
+ md,
129
+ code: opts.code || undefined,
130
+ title: opts.title || undefined,
134
131
  status: opts.status || undefined,
135
- })
132
+ }
133
+
134
+ let result
135
+ if (opts['product-id'] || opts.product_id) {
136
+ const productId = opts['product-id'] || opts.product_id
137
+ result = await api('POST', `/v1/products/${encodeURIComponent(productId)}/hypotheses`, body)
138
+ } else {
139
+ body.product = required(opts.product, '--product or --product-id')
140
+ result = await api('POST', '/v1/hypotheses', body)
141
+ }
136
142
  console.log(result.id)
137
143
  }
138
144
 
139
145
  async function hypothesisStatus(rest) {
140
146
  const opts = parseArgs(rest)
141
- const ref = required(opts._[0] || opts.hypothesis, 'hypothesis (e.g. chrome-relay/H01)')
142
147
  const status = required(opts.status, '--status')
143
- const [productSlug, code] = splitHypothesisRef(ref)
144
- const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
145
- const hypId = ctx.hypothesis?.id
146
- if (!hypId) throw new Error(`hypothesis not found: ${ref}`)
147
- const result = await api('PATCH', `/v1/hypotheses/${hypId}/status`, { status })
148
- console.log(`${ref} → ${result.status}`)
148
+ let hypId = opts['hypothesis-id'] || opts.hypothesis_id || opts.id
149
+ let label = hypId
150
+
151
+ if (!hypId) {
152
+ const ref = required(opts._[0] || opts.hypothesis, 'hypothesis id or product/Hxx')
153
+ const [productSlug, code] = splitHypothesisRef(ref)
154
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
155
+ hypId = ctx.hypothesis?.id
156
+ label = ref
157
+ }
158
+ if (!hypId) throw new Error('hypothesis not found')
159
+
160
+ const result = await api('PATCH', `/v1/hypotheses/${encodeURIComponent(hypId)}/status`, { status })
161
+ console.log(`${label} → ${result.status}`)
149
162
  }
150
163
 
151
164
  // =============================================================== runs
152
165
 
153
166
  async function runStart(rest) {
154
167
  const opts = parseArgs(rest)
155
- const ref = required(opts.hypothesis || opts._[0], '--hypothesis (e.g. chrome-relay/H01)')
156
- const [productSlug, code] = splitHypothesisRef(ref)
157
- const result = await api('POST', '/v1/runs', { product: productSlug, hypothesis: code })
168
+ let result
169
+ const hypId = opts['hypothesis-id'] || opts.hypothesis_id
170
+ if (hypId) {
171
+ result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/runs`, {})
172
+ } else {
173
+ const ref = required(opts.hypothesis || opts._[0], '--hypothesis-id <id> or --hypothesis <slug/Hxx>')
174
+ const [productSlug, code] = splitHypothesisRef(ref)
175
+ result = await api('POST', '/v1/runs', { product: productSlug, hypothesis: code })
176
+ }
158
177
  console.log(result.id)
159
178
  }
160
179
 
161
180
  async function runFinish(rest) {
162
181
  const opts = parseArgs(rest)
163
- const id = required(opts.run, '--run')
182
+ const id = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
164
183
  const narrative = readValue(required(opts.narrative, '--narrative'))
165
- const result = await api('PATCH', `/v1/runs/${id}/finish`, { narrative_md: narrative })
184
+ const result = await api('PATCH', `/v1/runs/${encodeURIComponent(id)}/finish`, { narrative_md: narrative })
166
185
  console.log(`${result.id} finished_at=${result.finished_at}`)
167
186
  }
168
187
 
@@ -170,52 +189,286 @@ async function runFinish(rest) {
170
189
 
171
190
  async function logAction(rest) {
172
191
  const opts = parseArgs(rest)
173
- const run = required(opts.run, '--run')
192
+ const run = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
174
193
  const channel = required(opts.channel, '--channel')
175
194
  const title = required(opts.title, '--title')
176
- const result = await api('POST', '/v1/actions', {
177
- run,
195
+ const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
178
196
  channel,
179
197
  title,
180
- url: opts.url || undefined,
181
- agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
182
- recipient: opts.recipient || undefined,
183
- metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
184
- occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
198
+ url: opts.url || undefined,
199
+ agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
200
+ agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
201
+ recipient: opts.recipient || undefined,
202
+ metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
203
+ occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
185
204
  })
186
205
  console.log(result.id)
187
206
  }
188
207
 
208
+ // ============================================================ mail
209
+
210
+ async function mail(command, rest) {
211
+ if (!command || command === '--help' || command === 'help') return mailUsage()
212
+ if (command === 'setup') return mailSetup(rest)
213
+ if (command === 'send') return mailSend(rest)
214
+ if (command === 'reply') return mailReply(rest, 'reply')
215
+ if (command === 'reply-all') return mailReply(rest, 'reply-all')
216
+ if (command === 'forward') return mailForward(rest)
217
+ if (command === 'threads') return mailThreads(rest)
218
+ if (command === 'thread') return mailThread(rest)
219
+ if (command === 'messages') return mailMessages(rest)
220
+ if (command === 'message') return mailMessage(rest)
221
+ if (command === 'raw') return mailRaw(rest)
222
+ if (command === 'attachment') return mailAttachment(rest)
223
+ if (command === 'request') return mailRequest(rest)
224
+ mailUsage()
225
+ process.exit(1)
226
+ }
227
+
228
+ async function mailSetup(rest) {
229
+ const opts = parseArgs(rest)
230
+ const existing = loadCredentials() || {}
231
+ if (existing.agentmail_token && existing.agentmail_email && !opts.force) {
232
+ console.log(existing.agentmail_email)
233
+ return
234
+ }
235
+ const prefix = required(opts.prefix || defaultInboxPrefix(existing.email), '--prefix')
236
+ const result = await api('POST', '/v1/onboard/agentmail', {
237
+ prefix,
238
+ display_name: opts['display-name'] || opts.display_name || existing.email || prefix,
239
+ purpose: opts.purpose || 'runner',
240
+ })
241
+ saveCredentials({
242
+ ...existing,
243
+ agentmail_email: result.agentmail_email || result.email,
244
+ agentmail_inbox_id: result.agentmail_inbox_id || result.inbox_id || result.email,
245
+ agentmail_api_key_id: result.agentmail_api_key_id || result.api_key_id,
246
+ agentmail_token: result.agentmail_token,
247
+ agentmail_saved_at: new Date().toISOString(),
248
+ })
249
+ console.log(result.agentmail_email || result.email)
250
+ }
251
+
252
+ async function mailSend(rest) {
253
+ const opts = parseArgs(rest)
254
+ const creds = requireAgentmailCredentials()
255
+ const to = listOpt(opts.to, '--to')
256
+ const subject = opts.subject || ''
257
+ const body = mailBody(opts, { to, subject })
258
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/send`, body)
259
+ const action = await maybeLogMailAction(opts, {
260
+ kind: 'send',
261
+ defaultTitle: opts.title || `Email: ${subject || to.join(', ')}`,
262
+ recipient: to.join(','),
263
+ subject,
264
+ response: result,
265
+ })
266
+ printJson({ ...result, autark_action_id: action?.id })
267
+ }
268
+
269
+ async function mailReply(rest, mode) {
270
+ const opts = parseArgs(rest)
271
+ const creds = requireAgentmailCredentials()
272
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
273
+ const body = mailBody(opts)
274
+ const endpoint = mode === 'reply-all' ? 'reply-all' : 'reply'
275
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/${endpoint}`, body)
276
+ const action = await maybeLogMailAction(opts, {
277
+ kind: mode,
278
+ defaultTitle: opts.title || `${mode === 'reply-all' ? 'Reply-all' : 'Reply'}: ${messageId}`,
279
+ recipient: listOpt(opts.to).join(','),
280
+ response: result,
281
+ metadata: { message_id: messageId },
282
+ })
283
+ printJson({ ...result, autark_action_id: action?.id })
284
+ }
285
+
286
+ async function mailForward(rest) {
287
+ const opts = parseArgs(rest)
288
+ const creds = requireAgentmailCredentials()
289
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
290
+ const to = listOpt(opts.to, '--to')
291
+ const body = mailBody(opts, { to, subject: opts.subject || '' })
292
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/forward`, body)
293
+ const action = await maybeLogMailAction(opts, {
294
+ kind: 'forward',
295
+ defaultTitle: opts.title || `Forward: ${opts.subject || messageId}`,
296
+ recipient: to.join(','),
297
+ response: result,
298
+ metadata: { message_id: messageId },
299
+ })
300
+ printJson({ ...result, autark_action_id: action?.id })
301
+ }
302
+
303
+ async function mailThreads(rest) {
304
+ const opts = parseArgs(rest)
305
+ const creds = requireAgentmailCredentials()
306
+ const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
307
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads${qs}`))
308
+ }
309
+
310
+ async function mailThread(rest) {
311
+ const opts = parseArgs(rest)
312
+ const creds = requireAgentmailCredentials()
313
+ const threadId = required(opts['thread-id'] || opts.thread_id || opts._[0], 'thread_id')
314
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads/${encodeURIComponent(threadId)}`))
315
+ }
316
+
317
+ async function mailMessages(rest) {
318
+ const opts = parseArgs(rest)
319
+ const creds = requireAgentmailCredentials()
320
+ const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
321
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages${qs}`))
322
+ }
323
+
324
+ async function mailMessage(rest) {
325
+ const opts = parseArgs(rest)
326
+ const creds = requireAgentmailCredentials()
327
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
328
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}`))
329
+ }
330
+
331
+ async function mailRaw(rest) {
332
+ const opts = parseArgs(rest)
333
+ const creds = requireAgentmailCredentials()
334
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
335
+ const raw = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/raw`, undefined, { parseJson: false })
336
+ process.stdout.write(raw)
337
+ }
338
+
339
+ async function mailAttachment(rest) {
340
+ const opts = parseArgs(rest)
341
+ const creds = requireAgentmailCredentials()
342
+ const messageId = required(opts['message-id'] || opts.message_id, '--message-id')
343
+ const attachmentId = required(opts['attachment-id'] || opts.attachment_id, '--attachment-id')
344
+ const body = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`, undefined, { parseJson: false })
345
+ if (opts.out) fs.writeFileSync(opts.out, body)
346
+ else process.stdout.write(body)
347
+ }
348
+
349
+ async function mailRequest(rest) {
350
+ const opts = parseArgs(rest)
351
+ const method = (opts.method || opts._[0] || 'GET').toUpperCase()
352
+ const requestPath = required(opts.path || opts._[1], '--path')
353
+ const body = opts.body ? JSON.parse(readValue(opts.body)) : undefined
354
+ const parseJson = opts.raw ? false : true
355
+ const result = await agentmailRequest(method, requestPath.startsWith('/') ? requestPath : `/${requestPath}`, body, { parseJson })
356
+ if (parseJson) printJson(result)
357
+ else process.stdout.write(result)
358
+ }
359
+
360
+ function mailBody(opts, base = {}) {
361
+ const body = { ...base }
362
+ if (opts.text !== undefined) body.text = readValue(opts.text)
363
+ if (opts.html !== undefined) body.html = readValue(opts.html)
364
+ if (opts.subject !== undefined) body.subject = opts.subject
365
+ for (const key of ['cc', 'bcc', 'reply-to', 'label', 'attachment']) {
366
+ const snake = key.replaceAll('-', '_')
367
+ const value = opts[key] ?? opts[snake]
368
+ if (value !== undefined) body[snake] = listOpt(value)
369
+ }
370
+ if (opts.headers !== undefined) body.headers = JSON.parse(readValue(opts.headers))
371
+ return body
372
+ }
373
+
374
+ async function maybeLogMailAction(opts, { kind, defaultTitle, recipient, subject, response, metadata = {} }) {
375
+ const runId = opts['run-id'] || opts.run_id || opts.run
376
+ if (!runId) return null
377
+ const creds = requireAgentmailCredentials()
378
+ const threadId = response?.thread_id
379
+ if (!threadId) throw new Error('AgentMail response missing thread_id; refusing to log email action')
380
+ return api('POST', `/v1/runs/${encodeURIComponent(runId)}/actions`, {
381
+ channel: 'email',
382
+ title: opts.title || defaultTitle,
383
+ recipient: recipient || undefined,
384
+ agentmail_thread_id: threadId,
385
+ agentmail_inbox_id: creds.inboxId,
386
+ metadata: compact({
387
+ kind,
388
+ subject,
389
+ message_id: response?.message_id,
390
+ ...metadata,
391
+ }),
392
+ })
393
+ }
394
+
395
+ async function agentmailRequest(method, pathStr, body, { parseJson = true } = {}) {
396
+ const creds = requireAgentmailCredentials()
397
+ const res = await fetch(AGENTMAIL_API + pathStr, {
398
+ method,
399
+ headers: {
400
+ authorization: `Bearer ${creds.token}`,
401
+ 'content-type': 'application/json',
402
+ },
403
+ body: body === undefined ? undefined : JSON.stringify(body),
404
+ })
405
+ const text = await res.text()
406
+ let data = text
407
+ if (parseJson) {
408
+ try { data = text ? JSON.parse(text) : null } catch { data = { raw: text } }
409
+ }
410
+ if (!res.ok) {
411
+ const msg = parseJson ? (data?.error || data?.message || JSON.stringify(data)) : text
412
+ throw new Error(`AgentMail ${res.status}: ${msg}`)
413
+ }
414
+ return data
415
+ }
416
+
417
+ function requireAgentmailCredentials() {
418
+ const creds = loadCredentials()
419
+ const token = process.env.AGENTMAIL_API_KEY || creds?.agentmail_token
420
+ const email = process.env.AGENTMAIL_EMAIL || creds?.agentmail_email
421
+ const inboxId = process.env.AGENTMAIL_INBOX_ID || creds?.agentmail_inbox_id || email
422
+ if (!token) throw new Error('missing agentmail_token. run: autark mail setup --prefix <name>')
423
+ if (!inboxId) throw new Error('missing agentmail_inbox_id. run: autark mail setup --prefix <name>')
424
+ return { token, email, inboxId }
425
+ }
426
+
189
427
  // ============================================================ context
190
428
 
191
429
  async function context(rest) {
192
430
  const opts = parseArgs(rest)
193
- const ref = required(opts._[0] || opts.hypothesis, 'product or hypothesis (e.g. chrome-relay or chrome-relay/H01)')
431
+
432
+ if (opts['product-id'] || opts.product_id) {
433
+ const productId = opts['product-id'] || opts.product_id
434
+ return printProductContext(await api('GET', `/v1/products/${encodeURIComponent(productId)}/context`))
435
+ }
436
+
437
+ if (opts['hypothesis-id'] || opts.hypothesis_id) {
438
+ const hypId = opts['hypothesis-id'] || opts.hypothesis_id
439
+ return printHypothesisContext(await api('GET', `/v1/hypotheses/${encodeURIComponent(hypId)}/context`))
440
+ }
441
+
442
+ const ref = required(opts._[0] || opts.hypothesis, 'product slug, product/Hxx, --product-id, or --hypothesis-id')
194
443
  const parts = ref.split('/')
195
444
  const productSlug = parts[0]
196
445
  const code = parts[1]
197
446
 
198
- if (!code) {
199
- // product-level: brief + every hypothesis with status + counts
200
- const r = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}`)
201
- console.log(`# ${r.product.slug} — ${r.product.name}`)
202
- if (r.product.tagline) console.log(`> ${r.product.tagline}`)
203
- if (r.product.url) console.log(`> ${r.product.url}`)
204
- console.log(`\n## Brief\n`)
205
- console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.kushalsm.com)')
206
- console.log(`\n## Hypotheses (${r.hypotheses.length})\n`)
207
- if (!r.hypotheses.length) {
208
- console.log('(none yetstart with H01)')
209
- } else {
210
- for (const h of r.hypotheses) {
211
- console.log(`- [${h.status}] ${h.code} ${h.title} (runs: ${h.run_count})`)
212
- }
447
+ if (!code) return printProductContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}`))
448
+ return printHypothesisContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`))
449
+ }
450
+
451
+ function printProductContext(r) {
452
+ console.log(`# ${r.product.slug} ${r.product.name}`)
453
+ console.log(`id: ${r.product.id}`)
454
+ if (r.product.tagline) console.log(`> ${r.product.tagline}`)
455
+ if (r.product.url) console.log(`> ${r.product.url}`)
456
+ console.log(`\n## Brief\n`)
457
+ console.log(r.product.brief?.trim() || '(no brief set owner can add one at https://autark.kushalsm.com)')
458
+ console.log(`\n## Hypotheses (${r.hypotheses.length})\n`)
459
+ if (!r.hypotheses.length) {
460
+ console.log('(none yetcreate one with: autark hypothesis create --product-id <product_id> --md @hyp.md)')
461
+ } else {
462
+ for (const h of r.hypotheses) {
463
+ console.log(`- [${h.status}] ${h.code} id=${h.id} — ${h.title} (runs: ${h.run_count})`)
213
464
  }
214
- return
215
465
  }
466
+ }
216
467
 
217
- const result = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
468
+ function printHypothesisContext(result) {
218
469
  console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
470
+ console.log(`product_id: ${result.product.id}`)
471
+ console.log(`hypothesis_id: ${result.hypothesis.id}`)
219
472
  console.log(`Status: ${result.hypothesis.status}\n`)
220
473
  console.log(result.hypothesis.hypothesis_md)
221
474
  for (const run of result.runs) {
@@ -228,9 +481,7 @@ async function context(rest) {
228
481
  console.log(` [${a.channel}] ${a.title}${pointer ? ` → ${pointer}` : ''}`)
229
482
  }
230
483
  }
231
- if (run.narrative_md) {
232
- console.log(`\n${run.narrative_md}`)
233
- }
484
+ if (run.narrative_md) console.log(`\n${run.narrative_md}`)
234
485
  }
235
486
  }
236
487
 
@@ -265,14 +516,10 @@ function saveCredentials(creds) {
265
516
 
266
517
  function loadCredentials() {
267
518
  if (!fs.existsSync(CREDS_PATH)) return null
268
- try {
269
- return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'))
270
- } catch {
271
- return null
272
- }
519
+ try { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')) } catch { return null }
273
520
  }
274
521
 
275
- // ============================================================ arg parsing
522
+ // ============================================================ utilities
276
523
 
277
524
  function parseArgs(list) {
278
525
  const opts = { _: [] }
@@ -284,12 +531,11 @@ function parseArgs(list) {
284
531
  }
285
532
  const key = arg.slice(2)
286
533
  const next = list[i + 1]
287
- if (!next || next.startsWith('--')) {
288
- opts[key] = true
289
- } else {
290
- opts[key] = next
291
- i++
292
- }
534
+ const value = (!next || next.startsWith('--')) ? true : next
535
+ if (value !== true) i++
536
+ if (opts[key] === undefined) opts[key] = value
537
+ else if (Array.isArray(opts[key])) opts[key].push(value)
538
+ else opts[key] = [opts[key], value]
293
539
  }
294
540
  return opts
295
541
  }
@@ -302,12 +548,43 @@ function required(value, label) {
302
548
  }
303
549
 
304
550
  function readValue(value) {
305
- if (typeof value === 'string' && value.startsWith('@')) {
306
- return fs.readFileSync(value.slice(1), 'utf8')
307
- }
551
+ if (typeof value === 'string' && value.startsWith('@')) return fs.readFileSync(value.slice(1), 'utf8')
308
552
  return String(value)
309
553
  }
310
554
 
555
+ function listOpt(value, label) {
556
+ if (value === undefined || value === null || value === '') {
557
+ if (label) throw new Error(`${label} is required`)
558
+ return []
559
+ }
560
+ const values = Array.isArray(value) ? value : [value]
561
+ return values.flatMap(v => String(v).split(',')).map(v => v.trim()).filter(Boolean)
562
+ }
563
+
564
+ function queryString(params) {
565
+ const q = new URLSearchParams()
566
+ for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== '') q.set(k, v)
567
+ const s = q.toString()
568
+ return s ? `?${s}` : ''
569
+ }
570
+
571
+ function defaultInboxPrefix(email) {
572
+ if (!email) return ''
573
+ return String(email).split('@')[0].toLowerCase().replace(/[^a-z0-9._-]/g, '').replace(/^[._-]+/, '').slice(0, 30)
574
+ }
575
+
576
+ function compact(obj) {
577
+ const out = {}
578
+ for (const [k, v] of Object.entries(obj || {})) {
579
+ if (v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && !v.length)) out[k] = v
580
+ }
581
+ return out
582
+ }
583
+
584
+ function printJson(value) {
585
+ console.log(JSON.stringify(value, null, 2))
586
+ }
587
+
311
588
  function splitHypothesisRef(ref) {
312
589
  const m = String(ref).match(/^([\w.-]+)\/(H\d{2})$/)
313
590
  if (!m) throw new Error(`expected product/Hxx, got ${ref}`)
@@ -317,8 +594,26 @@ function splitHypothesisRef(ref) {
317
594
  // ============================================================ usage
318
595
 
319
596
  function loginUsage() {
320
- console.log(`autark login send <email>
321
- autark login verify <email> --code <6-digit-code>`)
597
+ console.log(`autark login send <email>\nautark login verify <email> --code <6-digit-code>`)
598
+ }
599
+
600
+ function mailUsage() {
601
+ console.log(`autark mail
602
+
603
+ setup --prefix <name> [--force]
604
+ send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>]
605
+ reply --message-id <id> --text @reply.txt [--run-id <id>]
606
+ reply-all --message-id <id> --text @reply.txt [--run-id <id>]
607
+ forward --message-id <id> --to <email> [--text @body.txt] [--run-id <id>]
608
+ threads [--limit N]
609
+ thread <thread_id>
610
+ messages [--limit N]
611
+ message <message_id>
612
+ raw <message_id>
613
+ attachment --message-id <id> --attachment-id <id> [--out file]
614
+ request <METHOD> <path> [--body @json] [--raw]
615
+
616
+ Mail uses the inbox-scoped token in ~/.autark/credentials.json.`)
322
617
  }
323
618
 
324
619
  function usage() {
@@ -330,17 +625,25 @@ function usage() {
330
625
  autark me show signed-in user
331
626
 
332
627
  autark product upsert --slug <slug> --name <name> [--url <url>] [--tagline <text>] [--brief @./brief.md] [--visibility private|public]
333
- autark product list
628
+ autark product list prints slug, visibility, id, name
334
629
 
335
- autark hypothesis create --product <slug> --code H01 --md @./hypothesis.md [--title <t>]
336
- autark hypothesis status <slug>/<H01> --status active|inactive|dead
630
+ autark hypothesis create --product-id <id> --md @./hyp.md [--code H01] [--title <t>]
631
+ autark hypothesis create --product <slug> --md @./hyp.md [--code H01] [--title <t>] # alias
632
+ autark hypothesis status --hypothesis-id <id> --status active|inactive|dead
633
+ autark hypothesis status <slug>/<H01> --status active|inactive|dead # alias
337
634
 
338
- autark run start --hypothesis <slug>/<H01>
339
- autark run finish --run <id> --narrative @./run.md
635
+ autark run start --hypothesis-id <id>
636
+ autark run start --hypothesis <slug>/<H01> # alias
637
+ autark run finish --run-id <id> --narrative @./run.md
340
638
 
341
- autark log action --run <id> --channel <c> --title <t> [--url <u>]
342
- [--agentmail-thread-id <uuid>] [--recipient <email>]
639
+ autark log action --run-id <id> --channel <c> --title <t> [--url <u>]
640
+ [--agentmail-thread-id <uuid>] [--agentmail-inbox-id <inbox>] [--recipient <email>]
343
641
  [--metadata @./meta.json]
344
642
 
643
+ autark mail setup/send/reply/thread/messages/... AgentMail via autark credentials
644
+
645
+ autark context --product-id <id>
646
+ autark context --hypothesis-id <id>
647
+ autark context <slug>
345
648
  autark context <slug>/<H01>`)
346
649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autark-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI for autark — hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",