autark-cli 0.1.9 → 0.3.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 (2) hide show
  1. package/autark.mjs +228 -7
  2. package/package.json +3 -3
package/autark.mjs CHANGED
@@ -22,7 +22,7 @@ const CREDS_PATH = process.env.AUTARK_CREDENTIALS || path.join(AUTARK_HOME, 'cre
22
22
  // loop, so they need to be initialized at module-load time before the
23
23
  // main().catch() call below.
24
24
  const RAW_RUNTIME_BASE = process.env.AUTARK_RUNTIME_RAW_BASE
25
- || 'https://autark.kushalsm.com/runtime'
25
+ || 'https://autark.sh/runtime'
26
26
  const SKILL_NAMES = ['autark', 'plumcake', 'chrome-relay', 'email', 'outreach', 'email-finder']
27
27
  const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
28
28
  const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
@@ -163,13 +163,18 @@ async function main() {
163
163
  if (group === 'me') return me()
164
164
 
165
165
  if (group === 'product') {
166
- if (command === 'upsert') return productUpsert(rest)
167
- if (command === 'list') return productList()
166
+ if (command === 'upsert') return productUpsert(rest)
167
+ if (command === 'list') return productList()
168
+ if (command === 'pause') return productPause(rest, 'paused')
169
+ if (command === 'unpause') return productPause(rest, 'active')
170
+ if (command === 'run-status') return productPause(rest)
168
171
  if (!command || command === '--help' || command === '-h') return productUsage()
169
172
  }
170
173
  if (group === 'hypothesis') {
171
174
  if (command === 'create') return hypothesisCreate(rest)
172
175
  if (command === 'status') return hypothesisStatus(rest)
176
+ if (command === 'pause') return hypothesisPause(rest, 'inactive')
177
+ if (command === 'unpause') return hypothesisPause(rest, 'active')
173
178
  if (!command || command === '--help' || command === '-h') return hypothesisUsage()
174
179
  }
175
180
  if (group === 'run') {
@@ -181,6 +186,20 @@ async function main() {
181
186
  if (command === 'action') return logAction(rest)
182
187
  if (!command || command === '--help' || command === '-h') return logUsage()
183
188
  }
189
+ if (group === 'action') {
190
+ if (command === 'escalate') return actionEscalate(rest)
191
+ if (command === 'unescalate') return actionEscalate(rest, { clear: true })
192
+ if (!command || command === '--help' || command === '-h') return actionUsage()
193
+ }
194
+ if (group === 'feedback') {
195
+ if (command === 'record') return feedbackRecord(rest)
196
+ if (command === 'delete') return feedbackDelete(rest)
197
+ if (!command || command === '--help' || command === '-h') return feedbackUsage()
198
+ }
199
+ if (group === 'deliverability') {
200
+ if (command === 'check') return deliverabilityCheck(rest)
201
+ if (!command || command === '--help' || command === '-h') return deliverabilityUsage()
202
+ }
184
203
  if (group === 'context') {
185
204
  if (!command || command === '--help' || command === '-h') return contextUsage()
186
205
  return context([command, ...rest].filter(Boolean))
@@ -256,6 +275,9 @@ async function settingsShow(rest) {
256
275
  console.log(`agentmail_email ${fmt(s.agentmail_email)}`)
257
276
  console.log(`custom_domain ${fmt(s.custom_domain)}`)
258
277
  console.log(`domain_status ${fmt(s.custom_domain_status)}`)
278
+ if (Array.isArray(s.custom_domains) && s.custom_domains.length) {
279
+ console.log(`custom_domains ${s.custom_domains.map((d) => `${d.domain}:${d.status}`).join(', ')}`)
280
+ }
259
281
  }
260
282
 
261
283
  function settingsUsage() {
@@ -612,6 +634,20 @@ async function logAction(rest) {
612
634
  const run = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
613
635
  const channel = required(opts.channel, '--channel')
614
636
  const title = required(opts.title, '--title')
637
+ const needsHuman = Boolean(opts['needs-human'] || opts.needs_human || opts.escalate)
638
+ const escalationReason = opts['escalation-reason'] || opts.escalation_reason || opts.reason || undefined
639
+
640
+ // Merge `our_comment_id` into metadata if passed as a sugar flag. This is
641
+ // the per-channel anchor the reply-state cron uses to detect engagement on
642
+ // OUR specific comment (vs the parent we were replying to). Always pass
643
+ // it when you posted a comment somewhere we can re-fetch — github issue/PR
644
+ // comments, gists, HN replies, Substack comments, Reddit comments.
645
+ const meta = opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined
646
+ const ourCommentId = opts['our-comment-id'] || opts.our_comment_id
647
+ const metadata = ourCommentId
648
+ ? { ...(meta || {}), our_comment_id: String(ourCommentId) }
649
+ : meta
650
+
615
651
  const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
616
652
  channel,
617
653
  title,
@@ -619,12 +655,156 @@ async function logAction(rest) {
619
655
  agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
620
656
  agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
621
657
  recipient: opts.recipient || undefined,
622
- metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
658
+ metadata,
659
+ needs_human: needsHuman || undefined,
660
+ escalation_reason: needsHuman ? escalationReason : undefined,
623
661
  occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
624
662
  })
625
663
  console.log(result.id)
626
664
  }
627
665
 
666
+ // ============================================================ action escalate
667
+
668
+ async function actionEscalate(rest, { clear = false } = {}) {
669
+ const opts = parseArgs(rest)
670
+ const id = required(opts['action-id'] || opts.action_id || opts._[0], 'action id')
671
+ const reason = opts.reason || opts._.slice(1).join(' ') || undefined
672
+ const body = { needs_human: !clear }
673
+ if (!clear) body.reason = reason || ''
674
+ const result = await api('PATCH', `/v1/actions/${encodeURIComponent(id)}/escalate`, body)
675
+ console.log(`${result.id} needs_human=${result.needs_human}${result.escalation_reason ? ` reason="${result.escalation_reason}"` : ''}`)
676
+ }
677
+
678
+ function actionUsage() {
679
+ console.log(`autark action escalate <action-id> [--reason "<one-line why>"]
680
+ autark action unescalate <action-id>`)
681
+ }
682
+
683
+ // ============================================================ product pause
684
+
685
+ async function productPause(rest, fixedStatus) {
686
+ const opts = parseArgs(rest)
687
+ const slug = opts.slug || opts._[0]
688
+ let productId = opts['product-id'] || opts.product_id
689
+ if (!productId && slug) {
690
+ const list = await api('GET', '/v1/products')
691
+ const found = (list.products || []).find((p) => p.slug === slug)
692
+ if (!found) throw new Error(`product not found: ${slug}`)
693
+ productId = found.id
694
+ }
695
+ if (!productId) throw new Error('pass product slug or --product-id')
696
+ const status = fixedStatus || required(opts.status, '--status (active|paused)')
697
+ const result = await api('PATCH', `/v1/products/${encodeURIComponent(productId)}/run-status`, { run_status: status })
698
+ console.log(`${slug || productId} → run_status=${result.run_status}`)
699
+ }
700
+
701
+ // ============================================================ hypothesis pause
702
+ // Hypothesis-level pause is just a status flip. Convenience wrappers around the
703
+ // existing /hypotheses/:id/status endpoint so the CLI surface matches "pause".
704
+
705
+ async function hypothesisPause(rest, status) {
706
+ const opts = parseArgs(rest)
707
+ const ref = opts._[0] || opts.hypothesis
708
+ let hypId = opts['hypothesis-id'] || opts.hypothesis_id || opts.id
709
+ let label = hypId || ref
710
+ if (!hypId) {
711
+ if (!ref) throw new Error('pass product/Hxx or --hypothesis-id')
712
+ const [productSlug, code] = splitHypothesisRef(ref)
713
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
714
+ hypId = ctx.hypothesis?.id
715
+ label = ref
716
+ }
717
+ if (!hypId) throw new Error('hypothesis not found')
718
+ const result = await api('PATCH', `/v1/hypotheses/${encodeURIComponent(hypId)}/status`, { status })
719
+ console.log(`${label} → ${result.status}`)
720
+ }
721
+
722
+ // ============================================================ feedback
723
+
724
+ async function feedbackRecord(rest) {
725
+ const opts = parseArgs(rest)
726
+ const ref = opts.hypothesis || opts._[0]
727
+ let hypId = opts['hypothesis-id'] || opts.hypothesis_id
728
+ if (!hypId) {
729
+ if (!ref) throw new Error('pass product/Hxx or --hypothesis-id')
730
+ const [productSlug, code] = splitHypothesisRef(ref)
731
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
732
+ hypId = ctx.hypothesis?.id
733
+ }
734
+ if (!hypId) throw new Error('hypothesis not found')
735
+ const text = readValue(required(opts.text || opts.message || opts.body, '--text "<feedback>"'))
736
+ const body = { text }
737
+ if (opts['action-id'] || opts.action_id) body.action_id = opts['action-id'] || opts.action_id
738
+ const result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/feedback`, body)
739
+ console.log(result.id)
740
+ }
741
+
742
+ async function feedbackDelete(rest) {
743
+ const opts = parseArgs(rest)
744
+ const id = required(opts['feedback-id'] || opts.id || opts._[0], 'feedback id')
745
+ const result = await api('DELETE', `/v1/feedback/${encodeURIComponent(id)}`)
746
+ console.log(`${result.id} deleted`)
747
+ }
748
+
749
+ function feedbackUsage() {
750
+ console.log(`autark feedback record <product/Hxx> --text "<nudge>" [--action-id <id>]
751
+ autark feedback delete <feedback-id>`)
752
+ }
753
+
754
+ // ====================================================== deliverability
755
+
756
+ // Pre-send email validator. Calls AgentMail's recipient-check endpoint so the
757
+ // agent can avoid blasting a guessed/permutated address and inflating the
758
+ // bounce rate. Falls back to a syntactic check if the endpoint is unreachable.
759
+ async function deliverabilityCheck(rest) {
760
+ const opts = parseArgs(rest)
761
+ const email = required(opts.email || opts._[0], 'email')
762
+ const creds = loadCredentials() || {}
763
+ const token = creds.agentmail_token
764
+ let verdict = { email, deliverable: null, source: 'syntactic', reason: '' }
765
+
766
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
767
+ verdict = { ...verdict, deliverable: false, reason: 'malformed address' }
768
+ console.log(JSON.stringify(verdict, null, 2))
769
+ process.exitCode = 1
770
+ return
771
+ }
772
+ if (!token) {
773
+ verdict.reason = 'no agentmail token; syntactic check only'
774
+ console.log(JSON.stringify(verdict, null, 2))
775
+ return
776
+ }
777
+
778
+ try {
779
+ const res = await fetch('https://api.agentmail.to/v0/deliverability/check', {
780
+ method: 'POST',
781
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
782
+ body: JSON.stringify({ email }),
783
+ })
784
+ if (!res.ok) {
785
+ verdict.reason = `agentmail check ${res.status}; fell back to syntactic`
786
+ verdict.deliverable = null
787
+ } else {
788
+ const body = await res.json().catch(() => ({}))
789
+ verdict.source = 'agentmail'
790
+ verdict.deliverable = body.deliverable !== false
791
+ verdict.reason = body.reason || ''
792
+ if (body.risk) verdict.risk = body.risk
793
+ }
794
+ } catch (err) {
795
+ verdict.reason = `network error: ${err.message}`
796
+ }
797
+ console.log(JSON.stringify(verdict, null, 2))
798
+ if (verdict.deliverable === false) process.exitCode = 1
799
+ }
800
+
801
+ function deliverabilityUsage() {
802
+ console.log(`autark deliverability check <email>
803
+ Returns JSON {email, deliverable, source, reason}. Exits non-zero when the
804
+ address is known-undeliverable. Call this before logging an email action so
805
+ bounces don't tank your domain rep.`)
806
+ }
807
+
628
808
  // ============================================================ mail
629
809
 
630
810
  async function mail(command, rest) {
@@ -904,7 +1084,7 @@ function printProductContext(r) {
904
1084
  console.log(`# ${r.product.slug} — ${r.product.name}\n`)
905
1085
  if (r.product.tagline) console.log(`> ${r.product.tagline}\n`)
906
1086
  console.log(`## Brief\n`)
907
- console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.kushalsm.com)')
1087
+ console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.sh)')
908
1088
  if (!r.hypotheses?.length) {
909
1089
  console.log(`\n## Hypotheses\n\n(none yet — create one with: autark hypothesis create --product-id <product_id> --md @hyp.md)`)
910
1090
  } else {
@@ -1106,9 +1286,14 @@ function usage() {
1106
1286
  me show signed-in user
1107
1287
 
1108
1288
  product upsert|list create/edit/list products
1289
+ product pause|unpause stop / resume cron on a product
1109
1290
  hypothesis create|status create or update hypotheses
1291
+ hypothesis pause|unpause stop / resume work on a hypothesis
1110
1292
  run start|finish start / finish a run
1111
1293
  log action record one outreach touch
1294
+ action escalate flag an action for human attention
1295
+ feedback record|delete leave a free-text nudge on a hypothesis
1296
+ deliverability check pre-send check for an email address
1112
1297
  context [<slug>|...] pull product or hypothesis context
1113
1298
 
1114
1299
  mail setup|send|reply|... send/read mail via your AgentMail inbox
@@ -1126,6 +1311,9 @@ function productUsage() {
1126
1311
 
1127
1312
  list prints slug, visibility, id, name
1128
1313
 
1314
+ pause <slug> cron will skip this product on start ticks
1315
+ unpause <slug> resume cron
1316
+
1129
1317
  The brief is the markdown the agent reads at the top of every run
1130
1318
  to understand what the product is and who it might serve.`)
1131
1319
  }
@@ -1139,6 +1327,9 @@ function hypothesisUsage() {
1139
1327
  status --hypothesis-id <id> --status active|inactive|dead
1140
1328
  status <slug>/<H01> --status active|inactive|dead # slug/code alias
1141
1329
 
1330
+ pause <slug>/<H01> flip to inactive (cron skips it)
1331
+ unpause <slug>/<H01> flip back to active
1332
+
1142
1333
  Hypotheses are frozen on create. Only --status changes after.
1143
1334
  If --code is omitted, autark picks the next H## for the product.`)
1144
1335
  }
@@ -1162,10 +1353,40 @@ function logUsage() {
1162
1353
  [--agentmail-thread-id <uuid>] # email
1163
1354
  [--agentmail-inbox-id <email>] # email — defaults to your inbox
1164
1355
  [--recipient <email>] # email
1165
- [--metadata @./meta.json] # any channel-specific extras
1356
+ [--our-comment-id <id>] # see "Always capture our_comment_id" below
1357
+ [--metadata @./meta.json] # any other channel-specific extras
1358
+ [--escalate --reason "<why>"] # flag this action as needs_human
1166
1359
 
1167
1360
  One row per external touch. Body content stays in AgentMail/GitHub/etc;
1168
- this just records the pointer + the title agents see on the dashboard.`)
1361
+ this just records the pointer + the title agents see on the dashboard.
1362
+
1363
+ Log the URL of YOUR comment, not the parent
1364
+
1365
+ The reply-state cron parses the comment id out of the --url you log to
1366
+ detect "did anyone reply to you?". Most channels' URLs already contain
1367
+ the id; you just have to log the permalink of your OWN comment, not the
1368
+ issue/post URL you replied to.
1369
+
1370
+ github (issue or PR comment):
1371
+ use the response's html_url, ends in #issuecomment-<id>
1372
+ NEW_URL=$(gh api repos/<o>/<r>/issues/<n>/comments \\
1373
+ -X POST -f body=@body.md --jq .html_url)
1374
+
1375
+ gist:
1376
+ ends in #gistcomment-<id>
1377
+
1378
+ hn:
1379
+ .../item?id=<your_new_item_id> (your id, not the parent's)
1380
+
1381
+ reddit:
1382
+ .../r/<sub>/comments/<thread>/<title>/<your_comment_id>/
1383
+
1384
+ substack:
1385
+ Substack permalinks don't always include the comment id, so for
1386
+ this channel use --our-comment-id <id> from the response body.
1387
+
1388
+ Escape hatch: --our-comment-id <id> tucks the id into metadata. Use it
1389
+ when the URL pattern can't encode the comment (Substack, edge HN cases).`)
1169
1390
  }
1170
1391
 
1171
1392
  function contextUsage() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "autark-cli",
3
- "version": "0.1.9",
4
- "description": "CLI for autark hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
3
+ "version": "0.3.0",
4
+ "description": "CLI for autark \u2014 hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -19,4 +19,4 @@
19
19
  "engines": {
20
20
  "node": ">=18"
21
21
  }
22
- }
22
+ }