autark-cli 0.5.2 → 0.5.4
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 +102 -5
- package/package.json +1 -1
package/autark.mjs
CHANGED
|
@@ -320,7 +320,7 @@ function settingsUsage() {
|
|
|
320
320
|
// via pnpm. Re-exec autark if its own version moved.
|
|
321
321
|
// 1. GET /v1/me — compare runtime_version + agentmail_email against local.
|
|
322
322
|
// 2. If runtime_version differs: curl the canonical files from the repo,
|
|
323
|
-
// refresh the 6 kstack skills via pnpm dlx, reload
|
|
323
|
+
// refresh the 6 kstack skills via pnpm dlx, reload the local scheduler, stamp the
|
|
324
324
|
// new version into ~/.autark/runtime/version.txt.
|
|
325
325
|
// 3. If agentmail_email differs: POST /v1/inbox/rotate to re-mint a fresh
|
|
326
326
|
// inbox-scoped key, rewrite credentials.json.
|
|
@@ -413,8 +413,8 @@ async function update(rest) {
|
|
|
413
413
|
await refreshRuntimeFiles()
|
|
414
414
|
log('refreshing kstack skills…')
|
|
415
415
|
await refreshSkills()
|
|
416
|
-
log('reloading
|
|
417
|
-
await
|
|
416
|
+
log('reloading local scheduler…')
|
|
417
|
+
await reloadLocalScheduler()
|
|
418
418
|
// Only stamp the version AFTER every step succeeded.
|
|
419
419
|
writeLocalVersion(me.runtime_version)
|
|
420
420
|
sub(`stamped ~/.autark/runtime/version.txt → ${me.runtime_version}`)
|
|
@@ -512,6 +512,8 @@ async function refreshRuntimeFiles() {
|
|
|
512
512
|
const agentSh = path.join(AUTARK_HOME, 'agent.sh')
|
|
513
513
|
await curlFile(`${RAW_RUNTIME_BASE}/agent.sh`, agentSh)
|
|
514
514
|
fs.chmodSync(agentSh, 0o755)
|
|
515
|
+
const agentPs1 = path.join(AUTARK_HOME, 'agent.ps1')
|
|
516
|
+
await curlFile(`${RAW_RUNTIME_BASE}/agent.ps1`, agentPs1)
|
|
515
517
|
}
|
|
516
518
|
|
|
517
519
|
async function refreshSkills() {
|
|
@@ -547,6 +549,29 @@ async function reloadLaunchd() {
|
|
|
547
549
|
await spawnSync('launchctl', ['load', plist])
|
|
548
550
|
}
|
|
549
551
|
|
|
552
|
+
async function reloadWindowsTask() {
|
|
553
|
+
if (process.platform !== 'win32') return
|
|
554
|
+
const canonicalHome = path.join(os.homedir(), '.autark')
|
|
555
|
+
if (path.resolve(AUTARK_HOME) !== path.resolve(canonicalHome)) {
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
const agentPs1 = path.join(AUTARK_HOME, 'agent.ps1')
|
|
559
|
+
if (!fs.existsSync(agentPs1)) return
|
|
560
|
+
const taskName = 'Autark Runner'
|
|
561
|
+
const ps = [
|
|
562
|
+
`$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File "${agentPs1.replaceAll('"', '`"')}"'`,
|
|
563
|
+
`$morning = New-ScheduledTaskTrigger -Daily -At 9am`,
|
|
564
|
+
`$evening = New-ScheduledTaskTrigger -Daily -At 7pm`,
|
|
565
|
+
`Register-ScheduledTask -TaskName '${taskName}' -Action $action -Trigger @($morning, $evening) -Description 'Autark daily runner' -Force | Out-Null`,
|
|
566
|
+
].join('; ')
|
|
567
|
+
await spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps])
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function reloadLocalScheduler() {
|
|
571
|
+
await reloadLaunchd()
|
|
572
|
+
await reloadWindowsTask()
|
|
573
|
+
}
|
|
574
|
+
|
|
550
575
|
// Promise-wrapped spawn — returns { status: number }. stdio inherited so the
|
|
551
576
|
// user sees pnpm's output. Used for self-update + skills refresh.
|
|
552
577
|
function spawnSync(cmd, args) {
|
|
@@ -837,6 +862,7 @@ async function mailLint(rest) {
|
|
|
837
862
|
'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.',
|
|
838
863
|
'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.',
|
|
839
864
|
'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.',
|
|
865
|
+
'html-in-text': 'Layout HTML tags (<div>, <p>, <br>, <span>) in the body get sent through `autark mail send --text` as text/plain — the recipient sees the literal markup in their Gmail / Outlook (we have screenshots of this happening). For paragraph breaks use a blank line. For the signature link, the single <a href="...">name</a> is allowed (recipients see "name" rendered if the inbox does any html-detection, or the raw href if not — both fine).',
|
|
840
866
|
}
|
|
841
867
|
|
|
842
868
|
const violations = []
|
|
@@ -864,6 +890,14 @@ async function mailLint(rest) {
|
|
|
864
890
|
const hasMdAnchor = /\[[^\]]+\]\(https?:\/\/[^)]+\)/.test(body)
|
|
865
891
|
const hasHtmlAnchor = /<a\b[^>]*href\s*=\s*["']https?:\/\/[^"']+["'][^>]*>[^<]+<\/a>/i.test(body)
|
|
866
892
|
if (!hasMdAnchor && !hasHtmlAnchor) violations.push({ rule: 'no-anchor-sig', detail: 'no clickable name in signature', why: WHY['no-anchor-sig'] })
|
|
893
|
+
// Layout HTML tags in the body. Catches the autark-2026-05-28 footgun where
|
|
894
|
+
// an agent wrote <div>...</div><br> markup, passed it to `mail send --text`,
|
|
895
|
+
// and 6 recipients saw raw markup in their Gmail. The <a href> signature
|
|
896
|
+
// tag is whitelisted because no-anchor-sig depends on it.
|
|
897
|
+
const layoutTagMatch = body.match(/<\s*\/?\s*(div|p|br|span|hr|table|tr|td|th|font|b|i|u|strong|em)\b[^>]*>/gi) || []
|
|
898
|
+
for (const tag of layoutTagMatch) {
|
|
899
|
+
violations.push({ rule: 'html-in-text', detail: tag, why: WHY['html-in-text'] })
|
|
900
|
+
}
|
|
867
901
|
|
|
868
902
|
if (violations.length === 0) {
|
|
869
903
|
console.log(JSON.stringify({ clean: true, length: len }, null, 2))
|
|
@@ -923,6 +957,7 @@ async function mailSend(rest) {
|
|
|
923
957
|
subject,
|
|
924
958
|
response: result,
|
|
925
959
|
})
|
|
960
|
+
noteAutoLog(action, opts)
|
|
926
961
|
printJson({ ...result, autark_action_id: action?.id })
|
|
927
962
|
}
|
|
928
963
|
|
|
@@ -940,6 +975,7 @@ async function mailReply(rest, mode) {
|
|
|
940
975
|
response: result,
|
|
941
976
|
metadata: { message_id: messageId },
|
|
942
977
|
})
|
|
978
|
+
noteAutoLog(action, opts)
|
|
943
979
|
printJson({ ...result, autark_action_id: action?.id })
|
|
944
980
|
}
|
|
945
981
|
|
|
@@ -957,9 +993,20 @@ async function mailForward(rest) {
|
|
|
957
993
|
response: result,
|
|
958
994
|
metadata: { message_id: messageId },
|
|
959
995
|
})
|
|
996
|
+
noteAutoLog(action, opts)
|
|
960
997
|
printJson({ ...result, autark_action_id: action?.id })
|
|
961
998
|
}
|
|
962
999
|
|
|
1000
|
+
// Explicit human-readable cue so agents (and humans) see that the action
|
|
1001
|
+
// landed in autark and don't follow up with a redundant `autark log action`
|
|
1002
|
+
// for the same send. Stays on stderr so JSON consumers piping stdout aren't
|
|
1003
|
+
// affected. Only fires when --run-id was passed and the log succeeded.
|
|
1004
|
+
function noteAutoLog(action, opts) {
|
|
1005
|
+
if (!action?.id) return
|
|
1006
|
+
const runId = opts['run-id'] || opts.run_id || opts.run
|
|
1007
|
+
console.error(`autark: auto-logged action ${action.id} (run ${runId}) — do NOT run \`autark log action\` for this`)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
963
1010
|
async function mailThreads(rest) {
|
|
964
1011
|
const opts = parseArgs(rest)
|
|
965
1012
|
const creds = requireAgentmailCredentials()
|
|
@@ -1124,6 +1171,21 @@ function actionShortId(id) {
|
|
|
1124
1171
|
return String(id || '').slice(0, 8)
|
|
1125
1172
|
}
|
|
1126
1173
|
|
|
1174
|
+
// Aggregate sends + replies per channel. The cron writes inbound_count onto
|
|
1175
|
+
// each action; agents use this to score hypotheses without re-walking threads.
|
|
1176
|
+
// Channels are kept separate (email vs github vs reddit vs hn vs substack) —
|
|
1177
|
+
// a public comment reply is a different signal class than an email reply.
|
|
1178
|
+
function channelBreakdown(actions) {
|
|
1179
|
+
const by = {}
|
|
1180
|
+
for (const a of (actions || [])) {
|
|
1181
|
+
const ch = a.channel || 'unknown'
|
|
1182
|
+
if (!by[ch]) by[ch] = { sent: 0, replied: 0 }
|
|
1183
|
+
by[ch].sent += 1
|
|
1184
|
+
if ((a.inbound_count || 0) > 0) by[ch].replied += 1
|
|
1185
|
+
}
|
|
1186
|
+
return by
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1127
1189
|
function printProductContext(r) {
|
|
1128
1190
|
// ---- structured key-block ----
|
|
1129
1191
|
kv('product.id', r.product.id)
|
|
@@ -1138,6 +1200,15 @@ function printProductContext(r) {
|
|
|
1138
1200
|
kv(`hypothesis.${h.code}.title`, h.title)
|
|
1139
1201
|
kv(`hypothesis.${h.code}.status`, h.status)
|
|
1140
1202
|
kv(`hypothesis.${h.code}.run_count`, h.run_count)
|
|
1203
|
+
kv(`hypothesis.${h.code}.action_count`, h.action_count)
|
|
1204
|
+
kv(`hypothesis.${h.code}.reply_count`, h.reply_count)
|
|
1205
|
+
if (h.needs_human_count) kv(`hypothesis.${h.code}.needs_human_count`, h.needs_human_count)
|
|
1206
|
+
const actions = (h.runs || []).flatMap((run) => run.actions || [])
|
|
1207
|
+
const by = channelBreakdown(actions)
|
|
1208
|
+
for (const ch of Object.keys(by).sort()) {
|
|
1209
|
+
kv(`hypothesis.${h.code}.channel.${ch}.sent`, by[ch].sent)
|
|
1210
|
+
kv(`hypothesis.${h.code}.channel.${ch}.replied`, by[ch].replied)
|
|
1211
|
+
}
|
|
1141
1212
|
}
|
|
1142
1213
|
console.log('---')
|
|
1143
1214
|
// ---- narrative ----
|
|
@@ -1150,7 +1221,13 @@ function printProductContext(r) {
|
|
|
1150
1221
|
} else {
|
|
1151
1222
|
console.log(`\n## Hypotheses (${r.hypotheses.length})\n`)
|
|
1152
1223
|
for (const h of r.hypotheses) {
|
|
1153
|
-
|
|
1224
|
+
const actions = (h.runs || []).flatMap((run) => run.actions || [])
|
|
1225
|
+
const by = channelBreakdown(actions)
|
|
1226
|
+
const channelSummary = Object.keys(by).sort()
|
|
1227
|
+
.map((ch) => `${ch} ${by[ch].replied}/${by[ch].sent}`)
|
|
1228
|
+
.join(' ')
|
|
1229
|
+
const tail = channelSummary ? ` — replies/sends by channel: ${channelSummary}` : ''
|
|
1230
|
+
console.log(`- [${h.status}] ${h.code} — ${h.title} (runs: ${h.run_count}, actions: ${h.action_count || 0}, replies: ${h.reply_count || 0})${tail}`)
|
|
1154
1231
|
}
|
|
1155
1232
|
}
|
|
1156
1233
|
}
|
|
@@ -1173,6 +1250,17 @@ function printHypothesisContext(result) {
|
|
|
1173
1250
|
if (f.action_id) kv(`feedback.${shortF}.action_id`, f.action_id)
|
|
1174
1251
|
kv(`feedback.${shortF}.text`, f.text)
|
|
1175
1252
|
}
|
|
1253
|
+
// Per-channel rollup across all runs on this hypothesis. Lets the agent
|
|
1254
|
+
// grep `hypothesis.channel.*` to see what's actually landing without
|
|
1255
|
+
// counting actions itself.
|
|
1256
|
+
const allActions = (result.runs || []).flatMap((run) => run.actions || [])
|
|
1257
|
+
kv('hypothesis.action_count', allActions.length)
|
|
1258
|
+
kv('hypothesis.reply_count', allActions.filter((a) => (a.inbound_count || 0) > 0).length)
|
|
1259
|
+
const channelTotals = channelBreakdown(allActions)
|
|
1260
|
+
for (const ch of Object.keys(channelTotals).sort()) {
|
|
1261
|
+
kv(`hypothesis.channel.${ch}.sent`, channelTotals[ch].sent)
|
|
1262
|
+
kv(`hypothesis.channel.${ch}.replied`, channelTotals[ch].replied)
|
|
1263
|
+
}
|
|
1176
1264
|
for (const run of (result.runs || [])) {
|
|
1177
1265
|
const shortRun = actionShortId(run.id)
|
|
1178
1266
|
kv(`run.${shortRun}.id`, run.id)
|
|
@@ -1190,6 +1278,11 @@ function printHypothesisContext(result) {
|
|
|
1190
1278
|
if (a.agentmail_thread_id) kv(`action.${shortA}.agentmail_thread_id`, a.agentmail_thread_id)
|
|
1191
1279
|
if (a.agentmail_inbox_id) kv(`action.${shortA}.agentmail_inbox_id`, a.agentmail_inbox_id)
|
|
1192
1280
|
if (a.occurred_at) kv(`action.${shortA}.occurred_at`, a.occurred_at)
|
|
1281
|
+
kv(`action.${shortA}.inbound_count`, a.inbound_count || 0)
|
|
1282
|
+
if (a.last_inbound_at) kv(`action.${shortA}.last_inbound_at`, a.last_inbound_at)
|
|
1283
|
+
if (a.reply_checked_at) kv(`action.${shortA}.reply_checked_at`, a.reply_checked_at)
|
|
1284
|
+
if (a.needs_human) kv(`action.${shortA}.needs_human`, a.needs_human)
|
|
1285
|
+
if (a.escalation_reason) kv(`action.${shortA}.escalation_reason`, a.escalation_reason)
|
|
1193
1286
|
// Spread well-known metadata keys to their own lines for greppability.
|
|
1194
1287
|
const md = a.metadata || {}
|
|
1195
1288
|
for (const [mk, mv] of Object.entries(md)) {
|
|
@@ -1218,7 +1311,11 @@ function printHypothesisContext(result) {
|
|
|
1218
1311
|
console.log(`\nActions:`)
|
|
1219
1312
|
for (const a of run.actions) {
|
|
1220
1313
|
const pointer = a.url || a.agentmail_thread_id || a.recipient || ''
|
|
1221
|
-
|
|
1314
|
+
const flags = []
|
|
1315
|
+
if ((a.inbound_count || 0) > 0) flags.push(`replied×${a.inbound_count}`)
|
|
1316
|
+
if (a.needs_human) flags.push('needs_human')
|
|
1317
|
+
const flagStr = flags.length ? ` [${flags.join(', ')}]` : ''
|
|
1318
|
+
console.log(` [${a.channel}] ${a.title}${pointer ? ` → ${pointer}` : ''}${flagStr}`)
|
|
1222
1319
|
}
|
|
1223
1320
|
}
|
|
1224
1321
|
if (run.narrative_md) console.log(`\n${run.narrative_md}`)
|
package/package.json
CHANGED