autark-cli 0.5.6 → 0.5.8

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 +194 -8
  2. package/package.json +1 -1
package/autark.mjs CHANGED
@@ -25,7 +25,23 @@ const RAW_RUNTIME_BASE = process.env.AUTARK_RUNTIME_RAW_BASE
25
25
  || 'https://autark.sh/runtime'
26
26
  const SKILL_NAMES = ['autark', 'plumcake', 'chrome-relay', 'email', 'outreach', 'email-finder']
27
27
  const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
28
- const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
28
+ const PROGRAM_FILES = ['start.md', 'find_leads.md', 'double-down.md', 'check.md', 'followup.md']
29
+ const LEAD_TEMPLATE = {
30
+ person: {
31
+ full_name: 'Itay Rosen',
32
+ primary_email: 'itay@example.com',
33
+ email_status: 'guessed',
34
+ handles: {
35
+ github: 'itayrosen',
36
+ twitter: 'itayrosen',
37
+ },
38
+ headline: 'Maintainer of parallel-browser-mcp',
39
+ source: 'github',
40
+ },
41
+ lead: {
42
+ angle: 'Maintains a browser automation MCP project, so he is likely to care about real-browser access for agents.',
43
+ },
44
+ }
29
45
 
30
46
  // ============================================================ address reject
31
47
  // Two address shapes are bad enough to refuse at the CLI boundary, regardless
@@ -208,6 +224,11 @@ async function main() {
208
224
  if (command === 'unpause') return hypothesisPause(rest, 'active')
209
225
  if (!command || command === '--help' || command === '-h') return hypothesisUsage()
210
226
  }
