autark-cli 0.5.7 → 0.7.0
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/README.md +2 -2
- package/autark.mjs +309 -124
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CLI for [autark](https://autark.sh) — hypothesis-driven product runbooks.
|
|
4
4
|
|
|
5
|
-
A product has hypotheses. Each hypothesis
|
|
5
|
+
A product has hypotheses. Each hypothesis frames leads — deduped people with a concrete angle, sourced by runs. The dashboard shows it all.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -29,7 +29,7 @@ autark context chrome-relay
|
|
|
29
29
|
|
|
30
30
|
autark hypothesis create --product-id <product_id> --md @./H01.md --code H01 --title "..."
|
|
31
31
|
autark run start --hypothesis-id <hypothesis_id>
|
|
32
|
-
autark
|
|
32
|
+
autark lead add --hypothesis-id <hypothesis_id> --run-id <run_id> --input @/tmp/lead.json
|
|
33
33
|
autark run finish --run-id <run_id> --narrative @./narrative.md
|
|
34
34
|
```
|
|
35
35
|
|
package/autark.mjs
CHANGED
|
@@ -24,8 +24,25 @@ const CREDS_PATH = process.env.AUTARK_CREDENTIALS || path.join(AUTARK_HOME, 'cre
|
|
|
24
24
|
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
|
+
const SKILL_AGENTS = ['claude-code', 'codex', 'pi', 'opencode']
|
|
27
28
|
const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
|
|
28
|
-
const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
|
|
29
|
+
const PROGRAM_FILES = ['start.md', 'find_leads.md', 'double-down.md', 'check.md', 'followup.md']
|
|
30
|
+
const LEAD_TEMPLATE = {
|
|
31
|
+
person: {
|
|
32
|
+
full_name: 'Itay Rosen',
|
|
33
|
+
primary_email: 'itay@example.com',
|
|
34
|
+
email_status: 'guessed',
|
|
35
|
+
handles: {
|
|
36
|
+
github: 'itayrosen',
|
|
37
|
+
twitter: 'itayrosen',
|
|
38
|
+
},
|
|
39
|
+
headline: 'Maintainer of parallel-browser-mcp',
|
|
40
|
+
source: 'github',
|
|
41
|
+
},
|
|
42
|
+
lead: {
|
|
43
|
+
angle: 'Maintains a browser automation MCP project, so he is likely to care about real-browser access for agents.',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
// ============================================================ address reject
|
|
31
48
|
// Two address shapes are bad enough to refuse at the CLI boundary, regardless
|
|
@@ -208,20 +225,25 @@ async function main() {
|
|
|
208
225
|
if (command === 'unpause') return hypothesisPause(rest, 'active')
|
|
209
226
|
if (!command || command === '--help' || command === '-h') return hypothesisUsage()
|
|
210
227
|
}
|
|
228
|
+
if (group === 'lead') {
|
|
229
|
+
if (command === 'add') return leadAdd(rest)
|
|
230
|
+
if (command === 'list') return leadList(rest)
|
|
231
|
+
if (command === 'show') return leadShow(rest)
|
|
232
|
+
if (command === 'status') return leadStatus(rest)
|
|
233
|
+
if (command === 'template') return leadTemplate()
|
|
234
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') return leadUsage()
|
|
235
|
+
}
|
|
236
|
+
if (group === 'touch') {
|
|
237
|
+
if (command === 'add') return touchAdd(rest)
|
|
238
|
+
if (command === 'mute') return touchMute(rest, true)
|
|
239
|
+
if (command === 'unmute') return touchMute(rest, false)
|
|
240
|
+
if (!command || command === '--help' || command === '-h') return touchUsage()
|
|
241
|
+
}
|
|
211
242
|
if (group === 'run') {
|
|
212
243
|
if (command === 'start') return runStart(rest)
|
|
213
244
|
if (command === 'finish') return runFinish(rest)
|
|
214
245
|
if (!command || command === '--help' || command === '-h') return runUsage()
|
|
215
246
|
}
|
|
216
|
-
if (group === 'log') {
|
|
217
|
-
if (command === 'action') return logAction(rest)
|
|
218
|
-
if (!command || command === '--help' || command === '-h') return logUsage()
|
|
219
|
-
}
|
|
220
|
-
if (group === 'action') {
|
|
221
|
-
if (command === 'escalate') return actionEscalate(rest)
|
|
222
|
-
if (command === 'unescalate') return actionEscalate(rest, { clear: true })
|
|
223
|
-
if (!command || command === '--help' || command === '-h') return actionUsage()
|
|
224
|
-
}
|
|
225
247
|
if (group === 'feedback') {
|
|
226
248
|
if (command === 'record') return feedbackRecord(rest)
|
|
227
249
|
if (command === 'delete') return feedbackDelete(rest)
|
|
@@ -519,7 +541,13 @@ async function refreshRuntimeFiles() {
|
|
|
519
541
|
async function refreshSkills() {
|
|
520
542
|
// skills CLI switched from `-s name,name` to positional `name name`.
|
|
521
543
|
// The old comma-separated form silently matches nothing.
|
|
522
|
-
|
|
544
|
+
// Target only the agents we actually run — without -a the skills CLI
|
|
545
|
+
// sprays all ~71 known hosts and reports noisy failures for hosts that
|
|
546
|
+
// reject global installs (e.g. PromptScript).
|
|
547
|
+
// Like the skill names, agents must be repeated flags — `-a a,b` is read
|
|
548
|
+
// as one (invalid) agent name.
|
|
549
|
+
const agentFlags = SKILL_AGENTS.flatMap((a) => ['-a', a])
|
|
550
|
+
const res = await spawnSync('pnpm', ['dlx', '--silent', 'skills', 'add', 'kiluazen/kstack', ...SKILL_NAMES, ...agentFlags, '-y', '-g'])
|
|
523
551
|
if (res.status !== 0) throw new Error(`kstack skills install failed`)
|
|
524
552
|
}
|
|
525
553
|
|
|
@@ -655,6 +683,175 @@ async function hypothesisStatus(rest) {
|
|
|
655
683
|
console.log(`${label} → ${result.status}`)
|
|
656
684
|
}
|
|
657
685
|
|
|
686
|
+
// =============================================================== v2 leads
|
|
687
|
+
|
|
688
|
+
async function leadAdd(rest) {
|
|
689
|
+
const opts = parseArgs(rest)
|
|
690
|
+
const payload = parseJsonValue(readValue(required(opts.input, '--input @lead.json')), '--input')
|
|
691
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('--input must be a JSON object')
|
|
692
|
+
if (!payload.lead || typeof payload.lead !== 'object' || !String(payload.lead.angle || '').trim()) {
|
|
693
|
+
throw new Error('lead.angle is required in --input JSON')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let hypId = opts['hypothesis-id'] || opts.hypothesis_id
|
|
697
|
+
if (!hypId) {
|
|
698
|
+
const ref = opts.hypothesis || opts._[0]
|
|
699
|
+
if (ref) {
|
|
700
|
+
const [productSlug, code] = splitHypothesisRef(ref)
|
|
701
|
+
const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
|
|
702
|
+
hypId = ctx.hypothesis?.id
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!hypId) throw new Error('pass --hypothesis-id <id> or --hypothesis <slug/Hxx>')
|
|
706
|
+
|
|
707
|
+
const body = {
|
|
708
|
+
...payload,
|
|
709
|
+
source_run_id: opts['run-id'] || opts.run_id || opts.run || payload.source_run_id || undefined,
|
|
710
|
+
}
|
|
711
|
+
const result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/leads`, body)
|
|
712
|
+
console.log(JSON.stringify(result))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function leadStatus(rest) {
|
|
716
|
+
const opts = parseArgs(rest)
|
|
717
|
+
const id = required(opts['lead-id'] || opts.lead_id || opts._[0], 'lead id')
|
|
718
|
+
const status = required(opts.status, '--status (sourced|ready|contacted|replied|done|dead)')
|
|
719
|
+
const result = await api('PATCH', `/v1/leads/${encodeURIComponent(id)}/status`, { status })
|
|
720
|
+
console.log(JSON.stringify(result))
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// The agent's front door: one compact line per lead, id first, filtered by
|
|
724
|
+
// status. "Who replied?" = `autark lead list <slug> --status replied`.
|
|
725
|
+
async function leadList(rest) {
|
|
726
|
+
const opts = parseArgs(rest)
|
|
727
|
+
const product = required(opts.product || opts._[0], 'product slug')
|
|
728
|
+
const status = opts.status || ''
|
|
729
|
+
const qs = `?product=${encodeURIComponent(product)}${status ? `&status=${encodeURIComponent(status)}` : ''}`
|
|
730
|
+
const data = await api('GET', `/v1/leads${qs}`)
|
|
731
|
+
if (opts.json) { console.log(JSON.stringify(data, null, 2)); return }
|
|
732
|
+
if (!data.leads.length) { console.log(`no leads${status ? ` with status ${status}` : ''} in ${product}`); return }
|
|
733
|
+
for (const l of data.leads) {
|
|
734
|
+
const who = [l.full_name, l.email && `<${l.email}>`].filter(Boolean).join(' ')
|
|
735
|
+
console.log(`${l.lead_id} ${l.status}${l.hypothesis ? ` ${l.hypothesis}` : ''} ${who}`)
|
|
736
|
+
if (l.replied_at) console.log(` replied_at=${l.replied_at}${l.last_in_at ? ` last_in=${l.last_in_at}` : ''}`)
|
|
737
|
+
if (l.thread_ref) console.log(` thread=${l.thread_ref} inbox=${l.inbox_ref || ''}`)
|
|
738
|
+
if (l.angle) console.log(` angle: ${l.angle.length > 140 ? l.angle.slice(0, 140) + '…' : l.angle}`)
|
|
739
|
+
}
|
|
740
|
+
console.error(`${data.count} lead(s). Next: autark lead show <id> · autark mail thread <thread> · autark mail reply --lead-id <id>`)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function leadShow(rest) {
|
|
744
|
+
const opts = parseArgs(rest)
|
|
745
|
+
const id = required(opts['lead-id'] || opts.lead_id || opts._[0], 'lead id')
|
|
746
|
+
const lead = await api('GET', `/v1/leads/${encodeURIComponent(id)}`)
|
|
747
|
+
console.log(JSON.stringify(lead, null, 2))
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// muted=true marks an inbound touch as a non-reply (ticket bot, autoresponder).
|
|
751
|
+
// The worker recomputes the lead verdict in the same write and the reply sweep
|
|
752
|
+
// skips muted touches, so the judgment sticks. --action-id targets a v1 action
|
|
753
|
+
// row instead (legacy bridge; the replies tab filters those at render).
|
|
754
|
+
async function touchMute(rest, muted) {
|
|
755
|
+
const opts = parseArgs(rest)
|
|
756
|
+
const actionId = opts['action-id'] || opts.action_id
|
|
757
|
+
if (actionId) {
|
|
758
|
+
const result = await api('PATCH', `/v1/actions/${encodeURIComponent(actionId)}/mute`, { muted })
|
|
759
|
+
console.log(JSON.stringify(result))
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
const id = required(opts['touch-id'] || opts.touch_id || opts._[0], 'touch id (or --action-id for v1 rows)')
|
|
763
|
+
const result = await api('PATCH', `/v1/touches/${encodeURIComponent(id)}/mute`, { muted })
|
|
764
|
+
console.log(JSON.stringify(result))
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function touchAdd(rest) {
|
|
768
|
+
const opts = parseArgs(rest)
|
|
769
|
+
const leadId = required(opts['lead-id'] || opts.lead_id, '--lead-id')
|
|
770
|
+
const channel = required(opts.channel, '--channel')
|
|
771
|
+
const result = await api('POST', `/v1/leads/${encodeURIComponent(leadId)}/touches`, {
|
|
772
|
+
channel,
|
|
773
|
+
direction: opts.direction === 'in' ? 'in' : 'out',
|
|
774
|
+
thread_ref: opts['thread-ref'] || opts.thread_ref || undefined,
|
|
775
|
+
inbox_ref: opts['inbox-ref'] || opts.inbox_ref || undefined,
|
|
776
|
+
summary: opts.summary || undefined,
|
|
777
|
+
occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
|
|
778
|
+
status: opts.status || undefined,
|
|
779
|
+
})
|
|
780
|
+
console.log(JSON.stringify(result))
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function touchUsage() {
|
|
784
|
+
console.log(`autark touch
|
|
785
|
+
|
|
786
|
+
add --lead-id <id> --channel <c>
|
|
787
|
+
[--direction out|in] # default out
|
|
788
|
+
[--thread-ref <ref>] # AgentMail thread id or public URL of YOUR comment
|
|
789
|
+
[--summary "<one line>"]
|
|
790
|
+
[--occurred-at <iso>]
|
|
791
|
+
[--status <s>] # override the automatic advance
|
|
792
|
+
|
|
793
|
+
One row per outreach interaction, recorded against an explicit lead id —
|
|
794
|
+
the CLI never guesses which lead an interaction belongs to. Writing the
|
|
795
|
+
touch advances lead.status in the same transaction (out: ready→contacted,
|
|
796
|
+
in: →replied) unless --status overrides it.
|
|
797
|
+
|
|
798
|
+
Email is a special case: \`autark mail send --lead-id <id>\` records the
|
|
799
|
+
touch automatically — do not call \`touch add\` for the same send.
|
|
800
|
+
|
|
801
|
+
mute <touch-id> mark an inbound touch as a non-reply (bot,
|
|
802
|
+
autoresponder); lead verdict recomputes and
|
|
803
|
+
the reply sweep respects the judgment
|
|
804
|
+
mute --action-id <id> same, for a legacy v1 action row
|
|
805
|
+
unmute <touch-id> reverse it`)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function leadTemplate() {
|
|
809
|
+
printJson(LEAD_TEMPLATE)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function leadUsage() {
|
|
813
|
+
console.log(`autark lead
|
|
814
|
+
|
|
815
|
+
template
|
|
816
|
+
print a minimal JSON payload for lead add
|
|
817
|
+
|
|
818
|
+
add --hypothesis-id <id> --input @/tmp/lead.json [--run-id <id>]
|
|
819
|
+
add --hypothesis <slug>/<H01> --input @/tmp/lead.json [--run-id <id>]
|
|
820
|
+
|
|
821
|
+
list <slug> [--status replied|contacted|ready|...] [--json]
|
|
822
|
+
the front door: one line per lead, ids first, newest replies on top.
|
|
823
|
+
"who replied?" = lead list <slug> --status replied
|
|
824
|
+
|
|
825
|
+
show <lead-id>
|
|
826
|
+
person + bet + status + the ordered touch log with ids — run this
|
|
827
|
+
when handed a lead link, before replying or muting
|
|
828
|
+
|
|
829
|
+
status <lead-id> --status sourced|ready|contacted|replied|done|dead
|
|
830
|
+
explicit status write — normally NOT needed: sending via
|
|
831
|
+
\`autark mail send --lead-id\` or \`autark touch add\` advances
|
|
832
|
+
status automatically in the same transaction
|
|
833
|
+
|
|
834
|
+
lead add records one sourced lead: person identity + the angle connecting that
|
|
835
|
+
person to the hypothesis. It sends one ID-first request to the worker and
|
|
836
|
+
prints exactly one compact JSON object to stdout:
|
|
837
|
+
|
|
838
|
+
{"person_id":"...","lead_id":"...","person_existed":false,"lead_existed":false}
|
|
839
|
+
|
|
840
|
+
Minimum payload:
|
|
841
|
+
|
|
842
|
+
${JSON.stringify(LEAD_TEMPLATE, null, 2)}
|
|
843
|
+
|
|
844
|
+
Required:
|
|
845
|
+
- lead.angle
|
|
846
|
+
- enough person identity to dedupe: primary_email, a supported handle, or full_name
|
|
847
|
+
|
|
848
|
+
Optional person fields:
|
|
849
|
+
email_status, headline, source, bio, signals
|
|
850
|
+
|
|
851
|
+
find_leads should write /tmp/lead-N.json and call lead add with --input @file.
|
|
852
|
+
No outreach is sent and no touch rows are written.`)
|
|
853
|
+
}
|
|
854
|
+
|
|
658
855
|
// =============================================================== runs
|
|
659
856
|
|
|
660
857
|
async function runStart(rest) {
|
|
@@ -679,59 +876,6 @@ async function runFinish(rest) {
|
|
|
679
876
|
console.log(`${result.id} finished_at=${result.finished_at}`)
|
|
680
877
|
}
|
|
681
878
|
|
|
682
|
-
// ============================================================ actions
|
|
683
|
-
|
|
684
|
-
async function logAction(rest) {
|
|
685
|
-
const opts = parseArgs(rest)
|
|
686
|
-
const run = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
|
|
687
|
-
const channel = required(opts.channel, '--channel')
|
|
688
|
-
const title = required(opts.title, '--title')
|
|
689
|
-
const needsHuman = Boolean(opts['needs-human'] || opts.needs_human || opts.escalate)
|
|
690
|
-
const escalationReason = opts['escalation-reason'] || opts.escalation_reason || opts.reason || undefined
|
|
691
|
-
|
|
692
|
-
// Merge `our_comment_id` into metadata if passed as a sugar flag. This is
|
|
693
|
-
// the per-channel anchor the reply-state cron uses to detect engagement on
|
|
694
|
-
// OUR specific comment (vs the parent we were replying to). Always pass
|
|
695
|
-
// it when you posted a comment somewhere we can re-fetch — github issue/PR
|
|
696
|
-
// comments, gists, HN replies, Substack comments, Reddit comments.
|
|
697
|
-
const meta = opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined
|
|
698
|
-
const ourCommentId = opts['our-comment-id'] || opts.our_comment_id
|
|
699
|
-
const metadata = ourCommentId
|
|
700
|
-
? { ...(meta || {}), our_comment_id: String(ourCommentId) }
|
|
701
|
-
: meta
|
|
702
|
-
|
|
703
|
-
const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
|
|
704
|
-
channel,
|
|
705
|
-
title,
|
|
706
|
-
url: opts.url || undefined,
|
|
707
|
-
agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
|
|
708
|
-
agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
|
|
709
|
-
recipient: opts.recipient || undefined,
|
|
710
|
-
metadata,
|
|
711
|
-
needs_human: needsHuman || undefined,
|
|
712
|
-
escalation_reason: needsHuman ? escalationReason : undefined,
|
|
713
|
-
occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
|
|
714
|
-
})
|
|
715
|
-
console.log(result.id)
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// ============================================================ action escalate
|
|
719
|
-
|
|
720
|
-
async function actionEscalate(rest, { clear = false } = {}) {
|
|
721
|
-
const opts = parseArgs(rest)
|
|
722
|
-
const id = required(opts['action-id'] || opts.action_id || opts._[0], 'action id')
|
|
723
|
-
const reason = opts.reason || opts._.slice(1).join(' ') || undefined
|
|
724
|
-
const body = { needs_human: !clear }
|
|
725
|
-
if (!clear) body.reason = reason || ''
|
|
726
|
-
const result = await api('PATCH', `/v1/actions/${encodeURIComponent(id)}/escalate`, body)
|
|
727
|
-
console.log(`${result.id} needs_human=${result.needs_human}${result.escalation_reason ? ` reason="${result.escalation_reason}"` : ''}`)
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function actionUsage() {
|
|
731
|
-
console.log(`autark action escalate <action-id> [--reason "<one-line why>"]
|
|
732
|
-
autark action unescalate <action-id>`)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
879
|
// ============================================================ product pause
|
|
736
880
|
|
|
737
881
|
async function productPause(rest, fixedStatus) {
|
|
@@ -977,7 +1121,8 @@ async function mailSend(rest) {
|
|
|
977
1121
|
response: result,
|
|
978
1122
|
})
|
|
979
1123
|
noteAutoLog(action, opts)
|
|
980
|
-
|
|
1124
|
+
const touch = await maybeLogMailTouch(opts, { kind: 'send', subject, response: result })
|
|
1125
|
+
printJson({ ...result, autark_action_id: action?.id, autark_touch_id: touch?.touch_id, autark_lead_status: touch?.status })
|
|
981
1126
|
}
|
|
982
1127
|
|
|
983
1128
|
async function mailReply(rest, mode) {
|
|
@@ -995,7 +1140,8 @@ async function mailReply(rest, mode) {
|
|
|
995
1140
|
metadata: { message_id: messageId },
|
|
996
1141
|
})
|
|
997
1142
|
noteAutoLog(action, opts)
|
|
998
|
-
|
|
1143
|
+
const touch = await maybeLogMailTouch(opts, { kind: mode, subject: opts.subject, response: result })
|
|
1144
|
+
printJson({ ...result, autark_action_id: action?.id, autark_touch_id: touch?.touch_id, autark_lead_status: touch?.status })
|
|
999
1145
|
}
|
|
1000
1146
|
|
|
1001
1147
|
async function mailForward(rest) {
|
|
@@ -1013,17 +1159,18 @@ async function mailForward(rest) {
|
|
|
1013
1159
|
metadata: { message_id: messageId },
|
|
1014
1160
|
})
|
|
1015
1161
|
noteAutoLog(action, opts)
|
|
1016
|
-
|
|
1162
|
+
const touch = await maybeLogMailTouch(opts, { kind: 'forward', subject: opts.subject, response: result })
|
|
1163
|
+
printJson({ ...result, autark_action_id: action?.id, autark_touch_id: touch?.touch_id, autark_lead_status: touch?.status })
|
|
1017
1164
|
}
|
|
1018
1165
|
|
|
1019
|
-
// Explicit human-readable cue so agents (and humans) see that the
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1166
|
+
// Explicit human-readable cue so agents (and humans) see that the send was
|
|
1167
|
+
// recorded against the run — no separate logging step is needed or possible.
|
|
1168
|
+
// Stays on stderr so JSON consumers piping stdout aren't affected. Only
|
|
1169
|
+
// fires when --run-id was passed and the log succeeded.
|
|
1023
1170
|
function noteAutoLog(action, opts) {
|
|
1024
1171
|
if (!action?.id) return
|
|
1025
1172
|
const runId = opts['run-id'] || opts.run_id || opts.run
|
|
1026
|
-
console.error(`autark:
|
|
1173
|
+
console.error(`autark: send recorded as ${action.id} on run ${runId} — nothing else to log`)
|
|
1027
1174
|
}
|
|
1028
1175
|
|
|
1029
1176
|
async function mailThreads(rest) {
|
|
@@ -1097,6 +1244,28 @@ function mailBody(opts, base = {}) {
|
|
|
1097
1244
|
return body
|
|
1098
1245
|
}
|
|
1099
1246
|
|
|
1247
|
+
// v2: when --lead-id is passed, the send is recorded as a touch on that lead
|
|
1248
|
+
// and the lead's status advances (ready → contacted) in the same worker
|
|
1249
|
+
// transact. The send IS the bookkeeping — no separate status command needed.
|
|
1250
|
+
async function maybeLogMailTouch(opts, { kind, subject, response }) {
|
|
1251
|
+
const leadId = opts['lead-id'] || opts.lead_id
|
|
1252
|
+
if (!leadId) return null
|
|
1253
|
+
const threadId = response?.thread_id
|
|
1254
|
+
if (!threadId) throw new Error('AgentMail response missing thread_id; refusing to log email touch')
|
|
1255
|
+
// inbox_ref tells the reply sweep which inbox owns the thread — stamped
|
|
1256
|
+
// from the sending credentials so it survives later inbox rotation.
|
|
1257
|
+
const creds = requireAgentmailCredentials()
|
|
1258
|
+
const touch = await api('POST', `/v1/leads/${encodeURIComponent(leadId)}/touches`, {
|
|
1259
|
+
channel: 'email',
|
|
1260
|
+
direction: 'out',
|
|
1261
|
+
thread_ref: threadId,
|
|
1262
|
+
inbox_ref: creds.inboxId,
|
|
1263
|
+
summary: subject ? `${kind}: ${subject}` : kind,
|
|
1264
|
+
})
|
|
1265
|
+
console.error(`autark: touch ${touch.touch_id} on lead ${touch.lead_id} — status ${touch.status}${touch.status_changed ? ' (advanced)' : ''}`)
|
|
1266
|
+
return touch
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1100
1269
|
async function maybeLogMailAction(opts, { kind, defaultTitle, recipient, subject, response, metadata = {} }) {
|
|
1101
1270
|
const runId = opts['run-id'] || opts.run_id || opts.run
|
|
1102
1271
|
if (!runId) return null
|
|
@@ -1214,6 +1383,9 @@ function printProductContext(r) {
|
|
|
1214
1383
|
kv('product.url', r.product.url)
|
|
1215
1384
|
kv('product.visibility', r.product.visibility)
|
|
1216
1385
|
kv('product.hypothesis_count', (r.hypotheses || []).length)
|
|
1386
|
+
kv('product.person_count', r.product.person_count || (r.people || []).length || 0)
|
|
1387
|
+
kv('product.lead_count', r.product.lead_count || (r.leads || []).length || 0)
|
|
1388
|
+
kv('product.ready_lead_count', r.product.ready_lead_count || (r.leads || []).filter((l) => l.status === 'ready').length || 0)
|
|
1217
1389
|
// Product-level operator feedback (newest first). Append-only + timestamped,
|
|
1218
1390
|
// so each entry is a distinct nudge — re-read these every run.
|
|
1219
1391
|
kv('product.feedback_count', (r.feedback || []).length)
|
|
@@ -1229,6 +1401,8 @@ function printProductContext(r) {
|
|
|
1229
1401
|
kv(`hypothesis.${h.code}.run_count`, h.run_count)
|
|
1230
1402
|
kv(`hypothesis.${h.code}.action_count`, h.action_count)
|
|
1231
1403
|
kv(`hypothesis.${h.code}.reply_count`, h.reply_count)
|
|
1404
|
+
kv(`hypothesis.${h.code}.lead_count`, h.lead_count || (h.leads || []).length || 0)
|
|
1405
|
+
kv(`hypothesis.${h.code}.ready_lead_count`, h.ready_lead_count || (h.leads || []).filter((l) => l.status === 'ready').length || 0)
|
|
1232
1406
|
if (h.needs_human_count) kv(`hypothesis.${h.code}.needs_human_count`, h.needs_human_count)
|
|
1233
1407
|
const actions = (h.runs || []).flatMap((run) => run.actions || [])
|
|
1234
1408
|
const by = channelBreakdown(actions)
|
|
@@ -1237,6 +1411,16 @@ function printProductContext(r) {
|
|
|
1237
1411
|
kv(`hypothesis.${h.code}.channel.${ch}.replied`, by[ch].replied)
|
|
1238
1412
|
}
|
|
1239
1413
|
}
|
|
1414
|
+
for (const lead of (r.leads || [])) {
|
|
1415
|
+
const shortL = actionShortId(lead.id)
|
|
1416
|
+
kv(`lead.${shortL}.id`, lead.id)
|
|
1417
|
+
kv(`lead.${shortL}.status`, lead.status)
|
|
1418
|
+
kv(`lead.${shortL}.hypothesis_code`, lead.hypothesis_code)
|
|
1419
|
+
kv(`lead.${shortL}.person_name`, lead.person?.full_name)
|
|
1420
|
+
kv(`lead.${shortL}.person_email`, lead.person?.primary_email)
|
|
1421
|
+
kv(`lead.${shortL}.headline`, lead.person?.headline)
|
|
1422
|
+
kv(`lead.${shortL}.angle`, lead.angle)
|
|
1423
|
+
}
|
|
1240
1424
|
console.log('---')
|
|
1241
1425
|
// ---- narrative ----
|
|
1242
1426
|
console.log(`# ${r.product.slug} — ${r.product.name}\n`)
|
|
@@ -1261,7 +1445,18 @@ function printProductContext(r) {
|
|
|
1261
1445
|
.map((ch) => `${ch} ${by[ch].replied}/${by[ch].sent}`)
|
|
1262
1446
|
.join(' ')
|
|
1263
1447
|
const tail = channelSummary ? ` — replies/sends by channel: ${channelSummary}` : ''
|
|
1264
|
-
console.log(`- [${h.status}] ${h.code} — ${h.title} (runs: ${h.run_count}, actions: ${h.action_count || 0}, replies: ${h.reply_count || 0})${tail}`)
|
|
1448
|
+
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}`)
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (r.leads?.length) {
|
|
1452
|
+
console.log(`\n## Recent leads (${r.leads.length})\n`)
|
|
1453
|
+
for (const lead of r.leads) {
|
|
1454
|
+
const person = lead.person || {}
|
|
1455
|
+
const label = person.full_name || person.primary_email || Object.entries(person.handles || {})[0]?.join(':') || lead.person_id || 'unknown person'
|
|
1456
|
+
const hyp = lead.hypothesis_code ? `${lead.hypothesis_code} ` : ''
|
|
1457
|
+
const headline = person.headline ? ` — ${person.headline}` : ''
|
|
1458
|
+
console.log(`- [${lead.status}] ${hyp}${label}${headline}`)
|
|
1459
|
+
if (lead.angle) console.log(` angle: ${lead.angle}`)
|
|
1265
1460
|
}
|
|
1266
1461
|
}
|
|
1267
1462
|
}
|
|
@@ -1274,6 +1469,8 @@ function printHypothesisContext(result) {
|
|
|
1274
1469
|
kv('hypothesis.code', result.hypothesis.code)
|
|
1275
1470
|
kv('hypothesis.title', result.hypothesis.title)
|
|
1276
1471
|
kv('hypothesis.status', result.hypothesis.status)
|
|
1472
|
+
kv('hypothesis.lead_count', result.hypothesis.lead_count || (result.leads || []).length || 0)
|
|
1473
|
+
kv('hypothesis.ready_lead_count', result.hypothesis.ready_lead_count || (result.leads || []).filter((l) => l.status === 'ready').length || 0)
|
|
1277
1474
|
// Feedback — operator nudges left on this hypothesis, oldest first. The
|
|
1278
1475
|
// worker returns them; previously the CLI silently dropped them, so agents
|
|
1279
1476
|
// never saw the operator's running corrections. Now surface them up top
|
|
@@ -1297,9 +1494,11 @@ function printHypothesisContext(result) {
|
|
|
1297
1494
|
}
|
|
1298
1495
|
for (const run of (result.runs || [])) {
|
|
1299
1496
|
const shortRun = actionShortId(run.id)
|
|
1497
|
+
const runLeads = (result.leads || []).filter((l) => l.source_run_id === run.id)
|
|
1300
1498
|
kv(`run.${shortRun}.id`, run.id)
|
|
1301
1499
|
kv(`run.${shortRun}.run_number`, run.run_number)
|
|
1302
1500
|
kv(`run.${shortRun}.started_at`, run.started_at)
|
|
1501
|
+
kv(`run.${shortRun}.lead_count`, runLeads.length)
|
|
1303
1502
|
if (run.finished_at) kv(`run.${shortRun}.finished_at`, run.finished_at)
|
|
1304
1503
|
for (const a of (run.actions || [])) {
|
|
1305
1504
|
const shortA = actionShortId(a.id)
|
|
@@ -1326,6 +1525,16 @@ function printHypothesisContext(result) {
|
|
|
1326
1525
|
}
|
|
1327
1526
|
}
|
|
1328
1527
|
}
|
|
1528
|
+
for (const lead of (result.leads || [])) {
|
|
1529
|
+
const shortL = actionShortId(lead.id)
|
|
1530
|
+
kv(`lead.${shortL}.id`, lead.id)
|
|
1531
|
+
kv(`lead.${shortL}.status`, lead.status)
|
|
1532
|
+
kv(`lead.${shortL}.source_run_id`, lead.source_run_id)
|
|
1533
|
+
kv(`lead.${shortL}.person_name`, lead.person?.full_name)
|
|
1534
|
+
kv(`lead.${shortL}.person_email`, lead.person?.primary_email)
|
|
1535
|
+
kv(`lead.${shortL}.headline`, lead.person?.headline)
|
|
1536
|
+
kv(`lead.${shortL}.angle`, lead.angle)
|
|
1537
|
+
}
|
|
1329
1538
|
console.log('---')
|
|
1330
1539
|
// ---- narrative ----
|
|
1331
1540
|
console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
|
|
@@ -1338,9 +1547,21 @@ function printHypothesisContext(result) {
|
|
|
1338
1547
|
console.log(`- [${when}]${tag} ${f.text}`)
|
|
1339
1548
|
}
|
|
1340
1549
|
}
|
|
1550
|
+
if (result.leads?.length) {
|
|
1551
|
+
console.log(`\n\n## Leads (${result.leads.length})\n`)
|
|
1552
|
+
for (const lead of result.leads) {
|
|
1553
|
+
const person = lead.person || {}
|
|
1554
|
+
const label = person.full_name || person.primary_email || Object.entries(person.handles || {})[0]?.join(':') || lead.person_id || 'unknown person'
|
|
1555
|
+
const headline = person.headline ? ` — ${person.headline}` : ''
|
|
1556
|
+
const runTag = lead.source_run_id ? ` (run ${actionShortId(lead.source_run_id)})` : ''
|
|
1557
|
+
console.log(`- [${lead.status}] ${label}${headline}${runTag}`)
|
|
1558
|
+
if (lead.angle) console.log(` angle: ${lead.angle}`)
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1341
1561
|
for (const run of (result.runs || [])) {
|
|
1562
|
+
const runLeads = (result.leads || []).filter((l) => l.source_run_id === run.id)
|
|
1342
1563
|
console.log(`\n\n## Run ${run.run_number} (${run.id})`)
|
|
1343
|
-
console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}`)
|
|
1564
|
+
console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}${runLeads.length ? ` leads sourced: ${runLeads.length}` : ''}`)
|
|
1344
1565
|
if (run.actions?.length) {
|
|
1345
1566
|
console.log(`\nActions:`)
|
|
1346
1567
|
for (const a of run.actions) {
|
|
@@ -1423,6 +1644,14 @@ function readValue(value) {
|
|
|
1423
1644
|
return String(value)
|
|
1424
1645
|
}
|
|
1425
1646
|
|
|
1647
|
+
function parseJsonValue(value, label = 'json') {
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse(value)
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
throw new Error(`${label} must be valid JSON: ${e.message}`)
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1426
1655
|
function listOpt(value, label) {
|
|
1427
1656
|
if (value === undefined || value === null || value === '') {
|
|
1428
1657
|
if (label) throw new Error(`${label} is required`)
|
|
@@ -1472,10 +1701,10 @@ function mailUsage() {
|
|
|
1472
1701
|
console.log(`autark mail
|
|
1473
1702
|
|
|
1474
1703
|
setup --prefix <name> [--force]
|
|
1475
|
-
send --to <email[,email]> --subject <s> [--text @body.txt] [--html @body.html] [--cc <e>] [--bcc <e>] [--reply-to <e>] [--label <l>] [--attachment <a>] [--run-id <id>] [--dry-run]
|
|
1476
|
-
reply --message-id <id> [--text @reply.txt] [--html @reply.html] [--run-id <id>]
|
|
1477
|
-
reply-all --message-id <id> [--text @reply.txt] [--html @reply.html] [--run-id <id>]
|
|
1478
|
-
forward --message-id <id> --to <email> [--text @body.txt] [--html @body.html] [--run-id <id>]
|
|
1704
|
+
send --to <email[,email]> --subject <s> [--text @body.txt] [--html @body.html] [--cc <e>] [--bcc <e>] [--reply-to <e>] [--label <l>] [--attachment <a>] [--run-id <id>] [--lead-id <id>] [--dry-run]
|
|
1705
|
+
reply --message-id <id> [--text @reply.txt] [--html @reply.html] [--run-id <id>] [--lead-id <id>]
|
|
1706
|
+
reply-all --message-id <id> [--text @reply.txt] [--html @reply.html] [--run-id <id>] [--lead-id <id>]
|
|
1707
|
+
forward --message-id <id> --to <email> [--text @body.txt] [--html @body.html] [--run-id <id>] [--lead-id <id>]
|
|
1479
1708
|
threads [--limit N]
|
|
1480
1709
|
thread <thread_id>
|
|
1481
1710
|
messages [--limit N]
|
|
@@ -1509,9 +1738,9 @@ function usage() {
|
|
|
1509
1738
|
product pause|unpause stop / resume cron on a product
|
|
1510
1739
|
hypothesis create|status create or update hypotheses
|
|
1511
1740
|
hypothesis pause|unpause stop / resume work on a hypothesis
|
|
1741
|
+
lead list|add|show|status the sheet: list by status / record / inspect / set
|
|
1742
|
+
touch add|mute|unmute record an interaction / judge a non-reply
|
|
1512
1743
|
run start|finish start / finish a run
|
|
1513
|
-
log action record one outreach touch
|
|
1514
|
-
action escalate flag an action for human attention
|
|
1515
1744
|
feedback record|delete leave a free-text nudge on a hypothesis or product
|
|
1516
1745
|
context [<slug>|...] pull product or hypothesis context
|
|
1517
1746
|
|
|
@@ -1564,50 +1793,6 @@ function runUsage() {
|
|
|
1564
1793
|
→ seals the run with the narrative blob`)
|
|
1565
1794
|
}
|
|
1566
1795
|
|
|
1567
|
-
function logUsage() {
|
|
1568
|
-
console.log(`autark log
|
|
1569
|
-
|
|
1570
|
-
action --run-id <id> --channel <c> --title <t>
|
|
1571
|
-
[--url <u>] # github / reddit / hn / blog / gist / linkedin
|
|
1572
|
-
[--agentmail-thread-id <uuid>] # email
|
|
1573
|
-
[--agentmail-inbox-id <email>] # email — defaults to your inbox
|
|
1574
|
-
[--recipient <email>] # email
|
|
1575
|
-
[--our-comment-id <id>] # see "Always capture our_comment_id" below
|
|
1576
|
-
[--metadata @./meta.json] # any other channel-specific extras
|
|
1577
|
-
[--escalate --reason "<why>"] # flag this action as needs_human
|
|
1578
|
-
|
|
1579
|
-
One row per external touch. Body content stays in AgentMail/GitHub/etc;
|
|
1580
|
-
this just records the pointer + the title agents see on the dashboard.
|
|
1581
|
-
|
|
1582
|
-
Log the URL of YOUR comment, not the parent
|
|
1583
|
-
|
|
1584
|
-
The reply-state cron parses the comment id out of the --url you log to
|
|
1585
|
-
detect "did anyone reply to you?". Most channels' URLs already contain
|
|
1586
|
-
the id; you just have to log the permalink of your OWN comment, not the
|
|
1587
|
-
issue/post URL you replied to.
|
|
1588
|
-
|
|
1589
|
-
github (issue or PR comment):
|
|
1590
|
-
use the response's html_url, ends in #issuecomment-<id>
|
|
1591
|
-
NEW_URL=$(gh api repos/<o>/<r>/issues/<n>/comments \\
|
|
1592
|
-
-X POST -f body=@body.md --jq .html_url)
|
|
1593
|
-
|
|
1594
|
-
gist:
|
|
1595
|
-
ends in #gistcomment-<id>
|
|
1596
|
-
|
|
1597
|
-
hn:
|
|
1598
|
-
.../item?id=<your_new_item_id> (your id, not the parent's)
|
|
1599
|
-
|
|
1600
|
-
reddit:
|
|
1601
|
-
.../r/<sub>/comments/<thread>/<title>/<your_comment_id>/
|
|
1602
|
-
|
|
1603
|
-
substack:
|
|
1604
|
-
Substack permalinks don't always include the comment id, so for
|
|
1605
|
-
this channel use --our-comment-id <id> from the response body.
|
|
1606
|
-
|
|
1607
|
-
Escape hatch: --our-comment-id <id> tucks the id into metadata. Use it
|
|
1608
|
-
when the URL pattern can't encode the comment (Substack, edge HN cases).`)
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
1796
|
function contextUsage() {
|
|
1612
1797
|
console.log(`autark context
|
|
1613
1798
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autark-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for autark
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "CLI for autark — hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|