agent-afk 5.5.1 → 5.6.1
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/dist/agent/session/agent-session.d.ts +2 -0
- package/dist/agent/types/config-types.d.ts +1 -0
- package/dist/cli.mjs +287 -287
- package/dist/index.mjs +108 -108
- package/dist/postinstall.mjs +122 -8
- package/dist/telegram.mjs +124 -124
- package/package.json +1 -1
- package/scripts/postinstall.mjs +122 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-afk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"description": "Open-source coding-agent harness you can actually change — own the loop (prompts, gates, routing, skills, terminal states), use any model, run long tasks while you're away.",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Always exits 0 — never fails an install.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { execSync } from 'node:child_process';
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
|
|
@@ -35,6 +35,11 @@ export function detectPathGap(prefix, pathEnv) {
|
|
|
35
35
|
* given file. All errors are silently discarded — this is best-effort cleanup
|
|
36
36
|
* that must never fail an install.
|
|
37
37
|
*
|
|
38
|
+
* NOTE: the install flow no longer calls this. SIGTERM'ing a manually-started
|
|
39
|
+
* bot left it dead with nothing to relaunch it (postinstall cannot reliably
|
|
40
|
+
* spawn a detached long-lived process). The main block now NOTIFIES instead
|
|
41
|
+
* (see isManualBotRunning). Retained as a tested, reusable utility.
|
|
42
|
+
*
|
|
38
43
|
* @param {string} pidFilePath - Full path to the PID file to read.
|
|
39
44
|
* @param {function} [killFn] - Injectable kill function; defaults to process.kill.
|
|
40
45
|
* Pass a stub in tests to avoid needing process.kill.
|
|
@@ -50,6 +55,88 @@ export function killStaleDaemon(pidFilePath, killFn = process.kill) {
|
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Read-only probe: is a *manually-started* telegram bot still alive? Only the
|
|
60
|
+
* `afk telegram start` path records a child PID in bot.pid; a launchd-supervised
|
|
61
|
+
* bot does NOT (launchd owns its PID — see src/service/launchd/paths.ts), so a
|
|
62
|
+
* non-null result here always means a manual bot — exactly the instance the
|
|
63
|
+
* launchd kickstart below cannot reach.
|
|
64
|
+
*
|
|
65
|
+
* Unlike the manager's isRunning(), this does NOT unlink a stale PID file:
|
|
66
|
+
* postinstall must not mutate runtime state. Returns the live PID, or null when
|
|
67
|
+
* the file is missing/malformed or the process is gone.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} pidFilePath
|
|
70
|
+
* @param {function} [probeFn] - Injectable existence probe; defaults to process.kill.
|
|
71
|
+
* @returns {number|null}
|
|
72
|
+
*/
|
|
73
|
+
export function isManualBotRunning(pidFilePath, probeFn = process.kill) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = readFileSync(pidFilePath, 'utf8').trim();
|
|
76
|
+
const pid = parseInt(raw, 10);
|
|
77
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
78
|
+
probeFn(pid, 0); // signal 0 = existence check; delivers no signal
|
|
79
|
+
return pid;
|
|
80
|
+
} catch {
|
|
81
|
+
// File missing, non-numeric PID, ESRCH, EPERM — treat as not-running.
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Restart installed AFK launchd services so they pick up the just-installed
|
|
88
|
+
* code. A long-running Node process keeps the OLD module graph in memory after
|
|
89
|
+
* `npm install -g` overwrites the files on disk; only a restart swaps it.
|
|
90
|
+
* `launchctl kickstart -k` kills and relaunches the job against the (now
|
|
91
|
+
* updated) on-disk entrypoint — ProgramArguments are unchanged, only file
|
|
92
|
+
* contents, so no plist reload is needed.
|
|
93
|
+
*
|
|
94
|
+
* Fail-open and best-effort: a service whose plist is absent is skipped (never
|
|
95
|
+
* installed as a service); a launchctl error (job not loaded, launchctl wedged)
|
|
96
|
+
* is swallowed so the install never fails. macOS only — the caller gates on
|
|
97
|
+
* platform; elsewhere ~/Library/LaunchAgents won't exist so nothing restarts.
|
|
98
|
+
*
|
|
99
|
+
* Label / path / domain conventions mirror src/service/launchd/paths.ts
|
|
100
|
+
* (labelFor → `com.afk.<name>`, plist under ~/Library/LaunchAgents, guiDomain →
|
|
101
|
+
* `gui/<uid>`). Kept in sync by hand — this plain .mjs cannot import the
|
|
102
|
+
* compiled TS helpers.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [opts]
|
|
105
|
+
* @param {string} [opts.home] - Home dir; defaults to os.homedir().
|
|
106
|
+
* @param {number} [opts.uid] - Numeric uid; defaults to process.getuid().
|
|
107
|
+
* @param {string[]} [opts.labels] - launchd labels to consider.
|
|
108
|
+
* @param {function} [opts.existsFn] - Injectable plist existence check.
|
|
109
|
+
* @param {function} [opts.execFn] - Injectable launchctl runner; receives the argv array.
|
|
110
|
+
* @returns {string[]} labels that were successfully restarted.
|
|
111
|
+
*/
|
|
112
|
+
export function restartLaunchdServices(opts = {}) {
|
|
113
|
+
const home = opts.home ?? homedir();
|
|
114
|
+
const uid =
|
|
115
|
+
opts.uid ?? (typeof process.getuid === 'function' ? process.getuid() : 501);
|
|
116
|
+
const labels = opts.labels ?? ['com.afk.telegram', 'com.afk.daemon'];
|
|
117
|
+
const existsFn = opts.existsFn ?? existsSync;
|
|
118
|
+
const execFn =
|
|
119
|
+
opts.execFn ??
|
|
120
|
+
((argv) =>
|
|
121
|
+
execFileSync('launchctl', argv, {
|
|
122
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
123
|
+
timeout: 8000,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const restarted = [];
|
|
127
|
+
for (const label of labels) {
|
|
128
|
+
const plist = join(home, 'Library', 'LaunchAgents', `${label}.plist`);
|
|
129
|
+
if (!existsFn(plist)) continue; // not installed as a service
|
|
130
|
+
try {
|
|
131
|
+
execFn(['kickstart', '-k', `gui/${uid}/${label}`]);
|
|
132
|
+
restarted.push(label);
|
|
133
|
+
} catch {
|
|
134
|
+
// Job not loaded, or launchctl errored — skip; install must not fail.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return restarted;
|
|
138
|
+
}
|
|
139
|
+
|
|
53
140
|
// ─── Main block ─────────────────────────────────────────────────────────────
|
|
54
141
|
// Guard: only run when executed directly (not when imported by tests).
|
|
55
142
|
const isMain =
|
|
@@ -106,15 +193,42 @@ if (isMain) {
|
|
|
106
193
|
// execSync failed (npm not found, timeout, etc.) — silently ignore.
|
|
107
194
|
}
|
|
108
195
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
196
|
+
// Bring already-running AFK services onto the just-installed code. A long
|
|
197
|
+
// running process keeps the old module graph in memory after npm overwrites
|
|
198
|
+
// the files on disk — only a restart swaps it.
|
|
199
|
+
//
|
|
200
|
+
// 1. launchd-supervised services (telegram + daemon): restart in place so
|
|
201
|
+
// they re-exec the new entrypoint. macOS only; fail-open.
|
|
202
|
+
if (process.platform === 'darwin') {
|
|
203
|
+
try {
|
|
204
|
+
const restarted = restartLaunchdServices();
|
|
205
|
+
if (restarted.length > 0) {
|
|
206
|
+
const names = restarted.map((l) => l.replace(/^com\.afk\./, '')).join(', ');
|
|
207
|
+
process.stdout.write(
|
|
208
|
+
`\n↻ Restarted AFK service(s) onto the new version: ${names}\n`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// restartLaunchdServices is already fail-open; belt-and-suspenders.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. A manually-started telegram bot (`afk telegram start`) is not supervised
|
|
217
|
+
// by launchd, so we cannot relaunch it safely from an npm lifecycle script.
|
|
218
|
+
// Earlier versions SIGTERM'd it here, which left it dead with nothing to
|
|
219
|
+
// bring it back. Notify instead so the user can restart it deliberately.
|
|
112
220
|
try {
|
|
113
221
|
const afkHome = process.env['AFK_HOME'] ?? join(homedir(), '.afk');
|
|
114
|
-
const
|
|
115
|
-
|
|
222
|
+
const botPidPath = join(afkHome, 'state', 'telegram', 'bot.pid');
|
|
223
|
+
const manualPid = isManualBotRunning(botPidPath);
|
|
224
|
+
if (manualPid !== null) {
|
|
225
|
+
process.stdout.write(
|
|
226
|
+
`\n⚠ A manually-started telegram bot (PID ${manualPid}) is still running the previous version.\n` +
|
|
227
|
+
` Restart it to apply the update: afk telegram restart\n`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
116
230
|
} catch {
|
|
117
|
-
//
|
|
231
|
+
// Best-effort notice — never block the install.
|
|
118
232
|
}
|
|
119
233
|
|
|
120
234
|
process.exit(0);
|