autark-cli 0.1.4 → 0.1.6

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 +27 -21
  2. package/autark.mjs +377 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,47 +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
22
 
23
23
  Autark is ID-first. Use `product list` / `context` to discover IDs, then pass IDs for writes. `slug/H01` remains a convenience alias.
24
24
 
25
25
  ```sh
26
- # products
27
26
  autark product upsert --slug chrome-relay --name "Chrome Relay" --tagline "..."
28
27
  autark product list # prints slug, visibility, id, name
29
28
  autark context chrome-relay
30
29
 
31
- autark context --product-id <product_id>
32
-
33
- # hypotheses
34
30
  autark hypothesis create --product-id <product_id> --md @./H01.md --code H01 --title "..."
35
- autark context --hypothesis-id <hypothesis_id>
36
- autark hypothesis status --hypothesis-id <hypothesis_id> --status active # active|inactive|dead
37
-
38
- # aliases still work
39
- autark hypothesis create --product chrome-relay --md @./H01.md --code H01
40
- autark context chrome-relay/H01
41
- autark hypothesis status chrome-relay/H01 --status inactive
42
-
43
- # runs
44
31
  autark run start --hypothesis-id <hypothesis_id>
32
+ autark log action --run-id <run_id> --channel github --title "..." --url https://github.com/...
45
33
  autark run finish --run-id <run_id> --narrative @./narrative.md
34
+ ```
46
35
 
47
- # actions (anything the agent did during a run)
48
- autark log action --run-id <run_id> --channel email --title "..." \
49
- --recipient person@example.com --agentmail-thread-id <thread_id>
50
- autark log action --run-id <run_id> --channel github --title "..." --url https://github.com/...
36
+ ## Mail use
51
37
 
52
- autark me
53
- autark logout
38
+ `autark mail` covers the AgentMail surface Autark needs. It does not require a separate AgentMail CLI login.
39
+
40
+ ```sh
41
+ autark mail setup --prefix laksh
42
+
43
+ autark mail send --run-id <run_id> \
44
+ --to person@example.com \
45
+ --subject "Subject" \
46
+ --text @./body.txt
47
+
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
54
56
  ```
55
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
+
56
60
  ## ENV
57
61
 
58
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
59
65
 
60
66
  ## Architecture
61
67
 
62
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.
63
69
 
64
- The web at autark.kushalsm.com reads from InstantDB. Writes should go through the Worker/CLI path; dashboard direct writes are being phased out.
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,11 +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
- // The CLI is ID-first: once a product/hypothesis/run exists, use its id.
8
- // slug/Hxx remains as a convenience alias for humans.
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.
9
7
 
10
8
  import fs from 'node:fs'
11
9
  import os from 'node:os'
@@ -13,6 +11,7 @@ import path from 'node:path'
13
11
  import process from 'node:process'
14
12
 
15
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'
16
15
  const CREDS_PATH = path.join(os.homedir(), '.autark', 'credentials.json')
17
16
 
18
17
  const args = process.argv.slice(2)
