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.
Files changed (2) hide show
  1. package/autark.mjs +26 -131
  2. 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
- // 2. em-dash
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; cold outreach gets one max` })
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" splits attention; ask one thing' })
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 of body; cold outreach should fit in ~400` })
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({ clean: false, length: len, violations }, null, 2))
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, anchor } })
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. Suppression
1445
- verbs proxy through the autark worker (org-scoped). --dry-run prints the
1446
- payload without hitting SES. lint is fully local — runs the outreach skill's
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autark-cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
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",