autark-cli 0.1.3 → 0.1.5
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 +33 -19
- package/autark.mjs +384 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,39 +18,53 @@ autark login send your@email.com
|
|
|
18
18
|
autark login verify your@email.com --code 123456
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Runbook use
|
|
22
|
+
|
|
23
|
+
Autark is ID-first. Use `product list` / `context` to discover IDs, then pass IDs for writes. `slug/H01` remains a convenience alias.
|
|
22
24
|
|
|
23
25
|
```sh
|
|
24
|
-
|
|
25
|
-
autark product
|
|
26
|
-
autark
|
|
26
|
+
autark product upsert --slug chrome-relay --name "Chrome Relay" --tagline "..."
|
|
27
|
+
autark product list # prints slug, visibility, id, name
|
|
28
|
+
autark context chrome-relay
|
|
29
|
+
|
|
30
|
+
autark hypothesis create --product-id <product_id> --md @./H01.md --code H01 --title "..."
|
|
31
|
+
autark run start --hypothesis-id <hypothesis_id>
|
|
32
|
+
autark log action --run-id <run_id> --channel github --title "..." --url https://github.com/...
|
|
33
|
+
autark run finish --run-id <run_id> --narrative @./narrative.md
|
|
34
|
+
```
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
autark hypothesis create --product chrome-relay --code H01 --md @./H01.md --title "..."
|
|
30
|
-
autark hypothesis status chrome-relay/H01 --status active # active|inactive|dead
|
|
36
|
+
## Mail use
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
autark run start --hypothesis chrome-relay/H01
|
|
34
|
-
autark run finish --run <run_id> --narrative @./narrative.md
|
|
38
|
+
`autark mail` covers the AgentMail surface Autark needs. It does not require a separate AgentMail CLI login.
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
autark
|
|
38
|
-
--recipient person@example.com --agentmail-thread-id <thread_id>
|
|
39
|
-
autark log action --run <run_id> --channel github --title "..." --url https://github.com/...
|
|
40
|
+
```sh
|
|
41
|
+
autark mail setup --prefix laksh
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
autark mail send --run-id <run_id> \
|
|
44
|
+
--to person@example.com \
|
|
45
|
+
--subject "Subject" \
|
|
46
|
+
--text @./body.txt
|
|
43
47
|
|
|
44
|
-
autark
|
|
45
|
-
|
|
48
|
+
autark mail reply --run-id <run_id> --message-id <message_id> --text @./reply.txt
|
|
49
|
+
|
|
50
|
+
autark mail threads --limit 20
|
|
51
|
+
autark mail thread <thread_id>
|
|
52
|
+
autark mail messages --limit 20
|
|
53
|
+
autark mail message <message_id>
|
|
54
|
+
autark mail raw <message_id>
|
|
55
|
+
autark mail attachment --message-id <message_id> --attachment-id <attachment_id> --out file.bin
|
|
46
56
|
```
|
|
47
57
|
|
|
58
|
+
Mail sends/replies call AgentMail directly with the user's inbox-scoped key from `~/.autark/credentials.json`, then log an Autark `email` action containing `agentmail_thread_id` and `agentmail_inbox_id`.
|
|
59
|
+
|
|
48
60
|
## ENV
|
|
49
61
|
|
|
50
62
|
- `AUTARK_API_URL` — override the default Worker URL (`https://autark-api.kushalsokke.workers.dev`)
|
|
63
|
+
- `AGENTMAIL_API_URL` — override AgentMail API base
|
|
64
|
+
- `AGENTMAIL_API_KEY`, `AGENTMAIL_EMAIL`, `AGENTMAIL_INBOX_ID` — override local mail credentials for debugging
|
|
51
65
|
|
|
52
66
|
## Architecture
|
|
53
67
|
|
|
54
68
|
The CLI is a thin HTTP client over a Cloudflare Worker that holds the InstantDB admin token. Magic-link auth via InstantDB. Token saved to `~/.autark/credentials.json` after login.
|
|
55
69
|
|
|
56
|
-
The
|
|
70
|
+
The Worker uses the AgentMail org key only for provisioning and dashboard thread reads. Local mail send/reply uses the user's inbox-scoped key.
|
package/autark.mjs
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// autark CLI — thin HTTP client over the autark-api Worker.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// All business logic, schema knowledge, and InstantDB connectivity lives in
|
|
8
|
-
// the Worker. The CLI is intentionally thin — it parses args, makes HTTP
|
|
9
|
-
// calls, formats output. Replace this binary in any language tomorrow and
|
|
10
|
-
// the system still works.
|
|
4
|
+
// The runbook API is ID-first. slug/Hxx remains as a convenience alias.
|
|
5
|
+
// Mail commands call AgentMail directly with the user's inbox-scoped key;
|
|
6
|
+
// autark-api uses the org key only for provisioning and dashboard thread reads.
|
|
11
7
|
|
|
12
8
|
import fs from 'node:fs'
|
|
13
9
|
import os from 'node:os'
|
|
@@ -15,6 +11,7 @@ import path from 'node:path'
|
|
|
15
11
|
import process from 'node:process'
|
|
16
12
|
|
|
17
13
|
const API = process.env.AUTARK_API_URL || 'https://autark-api.kushalsokke.workers.dev'
|
|
14
|
+
const AGENTMAIL_API = process.env.AGENTMAIL_API_URL || 'https://api.agentmail.to/v0'
|
|
18
15
|
const CREDS_PATH = path.join(os.homedir(), '.autark', 'credentials.json')
|
|
19
16
|
|
|
20
17
|
const args = process.argv.slice(2)
|
|
@@ -49,6 +46,7 @@ async function main() {
|
|
|
49
46
|
}
|
|
50
47
|
if (group === 'log' && command === 'action') return logAction(rest)
|
|
51
48
|
if (group === 'context') return context([command, ...rest].filter(Boolean))
|
|
49
|
+
if (group === 'mail') return mail(command, rest)
|
|
52
50
|
|
|
53
51
|
usage()
|
|
54
52
|
process.exit(1)
|
|
@@ -71,6 +69,7 @@ async function loginVerify(rest) {
|
|
|
71
69
|
const result = await api('POST', '/v1/auth/login/verify', { email, code }, { auth: false })
|
|
72
70
|
if (!result.token) throw new Error('verify succeeded but no token returned')
|
|
73
71
|
saveCredentials({
|
|
72
|
+
...loadCredentials(),
|
|
74
73
|
user_id: result.user.id,
|
|
75
74
|
email: result.user.email,
|
|
76
75
|
token: result.token,
|
|
@@ -116,7 +115,7 @@ async function productList() {
|
|
|
116
115
|
return
|
|
117
116
|
}
|
|
118
117
|
for (const p of result.products) {
|
|
119
|
-
console.log(`${(p.slug || '').padEnd(20)} ${(p.visibility || '').padEnd(8)} ${p.name || ''}`)
|
|
118
|
+
console.log(`${(p.slug || '').padEnd(20)} ${(p.visibility || '').padEnd(8)} ${(p.id || '').padEnd(36)} ${p.name || ''}`)
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -124,45 +123,65 @@ async function productList() {
|
|
|
124
123
|
|
|
125
124
|
async function hypothesisCreate(rest) {
|
|
126
125
|
const opts = parseArgs(rest)
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
product, code, md,
|
|
133
|
-
title: title || undefined,
|
|
126
|
+
const md = readValue(required(opts.md, '--md'))
|
|
127
|
+
const body = {
|
|
128
|
+
md,
|
|
129
|
+
code: opts.code || undefined,
|
|
130
|
+
title: opts.title || undefined,
|
|
134
131
|
status: opts.status || undefined,
|
|
135
|
-
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let result
|
|
135
|
+
if (opts['product-id'] || opts.product_id) {
|
|
136
|
+
const productId = opts['product-id'] || opts.product_id
|
|
137
|
+
result = await api('POST', `/v1/products/${encodeURIComponent(productId)}/hypotheses`, body)
|
|
138
|
+
} else {
|
|
139
|
+
body.product = required(opts.product, '--product or --product-id')
|
|
140
|
+
result = await api('POST', '/v1/hypotheses', body)
|
|
141
|
+
}
|
|
136
142
|
console.log(result.id)
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
async function hypothesisStatus(rest) {
|
|
140
146
|
const opts = parseArgs(rest)
|
|
141
|
-
const ref = required(opts._[0] || opts.hypothesis, 'hypothesis (e.g. chrome-relay/H01)')
|
|
142
147
|
const status = required(opts.status, '--status')
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!hypId)
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
let hypId = opts['hypothesis-id'] || opts.hypothesis_id || opts.id
|
|
149
|
+
let label = hypId
|
|
150
|
+
|
|
151
|
+
if (!hypId) {
|
|
152
|
+
const ref = required(opts._[0] || opts.hypothesis, 'hypothesis id or product/Hxx')
|
|
153
|
+
const [productSlug, code] = splitHypothesisRef(ref)
|
|
154
|
+
const ctx = await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`)
|
|
155
|
+
hypId = ctx.hypothesis?.id
|
|
156
|
+
label = ref
|
|
157
|
+
}
|
|
158
|
+
if (!hypId) throw new Error('hypothesis not found')
|
|
159
|
+
|
|
160
|
+
const result = await api('PATCH', `/v1/hypotheses/${encodeURIComponent(hypId)}/status`, { status })
|
|
161
|
+
console.log(`${label} → ${result.status}`)
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
// =============================================================== runs
|
|
152
165
|
|
|
153
166
|
async function runStart(rest) {
|
|
154
167
|
const opts = parseArgs(rest)
|
|
155
|
-
|
|
156
|
-
const [
|
|
157
|
-
|
|
168
|
+
let result
|
|
169
|
+
const hypId = opts['hypothesis-id'] || opts.hypothesis_id
|
|
170
|
+
if (hypId) {
|
|
171
|
+
result = await api('POST', `/v1/hypotheses/${encodeURIComponent(hypId)}/runs`, {})
|
|
172
|
+
} else {
|
|
173
|
+
const ref = required(opts.hypothesis || opts._[0], '--hypothesis-id <id> or --hypothesis <slug/Hxx>')
|
|
174
|
+
const [productSlug, code] = splitHypothesisRef(ref)
|
|
175
|
+
result = await api('POST', '/v1/runs', { product: productSlug, hypothesis: code })
|
|
176
|
+
}
|
|
158
177
|
console.log(result.id)
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
async function runFinish(rest) {
|
|
162
181
|
const opts = parseArgs(rest)
|
|
163
|
-
const id
|
|
182
|
+
const id = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
|
|
164
183
|
const narrative = readValue(required(opts.narrative, '--narrative'))
|
|
165
|
-
const result = await api('PATCH', `/v1/runs/${id}/finish`, { narrative_md: narrative })
|
|
184
|
+
const result = await api('PATCH', `/v1/runs/${encodeURIComponent(id)}/finish`, { narrative_md: narrative })
|
|
166
185
|
console.log(`${result.id} finished_at=${result.finished_at}`)
|
|
167
186
|
}
|
|
168
187
|
|
|
@@ -170,52 +189,286 @@ async function runFinish(rest) {
|
|
|
170
189
|
|
|
171
190
|
async function logAction(rest) {
|
|
172
191
|
const opts = parseArgs(rest)
|
|
173
|
-
const run = required(opts.run, '--run')
|
|
192
|
+
const run = required(opts['run-id'] || opts.run_id || opts.run, '--run-id')
|
|
174
193
|
const channel = required(opts.channel, '--channel')
|
|
175
194
|
const title = required(opts.title, '--title')
|
|
176
|
-
const result = await api('POST',
|
|
177
|
-
run,
|
|
195
|
+
const result = await api('POST', `/v1/runs/${encodeURIComponent(run)}/actions`, {
|
|
178
196
|
channel,
|
|
179
197
|
title,
|
|
180
|
-
url:
|
|
181
|
-
agentmail_thread_id:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
url: opts.url || undefined,
|
|
199
|
+
agentmail_thread_id: opts['agentmail-thread-id'] || opts['thread-id'] || opts.thread_id || undefined,
|
|
200
|
+
agentmail_inbox_id: opts['agentmail-inbox-id'] || opts.inbox_id || opts.agentmail_inbox_id || undefined,
|
|
201
|
+
recipient: opts.recipient || undefined,
|
|
202
|
+
metadata: opts.metadata ? JSON.parse(readValue(opts.metadata)) : undefined,
|
|
203
|
+
occurred_at: opts['occurred-at'] || opts.occurred_at || undefined,
|
|
185
204
|
})
|
|
186
205
|
console.log(result.id)
|
|
187
206
|
}
|
|
188
207
|
|
|
208
|
+
// ============================================================ mail
|
|
209
|
+
|
|
210
|
+
async function mail(command, rest) {
|
|
211
|
+
if (!command || command === '--help' || command === 'help') return mailUsage()
|
|
212
|
+
if (command === 'setup') return mailSetup(rest)
|
|
213
|
+
if (command === 'send') return mailSend(rest)
|
|
214
|
+
if (command === 'reply') return mailReply(rest, 'reply')
|
|
215
|
+
if (command === 'reply-all') return mailReply(rest, 'reply-all')
|
|
216
|
+
if (command === 'forward') return mailForward(rest)
|
|
217
|
+
if (command === 'threads') return mailThreads(rest)
|
|
218
|
+
if (command === 'thread') return mailThread(rest)
|
|
219
|
+
if (command === 'messages') return mailMessages(rest)
|
|
220
|
+
if (command === 'message') return mailMessage(rest)
|
|
221
|
+
if (command === 'raw') return mailRaw(rest)
|
|
222
|
+
if (command === 'attachment') return mailAttachment(rest)
|
|
223
|
+
if (command === 'request') return mailRequest(rest)
|
|
224
|
+
mailUsage()
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function mailSetup(rest) {
|
|
229
|
+
const opts = parseArgs(rest)
|
|
230
|
+
const existing = loadCredentials() || {}
|
|
231
|
+
if (existing.agentmail_token && existing.agentmail_email && !opts.force) {
|
|
232
|
+
console.log(existing.agentmail_email)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const prefix = required(opts.prefix || defaultInboxPrefix(existing.email), '--prefix')
|
|
236
|
+
const result = await api('POST', '/v1/onboard/agentmail', {
|
|
237
|
+
prefix,
|
|
238
|
+
display_name: opts['display-name'] || opts.display_name || existing.email || prefix,
|
|
239
|
+
purpose: opts.purpose || 'runner',
|
|
240
|
+
})
|
|
241
|
+
saveCredentials({
|
|
242
|
+
...existing,
|
|
243
|
+
agentmail_email: result.agentmail_email || result.email,
|
|
244
|
+
agentmail_inbox_id: result.agentmail_inbox_id || result.inbox_id || result.email,
|
|
245
|
+
agentmail_api_key_id: result.agentmail_api_key_id || result.api_key_id,
|
|
246
|
+
agentmail_token: result.agentmail_token,
|
|
247
|
+
agentmail_saved_at: new Date().toISOString(),
|
|
248
|
+
})
|
|
249
|
+
console.log(result.agentmail_email || result.email)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function mailSend(rest) {
|
|
253
|
+
const opts = parseArgs(rest)
|
|
254
|
+
const creds = requireAgentmailCredentials()
|
|
255
|
+
const to = listOpt(opts.to, '--to')
|
|
256
|
+
const subject = opts.subject || ''
|
|
257
|
+
const body = mailBody(opts, { to, subject })
|
|
258
|
+
const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/send`, body)
|
|
259
|
+
const action = await maybeLogMailAction(opts, {
|
|
260
|
+
kind: 'send',
|
|
261
|
+
defaultTitle: opts.title || `Email: ${subject || to.join(', ')}`,
|
|
262
|
+
recipient: to.join(','),
|
|
263
|
+
subject,
|
|
264
|
+
response: result,
|
|
265
|
+
})
|
|
266
|
+
printJson({ ...result, autark_action_id: action?.id })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function mailReply(rest, mode) {
|
|
270
|
+
const opts = parseArgs(rest)
|
|
271
|
+
const creds = requireAgentmailCredentials()
|
|
272
|
+
const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
|
|
273
|
+
const body = mailBody(opts)
|
|
274
|
+
const endpoint = mode === 'reply-all' ? 'reply-all' : 'reply'
|
|
275
|
+
const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/${endpoint}`, body)
|
|
276
|
+
const action = await maybeLogMailAction(opts, {
|
|
277
|
+
kind: mode,
|
|
278
|
+
defaultTitle: opts.title || `${mode === 'reply-all' ? 'Reply-all' : 'Reply'}: ${messageId}`,
|
|
279
|
+
recipient: listOpt(opts.to).join(','),
|
|
280
|
+
response: result,
|
|
281
|
+
metadata: { message_id: messageId },
|
|
282
|
+
})
|
|
283
|
+
printJson({ ...result, autark_action_id: action?.id })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function mailForward(rest) {
|
|
287
|
+
const opts = parseArgs(rest)
|
|
288
|
+
const creds = requireAgentmailCredentials()
|
|
289
|
+
const messageId = required(opts['message-id'] || opts.message_id || opts._[0], '--message-id')
|
|
290
|
+
const to = listOpt(opts.to, '--to')
|
|
291
|
+
const body = mailBody(opts, { to, subject: opts.subject || '' })
|
|
292
|
+
const result = await agentmailRequest('POST', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/forward`, body)
|
|
293
|
+
const action = await maybeLogMailAction(opts, {
|
|
294
|
+
kind: 'forward',
|
|
295
|
+
defaultTitle: opts.title || `Forward: ${opts.subject || messageId}`,
|
|
296
|
+
recipient: to.join(','),
|
|
297
|
+
response: result,
|
|
298
|
+
metadata: { message_id: messageId },
|
|
299
|
+
})
|
|
300
|
+
printJson({ ...result, autark_action_id: action?.id })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function mailThreads(rest) {
|
|
304
|
+
const opts = parseArgs(rest)
|
|
305
|
+
const creds = requireAgentmailCredentials()
|
|
306
|
+
const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
|
|
307
|
+
printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads${qs}`))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function mailThread(rest) {
|
|
311
|
+
const opts = parseArgs(rest)
|
|
312
|
+
const creds = requireAgentmailCredentials()
|
|
313
|
+
const threadId = required(opts['thread-id'] || opts.thread_id || opts._[0], 'thread_id')
|
|
314
|
+
printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/threads/${encodeURIComponent(threadId)}`))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function mailMessages(rest) {
|
|
318
|
+
const opts = parseArgs(rest)
|
|
319
|
+
const creds = requireAgentmailCredentials()
|
|
320
|
+
const qs = queryString({ limit: opts.limit, page_token: opts['page-token'] || opts.page_token })
|
|
321
|
+
printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages${qs}`))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function mailMessage(rest) {
|
|
325
|
+
const opts = parseArgs(rest)
|
|
326
|
+
const creds = requireAgentmailCredentials()
|
|
327
|
+
const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
|
|
328
|
+
printJson(await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}`))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function mailRaw(rest) {
|
|
332
|
+
const opts = parseArgs(rest)
|
|
333
|
+
const creds = requireAgentmailCredentials()
|
|
334
|
+
const messageId = required(opts['message-id'] || opts.message_id || opts._[0], 'message_id')
|
|
335
|
+
const raw = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/raw`, undefined, { parseJson: false })
|
|
336
|
+
process.stdout.write(raw)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function mailAttachment(rest) {
|
|
340
|
+
const opts = parseArgs(rest)
|
|
341
|
+
const creds = requireAgentmailCredentials()
|
|
342
|
+
const messageId = required(opts['message-id'] || opts.message_id, '--message-id')
|
|
343
|
+
const attachmentId = required(opts['attachment-id'] || opts.attachment_id, '--attachment-id')
|
|
344
|
+
const body = await agentmailRequest('GET', `/inboxes/${encodeURIComponent(creds.inboxId)}/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`, undefined, { parseJson: false })
|
|
345
|
+
if (opts.out) fs.writeFileSync(opts.out, body)
|
|
346
|
+
else process.stdout.write(body)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function mailRequest(rest) {
|
|
350
|
+
const opts = parseArgs(rest)
|
|
351
|
+
const method = (opts.method || opts._[0] || 'GET').toUpperCase()
|
|
352
|
+
const requestPath = required(opts.path || opts._[1], '--path')
|
|
353
|
+
const body = opts.body ? JSON.parse(readValue(opts.body)) : undefined
|
|
354
|
+
const parseJson = opts.raw ? false : true
|
|
355
|
+
const result = await agentmailRequest(method, requestPath.startsWith('/') ? requestPath : `/${requestPath}`, body, { parseJson })
|
|
356
|
+
if (parseJson) printJson(result)
|
|
357
|
+
else process.stdout.write(result)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function mailBody(opts, base = {}) {
|
|
361
|
+
const body = { ...base }
|
|
362
|
+
if (opts.text !== undefined) body.text = readValue(opts.text)
|
|
363
|
+
if (opts.html !== undefined) body.html = readValue(opts.html)
|
|
364
|
+
if (opts.subject !== undefined) body.subject = opts.subject
|
|
365
|
+
for (const key of ['cc', 'bcc', 'reply-to', 'label', 'attachment']) {
|
|
366
|
+
const snake = key.replaceAll('-', '_')
|
|
367
|
+
const value = opts[key] ?? opts[snake]
|
|
368
|
+
if (value !== undefined) body[snake] = listOpt(value)
|
|
369
|
+
}
|
|
370
|
+
if (opts.headers !== undefined) body.headers = JSON.parse(readValue(opts.headers))
|
|
371
|
+
return body
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function maybeLogMailAction(opts, { kind, defaultTitle, recipient, subject, response, metadata = {} }) {
|
|
375
|
+
const runId = opts['run-id'] || opts.run_id || opts.run
|
|
376
|
+
if (!runId) return null
|
|
377
|
+
const creds = requireAgentmailCredentials()
|
|
378
|
+
const threadId = response?.thread_id
|
|
379
|
+
if (!threadId) throw new Error('AgentMail response missing thread_id; refusing to log email action')
|
|
380
|
+
return api('POST', `/v1/runs/${encodeURIComponent(runId)}/actions`, {
|
|
381
|
+
channel: 'email',
|
|
382
|
+
title: opts.title || defaultTitle,
|
|
383
|
+
recipient: recipient || undefined,
|
|
384
|
+
agentmail_thread_id: threadId,
|
|
385
|
+
agentmail_inbox_id: creds.inboxId,
|
|
386
|
+
metadata: compact({
|
|
387
|
+
kind,
|
|
388
|
+
subject,
|
|
389
|
+
message_id: response?.message_id,
|
|
390
|
+
...metadata,
|
|
391
|
+
}),
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function agentmailRequest(method, pathStr, body, { parseJson = true } = {}) {
|
|
396
|
+
const creds = requireAgentmailCredentials()
|
|
397
|
+
const res = await fetch(AGENTMAIL_API + pathStr, {
|
|
398
|
+
method,
|
|
399
|
+
headers: {
|
|
400
|
+
authorization: `Bearer ${creds.token}`,
|
|
401
|
+
'content-type': 'application/json',
|
|
402
|
+
},
|
|
403
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
404
|
+
})
|
|
405
|
+
const text = await res.text()
|
|
406
|
+
let data = text
|
|
407
|
+
if (parseJson) {
|
|
408
|
+
try { data = text ? JSON.parse(text) : null } catch { data = { raw: text } }
|
|
409
|
+
}
|
|
410
|
+
if (!res.ok) {
|
|
411
|
+
const msg = parseJson ? (data?.error || data?.message || JSON.stringify(data)) : text
|
|
412
|
+
throw new Error(`AgentMail ${res.status}: ${msg}`)
|
|
413
|
+
}
|
|
414
|
+
return data
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function requireAgentmailCredentials() {
|
|
418
|
+
const creds = loadCredentials()
|
|
419
|
+
const token = process.env.AGENTMAIL_API_KEY || creds?.agentmail_token
|
|
420
|
+
const email = process.env.AGENTMAIL_EMAIL || creds?.agentmail_email
|
|
421
|
+
const inboxId = process.env.AGENTMAIL_INBOX_ID || creds?.agentmail_inbox_id || email
|
|
422
|
+
if (!token) throw new Error('missing agentmail_token. run: autark mail setup --prefix <name>')
|
|
423
|
+
if (!inboxId) throw new Error('missing agentmail_inbox_id. run: autark mail setup --prefix <name>')
|
|
424
|
+
return { token, email, inboxId }
|
|
425
|
+
}
|
|
426
|
+
|
|
189
427
|
// ============================================================ context
|
|
190
428
|
|
|
191
429
|
async function context(rest) {
|
|
192
430
|
const opts = parseArgs(rest)
|
|
193
|
-
|
|
431
|
+
|
|
432
|
+
if (opts['product-id'] || opts.product_id) {
|
|
433
|
+
const productId = opts['product-id'] || opts.product_id
|
|
434
|
+
return printProductContext(await api('GET', `/v1/products/${encodeURIComponent(productId)}/context`))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (opts['hypothesis-id'] || opts.hypothesis_id) {
|
|
438
|
+
const hypId = opts['hypothesis-id'] || opts.hypothesis_id
|
|
439
|
+
return printHypothesisContext(await api('GET', `/v1/hypotheses/${encodeURIComponent(hypId)}/context`))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const ref = required(opts._[0] || opts.hypothesis, 'product slug, product/Hxx, --product-id, or --hypothesis-id')
|
|
194
443
|
const parts = ref.split('/')
|
|
195
444
|
const productSlug = parts[0]
|
|
196
445
|
const code = parts[1]
|
|
197
446
|
|
|
198
|
-
if (!code) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
447
|
+
if (!code) return printProductContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}`))
|
|
448
|
+
return printHypothesisContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`))
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function printProductContext(r) {
|
|
452
|
+
console.log(`# ${r.product.slug} — ${r.product.name}`)
|
|
453
|
+
console.log(`id: ${r.product.id}`)
|
|
454
|
+
if (r.product.tagline) console.log(`> ${r.product.tagline}`)
|
|
455
|
+
if (r.product.url) console.log(`> ${r.product.url}`)
|
|
456
|
+
console.log(`\n## Brief\n`)
|
|
457
|
+
console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.kushalsm.com)')
|
|
458
|
+
console.log(`\n## Hypotheses (${r.hypotheses.length})\n`)
|
|
459
|
+
if (!r.hypotheses.length) {
|
|
460
|
+
console.log('(none yet — create one with: autark hypothesis create --product-id <product_id> --md @hyp.md)')
|
|
461
|
+
} else {
|
|
462
|
+
for (const h of r.hypotheses) {
|
|
463
|
+
console.log(`- [${h.status}] ${h.code} id=${h.id} — ${h.title} (runs: ${h.run_count})`)
|
|
213
464
|
}
|
|
214
|
-
return
|
|
215
465
|
}
|
|
466
|
+
}
|
|
216
467
|
|
|
217
|
-
|
|
468
|
+
function printHypothesisContext(result) {
|
|
218
469
|
console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
|
|
470
|
+
console.log(`product_id: ${result.product.id}`)
|
|
471
|
+
console.log(`hypothesis_id: ${result.hypothesis.id}`)
|
|
219
472
|
console.log(`Status: ${result.hypothesis.status}\n`)
|
|
220
473
|
console.log(result.hypothesis.hypothesis_md)
|
|
221
474
|
for (const run of result.runs) {
|
|
@@ -228,9 +481,7 @@ async function context(rest) {
|
|
|
228
481
|
console.log(` [${a.channel}] ${a.title}${pointer ? ` → ${pointer}` : ''}`)
|
|
229
482
|
}
|
|
230
483
|
}
|
|
231
|
-
if (run.narrative_md) {
|
|
232
|
-
console.log(`\n${run.narrative_md}`)
|
|
233
|
-
}
|
|
484
|
+
if (run.narrative_md) console.log(`\n${run.narrative_md}`)
|
|
234
485
|
}
|
|
235
486
|
}
|
|
236
487
|
|
|
@@ -265,14 +516,10 @@ function saveCredentials(creds) {
|
|
|
265
516
|
|
|
266
517
|
function loadCredentials() {
|
|
267
518
|
if (!fs.existsSync(CREDS_PATH)) return null
|
|
268
|
-
try {
|
|
269
|
-
return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'))
|
|
270
|
-
} catch {
|
|
271
|
-
return null
|
|
272
|
-
}
|
|
519
|
+
try { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')) } catch { return null }
|
|
273
520
|
}
|
|
274
521
|
|
|
275
|
-
// ============================================================
|
|
522
|
+
// ============================================================ utilities
|
|
276
523
|
|
|
277
524
|
function parseArgs(list) {
|
|
278
525
|
const opts = { _: [] }
|
|
@@ -284,12 +531,11 @@ function parseArgs(list) {
|
|
|
284
531
|
}
|
|
285
532
|
const key = arg.slice(2)
|
|
286
533
|
const next = list[i + 1]
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
534
|
+
const value = (!next || next.startsWith('--')) ? true : next
|
|
535
|
+
if (value !== true) i++
|
|
536
|
+
if (opts[key] === undefined) opts[key] = value
|
|
537
|
+
else if (Array.isArray(opts[key])) opts[key].push(value)
|
|
538
|
+
else opts[key] = [opts[key], value]
|
|
293
539
|
}
|
|
294
540
|
return opts
|
|
295
541
|
}
|
|
@@ -302,12 +548,43 @@ function required(value, label) {
|
|
|
302
548
|
}
|
|
303
549
|
|
|
304
550
|
function readValue(value) {
|
|
305
|
-
if (typeof value === 'string' && value.startsWith('@'))
|
|
306
|
-
return fs.readFileSync(value.slice(1), 'utf8')
|
|
307
|
-
}
|
|
551
|
+
if (typeof value === 'string' && value.startsWith('@')) return fs.readFileSync(value.slice(1), 'utf8')
|
|
308
552
|
return String(value)
|
|
309
553
|
}
|
|
310
554
|
|
|
555
|
+
function listOpt(value, label) {
|
|
556
|
+
if (value === undefined || value === null || value === '') {
|
|
557
|
+
if (label) throw new Error(`${label} is required`)
|
|
558
|
+
return []
|
|
559
|
+
}
|
|
560
|
+
const values = Array.isArray(value) ? value : [value]
|
|
561
|
+
return values.flatMap(v => String(v).split(',')).map(v => v.trim()).filter(Boolean)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function queryString(params) {
|
|
565
|
+
const q = new URLSearchParams()
|
|
566
|
+
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== '') q.set(k, v)
|
|
567
|
+
const s = q.toString()
|
|
568
|
+
return s ? `?${s}` : ''
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function defaultInboxPrefix(email) {
|
|
572
|
+
if (!email) return ''
|
|
573
|
+
return String(email).split('@')[0].toLowerCase().replace(/[^a-z0-9._-]/g, '').replace(/^[._-]+/, '').slice(0, 30)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function compact(obj) {
|
|
577
|
+
const out = {}
|
|
578
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
579
|
+
if (v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && !v.length)) out[k] = v
|
|
580
|
+
}
|
|
581
|
+
return out
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function printJson(value) {
|
|
585
|
+
console.log(JSON.stringify(value, null, 2))
|
|
586
|
+
}
|
|
587
|
+
|
|
311
588
|
function splitHypothesisRef(ref) {
|
|
312
589
|
const m = String(ref).match(/^([\w.-]+)\/(H\d{2})$/)
|
|
313
590
|
if (!m) throw new Error(`expected product/Hxx, got ${ref}`)
|
|
@@ -317,8 +594,26 @@ function splitHypothesisRef(ref) {
|
|
|
317
594
|
// ============================================================ usage
|
|
318
595
|
|
|
319
596
|
function loginUsage() {
|
|
320
|
-
console.log(`autark login send <email>
|
|
321
|
-
|
|
597
|
+
console.log(`autark login send <email>\nautark login verify <email> --code <6-digit-code>`)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function mailUsage() {
|
|
601
|
+
console.log(`autark mail
|
|
602
|
+
|
|
603
|
+
setup --prefix <name> [--force]
|
|
604
|
+
send --to <email[,email]> --subject <s> --text @body.txt [--run-id <id>]
|
|
605
|
+
reply --message-id <id> --text @reply.txt [--run-id <id>]
|
|
606
|
+
reply-all --message-id <id> --text @reply.txt [--run-id <id>]
|
|
607
|
+
forward --message-id <id> --to <email> [--text @body.txt] [--run-id <id>]
|
|
608
|
+
threads [--limit N]
|
|
609
|
+
thread <thread_id>
|
|
610
|
+
messages [--limit N]
|
|
611
|
+
message <message_id>
|
|
612
|
+
raw <message_id>
|
|
613
|
+
attachment --message-id <id> --attachment-id <id> [--out file]
|
|
614
|
+
request <METHOD> <path> [--body @json] [--raw]
|
|
615
|
+
|
|
616
|
+
Mail uses the inbox-scoped token in ~/.autark/credentials.json.`)
|
|
322
617
|
}
|
|
323
618
|
|
|
324
619
|
function usage() {
|
|
@@ -330,17 +625,25 @@ function usage() {
|
|
|
330
625
|
autark me show signed-in user
|
|
331
626
|
|
|
332
627
|
autark product upsert --slug <slug> --name <name> [--url <url>] [--tagline <text>] [--brief @./brief.md] [--visibility private|public]
|
|
333
|
-
autark product list
|
|
628
|
+
autark product list prints slug, visibility, id, name
|
|
334
629
|
|
|
335
|
-
autark hypothesis create --product <
|
|
336
|
-
autark hypothesis
|
|
630
|
+
autark hypothesis create --product-id <id> --md @./hyp.md [--code H01] [--title <t>]
|
|
631
|
+
autark hypothesis create --product <slug> --md @./hyp.md [--code H01] [--title <t>] # alias
|
|
632
|
+
autark hypothesis status --hypothesis-id <id> --status active|inactive|dead
|
|
633
|
+
autark hypothesis status <slug>/<H01> --status active|inactive|dead # alias
|
|
337
634
|
|
|
338
|
-
autark run start --hypothesis <
|
|
339
|
-
autark run
|
|
635
|
+
autark run start --hypothesis-id <id>
|
|
636
|
+
autark run start --hypothesis <slug>/<H01> # alias
|
|
637
|
+
autark run finish --run-id <id> --narrative @./run.md
|
|
340
638
|
|
|
341
|
-
autark log action --run <id> --channel <c> --title <t> [--url <u>]
|
|
342
|
-
[--agentmail-thread-id <uuid>] [--recipient <email>]
|
|
639
|
+
autark log action --run-id <id> --channel <c> --title <t> [--url <u>]
|
|
640
|
+
[--agentmail-thread-id <uuid>] [--agentmail-inbox-id <inbox>] [--recipient <email>]
|
|
343
641
|
[--metadata @./meta.json]
|
|
344
642
|
|
|
643
|
+
autark mail setup/send/reply/thread/messages/... AgentMail via autark credentials
|
|
644
|
+
|
|
645
|
+
autark context --product-id <id>
|
|
646
|
+
autark context --hypothesis-id <id>
|
|
647
|
+
autark context <slug>
|
|
345
648
|
autark context <slug>/<H01>`)
|
|
346
649
|
}
|
package/package.json
CHANGED