autark-cli 0.4.0 → 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 +77 -1
- package/package.json +1 -1
package/autark.mjs
CHANGED
|
@@ -859,10 +859,84 @@ async function mail(command, rest) {
|
|
|
859
859
|
if (command === 'suppress') return mailSuppress(rest)
|
|
860
860
|
if (command === 'unsuppress') return mailUnsuppress(rest)
|
|
861
861
|
if (command === 'suppressed') return mailSuppressed(rest)
|
|
862
|
+
if (command === 'lint') return mailLint(rest)
|
|
862
863
|
mailUsage()
|
|
863
864
|
process.exit(1)
|
|
864
865
|
}
|
|
865
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
|
+
|
|
866
940
|
async function mailSuppress(rest) {
|
|
867
941
|
const opts = parseArgs(rest)
|
|
868
942
|
const entry = required(opts.email || opts._[0], 'email')
|
|
@@ -1343,10 +1417,12 @@ function mailUsage() {
|
|
|
1343
1417
|
suppress <email> [--reason "<text>"] add to org-wide send-block list
|
|
1344
1418
|
unsuppress <email> remove from send-block list
|
|
1345
1419
|
suppressed [--limit N] list blocked addresses
|
|
1420
|
+
lint --body @draft.txt | <stdin> grade a draft for AI-tells; exit 1 on violations
|
|
1346
1421
|
|
|
1347
1422
|
Mail uses the inbox-scoped token in ~/.autark/credentials.json. Suppression
|
|
1348
1423
|
verbs proxy through the autark worker (org-scoped). --dry-run prints the
|
|
1349
|
-
payload without hitting SES
|
|
1424
|
+
payload without hitting SES. lint is fully local — runs the outreach skill's
|
|
1425
|
+
hard-fail checks against a draft.`)
|
|
1350
1426
|
}
|
|
1351
1427
|
|
|
1352
1428
|
function usage() {
|
package/package.json
CHANGED