@@ -36,17 +35,27 @@ async function main() {
36
35
  if (group === 'product') {
37
36
  if (command === 'upsert') return productUpsert(rest)
38
37
  if (command === 'list') return productList()
38
+ if (!command || command === '--help' || command === '-h') return productUsage()
39
39
  }
40
40
  if (group === 'hypothesis') {
41
41
  if (command === 'create') return hypothesisCreate(rest)
42
42
  if (command === 'status') return hypothesisStatus(rest)
43
+ if (!command || command === '--help' || command === '-h') return hypothesisUsage()
43
44
  }
44
45
  if (group === 'run') {
45
46
  if (command === 'start') return runStart(rest)
46
47
  if (command === 'finish') return runFinish(rest)
48
+ if (!command || command === '--help' || command === '-h') return runUsage()
49
+ }
50
+ if (group === 'log') {
51
+ if (command === 'action') return logAction(rest)
52
+ if (!command || command === '--help' || command === '-h') return logUsage()
47
53
  }
48
- if (group === 'log' && command === 'action') return logAction(rest)
49
- if (group === 'context') return context([command, ...rest].filter(Boolean))
54
+ if (group === 'context') {
55
+ if (!command || command === '--help' || command === '-h') return contextUsage()
56
+ return context([command, ...rest].filter(Boolean))
57
+ }
58
+ if (group === 'mail') return mail(command, rest)
50
59
 
51
60
  usage()
52
61
  process.exit(1)
@@ -69,6 +78,7 @@ async function loginVerify(rest) {
69
78
  const result = await api('POST', '/v1/auth/login/verify', { email, code }, { auth: false })
70
79
  if (!result.token) throw new Error('verify succeeded but no token returned')
71
80
  saveCredentials({
81
+ ...loadCredentials(),
72
82
  user_id: result.user.id,
73
83
  email: result.user.email,
74
84
  token: result.token,
@@ -194,15 +204,235 @@ async function logAction(rest) {
194
204
  const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
195
205
  channel,
196
206
  title,
197
- url: opts.url || undefined,
198
- agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
199
- recipient: opts.recipient || undefined,
200
- metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
201
- occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
207
+ url: opts.url || undefined,
208
+ agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
209
+ agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
210
+ recipient: opts.recipient || undefined,
211
+ metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
212
+ occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
202
213
  })
203
214
  console.log(result.id)
204
215
  }
205
216
 
217
+ // ============================================================ mail
218
+
219
+ async function mail(command, rest) {
220
+ if (!command || command === '--help' || command === 'help') return mailUsage()
221
+ if (command === 'setup') return mailSetup(rest)
222
+ if (command === 'send') return mailSend(rest)
223
+ if (command === 'reply') return mailReply(rest, 'reply')
224
+ if (command === 'reply-all') return mailReply(rest, 'reply-all')
225
+ if (command === 'forward') return mailForward(rest)
226
+ if (command === 'threads') return mailThreads(rest)
227
+ if (command === 'thread') return mailThread(rest)
228
+ if (command === 'messages') return mailMessages(rest)
229
+ if (command === 'message') return mailMessage(rest)
230
+ if (command === 'raw') return mailRaw(rest)
231
+ if (command === 'attachment') return mailAttachment(rest)
232
+ if (command === 'request') return mailRequest(rest)
233
+ mailUsage()
234
+ process.exit(1)
235
+ }
236
+
237
+ async function mailSetup(rest) {
238
+ const opts = parseArgs(rest)
239
+ const existing = loadCredentials() || {}
240
+ if (existing.agentmail_token && existing.agentmail_email && !opts.force) {
241
+ console.log(existing.agentmail_email)
242
+ return
243
+ }
244
+ const prefix = required(opts.prefix || defaultInboxPrefix(existing.email), '--prefix')
245
+ const result = await api('POST', '/v1/onboard/agentmail', {
246
+ prefix,
247
+ display_name: opts['display-name'] || opts.display_name || existing.email || prefix,
248
+ purpose: opts.purpose || 'runner',
249
+ })
250
+ saveCredentials({
251
+ ...existing,
252
+ agentmail_email: result.agentmail_email || result.email,
253
+ agentmail_inbox_id: result.agentmail_inbox_id || result.inbox_id || result.email,
254
+ agentmail_api_key_id: result.agentmail_api_key_id || result.api_key_id,
255
+ agentmail_token: result.agentmail_token,
256
+ agentmail_saved_at: new Date().toISOString(),
257
+ })
258
+ console.log(result.agentmail_email || result.email)
259
+ }
260
+
261
+ async function mailSend(rest) {
262
+ const opts = parseArgs(rest)
263
+ const creds = requireAgentmailCredentials()
264
+ const to = listOpt(opts.to, '--to')
265
+ const subject = opts.subject || ''
266
+ const body = mailBody(opts, { to, subject })
267
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/send`, body)
268
+ const action = await maybeLogMailAction(opts, {
269
+ kind: 'send',
270
+ defaultTitle: opts.title || `Email: ${subject || to.join(', ')}`,
271
+ recipient: to.join(','),
272
+ subject,
273
+ response: result,
274
+ })
275
+ printJson({ ...result, autark_action_id: action?.id })
276
+ }
277
+
278
+ async function mailReply(rest, mode) {
279
+ const opts = parseArgs(rest)
280
+ const creds = requireAgentmailCredentials()
281
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
282
+ const body = mailBody(opts)
283
+ const endpoint = mode === 'reply-all' ? 'reply-all' : 'reply'
284
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/${endpoint}`, body)
285
+ const action = await maybeLogMailAction(opts, {
286
+ kind: mode,
287
+ defaultTitle: opts.title || `${mode === 'reply-all' ? 'Reply-all' : 'Reply'}: ${messageId}`,
288
+ recipient: listOpt(opts.to).join(','),
289
+ response: result,
290
+ metadata: { message_id: messageId },
291
+ })
292
+ printJson({ ...result, autark_action_id: action?.id })
293
+ }
294
+
295
+ async function mailForward(rest) {
296
+ const opts = parseArgs(rest)
297
+ const creds = requireAgentmailCredentials()
298
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
299
+ const to = listOpt(opts.to, '--to')
300
+ const body = mailBody(opts, { to, subject: opts.subject || '' })
301
+ const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/forward`, body)
302
+ const action = await maybeLogMailAction(opts, {
303
+ kind: 'forward',
304
+ defaultTitle: opts.title || `Forward: ${opts.subject || messageId}`,
305
+ recipient: to.join(','),
306
+ response: result,
307
+ metadata: { message_id: messageId },
308
+ })
309
+ printJson({ ...result, autark_action_id: action?.id })
310
+ }
311
+
312
+ async function mailThreads(rest) {
313
+ const opts = parseArgs(rest)
314
+ const creds = requireAgentmailCredentials()
315
+ const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
316
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads${qs}`))
317
+ }
318
+
319
+ async function mailThread(rest) {
320
+ const opts = parseArgs(rest)
321
+ const creds = requireAgentmailCredentials()
322
+ const threadId = required(opts['thread-id'] || opts.thread_id || opts._[0], 'thread_id')
323
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads/${encodeURIComponent(threadId)}`))
324
+ }
325
+
326
+ async function mailMessages(rest) {
327
+ const opts = parseArgs(rest)
328
+ const creds = requireAgentmailCredentials()
329
+ const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
330
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages${qs}`))
331
+ }
332
+
333
+ async function mailMessage(rest) {
334
+ const opts = parseArgs(rest)
335
+ const creds = requireAgentmailCredentials()
336
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
337
+ printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}`))
338
+ }
339
+
340
+ async function mailRaw(rest) {
341
+ const opts = parseArgs(rest)
342
+ const creds = requireAgentmailCredentials()
343
+ const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
344
+ const raw = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/raw`, undefined, { parseJson: false })
345
+ process.stdout.write(raw)
346
+ }
347
+
348
+ async function mailAttachment(rest) {
349
+ const opts = parseArgs(rest)
350
+ const creds = requireAgentmailCredentials()
351
+ const messageId = required(opts['message-id'] || opts.message_id, '--message-id')
352
+ const attachmentId = required(opts['attachment-id'] || opts.attachment_id, '--attachment-id')
353
+ const body = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`, undefined, { parseJson: false })
354
+ if (opts.out) fs.writeFileSync(opts.out, body)
355
+ else process.stdout.write(body)
356
+ }
357
+
358
+ async function mailRequest(rest) {
359
+ const opts = parseArgs(rest)
360
+ const method = (opts.method || opts._[0] || 'GET').toUpperCase()
361
+ const requestPath = required(opts.path || opts._[1], '--path')
362
+ const body = opts.body ? JSON.parse(readValue(opts.body)) : undefined
363
+ const parseJson = opts.raw ? false : true
364
+ const result = await agentmailRequest(method, requestPath.startsWith('/') ? requestPath : `/${requestPath}`, body, { parseJson })
365
+ if (parseJson) printJson(result)
366
+ else process.stdout.write(result)
367
+ }
368
+
369
+ function mailBody(opts, base = {}) {
370
+ const body = { ...base }
371
+ if (opts.text !== undefined) body.text = readValue(opts.text)
372
+ if (opts.html !== undefined) body.html = readValue(opts.html)
373
+ if (opts.subject !== undefined) body.subject = opts.subject
374
+ for (const key of ['cc', 'bcc', 'reply-to', 'label', 'attachment']) {
375
+ const snake = key.replaceAll('-', '_')
376
+ const value = opts[key] ?? opts[snake]
377
+ if (value !== undefined) body[snake] = listOpt(value)
378
+ }
379
+ if (opts.headers !== undefined) body.headers = JSON.parse(readValue(opts.headers))
380
+ return body
381
+ }
382
+
383
+ async function maybeLogMailAction(opts, { kind, defaultTitle, recipient, subject, response, metadata = {} }) {
384
+ const runId = opts['run-id'] || opts.run_id || opts.run
385
+ if (!runId) return null
386
+ const creds = requireAgentmailCredentials()
387
+ const threadId = response?.thread_id
388
+ if (!threadId) throw new Error('AgentMail response missing thread_id; refusing to log email action')
389
+ return api('POST', `/v1/runs/${encodeURIComponent(runId)}/actions`, {
390
+ channel: 'email',
391
+ title: opts.title || defaultTitle,
392
+ recipient: recipient || undefined,
393
+ agentmail_thread_id: threadId,
394
+ agentmail_inbox_id: creds.inboxId,
395
+ metadata: compact({
396
+ kind,
397
+ subject,
398
+ message_id: response?.message_id,
399
+ ...metadata,
400
+ }),
401
+ })
402
+ }
403
+
404
+ async function agentmailRequest(method, pathStr, body, { parseJson = true } = {}) {
405
+ const creds = requireAgentmailCredentials()
406
+ const res = await fetch(AGENTMAIL_API + pathStr, {
407
+ method,
408
+ headers: {
409
+ authorization: `Bearer ${creds.token}`,
410
+ 'content-type': 'application/json',
411
+ },
412
+ body: body === undefined ? undefined : JSON.stringify(body),
413
+ })
414
+ const text = await res.text()
415
+ let data = text
416
+ if (parseJson) {
417
+ try { data = text ? JSON.parse(text) : null } catch { data = { raw: text } }
418
+ }
419
+ if (!res.ok) {
420
+ const msg = parseJson ? (data?.error || data?.message || JSON.stringify(data)) : text
421
+ throw new Error(`AgentMail ${res.status}: ${msg}`)
422
+ }
423
+ return data
424
+ }
425
+
426
+ function requireAgentmailCredentials() {
427
+ const creds = loadCredentials()
428
+ const token = process.env.AGENTMAIL_API_KEY || creds?.agentmail_token
429
+ const email = process.env.AGENTMAIL_EMAIL || creds?.agentmail_email
430
+ const inboxId = process.env.AGENTMAIL_INBOX_ID || creds?.agentmail_inbox_id || email
431
+ if (!token) throw new Error('missing agentmail_token. run: autark mail setup --prefix <name>')
432
+ if (!inboxId) throw new Error('missing agentmail_inbox_id. run: autark mail setup --prefix <name>')
433
+ return { token, email, inboxId }
434
+ }
435
+
206
436
  // ============================================================ context
207
437
 
208
438
  async function context(rest) {
@@ -223,12 +453,8 @@ async function context(rest) {
223
453
  const productSlug = parts[0]
224
454
  const code = parts[1]
225
455
 
226
- if (!code) {
227
- return printProductContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}`))
228
- }
229
-
230
- const result = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
231
- return printHypothesisContext(result)
456
+ if (!code) return printProductContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}`))
457
+ return printHypothesisContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`))
232
458
  }
