autark-cli 0.1.8 → 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 +237 -13
- package/package.json +2 -2
package/autark.mjs
CHANGED
|
@@ -16,6 +16,17 @@ const AGENTMAIL_API = process.env.AGENTMAIL_API_URL || 'https://api.agentmail.to
|
|
|
16
16
|
const AUTARK_HOME = process.env.AUTARK_HOME || path.join(os.homedir(), '.autark')
|
|
17
17
|
const CREDS_PATH = process.env.AUTARK_CREDENTIALS || path.join(AUTARK_HOME, 'credentials.json')
|
|
18
18
|
|
|
19
|
+
// Constants used by `autark update` — hoisted before main() runs because
|
|
20
|
+
// async function bodies execute synchronously until their first await; main()
|
|
21
|
+
// dispatches to update() which references these in its first synchronous
|
|
22
|
+
// loop, so they need to be initialized at module-load time before the
|
|
23
|
+
// main().catch() call below.
|
|
24
|
+
const RAW_RUNTIME_BASE = process.env.AUTARK_RUNTIME_RAW_BASE
|
|
25
|
+
|| 'https://autark.kushalsm.com/runtime'
|
|
26
|
+
const SKILL_NAMES = ['autark', 'plumcake', 'chrome-relay', 'email', 'outreach', 'email-finder']
|
|
27
|
+
const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
|
|
28
|
+
const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
|
|
29
|
+
|
|
19
30
|
const args = process.argv.slice(2)
|
|
20
31
|
main()
|
|
21
32
|
.then(() => maybeNudge())
|
|
@@ -152,13 +163,18 @@ async function main() {
|
|
|
152
163
|
if (group === 'me') return me()
|
|
153
164
|
|
|
154
165
|
if (group === 'product') {
|
|
155
|
-
if (command === 'upsert')
|
|
156
|
-
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)
|
|
157
171
|
if (!command || command === '--help' || command === '-h') return productUsage()
|
|
158
172
|
}
|
|
159
173
|
if (group === 'hypothesis') {
|
|
160
174
|
if (command === 'create') return hypothesisCreate(rest)
|
|
161
175
|
if (command === 'status') return hypothesisStatus(rest)
|
|
176
|
+
if (command === 'pause') return hypothesisPause(rest, 'inactive')
|
|
177
|
+
if (command === 'unpause') return hypothesisPause(rest, 'active')
|
|
162
178
|
if (!command || command === '--help' || command === '-h') return hypothesisUsage()
|
|
163
179
|
}
|
|
164
180
|
if (group === 'run') {
|
|
@@ -170,6 +186,20 @@ async function main() {
|
|
|
170
186
|
if (command === 'action') return logAction(rest)
|
|
171
187
|
if (!command || command === '--help' || command === '-h') return logUsage()
|
|
172
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
|
+
}
|
|
173
203
|
if (group === 'context') {
|
|
174
204
|
if (!command || command === '--help' || command === '-h') return contextUsage()
|
|
175
205
|
return context([command, ...rest].filter(Boolean))
|
|
@@ -245,6 +275,9 @@ async function settingsShow(rest) {
|
|
|
245
275
|
console.log(`agentmail_email ${fmt(s.agentmail_email)}`)
|
|
246
276
|
console.log(`custom_domain ${fmt(s.custom_domain)}`)
|
|
247
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
|
+
}
|
|
248
281
|
}
|
|
249
282
|
|
|
250
283
|
function settingsUsage() {
|
|
@@ -272,14 +305,6 @@ function settingsUsage() {
|
|
|
272
305
|
// AUTARK_UPDATE_OFFLINE=1 skips the self-update step (for tests).
|
|
273
306
|
// AUTARK_HOME overrides the install root (for sandbox tests).
|
|
274
307
|
|
|
275
|
-
// The autark repo is private. Pages serves the canonical files at
|
|
276
|
-
// autark.kushalsm.com/runtime/* (synced from runtime/ at build time).
|
|
277
|
-
const RAW_RUNTIME_BASE = process.env.AUTARK_RUNTIME_RAW_BASE
|
|
278
|
-
|| 'https://autark.kushalsm.com/runtime'
|
|
279
|
-
const SKILL_NAMES = ['autark', 'plumcake', 'chrome-relay', 'email', 'outreach', 'email-finder']
|
|
280
|
-
const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
|
|
281
|
-
const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
|
|
282
|
-
|
|
283
308
|
async function update(rest) {
|
|
284
309
|
const opts = parseArgs(rest)
|
|
285
310
|
const dryRun = !!process.env.AUTARK_UPDATE_DRY_RUN || !!opts['dry-run']
|
|
@@ -609,6 +634,20 @@ async function logAction(rest) {
|
|
|
609
634
|
const run = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
|
|
610
635
|
const channel = required(opts.channel, '--channel')
|
|
611
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
|
+
|
|
612
651
|
const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
|
|
613
652
|
channel,
|
|
614
653
|
title,
|
|
@@ -616,12 +655,156 @@ async function logAction(rest) {
|
|
|
616
655
|
agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
|
|
617
656
|
agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
|
|
618
657
|
recipient: opts.recipient || undefined,
|
|
619
|
-
metadata
|
|
658
|
+
metadata,
|
|
659
|
+
needs_human: needsHuman || undefined,
|
|
660
|
+
escalation_reason: needsHuman ? escalationReason : undefined,
|
|
620
661
|
occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
|
|
621
662
|
})
|
|
622
663
|
console.log(result.id)
|
|
623
664
|
}
|
|
624
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
|
+
|
|
625
808
|
// ============================================================ mail
|
|
626
809
|
|
|
627
810
|
async function mail(command, rest) {
|
|
@@ -1103,9 +1286,14 @@ function usage() {
|
|
|
1103
1286
|
me show signed-in user
|
|
1104
1287
|
|
|
1105
1288
|
product upsert|list create/edit/list products
|
|
1289
|
+
product pause|unpause stop / resume cron on a product
|
|
1106
1290
|
hypothesis create|status create or update hypotheses
|
|
1291
|
+
hypothesis pause|unpause stop / resume work on a hypothesis
|
|
1107
1292
|
run start|finish start / finish a run
|
|
1108
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
|
|
1109
1297
|
context [<slug>|...] pull product or hypothesis context
|
|
1110
1298
|
|
|
1111
1299
|
mail setup|send|reply|... send/read mail via your AgentMail inbox
|
|
@@ -1123,6 +1311,9 @@ function productUsage() {
|
|
|
1123
1311
|
|
|
1124
1312
|
list prints slug, visibility, id, name
|
|
1125
1313
|
|
|
1314
|
+
pause <slug> cron will skip this product on start ticks
|
|
1315
|
+
unpause <slug> resume cron
|
|
1316
|
+
|
|
1126
1317
|
The brief is the markdown the agent reads at the top of every run
|
|
1127
1318
|
to understand what the product is and who it might serve.`)
|
|
1128
1319
|
}
|
|
@@ -1136,6 +1327,9 @@ function hypothesisUsage() {
|
|
|
1136
1327
|
status --hypothesis-id <id> --status active|inactive|dead
|
|
1137
1328
|
status <slug>/<H01> --status active|inactive|dead # slug/code alias
|
|
1138
1329
|
|
|
1330
|
+
pause <slug>/<H01> flip to inactive (cron skips it)
|
|
1331
|
+
unpause <slug>/<H01> flip back to active
|
|
1332
|
+
|
|
1139
1333
|
Hypotheses are frozen on create. Only --status changes after.
|
|
1140
1334
|
If --code is omitted, autark picks the next H## for the product.`)
|
|
1141
1335
|
}
|
|
@@ -1159,10 +1353,40 @@ function logUsage() {
|
|
|
1159
1353
|
[--agentmail-thread-id <uuid>] # email
|
|
1160
1354
|
[--agentmail-inbox-id <email>] # email — defaults to your inbox
|
|
1161
1355
|
[--recipient <email>] # email
|
|
1162
|
-
[--
|
|
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
|
|
1163
1359
|
|
|
1164
1360
|
One row per external touch. Body content stays in AgentMail/GitHub/etc;
|
|
1165
|
-
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).`)
|
|
1166
1390
|
}
|
|
1167
1391
|
|
|
1168
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
|
+
}
|