autark-cli 0.1.9 → 0.2.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/autark.mjs +226 -5
- package/package.json +2 -2
package/autark.mjs
CHANGED
|
@@ -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')
|
|
167
|
-
if (command === 'list')
|
|
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
|
|
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) {
|
|
@@ -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
|
-
[--
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autark-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
}
|
|
22
|
-
}
|
|
22
|
+
}
|