233
459
 
234
460
  function printProductContext(r) {
@@ -264,9 +490,7 @@ function printHypothesisContext(result) {
264
490
  console.log(` [${a.channel}] ${a.title}${pointer ? ` → ${pointer}` : ''}`)
265
491
  }
266
492
  }
267
- if (run.narrative_md) {
268
- console.log(`\n${run.narrative_md}`)
269
- }
493
+ if (run.narrative_md) console.log(`\n${run.narrative_md}`)
270
494
  }
271
495
  }
272
496
 
@@ -301,14 +525,10 @@ function saveCredentials(creds) {
301
525
 
302
526
  function loadCredentials() {
303
527
  if (!fs.existsSync(CREDS_PATH)) return null
304
- try {
305
- return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'))
306
- } catch {
307
- return null
308
- }
528
+ try { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')) } catch { return null }
309
529
  }
310
530
 
311
- // ============================================================ arg parsing
531
+ // ============================================================ utilities
312
532
 
313
533
  function parseArgs(list) {
314
534
  const opts = { _: [] }
@@ -320,12 +540,11 @@ function parseArgs(list) {
320
540
  }
321
541
  const key = arg.slice(2)
322
542
  const next = list[i + 1]
323
- if (!next || next.startsWith('--')) {
324
- opts[key] = true
325
- } else {
326
- opts[key] = next
327
- i++
328
- }
543
+ const value = (!next || next.startsWith('--')) ? true : next
544
+ if (value !== true) i++
545
+ if (opts[key] === undefined) opts[key] = value
546
+ else if (Array.isArray(opts[key])) opts[key].push(value)
547
+ else opts[key] = [opts[key], value]
329
548
  }
330
549
  return opts
331
550
  }
@@ -338,12 +557,43 @@ function required(value, label) {
338
557
  }
339
558
 
340
559
  function readValue(value) {
341
- if (typeof value === 'string' && value.startsWith('@')) {
342
- return fs.readFileSync(value.slice(1), 'utf8')
343
- }
560
+ if (typeof value === 'string' && value.startsWith('@')) return fs.readFileSync(value.slice(1), 'utf8')
344
561
  return String(value)
345
562
  }
346
563
 
564
+ function listOpt(value, label) {
565
+ if (value === undefined || value === null || value === '') {
566
+ if (label) throw new Error(`${label} is required`)
567
+ return []
568
+ }
569
+ const values = Array.isArray(value) ? value : [value]
570
+ return values.flatMap(v => String(v).split(',')).map(v => v.trim()).filter(Boolean)
571
+ }
572
+
573
+ function queryString(params) {
574
+ const q = new URLSearchParams()
575
+ for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== '') q.set(k, v)
576
+ const s = q.toString()
577
+ return s ? `?${s}` : ''
578
+ }
579
+
580
+ function defaultInboxPrefix(email) {
581
+ if (!email) return ''
582
+ return String(email).split('@')[0].toLowerCase().replace(/[^a-z0-9._-]/g, '').replace(/^[._-]+/, '').slice(0, 30)
583
+ }
584
+
585
+ function compact(obj) {
586
+ const out = {}
587
+ for (const [k, v] of Object.entries(obj || {})) {
588
+ if (v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && !v.length)) out[k] = v
589
+ }
590
+ return out
591
+ }
592
+
593
+ function printJson(value) {
594
+ console.log(JSON.stringify(value, null, 2))
595
+ }
596
+
347
597
  function splitHypothesisRef(ref) {
348
598
  const m = String(ref).match(/^([\w.-]+)\/(H\d{2})$/)
349
599
  if (!m) throw new Error(`expected product/Hxx, got ${ref}`)
@@ -353,36 +603,105 @@ function splitHypothesisRef(ref) {
353
603
  // ============================================================ usage
354
604
 
355
605
  function loginUsage() {
356
- console.log(`autark login send <email>
357
- autark login verify <email> --code <6-digit-code>`)
606
+ console.log(`autark login send <email>\nautark login verify <email> --code <6-digit-code>`)
607
+ }
608
+
609
+ function mailUsage() {
610
+ console.log(`autark mail
611
+
612
+ setup --prefix <name> [--force]
613
+ send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>]
614
+ reply --message-id <id> --text @reply.txt [--run-id <id>]
615
+ reply-all --message-id <id> --text @reply.txt [--run-id <id>]
616
+ forward --message-id <id> --to <email> [--text @body.txt] [--run-id <id>]
617
+ threads [--limit N]
618
+ thread <thread_id>
619
+ messages [--limit N]
620
+ message <message_id>
621
+ raw <message_id>
622
+ attachment --message-id <id> --attachment-id <id> [--out file]
623
+ request <METHOD> <path> [--body @json] [--raw]
624
+
625
+ Mail uses the inbox-scoped token in ~/.autark/credentials.json.`)
358
626
  }
359
627
 
360
628
  function usage() {
361
629
  console.log(`autark — agent runbook CLI
362
630
 
363
- autark login send <email> send a magic code
364
- autark login verify <email> --code <code> complete login
365
- autark logout clear local credentials
366
- autark me show signed-in user
631
+ login send|verify sign in / complete magic code
632
+ logout clear local credentials
633
+ me show signed-in user
634
+
635
+ product upsert|list create/edit/list products
636
+ hypothesis create|status create or update hypotheses
637
+ run start|finish start / finish a run
638
+ log action record one outreach touch
639
+ context [<slug>|...] pull product or hypothesis context
640
+
641
+ mail setup|send|reply|... send/read mail via your AgentMail inbox
367
642
 
368
- autark product upsert --slug <slug> --name <name> [--url <url>] [--tagline <text>] [--brief @./brief.md] [--visibility private|public]
369
- autark product list prints slug, visibility, id, name
643
+ For details on any group: autark <group> --help
644
+ Examples: autark product --help
645
+ autark mail --help`)
646
+ }
647
+
648
+ function productUsage() {
649
+ console.log(`autark product
650
+
651
+ upsert --slug <slug> --name <name> [--url <url>] [--tagline <text>]
652
+ [--brief @./brief.md] [--visibility private|public]
653
+
654
+ list prints slug, visibility, id, name
655
+
656
+ The brief is the markdown the agent reads at the top of every run
657
+ to understand what the product is and who it might serve.`)
658
+ }
370
659
 
371
- autark hypothesis create --product-id <id> --md @./hyp.md [--code H01] [--title <t>]
372
- autark hypothesis create --product <slug> --md @./hyp.md [--code H01] [--title <t>] # alias
373
- autark hypothesis status --hypothesis-id <id> --status active|inactive|dead
374
- autark hypothesis status <slug>/<H01> --status active|inactive|dead # alias
660
+ function hypothesisUsage() {
661
+ console.log(`autark hypothesis
662
+
663
+ create --product-id <id> --md @./hyp.md [--code H01] [--title <t>] [--status active|inactive|dead]
664
+ create --product <slug> --md @./hyp.md [--code H01] [--title <t>] # slug alias
665
+
666
+ status --hypothesis-id <id> --status active|inactive|dead
667
+ status <slug>/<H01> --status active|inactive|dead # slug/code alias
668
+
669
+ Hypotheses are frozen on create. Only --status changes after.
670
+ If --code is omitted, autark picks the next H## for the product.`)
671
+ }
672
+
673
+ function runUsage() {
674
+ console.log(`autark run
675
+
676
+ start --hypothesis-id <id>
677
+ start --hypothesis <slug>/<H01> # slug/code alias
678
+ → prints RUN_ID to stdout
679
+
680
+ finish --run-id <id> --narrative @./run.md
681
+ → seals the run with the narrative blob`)
682
+ }
683
+
684
+ function logUsage() {
685
+ console.log(`autark log
686
+
687
+ action --run-id <id> --channel <c> --title <t>
688
+ [--url <u>] # github / reddit / hn / blog / gist / linkedin
689
+ [--agentmail-thread-id <uuid>] # email
690
+ [--agentmail-inbox-id <email>] # email — defaults to your inbox
691
+ [--recipient <email>] # email
692
+ [--metadata @./meta.json] # any channel-specific extras
693
+
694
+ One row per external touch. Body content stays in AgentMail/GitHub/etc;
695
+ this just records the pointer + the title agents see on the dashboard.`)
696
+ }
375
697
 
376
- autark run start --hypothesis-id <id>
377
- autark run start --hypothesis <slug>/<H01> # alias
378
- autark run finish --run-id <id> --narrative @./run.md
698
+ function contextUsage() {
699
+ console.log(`autark context
379
700
 
380
- autark log action --run-id <id> --channel <c> --title <t> [--url <u>]
381
- [--agentmail-thread-id <uuid>] [--recipient <email>]
382
- [--metadata @./meta.json]
701
+ <slug> product brief + every hypothesis (active/inactive/dead)
702
+ <slug>/<H01> one hypothesis: text + every run + every action
703
+ --product-id <id> product brief + every hypothesis
704
+ --hypothesis-id <id> one hypothesis: text + every run + every action
383
705
 
384
- autark context --product-id <id>
385
- autark context --hypothesis-id <id>
386
- autark context <slug>
387
- autark context <slug>/<H01>`)
706
+ Use product-level first to pick which H## to drill into.`)
388
707
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autark-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",