autark-cli 0.4.2 → 0.5.1
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 +26 -131
- package/package.json +1 -1
package/autark.mjs
CHANGED
|
@@ -227,10 +227,6 @@ async function main() {
|
|
|
227
227
|
if (command === 'delete') return feedbackDelete(rest)
|
|
228
228
|
if (!command || command === '--help' || command === '-h') return feedbackUsage()
|
|
229
229
|
}
|
|
230
|
-
if (group === 'deliverability') {
|
|
231
|
-
if (command === 'check') return deliverabilityCheck(rest)
|
|
232
|
-
if (!command || command === '--help' || command === '-h') return deliverabilityUsage()
|
|
233
|
-
}
|
|
234
230
|
if (group === 'context') {
|
|
235
231
|
if (!command || command === '--help' || command === '-h') return contextUsage()
|
|
236
232
|
return context([command, ...rest].filter(Boolean))
|
|
@@ -782,60 +778,6 @@ function feedbackUsage() {
|
|
|
782
778
|
autark feedback delete <feedback-id>`)
|
|
783
779
|
}
|
|
784
780
|
|
|
785
|
-
// ====================================================== deliverability
|
|
786
|
-
|
|
787
|
-
// Pre-send email validator. Calls AgentMail's recipient-check endpoint so the
|
|
788
|
-
// agent can avoid blasting a guessed/permutated address and inflating the
|
|
789
|
-
// bounce rate. Falls back to a syntactic check if the endpoint is unreachable.
|
|
790
|
-
async function deliverabilityCheck(rest) {
|
|
791
|
-
const opts = parseArgs(rest)
|
|
792
|
-
const email = required(opts.email || opts._[0], 'email')
|
|
793
|
-
const creds = loadCredentials() || {}
|
|
794
|
-
const token = creds.agentmail_token
|
|
795
|
-
let verdict = { email, deliverable: null, source: 'syntactic', reason: '' }
|
|
796
|
-
|
|
797
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
798
|
-
verdict = { ...verdict, deliverable: false, reason: 'malformed address' }
|
|
799
|
-
console.log(JSON.stringify(verdict, null, 2))
|
|
800
|
-
process.exitCode = 1
|
|
801
|
-
return
|
|
802
|
-
}
|
|
803
|
-
if (!token) {
|
|
804
|
-
verdict.reason = 'no agentmail token; syntactic check only'
|
|
805
|
-
console.log(JSON.stringify(verdict, null, 2))
|
|
806
|
-
return
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
try {
|
|
810
|
-
const res = await fetch('https://api.agentmail.to/v0/deliverability/check', {
|
|
811
|
-
method: 'POST',
|
|
812
|
-
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
813
|
-
body: JSON.stringify({ email }),
|
|
814
|
-
})
|
|
815
|
-
if (!res.ok) {
|
|
816
|
-
verdict.reason = `agentmail check ${res.status}; fell back to syntactic`
|
|
817
|
-
verdict.deliverable = null
|
|
818
|
-
} else {
|
|
819
|
-
const body = await res.json().catch(() => ({}))
|
|
820
|
-
verdict.source = 'agentmail'
|
|
821
|
-
verdict.deliverable = body.deliverable !== false
|
|
822
|
-
verdict.reason = body.reason || ''
|
|
823
|
-
if (body.risk) verdict.risk = body.risk
|
|
824
|
-
}
|
|
825
|
-
} catch (err) {
|
|
826
|
-
verdict.reason = `network error: ${err.message}`
|
|
827
|
-
}
|
|
828
|
-
console.log(JSON.stringify(verdict, null, 2))
|
|
829
|
-
if (verdict.deliverable === false) process.exitCode = 1
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function deliverabilityUsage() {
|
|
833
|
-
console.log(`autark deliverability check <email>
|
|
834
|
-
Returns JSON {email, deliverable, source, reason}. Exits non-zero when the
|
|
835
|
-
address is known-undeliverable. Call this before logging an email action so
|
|
836
|
-
bounces don't tank your domain rep.`)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
781
|
// rejectUnsendableAddress is defined near the top of the file (before main()
|
|
840
782
|
// runs) so the const Sets it depends on are out of TDZ when mailSend invokes
|
|
841
783
|
// it. See block tagged "address reject" up top.
|
|
@@ -856,9 +798,6 @@ async function mail(command, rest) {
|
|
|
856
798
|
if (command === 'raw') return mailRaw(rest)
|
|
857
799
|
if (command === 'attachment') return mailAttachment(rest)
|
|
858
800
|
if (command === 'request') return mailRequest(rest)
|
|
859
|
-
if (command === 'suppress') return mailSuppress(rest)
|
|
860
|
-
if (command === 'unsuppress') return mailUnsuppress(rest)
|
|
861
|
-
if (command === 'suppressed') return mailSuppressed(rest)
|
|
862
801
|
if (command === 'lint') return mailLint(rest)
|
|
863
802
|
mailUsage()
|
|
864
803
|
process.exit(1)
|
|
@@ -892,72 +831,55 @@ async function mailLint(rest) {
|
|
|
892
831
|
'the key insight', 'what this means is', 'the answer is', 'the trick is',
|
|
893
832
|
'that\'s exactly', 'that\'s precisely', 'that\'s the whole point',
|
|
894
833
|
]
|
|
834
|
+
const WHY = {
|
|
835
|
+
'structure-word': 'AI-fingerprint vocabulary. Humans don\'t write "structurally different" or "fundamentally" in cold email; LLMs reach for them when hedging or sounding profound. Describe the thing directly instead.',
|
|
836
|
+
'em-dash': 'Em-dashes in cold outreach are a strong AI tell. Real people use commas, periods, or a new line break.',
|
|
837
|
+
'body-url': 'URLs mid-paragraph read like an A/B test variant or a campaign send. The product link belongs in the signature anchor; if it\'s anywhere else, the recipient classifies the email as automated.',
|
|
838
|
+
'too-many-questions': 'Asking >1 question is hedging. Pick the one question that, if answered, gives you the most signal. The rest can wait.',
|
|
839
|
+
'compound-question': 'Compound a/b questions ("are you X or Y...") read as AI hedging across alternatives. Humans ask one specific thing. Drop the "or" and pick the more specific half.',
|
|
840
|
+
'too-long': 'Cold outreach over 400 chars (~4-6 short lines) reads as a pitch deck, not a peer note. If you wrote more, you\'re explaining instead of asking. Cut to the single sharpest sentence.',
|
|
841
|
+
'no-anchor-sig': 'Without a clickable name in the signature, the recipient has to type your name into Google to figure out who you are. The HTML <a href> / markdown [name](url) sig is the single biggest "who is this person?" mitigator. Plain "Kushal" alone is a tell.',
|
|
842
|
+
}
|
|
895
843
|
|
|
896
844
|
const violations = []
|
|
897
845
|
const lower = body.toLowerCase()
|
|
898
846
|
|
|
899
|
-
// 1. structure-words
|
|
900
847
|
for (const w of STRUCTURE_WORDS) {
|
|
901
|
-
if (lower.includes(w)) violations.push({ rule: 'structure-word', detail: w })
|
|
848
|
+
if (lower.includes(w)) violations.push({ rule: 'structure-word', detail: w, why: WHY['structure-word'] })
|
|
902
849
|
}
|
|
903
|
-
|
|
904
|
-
if (/—/.test(body)) violations.push({ rule: 'em-dash', detail: 'use a period or comma instead' })
|
|
905
|
-
// 3. URLs in body (allow only inside markdown anchor [text](url) or HTML <a href>)
|
|
850
|
+
if (/—/.test(body)) violations.push({ rule: 'em-dash', detail: 'em-dash detected', why: WHY['em-dash'] })
|
|
906
851
|
const urlMatches = body.match(/https?:\/\/[^\s<>"')]+/g) || []
|
|
907
852
|
for (const u of urlMatches) {
|
|
908
853
|
const idx = body.indexOf(u)
|
|
909
854
|
const before = body.slice(Math.max(0, idx - 40), idx)
|
|
910
|
-
const after = body.slice(idx + u.length, idx + u.length + 10)
|
|
911
855
|
const insideMdAnchor = /\]\(\s*$/.test(before)
|
|
912
856
|
const insideHtmlAnchor = /href\s*=\s*["'][^"']*$/.test(before) || /<a\b[^>]*$/.test(before)
|
|
913
|
-
if (!insideMdAnchor && !insideHtmlAnchor) violations.push({ rule: 'body-url', detail: u })
|
|
857
|
+
if (!insideMdAnchor && !insideHtmlAnchor) violations.push({ rule: 'body-url', detail: u, why: WHY['body-url'] })
|
|
914
858
|
}
|
|
915
|
-
// 4. multiple question marks (more than one ? in the body)
|
|
916
859
|
const qmarks = (body.match(/\?/g) || []).length
|
|
917
|
-
if (qmarks > 1) violations.push({ rule: 'too-many-questions', detail: `${qmarks} question marks
|
|
918
|
-
// 5. compound question with "or" connecting clauses
|
|
860
|
+
if (qmarks > 1) violations.push({ rule: 'too-many-questions', detail: `${qmarks} question marks`, why: WHY['too-many-questions'] })
|
|
919
861
|
const compoundQ = /\b(are|is|do|does|did|will|would|can|could|should|have|has)\b[^?]{0,200}\bor\b[^?]{0,200}\?/i
|
|
920
|
-
if (compoundQ.test(body)) violations.push({ rule: 'compound-question', detail: 'a/b question with "or"
|
|
921
|
-
// 6. body length (excluding signature heuristically)
|
|
862
|
+
if (compoundQ.test(body)) violations.push({ rule: 'compound-question', detail: 'a/b question with "or"', why: WHY['compound-question'] })
|
|
922
863
|
const sigSplit = body.split(/\n\s*\n(?:best|thanks|cheers|—|-)[\s,]*\n/i)[0] || body
|
|
923
864
|
const len = sigSplit.trim().length
|
|
924
|
-
if (len > 400) violations.push({ rule: 'too-long', detail: `${len} chars
|
|
925
|
-
// 7. signature presence: should have `- <name>` and an anchor (md or html)
|
|
865
|
+
if (len > 400) violations.push({ rule: 'too-long', detail: `${len} chars`, why: WHY['too-long'] })
|
|
926
866
|
const hasMdAnchor = /\[[^\]]+\]\(https?:\/\/[^)]+\)/.test(body)
|
|
927
867
|
const hasHtmlAnchor = /<a\b[^>]*href\s*=\s*["']https?:\/\/[^"']+["'][^>]*>[^<]+<\/a>/i.test(body)
|
|
928
|
-
if (!hasMdAnchor && !hasHtmlAnchor) {
|
|
929
|
-
violations.push({ rule: 'no-anchor-sig', detail: 'signature must include a clickable link via [name](url) or <a href="url">name</a>' })
|
|
930
|
-
}
|
|
868
|
+
if (!hasMdAnchor && !hasHtmlAnchor) violations.push({ rule: 'no-anchor-sig', detail: 'no clickable name in signature', why: WHY['no-anchor-sig'] })
|
|
931
869
|
|
|
932
870
|
if (violations.length === 0) {
|
|
933
871
|
console.log(JSON.stringify({ clean: true, length: len }, null, 2))
|
|
934
872
|
return
|
|
935
873
|
}
|
|
936
|
-
console.error(JSON.stringify({
|
|
874
|
+
console.error(JSON.stringify({
|
|
875
|
+
clean: false,
|
|
876
|
+
length: len,
|
|
877
|
+
violations,
|
|
878
|
+
next: 'Rewrite the draft so each violation\'s "why" is internalized, not just regex-fixed. If you can\'t make it pass after 2 rewrites, abstain — re-read ~/.claude/skills/outreach/SKILL.md AI-tells section and ask whether you should send this at all.',
|
|
879
|
+
}, null, 2))
|
|
937
880
|
process.exit(1)
|
|
938
881
|
}
|
|
939
882
|
|
|
940
|
-
async function mailSuppress(rest) {
|
|
941
|
-
const opts = parseArgs(rest)
|
|
942
|
-
const entry = required(opts.email || opts._[0], 'email')
|
|
943
|
-
const reason = opts.reason || 'manual'
|
|
944
|
-
const result = await api('POST', '/v1/suppression', { entry, reason })
|
|
945
|
-
printJson(result)
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
async function mailUnsuppress(rest) {
|
|
949
|
-
const opts = parseArgs(rest)
|
|
950
|
-
const entry = required(opts.email || opts._[0], 'email')
|
|
951
|
-
const result = await api('DELETE', `/v1/suppression/${encodeURIComponent(entry)}`)
|
|
952
|
-
printJson(result)
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function mailSuppressed(rest) {
|
|
956
|
-
const opts = parseArgs(rest)
|
|
957
|
-
const limit = opts.limit ? Number(opts.limit) : 100
|
|
958
|
-
const result = await api('GET', `/v1/suppression?limit=${limit}`)
|
|
959
|
-
printJson(result)
|
|
960
|
-
}
|
|
961
883
|
|
|
962
884
|
async function mailSetup(rest) {
|
|
963
885
|
const opts = parseArgs(rest)
|
|
@@ -990,10 +912,9 @@ async function mailSend(rest) {
|
|
|
990
912
|
rejectUnsendableAddress(to)
|
|
991
913
|
const subject = opts.subject || ''
|
|
992
914
|
const body = mailBody(opts, { to, subject })
|
|
993
|
-
const anchor = readAnchorOpts(opts)
|
|
994
915
|
const dryRun = opts['dry-run'] === true || opts.dry_run === true
|
|
995
916
|
if (dryRun) {
|
|
996
|
-
printJson({ dry_run: true, would_send: { inbox: creds.inboxId, to, subject, body
|
|
917
|
+
printJson({ dry_run: true, would_send: { inbox: creds.inboxId, to, subject, body } })
|
|
997
918
|
return
|
|
998
919
|
}
|
|
999
920
|
const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/send`, body)
|
|
@@ -1003,30 +924,10 @@ async function mailSend(rest) {
|
|
|
1003
924
|
recipient: to.join(','),
|
|
1004
925
|
subject,
|
|
1005
926
|
response: result,
|
|
1006
|
-
metadata: anchor ? { anchor_quote: anchor.quote, anchor_url: anchor.url } : {},
|
|
1007
927
|
})
|
|
1008
928
|
printJson({ ...result, autark_action_id: action?.id })
|
|
1009
929
|
}
|
|
1010
930
|
|
|
1011
|
-
// Optional anchor-quote primitive. The cold-outreach skill recommends that
|
|
1012
|
-
// every send carry a verbatim quote from the recipient's public surface
|
|
1013
|
-
// (HN comment, GitHub issue, tweet) plus the URL it was lifted from. The
|
|
1014
|
-
// CLI doesn't (yet) enforce — when both are present they're validated and
|
|
1015
|
-
// logged into the action's metadata; when absent the send still goes
|
|
1016
|
-
// through. Flip to required once the prompt-side discipline catches up.
|
|
1017
|
-
function readAnchorOpts(opts) {
|
|
1018
|
-
let quote = opts['anchor-quote'] || opts.anchor_quote
|
|
1019
|
-
const url = opts['anchor-url'] || opts.anchor_url
|
|
1020
|
-
if (quote && String(quote).startsWith('@')) {
|
|
1021
|
-
quote = fs.readFileSync(String(quote).slice(1), 'utf8').trim()
|
|
1022
|
-
}
|
|
1023
|
-
if (!quote && !url) return null
|
|
1024
|
-
if (!quote || !url) throw new Error('--anchor-quote and --anchor-url must both be provided (or both omitted)')
|
|
1025
|
-
if (String(quote).length < 30) throw new Error(`--anchor-quote too short (${String(quote).length} chars); need >=30 chars of verbatim recipient text`)
|
|
1026
|
-
if (!/^https?:\/\//.test(String(url))) throw new Error(`--anchor-url must be a full URL, got: ${url}`)
|
|
1027
|
-
return { quote: String(quote).trim(), url: String(url).trim() }
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
931
|
async function mailReply(rest, mode) {
|
|
1031
932
|
const opts = parseArgs(rest)
|
|
1032
933
|
const creds = requireAgentmailCredentials()
|
|
@@ -1425,7 +1326,6 @@ function mailUsage() {
|
|
|
1425
1326
|
|
|
1426
1327
|
setup --prefix <name> [--force]
|
|
1427
1328
|
send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>] [--dry-run]
|
|
1428
|
-
[--anchor-quote @./q.txt --anchor-url <url>] record the recipient's verbatim public quote
|
|
1429
1329
|
reply --message-id <id> --text @reply.txt [--run-id <id>]
|
|
1430
1330
|
reply-all --message-id <id> --text @reply.txt [--run-id <id>]
|
|
1431
1331
|
forward --message-id <id> --to <email> [--text @body.txt] [--run-id <id>]
|
|
@@ -1436,15 +1336,11 @@ function mailUsage() {
|
|
|
1436
1336
|
raw <message_id>
|
|
1437
1337
|
attachment --message-id <id> --attachment-id <id> [--out file]
|
|
1438
1338
|
request <METHOD> <path> [--body @json] [--raw]
|
|
1439
|
-
suppress <email> [--reason "<text>"] add to org-wide send-block list
|
|
1440
|
-
unsuppress <email> remove from send-block list
|
|
1441
|
-
suppressed [--limit N] list blocked addresses
|
|
1442
1339
|
lint --body @draft.txt | <stdin> grade a draft for AI-tells; exit 1 on violations
|
|
1443
1340
|
|
|
1444
|
-
Mail uses the inbox-scoped token in ~/.autark/credentials.json.
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
hard-fail checks against a draft.`)
|
|
1341
|
+
Mail uses the inbox-scoped token in ~/.autark/credentials.json. --dry-run
|
|
1342
|
+
prints the payload without hitting SES. lint is fully local — runs the
|
|
1343
|
+
outreach skill's hard-fail checks against a draft.`)
|
|
1448
1344
|
}
|
|
1449
1345
|
|
|
1450
1346
|
function usage() {
|
|
@@ -1462,7 +1358,6 @@ function usage() {
|
|
|
1462
1358
|
log action record one outreach touch
|
|
1463
1359
|
action escalate flag an action for human attention
|
|
1464
1360
|
feedback record|delete leave a free-text nudge on a hypothesis
|
|
1465
|
-
deliverability check pre-send check for an email address
|
|
1466
1361
|
context [<slug>|...] pull product or hypothesis context
|
|
1467
1362
|
|
|
1468
1363
|
mail setup|send|reply|... send/read mail via your AgentMail inbox
|
package/package.json
CHANGED