autark-cli 0.1.7 → 0.1.9
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 +489 -17
- package/package.json +2 -2
package/autark.mjs
CHANGED
|
@@ -9,20 +9,149 @@ import fs from 'node:fs'
|
|
|
9
9
|
import os from 'node:os'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import process from 'node:process'
|
|
12
|
+
import { spawn, spawnSync as childSpawnSync } from 'node:child_process'
|
|
12
13
|
|
|
13
14
|
const API = process.env.AUTARK_API_URL || 'https://autark-api.kushalsokke.workers.dev'
|
|
14
15
|
const AGENTMAIL_API = process.env.AGENTMAIL_API_URL || 'https://api.agentmail.to/v0'
|
|
15
16
|
const AUTARK_HOME = process.env.AUTARK_HOME || path.join(os.homedir(), '.autark')
|
|
16
17
|
const CREDS_PATH = process.env.AUTARK_CREDENTIALS || path.join(AUTARK_HOME, 'credentials.json')
|
|
17
18
|
|
|
19
|
+
// Constants used by `autark update` — hoisted before main() runs because
|
|
20
|
+
// async function bodies execute synchronously until their first await; main()
|
|
21
|
+
// dispatches to update() which references these in its first synchronous
|
|
22
|
+
// loop, so they need to be initialized at module-load time before the
|
|
23
|
+
// main().catch() call below.
|
|
24
|
+
const RAW_RUNTIME_BASE = process.env.AUTARK_RUNTIME_RAW_BASE
|
|
25
|
+
|| 'https://autark.kushalsm.com/runtime'
|
|
26
|
+
const SKILL_NAMES = ['autark', 'plumcake', 'chrome-relay', 'email', 'outreach', 'email-finder']
|
|
27
|
+
const ECOSYSTEM_CLIS = ['autark-cli', 'plumcake-cli', 'chrome-relay']
|
|
28
|
+
const PROGRAM_FILES = ['start.md', 'double-down.md', 'check.md', 'followup.md']
|
|
29
|
+
|
|
18
30
|
const args = process.argv.slice(2)
|
|
19
|
-
main()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
main()
|
|
32
|
+
.then(() => maybeNudge())
|
|
33
|
+
.catch(err => {
|
|
34
|
+
console.error(err.message)
|
|
35
|
+
maybeNudge()
|
|
36
|
+
process.exit(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ============================================================== nudge
|
|
40
|
+
// Tail-print "[autark] update available" on stderr at the end of every
|
|
41
|
+
// command if the cached cloud state doesn't match local. The actual check
|
|
42
|
+
// runs in a detached child so it doesn't slow down the current command
|
|
43
|
+
// (the result lands in the cache for the NEXT invocation to read).
|
|
44
|
+
//
|
|
45
|
+
// Disable in `autark update` itself (don't nudge the user to do the thing
|
|
46
|
+
// they're already doing) and when AUTARK_NO_NUDGE=1.
|
|
47
|
+
|
|
48
|
+
const NUDGE_CACHE = path.join(
|
|
49
|
+
process.env.AUTARK_HOME || path.join(os.homedir(), '.autark'),
|
|
50
|
+
'runtime',
|
|
51
|
+
'update-check.json',
|
|
52
|
+
)
|
|
53
|
+
const NUDGE_TTL_MS = 24 * 60 * 60 * 1000 // 24h
|
|
54
|
+
|
|
55
|
+
function maybeNudge() {
|
|
56
|
+
try {
|
|
57
|
+
if (process.env.AUTARK_NO_NUDGE === '1') return
|
|
58
|
+
// The first arg group governs whether we print. update / login / logout
|
|
59
|
+
// don't need a nudge (login/logout aren't authenticated yet; update is
|
|
60
|
+
// already the action).
|
|
61
|
+
const group = args[0]
|
|
62
|
+
if (group === 'update' || group === 'login' || group === 'logout' || group === 'help'
|
|
63
|
+
|| group === '--help' || group === '-h' || group === '--version' || !group) return
|
|
64
|
+
|
|
65
|
+
// Schedule a detached refresh — this exits immediately, the child
|
|
66
|
+
// writes to the cache file in the background.
|
|
67
|
+
scheduleBackgroundRefresh()
|
|
68
|
+
|
|
69
|
+
// Read whatever's in the cache RIGHT NOW (possibly stale) and decide
|
|
70
|
+
// whether to print. The cache holds the cloud state from the previous
|
|
71
|
+
// refresh; first invocation sees no cache and prints nothing.
|
|
72
|
+
const cache = readNudgeCache()
|
|
73
|
+
if (!cache) return
|
|
74
|
+
const localVersion = readLocalVersion()
|
|
75
|
+
const localInbox = (loadCredentials() || {}).agentmail_email || ''
|
|
76
|
+
const localCliVer = readInstalledVersion('autark-cli') || '0.0.0'
|
|
77
|
+
|
|
78
|
+
const runtimeBehind = cache.runtime_version && cache.runtime_version !== localVersion
|
|
79
|
+
const inboxBehind = cache.agentmail_email && cache.agentmail_email !== localInbox
|
|
80
|
+
const cliBehind = cache.autark_cli_latest && cmpVersions(localCliVer, cache.autark_cli_latest) < 0
|
|
81
|
+
|
|
82
|
+
if (runtimeBehind || inboxBehind || cliBehind) {
|
|
83
|
+
// Throttle: at most one nudge per CLI session (which is one
|
|
84
|
+
// invocation, so always true). Mark cache.nudged_at so future
|
|
85
|
+
// ticks within the same minute don't re-print (rapid agent loops).
|
|
86
|
+
const now = Date.now()
|
|
87
|
+
if (!cache.nudged_at || (now - cache.nudged_at) > 60_000) {
|
|
88
|
+
const reasons = []
|
|
89
|
+
if (cliBehind) reasons.push(`cli ${localCliVer} → ${cache.autark_cli_latest}`)
|
|
90
|
+
if (runtimeBehind) reasons.push(`runtime ${localVersion?.slice(0, 8) || '<none>'} → ${cache.runtime_version?.slice(0, 8) || '?'}`)
|
|
91
|
+
if (inboxBehind) reasons.push('inbox')
|
|
92
|
+
process.stderr.write(`[autark] update available (${reasons.join(', ')}) — run: autark update\n`)
|
|
93
|
+
cache.nudged_at = now
|
|
94
|
+
writeNudgeCache(cache)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Nudge errors are silent. The CLI does its real work; this is decoration.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readNudgeCache() {
|
|
103
|
+
try {
|
|
104
|
+
if (!fs.existsSync(NUDGE_CACHE)) return null
|
|
105
|
+
return JSON.parse(fs.readFileSync(NUDGE_CACHE, 'utf8'))
|
|
106
|
+
} catch { return null }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeNudgeCache(obj) {
|
|
110
|
+
try {
|
|
111
|
+
fs.mkdirSync(path.dirname(NUDGE_CACHE), { recursive: true })
|
|
112
|
+
fs.writeFileSync(NUDGE_CACHE, JSON.stringify(obj))
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function scheduleBackgroundRefresh() {
|
|
117
|
+
try {
|
|
118
|
+
const cache = readNudgeCache()
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
if (cache?.fetched_at && (now - cache.fetched_at) < NUDGE_TTL_MS) return
|
|
121
|
+
// Spawn a detached child that fetches /v1/me + npm latest, writes the
|
|
122
|
+
// cache, exits. Doesn't block the parent.
|
|
123
|
+
const child = spawn(
|
|
124
|
+
process.argv[0],
|
|
125
|
+
[process.argv[1], '__nudge_refresh__'],
|
|
126
|
+
{ detached: true, stdio: 'ignore', env: { ...process.env, AUTARK_NO_NUDGE: '1' } },
|
|
127
|
+
)
|
|
128
|
+
child.unref()
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Invoked by scheduleBackgroundRefresh as a detached child. Refreshes the
|
|
133
|
+
// cache then exits. Hidden internal subcommand — not in the usage doc.
|
|
134
|
+
async function nudgeRefresh() {
|
|
135
|
+
try {
|
|
136
|
+
const creds = loadCredentials() || {}
|
|
137
|
+
if (!creds.token) return
|
|
138
|
+
const me = await api('GET', '/v1/me')
|
|
139
|
+
const cliLatest = await fetchNpmLatest('autark-cli')
|
|
140
|
+
const cache = readNudgeCache() || {}
|
|
141
|
+
writeNudgeCache({
|
|
142
|
+
...cache,
|
|
143
|
+
runtime_version: me.runtime_version,
|
|
144
|
+
agentmail_email: me.agentmail_email,
|
|
145
|
+
autark_cli_latest: cliLatest,
|
|
146
|
+
fetched_at: Date.now(),
|
|
147
|
+
})
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
23
150
|
|
|
24
151
|
async function main() {
|
|
25
152
|
const [group, command, ...rest] = args
|
|
153
|
+
// Hidden subcommand used by the background nudge refresh.
|
|
154
|
+
if (group === '__nudge_refresh__') return nudgeRefresh()
|
|
26
155
|
if (!group || group === 'help' || group === '--help' || group === '-h') return usage()
|
|
27
156
|
|
|
28
157
|
if (group === 'login') {
|
|
@@ -57,6 +186,8 @@ async function main() {
|
|
|
57
186
|
return context([command, ...rest].filter(Boolean))
|
|
58
187
|
}
|
|
59
188
|
if (group === 'mail') return mail(command, rest)
|
|
189
|
+
if (group === 'settings') return settings(command, rest)
|
|
190
|
+
if (group === 'update') return update(rest)
|
|
60
191
|
|
|
61
192
|
usage()
|
|
62
193
|
process.exit(1)
|
|
@@ -100,6 +231,285 @@ async function me() {
|
|
|
100
231
|
console.log(`${result.email} (${result.id})`)
|
|
101
232
|
}
|
|
102
233
|
|
|
234
|
+
// ============================================================== settings
|
|
235
|
+
// Account-wide settings the agent reads at run start.
|
|
236
|
+
|
|
237
|
+
async function settings(command, rest) {
|
|
238
|
+
if (!command || command === 'show') return settingsShow(rest)
|
|
239
|
+
if (command === '--help' || command === '-h') return settingsUsage()
|
|
240
|
+
throw new Error(`unknown settings subcommand: ${command}`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function settingsShow(rest) {
|
|
244
|
+
const opts = parseArgs(rest)
|
|
245
|
+
const result = await api('GET', '/v1/settings')
|
|
246
|
+
const s = result?.settings || {}
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
console.log(JSON.stringify(s, null, 2))
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
// Plain key=value output the prompt can grep against. Empty values render
|
|
252
|
+
// as `<unset>` so the agent can branch.
|
|
253
|
+
const fmt = (v) => (v === null || v === undefined || v === '') ? '<unset>' : v
|
|
254
|
+
console.log(`scheduler_link ${fmt(s.scheduler_link)}`)
|
|
255
|
+
console.log(`personal_link ${fmt(s.personal_link)}`)
|
|
256
|
+
console.log(`agentmail_email ${fmt(s.agentmail_email)}`)
|
|
257
|
+
console.log(`custom_domain ${fmt(s.custom_domain)}`)
|
|
258
|
+
console.log(`domain_status ${fmt(s.custom_domain_status)}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function settingsUsage() {
|
|
262
|
+
console.log('usage:')
|
|
263
|
+
console.log(' autark settings show [--json] account-wide settings (scheduler link, inbox, custom domain)')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ================================================================ update
|
|
267
|
+
// `autark update` — keep the laptop in sync with the cloud canon.
|
|
268
|
+
//
|
|
269
|
+
// Steps (see docs/autark-update.md):
|
|
270
|
+
// 0. Self-update the CLI ecosystem (autark-cli + plumcake-cli + chrome-relay)
|
|
271
|
+
// via pnpm. Re-exec autark if its own version moved.
|
|
272
|
+
// 1. GET /v1/me — compare runtime_version + agentmail_email against local.
|
|
273
|
+
// 2. If runtime_version differs: curl the canonical files from the repo,
|
|
274
|
+
// refresh the 6 kstack skills via pnpm dlx, reload launchd, stamp the
|
|
275
|
+
// new version into ~/.autark/runtime/version.txt.
|
|
276
|
+
// 3. If agentmail_email differs: POST /v1/inbox/rotate to re-mint a fresh
|
|
277
|
+
// inbox-scoped key, rewrite credentials.json.
|
|
278
|
+
//
|
|
279
|
+
// Rollback on partial failure: the version stamp is only written if every
|
|
280
|
+
// step succeeded. "Either fully on version X or fully on version Y."
|
|
281
|
+
//
|
|
282
|
+
// AUTARK_UPDATE_DRY_RUN=1 prints what would happen, doesn't execute.
|
|
283
|
+
// AUTARK_UPDATE_OFFLINE=1 skips the self-update step (for tests).
|
|
284
|
+
// AUTARK_HOME overrides the install root (for sandbox tests).
|
|
285
|
+
|
|
286
|
+
async function update(rest) {
|
|
287
|
+
const opts = parseArgs(rest)
|
|
288
|
+
const dryRun = !!process.env.AUTARK_UPDATE_DRY_RUN || !!opts['dry-run']
|
|
289
|
+
const skipSelfUpdate = !!process.env.AUTARK_UPDATE_OFFLINE
|
|
290
|
+
const log = (msg) => console.log(msg)
|
|
291
|
+
const sub = (msg) => console.log(` ${msg}`)
|
|
292
|
+
|
|
293
|
+
log('autark update')
|
|
294
|
+
|
|
295
|
+
// ---- Step 0: self-update CLI ecosystem ---------------------------------
|
|
296
|
+
if (!skipSelfUpdate) {
|
|
297
|
+
log('checking CLI ecosystem versions…')
|
|
298
|
+
const upgrades = []
|
|
299
|
+
for (const pkg of ECOSYSTEM_CLIS) {
|
|
300
|
+
const installed = readInstalledVersion(pkg)
|
|
301
|
+
const latest = await fetchNpmLatest(pkg)
|
|
302
|
+
if (!latest) { sub(`! ${pkg}: could not reach npm registry, skipping`); continue }
|
|
303
|
+
if (!installed || cmpVersions(installed, latest) < 0) {
|
|
304
|
+
upgrades.push({ pkg, installed, latest })
|
|
305
|
+
sub(`${pkg}: ${installed || '(not installed)'} → ${latest}`)
|
|
306
|
+
} else {
|
|
307
|
+
sub(`${pkg}: ${installed} (current)`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (upgrades.length && !dryRun) {
|
|
312
|
+
log(`upgrading ${upgrades.length} CLI(s) via pnpm…`)
|
|
313
|
+
const args = ['add', '-g', ...upgrades.map((u) => `${u.pkg}@latest`)]
|
|
314
|
+
const res = await spawnSync('pnpm', args)
|
|
315
|
+
if (res.status !== 0) {
|
|
316
|
+
console.error(' pnpm add -g failed; aborting (rollback). Stay on previous version.')
|
|
317
|
+
process.exit(2)
|
|
318
|
+
}
|
|
319
|
+
// chrome-relay's npm package ships with a native-host installer that
|
|
320
|
+
// registers the host with Chrome at the OS level. Refresh after upgrade.
|
|
321
|
+
if (upgrades.some((u) => u.pkg === 'chrome-relay')) {
|
|
322
|
+
sub('refreshing chrome-relay native host…')
|
|
323
|
+
await spawnSync('chrome-relay', ['install']) // non-fatal if it fails
|
|
324
|
+
}
|
|
325
|
+
// If autark-cli itself was upgraded, re-exec with the new binary so the
|
|
326
|
+
// rest of the run uses the new code.
|
|
327
|
+
if (upgrades.some((u) => u.pkg === 'autark-cli')) {
|
|
328
|
+
sub('re-exec autark with new binary…')
|
|
329
|
+
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
330
|
+
stdio: 'inherit',
|
|
331
|
+
env: { ...process.env, AUTARK_UPDATE_OFFLINE: '1' }, // don't re-self-update
|
|
332
|
+
})
|
|
333
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
334
|
+
return // hand off to child
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- Step 1+2: compare /v1/me + refresh runtime + credentials ---------
|
|
340
|
+
log('fetching /v1/me…')
|
|
341
|
+
const me = await api('GET', '/v1/me')
|
|
342
|
+
const localVersion = readLocalVersion()
|
|
343
|
+
const localInbox = (loadCredentials() || {}).agentmail_email || ''
|
|
344
|
+
|
|
345
|
+
const runtimeBehind = me.runtime_version && me.runtime_version !== localVersion
|
|
346
|
+
const inboxBehind = me.agentmail_email && me.agentmail_email !== localInbox
|
|
347
|
+
|
|
348
|
+
sub(`runtime_version local=${localVersion || '<none>'} cloud=${me.runtime_version} ${runtimeBehind ? '→ refresh' : 'ok'}`)
|
|
349
|
+
sub(`agentmail_email local=${localInbox || '<none>'} cloud=${me.agentmail_email} ${inboxBehind ? '→ rotate' : 'ok'}`)
|
|
350
|
+
|
|
351
|
+
if (!runtimeBehind && !inboxBehind) {
|
|
352
|
+
log('already up to date')
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
if (dryRun) {
|
|
356
|
+
log(`(dry-run) would refresh runtime=${runtimeBehind} inbox=${inboxBehind}`)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Refresh runtime files + skills (all-or-nothing for rollback safety).
|
|
361
|
+
if (runtimeBehind) {
|
|
362
|
+
try {
|
|
363
|
+
log('refreshing runtime files…')
|
|
364
|
+
await refreshRuntimeFiles()
|
|
365
|
+
log('refreshing kstack skills…')
|
|
366
|
+
await refreshSkills()
|
|
367
|
+
log('reloading launchd…')
|
|
368
|
+
await reloadLaunchd()
|
|
369
|
+
// Only stamp the version AFTER every step succeeded.
|
|
370
|
+
writeLocalVersion(me.runtime_version)
|
|
371
|
+
sub(`stamped ~/.autark/runtime/version.txt → ${me.runtime_version}`)
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(` runtime refresh failed: ${e.message}`)
|
|
374
|
+
console.error(' rolled back: version stamp NOT written. Re-run autark update.')
|
|
375
|
+
process.exit(2)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Rotate credentials (separate from runtime refresh — independent stream).
|
|
380
|
+
if (inboxBehind) {
|
|
381
|
+
log('rotating AgentMail credentials…')
|
|
382
|
+
const rotated = await api('POST', '/v1/inbox/rotate', {})
|
|
383
|
+
saveCredentials({
|
|
384
|
+
...loadCredentials(),
|
|
385
|
+
agentmail_email: rotated.agentmail_email,
|
|
386
|
+
agentmail_inbox_id: rotated.agentmail_inbox_id,
|
|
387
|
+
agentmail_token: rotated.agentmail_token,
|
|
388
|
+
rotated_at: new Date().toISOString(),
|
|
389
|
+
})
|
|
390
|
+
sub(`new inbox: ${rotated.agentmail_email}`)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
log(`autark updated → ${me.runtime_version}`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// -----------------------------------------------------------------------
|
|
397
|
+
// update() helpers — kept inline because they're only used by `autark update`.
|
|
398
|
+
|
|
399
|
+
function readInstalledVersion(pkg) {
|
|
400
|
+
// Locate the global pnpm install path and read package.json's version.
|
|
401
|
+
try {
|
|
402
|
+
const child = childSpawnSync('pnpm', ['root', '-g'], { encoding: 'utf8' })
|
|
403
|
+
if (child.status !== 0) return null
|
|
404
|
+
const root = child.stdout.toString().trim()
|
|
405
|
+
const pj = path.join(root, pkg, 'package.json')
|
|
406
|
+
if (!fs.existsSync(pj)) return null
|
|
407
|
+
return JSON.parse(fs.readFileSync(pj, 'utf8')).version || null
|
|
408
|
+
} catch {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function fetchNpmLatest(pkg) {
|
|
414
|
+
try {
|
|
415
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`)
|
|
416
|
+
if (!res.ok) return null
|
|
417
|
+
const j = await res.json()
|
|
418
|
+
return j?.version || null
|
|
419
|
+
} catch {
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Tiny semver compare — handles N.N.N strings. Returns -1/0/1.
|
|
425
|
+
function cmpVersions(a, b) {
|
|
426
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0)
|
|
427
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0)
|
|
428
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
429
|
+
const x = pa[i] || 0, y = pb[i] || 0
|
|
430
|
+
if (x !== y) return x < y ? -1 : 1
|
|
431
|
+
}
|
|
432
|
+
return 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readLocalVersion() {
|
|
436
|
+
const p = path.join(AUTARK_HOME, 'runtime', 'version.txt')
|
|
437
|
+
if (!fs.existsSync(p)) return ''
|
|
438
|
+
return fs.readFileSync(p, 'utf8').trim()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function writeLocalVersion(v) {
|
|
442
|
+
const p = path.join(AUTARK_HOME, 'runtime', 'version.txt')
|
|
443
|
+
fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
444
|
+
fs.writeFileSync(p, v + '\n')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function curlFile(url, dest) {
|
|
448
|
+
const res = await fetch(url)
|
|
449
|
+
if (!res.ok) throw new Error(`${res.status} fetching ${url}`)
|
|
450
|
+
const body = await res.text()
|
|
451
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
452
|
+
fs.writeFileSync(dest, body)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function refreshRuntimeFiles() {
|
|
456
|
+
const runtime = path.join(AUTARK_HOME, 'runtime')
|
|
457
|
+
fs.mkdirSync(path.join(runtime, 'programs'), { recursive: true })
|
|
458
|
+
await curlFile(`${RAW_RUNTIME_BASE}/agent_keepalive.py`, path.join(runtime, 'agent_keepalive.py'))
|
|
459
|
+
fs.chmodSync(path.join(runtime, 'agent_keepalive.py'), 0o755)
|
|
460
|
+
for (const f of PROGRAM_FILES) {
|
|
461
|
+
await curlFile(`${RAW_RUNTIME_BASE}/programs/${f}`, path.join(runtime, 'programs', f))
|
|
462
|
+
}
|
|
463
|
+
const agentSh = path.join(AUTARK_HOME, 'agent.sh')
|
|
464
|
+
await curlFile(`${RAW_RUNTIME_BASE}/agent.sh`, agentSh)
|
|
465
|
+
fs.chmodSync(agentSh, 0o755)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function refreshSkills() {
|
|
469
|
+
for (const name of SKILL_NAMES) {
|
|
470
|
+
const res = await spawnSync('pnpm', ['dlx', '--silent', 'skills', 'add', `kiluazen/skills@${name}`, '-y', '-g'])
|
|
471
|
+
if (res.status !== 0) throw new Error(`skill ${name} install failed`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function reloadLaunchd() {
|
|
476
|
+
if (process.platform !== 'darwin') return
|
|
477
|
+
// Safety: only touch launchd when AUTARK_HOME is the canonical install
|
|
478
|
+
// location. Sandbox tests (AUTARK_HOME=/tmp/...) must never rewrite the
|
|
479
|
+
// global plist — it would point launchd at a tempdir that disappears.
|
|
480
|
+
const canonicalHome = path.join(os.homedir(), '.autark')
|
|
481
|
+
if (path.resolve(AUTARK_HOME) !== path.resolve(canonicalHome)) {
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.autark.runner.plist')
|
|
485
|
+
if (!fs.existsSync(plist)) return // user hasn't installed launchd unit
|
|
486
|
+
// Re-render plist from template (template was refreshed alongside agent.sh).
|
|
487
|
+
const tmplUrl = `${RAW_RUNTIME_BASE}/com.autark.runner.plist.tmpl`
|
|
488
|
+
const tmplRes = await fetch(tmplUrl)
|
|
489
|
+
if (tmplRes.ok) {
|
|
490
|
+
const tmpl = await tmplRes.text()
|
|
491
|
+
const agentSh = path.join(AUTARK_HOME, 'agent.sh')
|
|
492
|
+
const agentLog = path.join(AUTARK_HOME, 'agent.log')
|
|
493
|
+
const rendered = tmpl
|
|
494
|
+
.replaceAll('${HOME_AUTARK}', AUTARK_HOME)
|
|
495
|
+
.replaceAll('${AGENT_SH}', agentSh)
|
|
496
|
+
.replaceAll('${AGENT_LOG}', agentLog)
|
|
497
|
+
fs.writeFileSync(plist, rendered)
|
|
498
|
+
}
|
|
499
|
+
await spawnSync('launchctl', ['unload', plist])
|
|
500
|
+
await spawnSync('launchctl', ['load', plist])
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Promise-wrapped spawn — returns { status: number }. stdio inherited so the
|
|
504
|
+
// user sees pnpm's output. Used for self-update + skills refresh.
|
|
505
|
+
function spawnSync(cmd, args) {
|
|
506
|
+
return new Promise((resolve) => {
|
|
507
|
+
const child = spawn(cmd, args, { stdio: 'inherit' })
|
|
508
|
+
child.on('exit', (code) => resolve({ status: code ?? 0 }))
|
|
509
|
+
child.on('error', () => resolve({ status: 1 }))
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
103
513
|
// =========================================================== products
|
|
104
514
|
|
|
105
515
|
async function productUpsert(rest) {
|
|
@@ -458,30 +868,92 @@ async function context(rest) {
|
|
|
458
868
|
return printHypothesisContext(await api('GET', `/v1/context/${encodeURIComponent(productSlug)}/${encodeURIComponent(code)}`))
|
|
459
869
|
}
|
|
460
870
|
|
|
871
|
+
// Output shape: a structured key-block (one fact per line, stable dotted-path
|
|
872
|
+
// keys) first, then a `---` separator, then the narrative prose. Agents can
|
|
873
|
+
// `grep '^action\.[a-f0-9]+\.meeting_link_sent_at'` for clean structured
|
|
874
|
+
// access; the narrative is still there for context.
|
|
875
|
+
|
|
876
|
+
function kv(k, v) {
|
|
877
|
+
if (v === undefined || v === null || v === '') return
|
|
878
|
+
// Single line, value as-is. No quoting — values with spaces are fine; the
|
|
879
|
+
// first whitespace run after the key marks the boundary.
|
|
880
|
+
console.log(`${k.padEnd(40)} ${v}`)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function actionShortId(id) {
|
|
884
|
+
return String(id || '').slice(0, 8)
|
|
885
|
+
}
|
|
886
|
+
|
|
461
887
|
function printProductContext(r) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
888
|
+
// ---- structured key-block ----
|
|
889
|
+
kv('product.id', r.product.id)
|
|
890
|
+
kv('product.slug', r.product.slug)
|
|
891
|
+
kv('product.name', r.product.name)
|
|
892
|
+
kv('product.tagline', r.product.tagline)
|
|
893
|
+
kv('product.url', r.product.url)
|
|
894
|
+
kv('product.visibility', r.product.visibility)
|
|
895
|
+
kv('product.hypothesis_count', (r.hypotheses || []).length)
|
|
896
|
+
for (const h of (r.hypotheses || [])) {
|
|
897
|
+
kv(`hypothesis.${h.code}.id`, h.id)
|
|
898
|
+
kv(`hypothesis.${h.code}.title`, h.title)
|
|
899
|
+
kv(`hypothesis.${h.code}.status`, h.status)
|
|
900
|
+
kv(`hypothesis.${h.code}.run_count`, h.run_count)
|
|
901
|
+
}
|
|
902
|
+
console.log('---')
|
|
903
|
+
// ---- narrative ----
|
|
904
|
+
console.log(`# ${r.product.slug} — ${r.product.name}\n`)
|
|
905
|
+
if (r.product.tagline) console.log(`> ${r.product.tagline}\n`)
|
|
906
|
+
console.log(`## Brief\n`)
|
|
467
907
|
console.log(r.product.brief?.trim() || '(no brief set — owner can add one at https://autark.kushalsm.com)')
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
console.log('(none yet — create one with: autark hypothesis create --product-id <product_id> --md @hyp.md)')
|
|
908
|
+
if (!r.hypotheses?.length) {
|
|
909
|
+
console.log(`\n## Hypotheses\n\n(none yet — create one with: autark hypothesis create --product-id <product_id> --md @hyp.md)`)
|
|
471
910
|
} else {
|
|
911
|
+
console.log(`\n## Hypotheses (${r.hypotheses.length})\n`)
|
|
472
912
|
for (const h of r.hypotheses) {
|
|
473
|
-
console.log(`- [${h.status}] ${h.code}
|
|
913
|
+
console.log(`- [${h.status}] ${h.code} — ${h.title} (runs: ${h.run_count})`)
|
|
474
914
|
}
|
|
475
915
|
}
|
|
476
916
|
}
|
|
477
917
|
|
|
478
918
|
function printHypothesisContext(result) {
|
|
919
|
+
// ---- structured key-block ----
|
|
920
|
+
kv('product.id', result.product.id)
|
|
921
|
+
kv('product.slug', result.product.slug)
|
|
922
|
+
kv('hypothesis.id', result.hypothesis.id)
|
|
923
|
+
kv('hypothesis.code', result.hypothesis.code)
|
|
924
|
+
kv('hypothesis.title', result.hypothesis.title)
|
|
925
|
+
kv('hypothesis.status', result.hypothesis.status)
|
|
926
|
+
for (const run of (result.runs || [])) {
|
|
927
|
+
const shortRun = actionShortId(run.id)
|
|
928
|
+
kv(`run.${shortRun}.id`, run.id)
|
|
929
|
+
kv(`run.${shortRun}.run_number`, run.run_number)
|
|
930
|
+
kv(`run.${shortRun}.started_at`, run.started_at)
|
|
931
|
+
if (run.finished_at) kv(`run.${shortRun}.finished_at`, run.finished_at)
|
|
932
|
+
for (const a of (run.actions || [])) {
|
|
933
|
+
const shortA = actionShortId(a.id)
|
|
934
|
+
kv(`action.${shortA}.id`, a.id)
|
|
935
|
+
kv(`action.${shortA}.run_id`, run.id)
|
|
936
|
+
kv(`action.${shortA}.channel`, a.channel)
|
|
937
|
+
kv(`action.${shortA}.title`, a.title)
|
|
938
|
+
if (a.url) kv(`action.${shortA}.url`, a.url)
|
|
939
|
+
if (a.recipient) kv(`action.${shortA}.recipient`, a.recipient)
|
|
940
|
+
if (a.agentmail_thread_id) kv(`action.${shortA}.agentmail_thread_id`, a.agentmail_thread_id)
|
|
941
|
+
if (a.agentmail_inbox_id) kv(`action.${shortA}.agentmail_inbox_id`, a.agentmail_inbox_id)
|
|
942
|
+
if (a.occurred_at) kv(`action.${shortA}.occurred_at`, a.occurred_at)
|
|
943
|
+
// Spread well-known metadata keys to their own lines for greppability.
|
|
944
|
+
const md = a.metadata || {}
|
|
945
|
+
for (const [mk, mv] of Object.entries(md)) {
|
|
946
|
+
if (typeof mv === 'string' || typeof mv === 'number' || typeof mv === 'boolean') {
|
|
947
|
+
kv(`action.${shortA}.metadata.${mk}`, mv)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
console.log('---')
|
|
953
|
+
// ---- narrative ----
|
|
479
954
|
console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
|
|
480
|
-
console.log(`product_id: ${result.product.id}`)
|
|
481
|
-
console.log(`hypothesis_id: ${result.hypothesis.id}`)
|
|
482
|
-
console.log(`Status: ${result.hypothesis.status}\n`)
|
|
483
955
|
console.log(result.hypothesis.hypothesis_md)
|
|
484
|
-
for (const run of result.runs) {
|
|
956
|
+
for (const run of (result.runs || [])) {
|
|
485
957
|
console.log(`\n\n## Run ${run.run_number} (${run.id})`)
|
|
486
958
|
console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}`)
|
|
487
959
|
if (run.actions?.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autark-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
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",
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
}
|
|
22
|
-
}
|
|
22
|
+
}
|