227
+ if (group === 'lead') {
228
+ if (command === 'add') return leadAdd(rest)
229
+ if (command === 'template') return leadTemplate()
230
+ if (!command || command === 'help' || command === '--help' || command === '-h') return leadUsage()
231
+ }
211
232
  if (group === 'run') {
212
233
  if (command === 'start') return runStart(rest)
213
234
  if (command === 'finish') return runFinish(rest)
@@ -655,6 +676,69 @@ async function hypothesisStatus(rest) {
655
676
  console.log(`${label} → ${result.status}`)
656
677
  }
657
678
 
679
+ // =============================================================== v2 leads
680
+
681
+ async function leadAdd(rest) {
682
+ const opts = parseArgs(rest)
683
+ const payload = parseJsonValue(readValue(required(opts.input, '--input @lead.json')), '--input')
684
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('--input must be a JSON object')
685
+ if (!payload.lead || typeof payload.lead !== 'object' || !String(payload.lead.angle || '').trim()) {
686
+ throw new Error('lead.angle is required in --input JSON')
687
+ }
688
+
689
+ let hypId = opts['hypothesis-id'] || opts.hypothesis_id
690
+ if (!hypId) {
691
+ const ref = opts.hypothesis || opts._[0]
692
+ if (ref) {
693
+ const [productSlug, code] = splitHypothesisRef(ref)
694
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
695
+ hypId = ctx.hypothesis?.id
696
+ }
697
+ }
698
+ if (!hypId) throw new Error('pass --hypothesis-id <id> or --hypothesis <slug/Hxx>')
699
+
700
+ const body = {
701
+ ...payload,
702
+ source_run_id: opts['run-id'] || opts.run_id || opts.run || payload.source_run_id || undefined,
703
+ }
704
+ const result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/leads`, body)
705
+ console.log(JSON.stringify(result))
706
+ }
707
+
708
+ function leadTemplate() {
709
+ printJson(LEAD_TEMPLATE)
710
+ }
711
+
712
+ function leadUsage() {
713
+ console.log(`autark lead
714
+
715
+ template
716
+ print a minimal JSON payload for lead add
717
+
718
+ add --hypothesis-id <id> --input @/tmp/lead.json [--run-id <id>]
719
+ add --hypothesis <slug>/<H01> --input @/tmp/lead.json [--run-id <id>]
720
+
721
+ lead add records one sourced lead: person identity + the angle connecting that
722
+ person to the hypothesis. It sends one ID-first request to the worker and
723
+ prints exactly one compact JSON object to stdout:
724
+
725
+ {"person_id":"...","lead_id":"...","person_existed":false,"lead_existed":false}
726
+
727
+ Minimum payload:
728
+
729
+ ${JSON.stringify(LEAD_TEMPLATE, null, 2)}
730
+
731
+ Required:
732
+ - lead.angle
733
+ - enough person identity to dedupe: primary_email, a supported handle, or full_name
734
+
735
+ Optional person fields:
736
+ email_status, headline, source, bio, signals
737
+
738
+ find_leads should write /tmp/lead-N.json and call lead add with --input @file.
739
+ No outreach is sent and no touch rows are written.`)
740
+ }
741
+
658
742
  // =============================================================== runs
659
743
 
660
744
  async function runStart(rest) {
@@ -776,15 +860,31 @@ async function hypothesisPause(rest, status) {
776
860
  async function feedbackRecord(rest) {
777
861
  const opts = parseArgs(rest)
778
862
  const ref = opts.hypothesis || opts._[0]
863
+ const text = readValue(required(opts.text || opts.message || opts.body, '--text "<feedback>"'))
864
+
865
+ // --product-id (or a bare product slug with no /Hxx) → product-level feedback.
866
+ const productId = opts['product-id'] || opts.product_id
867
+ const isProductSlug = ref && !opts['hypothesis-id'] && !opts.hypothesis_id && !/\/H\d{2}$/.test(ref)
868
+ if (productId || isProductSlug) {
869
+ let pid = productId
870
+ if (!pid) {
871
+ const ctx = await api('GET', `/v1/context/${encodeURIComponent(ref)}`)
872
+ pid = ctx.product?.id
873
+ }
874
+ if (!pid) throw new Error('product not found')
875
+ const result = await api('POST', `/v1/products/${encodeURIComponent(pid)}/feedback`, { text })
876
+ console.log(result.id)
877
+ return
878
+ }
879
+
779
880
  let hypId = opts['hypothesis-id'] || opts.hypothesis_id
780
881
  if (!hypId) {
781
- if (!ref) throw new Error('pass product/Hxx or --hypothesis-id')
882
+ if (!ref) throw new Error('pass product, product/Hxx, --product-id, or --hypothesis-id')
782
883
  const [productSlug, code] = splitHypothesisRef(ref)
783
884
  const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
784
885
  hypId = ctx.hypothesis?.id
785
886
  }
786
887
  if (!hypId) throw new Error('hypothesis not found')
787
- const text = readValue(required(opts.text || opts.message || opts.body, '--text "<feedback>"'))
788
888
  const body = { text }
789
889
  if (opts['action-id'] || opts.action_id) body.action_id = opts['action-id'] || opts.action_id
790
890
  const result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/feedback`, body)
@@ -799,7 +899,8 @@ async function feedbackDelete(rest) {
799
899
  }
800
900
 
801
901
  function feedbackUsage() {
802
- console.log(`autark feedback record <product/Hxx> --text "<nudge>" [--action-id <id>]
902
+ console.log(`autark feedback record <product/Hxx> --text "<nudge>" [--action-id <id>] per-hypothesis nudge
903
+ autark feedback record <product> --text "<nudge>" product-level note (any hypothesis)
803
904
  autark feedback delete <feedback-id>`)
804
905
  }
805
906
 
@@ -886,7 +987,16 @@ async function mailLint(rest) {
886
987
  const compoundQ = /\b(are|is|do|does|did|will|would|can|could|should|have|has)\b[^?]{0,200}\bor\b[^?]{0,200}\?/i
887
988
  if (compoundQ.test(body)) violations.push({ rule: 'compound-question', detail: 'a/b question with "or"', why: WHY['compound-question'] })
888
989
  const sigSplit = body.split(/\n\s*\n(?:best|thanks|cheers|—|-)[\s,]*\n/i)[0] || body
889
- const len = sigSplit.trim().length
990
+ // Measure VISIBLE prose length, not raw markup. An <a href>/[text](url) signature
991
+ // anchor or any HTML wrapper must not eat into the 400-char content budget — only
992
+ // what the recipient actually reads counts.
993
+ const visibleText = sigSplit
994
+ .replace(/<a\b[^>]*>(.*?)<\/a>/gis, '$1') // <a ...>Kushal</a> -> Kushal
995
+ .replace(/\[([^\]]+)\]\(\s*https?:\/\/[^)]+\)/g, '$1') // [Kushal](url) -> Kushal
996
+ .replace(/<[^>]+>/g, '') // drop remaining html tags
997
+ .replace(/\s+/g, ' ') // collapse whitespace
998
+ .trim()
999
+ const len = visibleText.length
890
1000
  if (len > 400) violations.push({ rule: 'too-long', detail: `${len} chars`, why: WHY['too-long'] })
891
1001
  const hasMdAnchor = /\[[^\]]+\]\(https?:\/\/[^)]+\)/.test(body)
892
1002
  const hasHtmlAnchor = /<a\b[^>]*href\s*=\s*["']https?:\/\/[^"']+["'][^>]*>[^<]+<\/a>/i.test(body)
@@ -1188,6 +1298,17 @@ function printProductContext(r) {
1188
1298
  kv('product.url', r.product.url)
1189
1299
  kv('product.visibility', r.product.visibility)
1190
1300
  kv('product.hypothesis_count', (r.hypotheses || []).length)
1301
+ kv('product.person_count', r.product.person_count || (r.people || []).length || 0)
1302
+ kv('product.lead_count', r.product.lead_count || (r.leads || []).length || 0)
1303
+ kv('product.ready_lead_count', r.product.ready_lead_count || (r.leads || []).filter((l) => l.status === 'ready').length || 0)
1304
+ // Product-level operator feedback (newest first). Append-only + timestamped,
1305
+ // so each entry is a distinct nudge — re-read these every run.
1306
+ kv('product.feedback_count', (r.feedback || []).length)
1307
+ for (const f of (r.feedback || [])) {
1308
+ const shortF = String(f.id || '').slice(0, 8)
1309
+ kv(`product.feedback.${shortF}.created_at`, f.created_at)
1310
+ kv(`product.feedback.${shortF}.text`, f.text)
1311
+ }
1191
1312
  for (const h of (r.hypotheses || [])) {
1192
1313
  kv(`hypothesis.${h.code}.id`, h.id)
1193
1314
  kv(`hypothesis.${h.code}.title`, h.title)
@@ -1195,6 +1316,8 @@ function printProductContext(r) {
1195
1316
  kv(`hypothesis.${h.code}.run_count`, h.run_count)
1196
1317
  kv(`hypothesis.${h.code}.action_count`, h.action_count)
1197
1318
  kv(`hypothesis.${h.code}.reply_count`, h.reply_count)
1319
+ kv(`hypothesis.${h.code}.lead_count`, h.lead_count || (h.leads || []).length || 0)
1320
+ kv(`hypothesis.${h.code}.ready_lead_count`, h.ready_lead_count || (h.leads || []).filter((l) => l.status === 'ready').length || 0)
1198
1321
  if (h.needs_human_count) kv(`hypothesis.${h.code}.needs_human_count`, h.needs_human_count)
1199
1322
  const actions = (h.runs || []).flatMap((run) => run.actions || [])
1200
1323
  const by = channelBreakdown(actions)
@@ -1203,12 +1326,29 @@ function printProductContext(r) {
1203
1326
  kv(`hypothesis.${h.code}.channel.${ch}.replied`, by[ch].replied)
1204
1327
  }
1205
1328
  }
1329
+ for (const lead of (r.leads || [])) {
1330
+ const shortL = actionShortId(lead.id)
1331
+ kv(`lead.${shortL}.id`, lead.id)
1332
+ kv(`lead.${shortL}.status`, lead.status)
1333
+ kv(`lead.${shortL}.hypothesis_code`, lead.hypothesis_code)
1334
+ kv(`lead.${shortL}.person_name`, lead.person?.full_name)
1335
+ kv(`lead.${shortL}.person_email`, lead.person?.primary_email)
1336
+ kv(`lead.${shortL}.headline`, lead.person?.headline)
1337
+ kv(`lead.${shortL}.angle`, lead.angle)
1338
+ }
1206
1339
  console.log('---')
1207
1340
  // ---- narrative ----
1208
1341
  console.log(`# ${r.product.slug} — ${r.product.name}\n`)
1209
1342
  if (r.product.tagline) console.log(`> ${r.product.tagline}\n`)
1210
1343
  console.log(`## Brief\n`)
1211
1344
  console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.sh)')
1345
+ if ((r.feedback || []).length > 0) {
1346
+ console.log(`\n## Operator feedback (${r.feedback.length}) — product-level, newest first\n`)
1347
+ console.log(`These are general operator nudges, not tied to one hypothesis. Treat the most recent as the freshest signal and let them re-shape your next move.\n`)
1348
+ for (const f of r.feedback) {
1349
+ console.log(`- (${f.created_at}) ${f.text}`)
1350
+ }
1351
+ }
1212
1352
  if (!r.hypotheses?.length) {
1213
1353
  console.log(`\n## Hypotheses\n\n(none yet — create one with: autark hypothesis create --product-id <product_id> --md @hyp.md)`)
1214
1354
  } else {
@@ -1220,7 +1360,18 @@ function printProductContext(r) {
1220
1360
  .map((ch) => `${ch} ${by[ch].replied}/${by[ch].sent}`)
1221
1361
  .join(' ')
1222
1362
  const tail = channelSummary ? ` — replies/sends by channel: ${channelSummary}` : ''
1223
- console.log(`- [${h.status}] ${h.code} — ${h.title} (runs: ${h.run_count}, actions: ${h.action_count || 0}, replies: ${h.reply_count || 0})${tail}`)
1363
+ console.log(`- [${h.status}] ${h.code} — ${h.title} (runs: ${h.run_count}, actions: ${h.action_count || 0}, replies: ${h.reply_count || 0}, leads: ${h.lead_count || 0})${tail}`)
1364
+ }
1365
+ }
1366
+ if (r.leads?.length) {
1367
+ console.log(`\n## Recent leads (${r.leads.length})\n`)
1368
+ for (const lead of r.leads) {
1369
+ const person = lead.person || {}
1370
+ const label = person.full_name || person.primary_email || Object.entries(person.handles || {})[0]?.join(':') || lead.person_id || 'unknown person'
1371
+ const hyp = lead.hypothesis_code ? `${lead.hypothesis_code} ` : ''
1372
+ const headline = person.headline ? ` — ${person.headline}` : ''
1373
+ console.log(`- [${lead.status}] ${hyp}${label}${headline}`)
1374
+ if (lead.angle) console.log(` angle: ${lead.angle}`)
1224
1375
  }
1225
1376
  }
1226
1377
  }
@@ -1233,6 +1384,8 @@ function printHypothesisContext(result) {
1233
1384
  kv('hypothesis.code', result.hypothesis.code)
1234
1385
  kv('hypothesis.title', result.hypothesis.title)
1235
1386
  kv('hypothesis.status', result.hypothesis.status)
1387
+ kv('hypothesis.lead_count', result.hypothesis.lead_count || (result.leads || []).length || 0)
1388
+ kv('hypothesis.ready_lead_count', result.hypothesis.ready_lead_count || (result.leads || []).filter((l) => l.status === 'ready').length || 0)
1236
1389
  // Feedback — operator nudges left on this hypothesis, oldest first. The
1237
1390
  // worker returns them; previously the CLI silently dropped them, so agents
1238
1391
  // never saw the operator's running corrections. Now surface them up top
@@ -1256,9 +1409,11 @@ function printHypothesisContext(result) {
1256
1409
  }
1257
1410
  for (const run of (result.runs || [])) {
1258
1411
  const shortRun = actionShortId(run.id)
1412
+ const runLeads = (result.leads || []).filter((l) => l.source_run_id === run.id)
1259
1413
  kv(`run.${shortRun}.id`, run.id)
1260
1414
  kv(`run.${shortRun}.run_number`, run.run_number)
1261
1415
  kv(`run.${shortRun}.started_at`, run.started_at)
1416
+ kv(`run.${shortRun}.lead_count`, runLeads.length)
1262
1417
  if (run.finished_at) kv(`run.${shortRun}.finished_at`, run.finished_at)
1263
1418
  for (const a of (run.actions || [])) {
1264
1419
  const shortA = actionShortId(a.id)
@@ -1285,6 +1440,16 @@ function printHypothesisContext(result) {
1285
1440
  }
1286
1441
  }
1287
1442
  }
1443
+ for (const lead of (result.leads || [])) {
1444
+ const shortL = actionShortId(lead.id)
1445
+ kv(`lead.${shortL}.id`, lead.id)
1446
+ kv(`lead.${shortL}.status`, lead.status)
1447
+ kv(`lead.${shortL}.source_run_id`, lead.source_run_id)
1448
+ kv(`lead.${shortL}.person_name`, lead.person?.full_name)
1449
+ kv(`lead.${shortL}.person_email`, lead.person?.primary_email)
1450
+ kv(`lead.${shortL}.headline`, lead.person?.headline)
1451
+ kv(`lead.${shortL}.angle`, lead.angle)
1452
+ }
1288
1453
  console.log('---')
1289
1454
  // ---- narrative ----
1290
1455
  console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
@@ -1297,9 +1462,21 @@ function printHypothesisContext(result) {
1297
1462
  console.log(`- [${when}]${tag} ${f.text}`)
1298
1463
  }
1299
1464
  }
1465
+ if (result.leads?.length) {
1466
+ console.log(`\n\n## Leads (${result.leads.length})\n`)
1467
+ for (const lead of result.leads) {
1468
+ const person = lead.person || {}
1469
+ const label = person.full_name || person.primary_email || Object.entries(person.handles || {})[0]?.join(':') || lead.person_id || 'unknown person'
1470
+ const headline = person.headline ? ` — ${person.headline}` : ''
1471
+ const runTag = lead.source_run_id ? ` (run ${actionShortId(lead.source_run_id)})` : ''
1472
+ console.log(`- [${lead.status}] ${label}${headline}${runTag}`)
1473
+ if (lead.angle) console.log(` angle: ${lead.angle}`)
1474
+ }
1475
+ }
1300
1476
  for (const run of (result.runs || [])) {
1477
+ const runLeads = (result.leads || []).filter((l) => l.source_run_id === run.id)
1301
1478
  console.log(`\n\n## Run ${run.run_number} (${run.id})`)
1302
- console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}`)
1479
+ console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}${runLeads.length ? ` leads sourced: ${runLeads.length}` : ''}`)
1303
1480
  if (run.actions?.length) {
1304
1481
  console.log(`\nActions:`)
1305
1482
  for (const a of run.actions) {
@@ -1382,6 +1559,14 @@ function readValue(value) {
1382
1559
  return String(value)
1383
1560
  }
1384
1561
 
1562
+ function parseJsonValue(value, label = 'json') {
1563
+ try {
1564
+ return JSON.parse(value)
1565
+ } catch (e) {
1566
+ throw new Error(`${label} must be valid JSON: ${e.message}`)
1567
+ }
1568
+ }
1569
+
1385
1570
  function listOpt(value, label) {
1386
1571
  if (value === undefined || value === null || value === '') {
1387
1572
  if (label) throw new Error(`${label} is required`)
@@ -1468,10 +1653,11 @@ function usage() {
1468
1653
  product pause|unpause stop / resume cron on a product
1469
1654
  hypothesis create|status create or update hypotheses
1470
1655
  hypothesis pause|unpause stop / resume work on a hypothesis
1656
+ lead add|template record sourced v2 leads
1471
1657
  run start|finish start / finish a run
1472
1658
  log action record one outreach touch
1473
1659
  action escalate flag an action for human attention
1474
- feedback record|delete leave a free-text nudge on a hypothesis
1660
+ feedback record|delete leave a free-text nudge on a hypothesis or product
1475
1661
  context [<slug>|...] pull product or hypothesis context
1476
1662
 
1477
1663
  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.5.6",
3
+ "version": "0.5.8",
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",