autark-cli 0.3.2 → 0.4.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 +109 -3
- package/package.json +1 -1
package/autark.mjs
CHANGED
|
@@ -856,10 +856,109 @@ async function mail(command, rest) {
|
|
|
856
856
|
if (command === 'raw') return mailRaw(rest)
|
|
857
857
|
if (command === 'attachment') return mailAttachment(rest)
|
|
858
858
|
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
|
+
if (command === 'lint') return mailLint(rest)
|
|
859
863
|
mailUsage()
|
|
860
864
|
process.exit(1)
|
|
861
865
|
}
|
|
862
866
|
|
|
867
|
+
// Read a draft from stdin or a @file path and grade against the AI-tells
|
|
868
|
+
// rules defined in the outreach skill. Exits 0 on clean, 1 on violations.
|
|
869
|
+
// The lint rules are intentionally aggressive and slightly opinionated —
|
|
870
|
+
// the goal is to make agents abstain on slop, not to nitpick humans.
|
|
871
|
+
async function mailLint(rest) {
|
|
872
|
+
const opts = parseArgs(rest)
|
|
873
|
+
let body = ''
|
|
874
|
+
if (opts.body && String(opts.body).startsWith('@')) {
|
|
875
|
+
body = fs.readFileSync(String(opts.body).slice(1), 'utf8')
|
|
876
|
+
} else if (opts._?.[0] && String(opts._[0]).startsWith('@')) {
|
|
877
|
+
body = fs.readFileSync(String(opts._[0]).slice(1), 'utf8')
|
|
878
|
+
} else if (!process.stdin.isTTY) {
|
|
879
|
+
body = await new Promise((resolve) => {
|
|
880
|
+
const chunks = []
|
|
881
|
+
process.stdin.on('data', (c) => chunks.push(c))
|
|
882
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
883
|
+
})
|
|
884
|
+
} else {
|
|
885
|
+
console.error('autark mail lint: pass a draft via --body @./draft.txt or pipe on stdin')
|
|
886
|
+
process.exit(2)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const STRUCTURE_WORDS = [
|
|
890
|
+
'structurally', 'structurally different', 'fundamentally', 'specifically',
|
|
891
|
+
'essentially', 'actually', 'notably',
|
|
892
|
+
'the key insight', 'what this means is', 'the answer is', 'the trick is',
|
|
893
|
+
'that\'s exactly', 'that\'s precisely', 'that\'s the whole point',
|
|
894
|
+
]
|
|
895
|
+
|
|
896
|
+
const violations = []
|
|
897
|
+
const lower = body.toLowerCase()
|
|
898
|
+
|
|
899
|
+
// 1. structure-words
|
|
900
|
+
for (const w of STRUCTURE_WORDS) {
|
|
901
|
+
if (lower.includes(w)) violations.push({ rule: 'structure-word', detail: w })
|
|
902
|
+
}
|
|
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>)
|
|
906
|
+
const urlMatches = body.match(/https?:\/\/[^\s<>"')]+/g) || []
|
|
907
|
+
for (const u of urlMatches) {
|
|
908
|
+
const idx = body.indexOf(u)
|
|
909
|
+
const before = body.slice(Math.max(0, idx - 40), idx)
|
|
910
|
+
const after = body.slice(idx + u.length, idx + u.length + 10)
|
|
911
|
+
const insideMdAnchor = /\]\(\s*$/.test(before)
|
|
912
|
+
const insideHtmlAnchor = /href\s*=\s*["'][^"']*$/.test(before) || /<a\b[^>]*$/.test(before)
|
|
913
|
+
if (!insideMdAnchor && !insideHtmlAnchor) violations.push({ rule: 'body-url', detail: u })
|
|
914
|
+
}
|
|
915
|
+
// 4. multiple question marks (more than one ? in the body)
|
|
916
|
+
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
|
|
919
|
+
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)
|
|
922
|
+
const sigSplit = body.split(/\n\s*\n(?:best|thanks|cheers|—|-)[\s,]*\n/i)[0] || body
|
|
923
|
+
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)
|
|
926
|
+
const hasMdAnchor = /\[[^\]]+\]\(https?:\/\/[^)]+\)/.test(body)
|
|
927
|
+
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
|
+
}
|
|
931
|
+
|
|
932
|
+
if (violations.length === 0) {
|
|
933
|
+
console.log(JSON.stringify({ clean: true, length: len }, null, 2))
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
console.error(JSON.stringify({ clean: false, length: len, violations }, null, 2))
|
|
937
|
+
process.exit(1)
|
|
938
|
+
}
|
|
939
|
+
|
|
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
|
+
|
|
863
962
|
async function mailSetup(rest) {
|
|
864
963
|
const opts = parseArgs(rest)
|
|
865
964
|
const existing = loadCredentials() || {}
|
|
@@ -1304,7 +1403,7 @@ function mailUsage() {
|
|
|
1304
1403
|
console.log(`autark mail
|
|
1305
1404
|
|
|
1306
1405
|
setup --prefix <name> [--force]
|
|
1307
|
-
send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>]
|
|
1406
|
+
send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>] [--dry-run]
|
|
1308
1407
|
reply --message-id <id> --text @reply.txt [--run-id <id>]
|
|
1309
1408
|
reply-all --message-id <id> --text @reply.txt [--run-id <id>]
|
|
1310
1409
|
forward --message-id <id> --to <email> [--text @body.txt] [--run-id <id>]
|
|
@@ -1315,8 +1414,15 @@ function mailUsage() {
|
|
|
1315
1414
|
raw <message_id>
|
|
1316
1415
|
attachment --message-id <id> --attachment-id <id> [--out file]
|
|
1317
1416
|
request <METHOD> <path> [--body @json] [--raw]
|
|
1318
|
-
|
|
1319
|
-
|
|
1417
|
+
suppress <email> [--reason "<text>"] add to org-wide send-block list
|
|
1418
|
+
unsuppress <email> remove from send-block list
|
|
1419
|
+
suppressed [--limit N] list blocked addresses
|
|
1420
|
+
lint --body @draft.txt | <stdin> grade a draft for AI-tells; exit 1 on violations
|
|
1421
|
+
|
|
1422
|
+
Mail uses the inbox-scoped token in ~/.autark/credentials.json. Suppression
|
|
1423
|
+
verbs proxy through the autark worker (org-scoped). --dry-run prints the
|
|
1424
|
+
payload without hitting SES. lint is fully local — runs the outreach skill's
|
|
1425
|
+
hard-fail checks against a draft.`)
|
|
1320
1426
|
}
|
|
1321
1427
|
|
|
1322
1428
|
function usage() {
|
package/package.json
CHANGED