agent-afk 5.275.5 → 5.276.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/dist/agent/facets/raw-input.d.ts +0 -2
- package/dist/agent/provider.d.ts +0 -1
- package/dist/agent/providers/openai-compatible/query/overload-pause-tier.d.ts +13 -0
- package/dist/agent/session/agent-session.d.ts +0 -1
- package/dist/agent/session/turn-stream-runner.d.ts +0 -3
- package/dist/agent/tools/handlers/read-denylist.d.ts +4 -0
- package/dist/agent/types/message-types.d.ts +0 -1
- package/dist/cli.mjs +358 -374
- package/dist/config/deprecated-capture-flags.d.ts +1 -0
- package/dist/config/env.d.ts +0 -14
- package/dist/http-client/extract-data-uri.d.ts +13 -0
- package/dist/index.mjs +133 -152
- package/dist/paths.d.ts +1 -1
- package/dist/paths.witness.d.ts +0 -2
- package/dist/telegram.mjs +177 -196
- package/package.json +1 -1
- package/dist/agent/session/subagent-output-capture.d.ts +0 -24
- package/dist/agent/session/subagent-prompt-capture.d.ts +0 -22
- package/dist/postinstall.mjs +0 -425
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-afk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.276.0",
|
|
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",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { OutputEvent } from '../types/session-types.js';
|
|
2
|
-
export declare const MAX_RECORD_BYTES: number;
|
|
3
|
-
export declare const MAX_TRANSCRIPT_BYTES: number;
|
|
4
|
-
export declare const OUTPUT_CAPTURE_BANNER = "<!-- Best-effort secret redaction only. Connection strings, PEM blocks, and PII are NOT caught. -->";
|
|
5
|
-
export interface SubagentOutputCaptureInput {
|
|
6
|
-
sessionId: string | undefined;
|
|
7
|
-
subagentId: string | undefined;
|
|
8
|
-
isSubagentFork: boolean;
|
|
9
|
-
model: string | undefined;
|
|
10
|
-
}
|
|
11
|
-
export declare function shouldCaptureSubagentOutput(input: SubagentOutputCaptureInput): boolean;
|
|
12
|
-
export declare function sanitizeRecordText(text: string): string;
|
|
13
|
-
export declare function buildOutputHeader(input: SubagentOutputCaptureInput): string;
|
|
14
|
-
export declare function buildRecord(kind: 'say' | 'tool' | 'end', detail: {
|
|
15
|
-
text?: string;
|
|
16
|
-
toolName?: string;
|
|
17
|
-
toolInput?: string;
|
|
18
|
-
reason?: string;
|
|
19
|
-
}): string;
|
|
20
|
-
export interface SubagentOutputRecorder {
|
|
21
|
-
observe(event: OutputEvent): void;
|
|
22
|
-
end(reason: string): void;
|
|
23
|
-
}
|
|
24
|
-
export declare function createSubagentOutputRecorder(input: SubagentOutputCaptureInput): SubagentOutputRecorder | null;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export declare const MAX_CAPTURED_PROMPT_BYTES: number;
|
|
2
|
-
export declare const PROMPT_CAPTURE_BANNER = "<!-- Best-effort secret redaction only. Connection strings, PEM blocks, and PII are NOT caught. -->";
|
|
3
|
-
export interface CaptureSubagentPromptInput {
|
|
4
|
-
sessionId: string | undefined;
|
|
5
|
-
subagentId: string | undefined;
|
|
6
|
-
isSubagentFork: boolean;
|
|
7
|
-
model: string | undefined;
|
|
8
|
-
turn: number;
|
|
9
|
-
prompt: string;
|
|
10
|
-
}
|
|
11
|
-
export declare function shouldCaptureSubagentPrompt(input: CaptureSubagentPromptInput): boolean;
|
|
12
|
-
export declare function truncateToBytes(text: string, maxBytes: number): {
|
|
13
|
-
text: string;
|
|
14
|
-
originalBytes: number;
|
|
15
|
-
truncated: boolean;
|
|
16
|
-
};
|
|
17
|
-
export declare function buildPromptDocument(input: CaptureSubagentPromptInput, capture: {
|
|
18
|
-
text: string;
|
|
19
|
-
originalBytes: number;
|
|
20
|
-
truncated: boolean;
|
|
21
|
-
}): string;
|
|
22
|
-
export declare function captureSubagentPrompt(input: CaptureSubagentPromptInput): Promise<void>;
|
package/dist/postinstall.mjs
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* postinstall.mjs — Runs after `npm install` to check whether the npm bin
|
|
4
|
-
* directory is on PATH and prints a remediation hint if not.
|
|
5
|
-
*
|
|
6
|
-
* Named export `detectPathGap` is a pure function for unit testing.
|
|
7
|
-
* The main block is guarded by an import.meta.url check so this file can be
|
|
8
|
-
* imported in tests without triggering the subprocess/print side-effects.
|
|
9
|
-
*
|
|
10
|
-
* Always exits 0 — never fails an install.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { execSync, execFileSync } from 'node:child_process';
|
|
14
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
|
|
19
|
-
// ─── Module-scope derived paths ────────────────────────────────────────────
|
|
20
|
-
// Computed once so restartLaunchdServices and the main block share the same
|
|
21
|
-
// decoded, symlink-resolved package root rather than each re-deriving it.
|
|
22
|
-
// fileURLToPath decodes percent-encoded characters (e.g. spaces → %20) that
|
|
23
|
-
// new URL().pathname leaves encoded and that path.join then mis-handles.
|
|
24
|
-
const _pkgRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
25
|
-
|
|
26
|
-
// ─── Exported helpers ───────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Pure function. Returns true when this module is the Node.js entry-point
|
|
30
|
-
* (i.e. run directly, not imported). Accepts the two pieces of identity as
|
|
31
|
-
* parameters so it can be unit-tested with arbitrary values.
|
|
32
|
-
*
|
|
33
|
-
* Comparison uses fileURLToPath (not a raw `file://` string) so that
|
|
34
|
-
* percent-encoded characters in the install path — e.g. `/my%20dir/` — do not
|
|
35
|
-
* cause a false negative. realpathSync resolves symlinks in argv[1] so that
|
|
36
|
-
* globally-installed bins (typically symlinked into /usr/local/bin) compare
|
|
37
|
-
* equal to the real file recorded in import.meta.url.
|
|
38
|
-
*
|
|
39
|
-
* @param {string} metaUrl - import.meta.url of the calling module.
|
|
40
|
-
* @param {string} [argv1] - process.argv[1]; may be absent or non-existent.
|
|
41
|
-
* @returns {boolean}
|
|
42
|
-
*/
|
|
43
|
-
export function isMainModule(metaUrl, argv1) {
|
|
44
|
-
if (!argv1) return false;
|
|
45
|
-
try {
|
|
46
|
-
return fileURLToPath(metaUrl) === realpathSync(argv1);
|
|
47
|
-
} catch {
|
|
48
|
-
// argv[1] does not exist on disk (e.g. a piped script, a test runner
|
|
49
|
-
// that synthesises a path). Fall back to a plain string comparison so
|
|
50
|
-
// the guard never throws at module evaluation time.
|
|
51
|
-
return fileURLToPath(metaUrl) === argv1;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Pure function. Determines whether the npm bin directory is on PATH.
|
|
57
|
-
*
|
|
58
|
-
* @param {string} prefix - Result of `npm config get prefix` (may have trailing slash)
|
|
59
|
-
* @param {string} pathEnv - The PATH string to check against (e.g. process.env.PATH)
|
|
60
|
-
* @returns {{ onPath: boolean, binDir: string }}
|
|
61
|
-
*/
|
|
62
|
-
export function detectPathGap(prefix, pathEnv) {
|
|
63
|
-
const normalizedPrefix = prefix.trim().replace(/\/$/, '');
|
|
64
|
-
const binDir = `${normalizedPrefix}/bin`;
|
|
65
|
-
const pathParts = (pathEnv ?? '').split(':').map((p) => p.replace(/\/$/, ''));
|
|
66
|
-
const onPath = pathParts.includes(binDir);
|
|
67
|
-
return { onPath, binDir };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Pure function. Determines whether this postinstall run is part of a global
|
|
72
|
-
* package install (`npm install -g` / `pnpm add -g`) as opposed to a local
|
|
73
|
-
* dev-checkout `pnpm install` or a worktree install.
|
|
74
|
-
*
|
|
75
|
-
* Detection uses two independent signals and fails CLOSED toward NOT-global
|
|
76
|
-
* (i.e. no restart) whenever either signal is uncertain:
|
|
77
|
-
*
|
|
78
|
-
* 1. npm_config_global env var — npm and pnpm both set this to the string
|
|
79
|
-
* "true" for every global install. A local `pnpm install` inside a repo
|
|
80
|
-
* never sets it. This is the primary, cheapest signal.
|
|
81
|
-
*
|
|
82
|
-
* 2. git-worktree presence check — if the package root contains a .git
|
|
83
|
-
* entry (file or directory) it is a source checkout or managed worktree,
|
|
84
|
-
* never a globally-installed package. A global install lands under
|
|
85
|
-
* node_modules inside the global npm prefix, never inside a git tree.
|
|
86
|
-
* This is a belt-and-suspenders guard: even if npm_config_global were
|
|
87
|
-
* wrong or missing, a .git marker unambiguously rules out a global install.
|
|
88
|
-
*
|
|
89
|
-
* History: Daemon was unconditionally restarted on every postinstall.
|
|
90
|
-
* A cron job that ran `pnpm install` inside a repo worktree triggered this
|
|
91
|
-
* path and restarted the running daemon mid-session, orphaning 438 subagents
|
|
92
|
-
* and leaving the daemon stopped. The guard introduced here ensures restarts
|
|
93
|
-
* occur ONLY for genuine global upgrades. See PR #2195.
|
|
94
|
-
*
|
|
95
|
-
* @param {string} pkgRoot - Absolute path to the package root.
|
|
96
|
-
* @param {object} [env] - Environment variable bag; defaults to process.env.
|
|
97
|
-
* @param {function} [existsFn] - Injectable fs.existsSync for testing.
|
|
98
|
-
* @returns {boolean} true only when both signals confirm a global install.
|
|
99
|
-
*/
|
|
100
|
-
export function isGlobalInstall(pkgRoot, env = process.env, existsFn = existsSync) {
|
|
101
|
-
// Signal 1: npm/pnpm lifecycle env var. Absent or non-"true" → not global.
|
|
102
|
-
if (env['npm_config_global'] !== 'true') return false;
|
|
103
|
-
|
|
104
|
-
// Signal 2: .git marker. Present → source checkout / worktree → not global.
|
|
105
|
-
const gitMarker = join(pkgRoot, '.git');
|
|
106
|
-
if (existsFn(gitMarker)) return false;
|
|
107
|
-
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Pure function. Attempts to SIGTERM a daemon whose PID is written in the
|
|
113
|
-
* given file. All errors are silently discarded — this is best-effort cleanup
|
|
114
|
-
* that must never fail an install.
|
|
115
|
-
*
|
|
116
|
-
* NOTE: the install flow no longer calls this. SIGTERM'ing a manually-started
|
|
117
|
-
* bot left it dead with nothing to relaunch it (postinstall cannot reliably
|
|
118
|
-
* spawn a detached long-lived process). The main block now NOTIFIES instead
|
|
119
|
-
* (see isManualBotRunning). Retained as a tested, reusable utility.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} pidFilePath - Full path to the PID file to read.
|
|
122
|
-
* @param {function} [killFn] - Injectable kill function; defaults to process.kill.
|
|
123
|
-
* Pass a stub in tests to avoid needing process.kill.
|
|
124
|
-
*/
|
|
125
|
-
export function killStaleDaemon(pidFilePath, killFn = process.kill) {
|
|
126
|
-
try {
|
|
127
|
-
const raw = readFileSync(pidFilePath, 'utf8').trim();
|
|
128
|
-
const pid = parseInt(raw, 10);
|
|
129
|
-
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
130
|
-
killFn(pid, 'SIGTERM');
|
|
131
|
-
} catch {
|
|
132
|
-
// File missing, non-numeric PID, ESRCH, EPERM, any other error — discard.
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Read-only probe: is a *manually-started* telegram bot still alive? Only the
|
|
138
|
-
* `afk telegram start` path records a child PID in bot.pid; a launchd-supervised
|
|
139
|
-
* bot does NOT (launchd owns its PID — see src/service/launchd/paths.ts), so a
|
|
140
|
-
* non-null result here always means a manual bot — exactly the instance the
|
|
141
|
-
* launchd kickstart below cannot reach.
|
|
142
|
-
*
|
|
143
|
-
* Unlike the manager's isRunning(), this does NOT unlink a stale PID file:
|
|
144
|
-
* postinstall must not mutate runtime state. Returns the live PID, or null when
|
|
145
|
-
* the file is missing/malformed or the process is gone.
|
|
146
|
-
*
|
|
147
|
-
* @param {string} pidFilePath
|
|
148
|
-
* @param {function} [probeFn] - Injectable existence probe; defaults to process.kill.
|
|
149
|
-
* @returns {number|null}
|
|
150
|
-
*/
|
|
151
|
-
export function isManualBotRunning(pidFilePath, probeFn = process.kill) {
|
|
152
|
-
try {
|
|
153
|
-
const raw = readFileSync(pidFilePath, 'utf8').trim();
|
|
154
|
-
const pid = parseInt(raw, 10);
|
|
155
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
156
|
-
probeFn(pid, 0); // signal 0 = existence check; delivers no signal
|
|
157
|
-
return pid;
|
|
158
|
-
} catch {
|
|
159
|
-
// File missing, non-numeric PID, ESRCH, EPERM — treat as not-running.
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Pure function. Encapsulates the platform + isGlobalInstall gate that guards
|
|
166
|
-
* daemon restarts in the main block. Extracted so unit tests can exercise the
|
|
167
|
-
* decision logic without running the `isMain` block.
|
|
168
|
-
*
|
|
169
|
-
* The function returns the array of labels that were restarted (empty when the
|
|
170
|
-
* guard fires and the restart is skipped), mirroring restartLaunchdServices.
|
|
171
|
-
*
|
|
172
|
-
* @param {object} [opts]
|
|
173
|
-
* @param {string} [opts.platform] - Platform string; defaults to process.platform.
|
|
174
|
-
* @param {object} [opts.env] - Environment variable bag; defaults to process.env.
|
|
175
|
-
* @param {string} [opts.pkgRoot] - Absolute path to the package root.
|
|
176
|
-
* @param {function} [opts.existsFn] - Injectable fs.existsSync for isGlobalInstall.
|
|
177
|
-
* @param {function} [opts.restartFn] - Injectable restartLaunchdServices; defaults to the module export.
|
|
178
|
-
* @returns {string[]} Labels that were restarted, or [] when the guard fires.
|
|
179
|
-
*/
|
|
180
|
-
export function maybeRestartServices(opts = {}) {
|
|
181
|
-
const platform = opts.platform ?? process.platform;
|
|
182
|
-
const env = opts.env ?? process.env;
|
|
183
|
-
const pkgRoot =
|
|
184
|
-
opts.pkgRoot ?? fileURLToPath(new URL('..', import.meta.url));
|
|
185
|
-
const existsFn = opts.existsFn ?? existsSync;
|
|
186
|
-
const restartFn = opts.restartFn ?? restartLaunchdServices;
|
|
187
|
-
|
|
188
|
-
if (platform !== 'darwin') return [];
|
|
189
|
-
if (!isGlobalInstall(pkgRoot, env, existsFn)) return [];
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
return restartFn({ labels: ['com.afk.daemon'] });
|
|
193
|
-
} catch {
|
|
194
|
-
// restartLaunchdServices is already fail-open; belt-and-suspenders.
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Restart installed AFK launchd services so they pick up the just-installed
|
|
201
|
-
* code. A long-running Node process keeps the OLD module graph in memory after
|
|
202
|
-
* `npm install -g` overwrites the files on disk; only a restart swaps it.
|
|
203
|
-
* `launchctl kickstart -k` kills and relaunches the job against the (now
|
|
204
|
-
* updated) on-disk entrypoint.
|
|
205
|
-
*
|
|
206
|
-
* When dist/cli.mjs is available, this function delegates to
|
|
207
|
-
* `node <dist>/cli.mjs service restart <name>` which internally upgrades
|
|
208
|
-
* the plist (if stale) and uses the correct launchctl reload strategy
|
|
209
|
-
* (bootout+bootstrap for an upgraded plist, kickstart for unchanged).
|
|
210
|
-
* Without the CLI, falls back to raw `launchctl kickstart -k`.
|
|
211
|
-
* The restart is best-effort — a failure is swallowed so the install never fails.
|
|
212
|
-
*
|
|
213
|
-
* Fail-open and best-effort: a service whose plist is absent is skipped (never
|
|
214
|
-
* installed as a service); a launchctl error (job not loaded, launchctl wedged)
|
|
215
|
-
* is swallowed so the install never fails. macOS only — the caller gates on
|
|
216
|
-
* platform; elsewhere ~/Library/LaunchAgents won't exist so nothing restarts.
|
|
217
|
-
*
|
|
218
|
-
* Invariant: the daemon is NOT stateless. It may be mid-run on one or more
|
|
219
|
-
* scheduled agent sessions. This function MUST only be called from a global
|
|
220
|
-
* package upgrade path (verified by isGlobalInstall()) — never from a local
|
|
221
|
-
* `pnpm install` inside a source checkout or worktree. The caller in the main
|
|
222
|
-
* block enforces this via isGlobalInstall(); callers in tests must pass stubs
|
|
223
|
-
* for execFn/restartFn so real launchctl is never invoked.
|
|
224
|
-
*
|
|
225
|
-
* Label / path / domain conventions mirror src/service/launchd/paths.ts
|
|
226
|
-
* (labelFor → `com.afk.<name>`, plist under ~/Library/LaunchAgents, guiDomain →
|
|
227
|
-
* `gui/<uid>`). Kept in sync by hand — this plain .mjs cannot import the
|
|
228
|
-
* compiled TS helpers.
|
|
229
|
-
*
|
|
230
|
-
* @param {object} [opts]
|
|
231
|
-
* @param {string} [opts.home] - Home dir; defaults to os.homedir().
|
|
232
|
-
* @param {number} [opts.uid] - Numeric uid; defaults to process.getuid().
|
|
233
|
-
* @param {string[]} [opts.labels] - launchd labels to consider.
|
|
234
|
-
* @param {function} [opts.existsFn] - Injectable plist existence check.
|
|
235
|
-
* @param {function} [opts.execFn] - Injectable launchctl runner; receives the argv array.
|
|
236
|
-
* @param {function} [opts.restartFn] - Injectable CLI restart runner; receives (node, cliMjs, name).
|
|
237
|
-
* Defaults to execFileSync. Pass a no-op stub in tests.
|
|
238
|
-
* @returns {string[]} labels that were successfully restarted.
|
|
239
|
-
*/
|
|
240
|
-
export function restartLaunchdServices(opts = {}) {
|
|
241
|
-
const home = opts.home ?? homedir();
|
|
242
|
-
const uid =
|
|
243
|
-
opts.uid ?? (typeof process.getuid === 'function' ? process.getuid() : 501);
|
|
244
|
-
const labels = opts.labels ?? ['com.afk.telegram', 'com.afk.daemon'];
|
|
245
|
-
const existsFn = opts.existsFn ?? existsSync;
|
|
246
|
-
const execFn =
|
|
247
|
-
opts.execFn ??
|
|
248
|
-
((argv) =>
|
|
249
|
-
execFileSync('launchctl', argv, {
|
|
250
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
251
|
-
timeout: 8000,
|
|
252
|
-
}));
|
|
253
|
-
|
|
254
|
-
// Resolve the path to dist/cli.mjs relative to this script so we can invoke
|
|
255
|
-
// `afk service upgrade <name>` via the just-installed compiled CLI. This is
|
|
256
|
-
// the only way a plain .mjs postinstall script can reach the compiled TS
|
|
257
|
-
// helpers — it cannot import them directly.
|
|
258
|
-
//
|
|
259
|
-
// __dirname equivalent for ESM: derive from import.meta.url.
|
|
260
|
-
// scripts/postinstall.mjs → dist/cli.mjs (package root / dist/).
|
|
261
|
-
// fileURLToPath is used (not .pathname) so percent-encoded characters in the
|
|
262
|
-
// path are decoded correctly — e.g. a space-containing install dir on macOS.
|
|
263
|
-
const pkgRoot = _pkgRoot;
|
|
264
|
-
const cliMjs = join(pkgRoot, 'dist', 'cli.mjs');
|
|
265
|
-
|
|
266
|
-
// Invariant: `afk service restart` is the correct single command — it
|
|
267
|
-
// internally runs upgradeService() and then chooses bootout+bootstrap
|
|
268
|
-
// (when the plist was rewritten) or kickstart -k (when unchanged).
|
|
269
|
-
// Calling upgrade then kickstart separately would leave the old launchd
|
|
270
|
-
// in-memory definition active even after the plist file was rewritten.
|
|
271
|
-
const restartFn =
|
|
272
|
-
opts.restartFn ??
|
|
273
|
-
((node, cli, name) =>
|
|
274
|
-
execFileSync(node, [cli, 'service', 'restart', name], {
|
|
275
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
276
|
-
timeout: 15000,
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
const restarted = [];
|
|
280
|
-
for (const label of labels) {
|
|
281
|
-
const plist = join(home, 'Library', 'LaunchAgents', `${label}.plist`);
|
|
282
|
-
if (!existsFn(plist)) continue; // not installed as a service
|
|
283
|
-
|
|
284
|
-
// Derive the short service name from the reverse-DNS label (mirrors labelFor()).
|
|
285
|
-
const name = label.replace(/^com\.afk\./, '');
|
|
286
|
-
|
|
287
|
-
// Best-effort restart via the compiled CLI. `afk service restart`
|
|
288
|
-
// handles plist upgrade + the correct launchctl reload strategy
|
|
289
|
-
// (bootout+bootstrap when upgraded, kickstart when unchanged).
|
|
290
|
-
// Falls back to raw kickstart when dist/cli.mjs is unavailable
|
|
291
|
-
// (e.g. source checkout without a build).
|
|
292
|
-
let restarted_via_cli = false;
|
|
293
|
-
if (existsFn(cliMjs)) {
|
|
294
|
-
try {
|
|
295
|
-
restartFn(process.execPath, cliMjs, name);
|
|
296
|
-
restarted_via_cli = true;
|
|
297
|
-
restarted.push(label);
|
|
298
|
-
} catch {
|
|
299
|
-
// CLI restart failed — fall through to raw kickstart.
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (!restarted_via_cli) {
|
|
304
|
-
try {
|
|
305
|
-
execFn(['kickstart', '-k', `gui/${uid}/${label}`]);
|
|
306
|
-
restarted.push(label);
|
|
307
|
-
} catch {
|
|
308
|
-
// Job not loaded, or launchctl errored — skip; install must not fail.
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return restarted;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ─── Main block ─────────────────────────────────────────────────────────────
|
|
316
|
-
// Guard: only run when executed directly (not when imported by tests).
|
|
317
|
-
// isMainModule() handles percent-encoded paths and symlinked global bins
|
|
318
|
-
// correctly; the raw `file://${process.argv[1]}` comparison it replaces
|
|
319
|
-
// would return false when the install path contains spaces or other chars
|
|
320
|
-
// that import.meta.url encodes as percent-sequences.
|
|
321
|
-
const isMain =
|
|
322
|
-
typeof process !== 'undefined' &&
|
|
323
|
-
typeof process.argv !== 'undefined' &&
|
|
324
|
-
isMainModule(import.meta.url, process.argv[1]);
|
|
325
|
-
|
|
326
|
-
if (isMain) {
|
|
327
|
-
// Only meaningful on macOS/Linux where PATH-based installs are common.
|
|
328
|
-
if (process.platform === 'win32') {
|
|
329
|
-
process.exit(0);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const prefix = execSync('npm config get prefix', {
|
|
334
|
-
timeout: 2000,
|
|
335
|
-
encoding: 'utf-8',
|
|
336
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const { onPath, binDir } = detectPathGap(prefix, process.env['PATH'] ?? '');
|
|
340
|
-
|
|
341
|
-
if (!onPath) {
|
|
342
|
-
const shell = process.env['SHELL'] ?? '';
|
|
343
|
-
const rcFile = shell.includes('zsh')
|
|
344
|
-
? '~/.zshrc'
|
|
345
|
-
: shell.includes('fish')
|
|
346
|
-
? '~/.config/fish/config.fish'
|
|
347
|
-
: '~/.bashrc';
|
|
348
|
-
|
|
349
|
-
const exportLine =
|
|
350
|
-
shell.includes('fish')
|
|
351
|
-
? `fish_add_path ${binDir}`
|
|
352
|
-
: `export PATH="${binDir}:$PATH"`;
|
|
353
|
-
|
|
354
|
-
process.stdout.write(
|
|
355
|
-
[
|
|
356
|
-
'',
|
|
357
|
-
'┌─────────────────────────────────────────────────────────────┐',
|
|
358
|
-
'│ agent-afk: npm bin directory is not on your PATH │',
|
|
359
|
-
'│ │',
|
|
360
|
-
`│ Run this to fix it: │`,
|
|
361
|
-
`│ echo '${exportLine}' >> ${rcFile}`,
|
|
362
|
-
'│ source ' + rcFile + ' │',
|
|
363
|
-
'│ │',
|
|
364
|
-
'│ Or add this line to your shell profile manually: │',
|
|
365
|
-
`│ ${exportLine}`,
|
|
366
|
-
'└─────────────────────────────────────────────────────────────┘',
|
|
367
|
-
'',
|
|
368
|
-
].join('\n'),
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
} catch {
|
|
372
|
-
// execSync failed (npm not found, timeout, etc.) — silently ignore.
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Bring already-running AFK services onto the just-installed code. A long
|
|
376
|
-
// running process keeps the old module graph in memory after npm overwrites
|
|
377
|
-
// the files on disk — only a restart swaps it.
|
|
378
|
-
//
|
|
379
|
-
// Invariant: the telegram service is EXCLUDED from postinstall restart.
|
|
380
|
-
// `launchctl kickstart -k` sends SIGTERM which kills all active sessions
|
|
381
|
-
// mid-turn — the relaunched binary starts cold and cannot resume them. The
|
|
382
|
-
// telegram bot has its own version-drift watchdog (stats-ticker.ts) that
|
|
383
|
-
// detects the on-disk version mismatch within 5 minutes and exits ONLY when
|
|
384
|
-
// no session is mid-turn (or after a bounded deferral window). Restarting it
|
|
385
|
-
// here bypasses that session-safety logic and is the root cause of "sessions
|
|
386
|
-
// that work but never respond" — see telegram-stuck-diagnosis.md.
|
|
387
|
-
//
|
|
388
|
-
// Invariant: the daemon may be mid-run on scheduled agent sessions and is
|
|
389
|
-
// NOT safe to restart unconditionally. Service restart is only correct for a
|
|
390
|
-
// global package upgrade (`npm install -g`). A local `pnpm install` inside a
|
|
391
|
-
// source checkout or managed worktree must NEVER trigger a restart — doing so
|
|
392
|
-
// kills any in-flight sessions owned by that daemon. The isGlobalInstall()
|
|
393
|
-
// guard inside maybeRestartServices enforces this; it fails closed toward NOT
|
|
394
|
-
// restarting whenever either detection signal is absent or ambiguous.
|
|
395
|
-
try {
|
|
396
|
-
const restarted = maybeRestartServices({ pkgRoot: _pkgRoot });
|
|
397
|
-
if (restarted.length > 0) {
|
|
398
|
-
const names = restarted.map((l) => l.replace(/^com\.afk\./, '')).join(', ');
|
|
399
|
-
process.stdout.write(
|
|
400
|
-
`\n↻ Restarted AFK service(s) onto the new version: ${names}\n`,
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
} catch {
|
|
404
|
-
// maybeRestartServices is already fail-open; belt-and-suspenders.
|
|
405
|
-
}
|
|
406
|
-
// 2. A manually-started telegram bot (`afk telegram start`) is not supervised
|
|
407
|
-
// by launchd, so we cannot relaunch it safely from an npm lifecycle script.
|
|
408
|
-
// Earlier versions SIGTERM'd it here, which left it dead with nothing to
|
|
409
|
-
// bring it back. Notify instead so the user can restart it deliberately.
|
|
410
|
-
try {
|
|
411
|
-
const afkHome = process.env['AFK_HOME'] ?? join(homedir(), '.afk');
|
|
412
|
-
const botPidPath = join(afkHome, 'state', 'telegram', 'bot.pid');
|
|
413
|
-
const manualPid = isManualBotRunning(botPidPath);
|
|
414
|
-
if (manualPid !== null) {
|
|
415
|
-
process.stdout.write(
|
|
416
|
-
`\n⚠ A manually-started telegram bot (PID ${manualPid}) is still running the previous version.\n` +
|
|
417
|
-
` Restart it to apply the update: afk telegram restart\n`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
// Best-effort notice — never block the install.
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
process.exit(0);
|
|
425
|
-
}
|