@integrity-labs/agt-cli 0.15.27 → 0.15.31

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.
@@ -12,14 +12,12 @@ import { homedir, platform } from "os";
12
12
  import { dirname, join } from "path";
13
13
  var MACOS_LABEL = "team.augmented.agt-manager";
14
14
  var MACOS_PLIST = join(homedir(), "Library", "LaunchAgents", `${MACOS_LABEL}.plist`);
15
- var LINUX_UNIT_NAME = "agt-manager.service";
16
- var LINUX_UNIT_PATH = join(homedir(), ".config", "systemd", "user", LINUX_UNIT_NAME);
17
15
  async function installSupervisor(opts) {
18
16
  switch (platform()) {
19
17
  case "darwin":
20
18
  return installMacos(opts);
21
19
  case "linux":
22
- return installLinux(opts);
20
+ return { ok: false, error: "Linux systemd support lands in Phase 2 (ENG-4593). For now, run `agt manager start --supervise` directly." };
23
21
  default:
24
22
  return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor install is macOS / Linux only.` };
25
23
  }
@@ -29,7 +27,7 @@ async function uninstallSupervisor() {
29
27
  case "darwin":
30
28
  return uninstallMacos();
31
29
  case "linux":
32
- return uninstallLinux();
30
+ return { ok: false, error: "Linux systemd support lands in Phase 2 (ENG-4593)." };
33
31
  default:
34
32
  return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor uninstall is macOS / Linux only.` };
35
33
  }
@@ -39,7 +37,7 @@ function supervisorStatus() {
39
37
  case "darwin":
40
38
  return statusMacos();
41
39
  case "linux":
42
- return statusLinux();
40
+ return { kind: "unsupported-platform", platform: "linux (Phase 2)" };
43
41
  default:
44
42
  return { kind: "unsupported-platform", platform: platform() };
45
43
  }
@@ -168,142 +166,12 @@ function bestEffort(fn) {
168
166
  function escapeXml(s) {
169
167
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
170
168
  }
171
- function installLinux(opts) {
172
- if (!existsSync(opts.agtBin)) {
173
- return { ok: false, error: `agt binary not found at ${opts.agtBin}` };
174
- }
175
- if (opts.intervalSec < 5) {
176
- return { ok: false, error: "intervalSec must be >= 5" };
177
- }
178
- if (!opts.env.AGT_HOST || !opts.env.AGT_API_KEY) {
179
- return { ok: false, error: "AGT_HOST and AGT_API_KEY must be set in the calling shell so the supervisor can pass them to the unit. Run `agt setup <token>` first." };
180
- }
181
- bestEffort(() => execFileSync("systemctl", ["--user", "stop", LINUX_UNIT_NAME], { stdio: "ignore" }));
182
- mkdirSync(dirname(LINUX_UNIT_PATH), { recursive: true, mode: 448 });
183
- writeFileSync(LINUX_UNIT_PATH, renderLinuxUnit(opts), { mode: 384 });
184
- chmodSync(LINUX_UNIT_PATH, 384);
185
- try {
186
- execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
187
- } catch (err) {
188
- return { ok: false, error: `systemctl --user daemon-reload failed: ${err.message}` };
189
- }
190
- try {
191
- execFileSync("systemctl", ["--user", "enable", "--now", LINUX_UNIT_NAME], { stdio: "pipe" });
192
- } catch (err) {
193
- return { ok: false, error: `systemctl --user enable --now failed: ${err.message}. (Hint: 'systemctl --user' requires a per-user dbus session; on a fresh SSH login try 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' first.)` };
194
- }
195
- let isActive = "";
196
- try {
197
- isActive = execFileSync("systemctl", ["--user", "is-active", LINUX_UNIT_NAME], { encoding: "utf-8" }).trim();
198
- } catch (err) {
199
- isActive = (err.stdout?.toString() ?? "").trim() || "unknown";
200
- }
201
- return {
202
- ok: true,
203
- details: `Loaded systemd --user unit ${LINUX_UNIT_NAME} (status: ${isActive}). Logs: ${join(opts.configDir, "manager.log")}.
204
- For reboot survival on a headless host run as root: 'loginctl enable-linger ${process.env["USER"] ?? "<your-user>"}'`
205
- };
206
- }
207
- function uninstallLinux() {
208
- bestEffort(() => execFileSync("systemctl", ["--user", "disable", "--now", LINUX_UNIT_NAME], { stdio: "ignore" }));
209
- let removed = false;
210
- if (existsSync(LINUX_UNIT_PATH)) {
211
- try {
212
- unlinkSync(LINUX_UNIT_PATH);
213
- removed = true;
214
- } catch (err) {
215
- return { ok: false, error: `Failed to remove ${LINUX_UNIT_PATH}: ${err.message}` };
216
- }
217
- }
218
- bestEffort(() => execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" }));
219
- return {
220
- ok: true,
221
- details: removed ? `Disabled systemd --user unit ${LINUX_UNIT_NAME} and deleted ${LINUX_UNIT_PATH}.` : `${LINUX_UNIT_NAME} was already uninstalled (no unit file at ${LINUX_UNIT_PATH}).`
222
- };
223
- }
224
- function statusLinux() {
225
- if (!existsSync(LINUX_UNIT_PATH)) return { kind: "not-installed" };
226
- let pid = null;
227
- let activeState = "unknown";
228
- try {
229
- const out = execFileSync(
230
- "systemctl",
231
- ["--user", "show", LINUX_UNIT_NAME, "--property=MainPID,ActiveState"],
232
- { encoding: "utf-8" }
233
- );
234
- const pidMatch = /^MainPID=(\d+)/m.exec(out);
235
- if (pidMatch && pidMatch[1] !== "0") pid = Number(pidMatch[1]);
236
- const stateMatch = /^ActiveState=(\S+)/m.exec(out);
237
- if (stateMatch) activeState = stateMatch[1] ?? "unknown";
238
- } catch {
239
- return { kind: "not-installed" };
240
- }
241
- return { kind: "installed", pid, details: `Unit: ${LINUX_UNIT_PATH} (${activeState})` };
242
- }
243
- function renderLinuxUnit(opts) {
244
- const envLines = Object.entries(opts.env).filter(([k, v]) => k.length > 0 && v != null).map(([k, v]) => `Environment="${k}=${escapeForSystemdEnv(v)}"`).join("\n");
245
- const execArgs = [
246
- opts.agtBin,
247
- "manager",
248
- "start",
249
- "--interval",
250
- String(opts.intervalSec),
251
- "--config-dir",
252
- opts.configDir
253
- ].map(shellQuoteForSystemd).join(" ");
254
- const logPath = join(opts.configDir, "manager.log");
255
- return `# agt manager supervisor (ENG-4593)
256
- # Managed by \`agt manager install\` \u2014 edits will be overwritten.
257
-
258
- [Unit]
259
- Description=Augmented host manager daemon
260
- After=network-online.target
261
- Wants=network-online.target
262
-
263
- [Service]
264
- Type=simple
265
- WorkingDirectory=${opts.configDir}
266
- ${envLines}
267
- ExecStart=${execArgs}
268
-
269
- # Restart policy \u2014 always come back, with throttling so a crashloop
270
- # can't peg the CPU. StartLimitBurst/IntervalSec follow systemd's
271
- # defaults but are stated explicitly so future operators don't have
272
- # to read the man page to understand the recovery envelope.
273
- Restart=always
274
- RestartSec=5
275
- StartLimitBurst=5
276
- StartLimitIntervalSec=60
277
-
278
- # stdout/stderr \u2192 manager.log. journalctl --user -u agt-manager
279
- # still captures everything; this just gives operators a single file
280
- # to tail without learning systemd plumbing.
281
- StandardOutput=append:${logPath}
282
- StandardError=append:${logPath}
283
-
284
- [Install]
285
- WantedBy=default.target
286
- `;
287
- }
288
- function escapeForSystemdEnv(value) {
289
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
290
- }
291
- function shellQuoteForSystemd(arg) {
292
- if (/^[A-Za-z0-9_./:=-]+$/.test(arg)) return arg;
293
- return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
294
- }
295
169
  var _internals = {
296
170
  MACOS_LABEL,
297
171
  MACOS_PLIST,
298
- LINUX_UNIT_NAME,
299
- LINUX_UNIT_PATH,
300
172
  renderMacosPlist,
301
- renderLinuxUnit,
302
173
  escapeXml,
303
- escapeForSystemdEnv,
304
- shellQuoteForSystemd,
305
- readPlist: () => existsSync(MACOS_PLIST) ? readFileSync(MACOS_PLIST, "utf-8") : null,
306
- readUnit: () => existsSync(LINUX_UNIT_PATH) ? readFileSync(LINUX_UNIT_PATH, "utf-8") : null
174
+ readPlist: () => existsSync(MACOS_PLIST) ? readFileSync(MACOS_PLIST, "utf-8") : null
307
175
  };
308
176
  export {
309
177
  _internals,
@@ -311,4 +179,4 @@ export {
311
179
  supervisorStatus,
312
180
  uninstallSupervisor
313
181
  };
314
- //# sourceMappingURL=manager-supervisor-HKN2GL7L.js.map
182
+ //# sourceMappingURL=manager-supervisor-A23IZIWC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/manager-supervisor.ts"],"sourcesContent":["/**\n * OS-level supervision for the agt manager daemon (ENG-4593).\n *\n * Hands manager lifecycle off to the OS so a missing process is the OS's\n * problem, not ours. Replaces the in-process `agt manager start\n * --supervise` wrapper from ENG-4488.\n *\n * macOS: launchd LaunchAgent (KeepAlive=true + ThrottleInterval=10)\n * Linux: systemd --user unit (Restart=always + RestartSec=5) [Phase 2]\n *\n * Public surface:\n * - installSupervisor(opts) — write the unit, load/enable it, verify\n * - uninstallSupervisor() — unload/disable + remove the unit (idempotent)\n * - supervisorStatus() — quick readout of whether the OS knows about us\n *\n * Each platform implementation lives in its own helper. The dispatcher\n * picks one based on `process.platform` and rejects unsupported OSes\n * with a clear error rather than silently no-oping.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n unlinkSync,\n writeFileSync,\n chmodSync,\n} from 'node:fs';\nimport { homedir, platform } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// macOS LaunchAgent label and on-disk path. Stable so install/uninstall\n// can find each other without a state file.\nconst MACOS_LABEL = 'team.augmented.agt-manager';\nconst MACOS_PLIST = join(homedir(), 'Library', 'LaunchAgents', `${MACOS_LABEL}.plist`);\n\nexport interface InstallOpts {\n /** Absolute path to the agt binary the supervisor should launch. */\n agtBin: string;\n /** Poll interval passed to `agt manager start`. Min 5 seconds. */\n intervalSec: number;\n /** Manager config directory (where pid/log/state files live). */\n configDir: string;\n /**\n * Environment variables the supervised manager needs at launch time.\n * AGT_HOST and AGT_API_KEY are required for the manager to call the\n * API; AGT_TEAM is optional but typical.\n */\n env: Record<string, string>;\n}\n\nexport type SupervisorPresence =\n | { kind: 'installed'; pid: number | null; details: string }\n | { kind: 'not-installed' }\n | { kind: 'unsupported-platform'; platform: string };\n\n// ---------------------------------------------------------------------------\n// Public dispatcher\n// ---------------------------------------------------------------------------\n\nexport async function installSupervisor(opts: InstallOpts): Promise<{ ok: true; details: string } | { ok: false; error: string }> {\n switch (platform()) {\n case 'darwin':\n return installMacos(opts);\n case 'linux':\n return { ok: false, error: 'Linux systemd support lands in Phase 2 (ENG-4593). For now, run `agt manager start --supervise` directly.' };\n default:\n return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor install is macOS / Linux only.` };\n }\n}\n\nexport async function uninstallSupervisor(): Promise<{ ok: true; details: string } | { ok: false; error: string }> {\n switch (platform()) {\n case 'darwin':\n return uninstallMacos();\n case 'linux':\n return { ok: false, error: 'Linux systemd support lands in Phase 2 (ENG-4593).' };\n default:\n return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor uninstall is macOS / Linux only.` };\n }\n}\n\nexport function supervisorStatus(): SupervisorPresence {\n switch (platform()) {\n case 'darwin':\n return statusMacos();\n case 'linux':\n return { kind: 'unsupported-platform', platform: 'linux (Phase 2)' };\n default:\n return { kind: 'unsupported-platform', platform: platform() };\n }\n}\n\n// ---------------------------------------------------------------------------\n// macOS — launchd LaunchAgent\n// ---------------------------------------------------------------------------\n\nfunction installMacos(opts: InstallOpts): { ok: true; details: string } | { ok: false; error: string } {\n if (!existsSync(opts.agtBin)) {\n return { ok: false, error: `agt binary not found at ${opts.agtBin}` };\n }\n if (opts.intervalSec < 5) {\n return { ok: false, error: 'intervalSec must be >= 5' };\n }\n if (!opts.env.AGT_HOST || !opts.env.AGT_API_KEY) {\n return { ok: false, error: 'AGT_HOST and AGT_API_KEY must be set in the calling shell so the supervisor can pass them to the manager. Run `agt setup <token>` first.' };\n }\n\n // If a previous version is already loaded, unload it before rewriting\n // the plist — otherwise launchctl bootstrap will fail with EALREADY.\n bestEffort(() => execFileSync('launchctl', ['unload', '-w', MACOS_PLIST], { stdio: 'ignore' }));\n\n // Ensure the LaunchAgents directory exists. macOS creates it on first\n // login, but freshly-installed admin users may not have it yet.\n mkdirSync(dirname(MACOS_PLIST), { recursive: true });\n // 0o600 — plist embeds AGT_API_KEY in EnvironmentVariables. World-\n // readable would expose the host's tlk_ token to every local user.\n writeFileSync(MACOS_PLIST, renderMacosPlist(opts), { mode: 0o600 });\n chmodSync(MACOS_PLIST, 0o600);\n\n try {\n execFileSync('launchctl', ['load', '-w', MACOS_PLIST], { stdio: 'pipe' });\n } catch (err) {\n return { ok: false, error: `launchctl load failed: ${(err as Error).message}` };\n }\n\n // Verify launchd actually picked it up. `list <label>` returns 0 with\n // a plist of the loaded job's metadata; non-zero if not loaded.\n try {\n const out = execFileSync('launchctl', ['list', MACOS_LABEL], { encoding: 'utf-8' });\n return {\n ok: true,\n details: `Loaded launchd LaunchAgent ${MACOS_LABEL}. Logs: ${join(opts.configDir, 'manager.log')}.\\n${out.trim().split('\\n').slice(0, 10).join('\\n')}`,\n };\n } catch (err) {\n return { ok: false, error: `Plist written but launchctl list reported nothing for ${MACOS_LABEL}: ${(err as Error).message}` };\n }\n}\n\nfunction uninstallMacos(): { ok: true; details: string } | { ok: false; error: string } {\n // Both steps are idempotent — unload reports an error if nothing's\n // loaded, removing the file errors if it doesn't exist. Swallow both;\n // the desired end state is \"no LaunchAgent for this label\".\n bestEffort(() => execFileSync('launchctl', ['unload', '-w', MACOS_PLIST], { stdio: 'ignore' }));\n let removed = false;\n if (existsSync(MACOS_PLIST)) {\n try {\n unlinkSync(MACOS_PLIST);\n removed = true;\n } catch (err) {\n return { ok: false, error: `Failed to remove ${MACOS_PLIST}: ${(err as Error).message}` };\n }\n }\n return {\n ok: true,\n details: removed\n ? `Removed ${MACOS_LABEL} from launchd and deleted ${MACOS_PLIST}.`\n : `${MACOS_LABEL} was already uninstalled (no plist at ${MACOS_PLIST}).`,\n };\n}\n\nfunction statusMacos(): SupervisorPresence {\n if (!existsSync(MACOS_PLIST)) return { kind: 'not-installed' };\n // launchctl list emits a 3-column line: PID Status Label. PID is \"-\"\n // when the job is loaded but not currently running.\n try {\n const out = execFileSync('launchctl', ['list', MACOS_LABEL], { encoding: 'utf-8' });\n const pidMatch = /\"PID\"\\s*=\\s*(\\d+);/.exec(out);\n const pid = pidMatch ? Number(pidMatch[1]) : null;\n return { kind: 'installed', pid, details: `Plist: ${MACOS_PLIST}` };\n } catch {\n // File exists but launchctl can't find the label → user removed it\n // out-of-band. Surface as \"not installed\" so re-install just works.\n return { kind: 'not-installed' };\n }\n}\n\nfunction renderMacosPlist(opts: InstallOpts): string {\n // Load the operator's env into the plist so the manager has the same\n // AGT_HOST / AGT_API_KEY / AGT_TEAM the shell session does. launchd\n // does NOT inherit env from the user's shell — anything we don't bake\n // in here won't be visible to the process.\n const envEntries = Object.entries(opts.env)\n .filter(([k, v]) => k.length > 0 && v != null)\n .map(([k, v]) => ` <key>${escapeXml(k)}</key>\\n <string>${escapeXml(v)}</string>`)\n .join('\\n');\n\n const args = [\n 'manager',\n 'start',\n '--interval',\n String(opts.intervalSec),\n '--config-dir',\n opts.configDir,\n ];\n const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join('\\n');\n\n const logPath = join(opts.configDir, 'manager.log');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>${MACOS_LABEL}</string>\n\n <key>ProgramArguments</key>\n <array>\n <string>${escapeXml(opts.agtBin)}</string>\n${argsXml}\n </array>\n\n <key>RunAtLoad</key>\n <true/>\n\n <key>KeepAlive</key>\n <true/>\n\n <!-- Throttle restarts so a crashloop doesn't peg the CPU. 10 seconds\n between launches is enough room for transient API blips while\n still recovering quickly from a real crash. -->\n <key>ThrottleInterval</key>\n <integer>10</integer>\n\n <!-- Where the manager's stdout / stderr land. The manager's logger\n redirects stderr into manager.log itself, but launchd needs an\n explicit destination at the OS level for any pre-logger output\n (e.g., Node startup errors before the watchdog wires up). -->\n <key>StandardOutPath</key>\n <string>${escapeXml(logPath)}</string>\n <key>StandardErrorPath</key>\n <string>${escapeXml(logPath)}</string>\n\n <key>WorkingDirectory</key>\n <string>${escapeXml(opts.configDir)}</string>\n\n <key>EnvironmentVariables</key>\n <dict>\n${envEntries}\n </dict>\n</dict>\n</plist>\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction bestEffort(fn: () => void): void {\n try { fn(); } catch { /* swallow */ }\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Exposed for unit tests / introspection.\nexport const _internals = {\n MACOS_LABEL,\n MACOS_PLIST,\n renderMacosPlist,\n escapeXml,\n readPlist: () => (existsSync(MACOS_PLIST) ? readFileSync(MACOS_PLIST, 'utf-8') : null),\n};\n"],"mappings":"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","names":[]}
@@ -14982,6 +14982,34 @@ async function postSlackMessage(body) {
14982
14982
  return { ok: false, error: isTimeout ? "timeout" : err.message };
14983
14983
  }
14984
14984
  }
14985
+ function buildSlackHelpMessage(codeName) {
14986
+ return [
14987
+ `\u{1F916} *Available commands for \`${codeName}\`*`,
14988
+ "",
14989
+ "_Type these in a thread or channel where the bot is present (intercepted by the agent):_",
14990
+ "\u2022 `/help` \u2014 show this help",
14991
+ "\u2022 `/restart` \u2014 restart this agent",
14992
+ "",
14993
+ "_Real Slack slash commands (also discoverable via `/`-autocomplete):_",
14994
+ "\u2022 `/kill` \u2014 silence all agents in this thread for 6h (use as a thread reply)",
14995
+ "\u2022 `/unkill` \u2014 clear a kill (use as a thread reply)"
14996
+ ].join("\n");
14997
+ }
14998
+ async function handleHelpCommand(opts) {
14999
+ const codeName = AGENT_CODE_NAME ?? "unknown";
15000
+ const replyThread = opts.threadTs ?? opts.ts;
15001
+ const ack = await postSlackMessage({
15002
+ channel: opts.channel,
15003
+ text: buildSlackHelpMessage(codeName),
15004
+ ...replyThread ? { thread_ts: replyThread } : {}
15005
+ });
15006
+ if (!ack.ok) {
15007
+ process.stderr.write(
15008
+ `slack-channel(${codeName}): /help reply failed: ${ack.error ?? "unknown"}
15009
+ `
15010
+ );
15011
+ }
15012
+ }
14985
15013
  async function handleRestartCommand(opts) {
14986
15014
  const codeName = AGENT_CODE_NAME ?? "unknown";
14987
15015
  try {
@@ -16221,6 +16249,15 @@ async function connectSocketMode() {
16221
16249
  const rawText = evt.text ?? "";
16222
16250
  const strippedText = rawText.replace(/^\s*<@[^>]+>\s*/, "").trim();
16223
16251
  const isRestartCommand = strippedText === "/restart" || strippedText.startsWith("/restart ");
16252
+ const isHelpCommand = strippedText === "/help" || strippedText.startsWith("/help ");
16253
+ if (isHelpCommand) {
16254
+ await handleHelpCommand({
16255
+ channel: evt.channel ?? "",
16256
+ threadTs: evt.thread_ts,
16257
+ ts: evt.ts ?? ""
16258
+ });
16259
+ return;
16260
+ }
16224
16261
  if (isRestartCommand) {
16225
16262
  const senderAllowed = ALLOWED_USERS.size === 0 || evt.user != null && ALLOWED_USERS.has(evt.user);
16226
16263
  if (!senderAllowed) {
@@ -14316,6 +14316,40 @@ async function setMessageReaction(chatId, messageId, emoji2) {
14316
14316
  }
14317
14317
  }
14318
14318
  var RESTART_FLAGS_DIR = join2(homedir2(), ".augmented", "restart-flags");
14319
+ function buildTelegramHelpMessage(codeName) {
14320
+ return [
14321
+ `\u{1F916} *Available commands for \`${codeName}\`*`,
14322
+ "",
14323
+ `_Type these in any chat where the bot is present (intercepted by the agent):_`,
14324
+ `\u2022 /help \u2014 show this help`,
14325
+ `\u2022 /restart \u2014 restart this agent`
14326
+ ].join("\n");
14327
+ }
14328
+ async function handleHelpCommand(opts) {
14329
+ try {
14330
+ const resp = await telegramApiCall(
14331
+ "sendMessage",
14332
+ {
14333
+ chat_id: opts.chatId,
14334
+ text: buildTelegramHelpMessage(AGENT_CODE_NAME),
14335
+ parse_mode: "Markdown",
14336
+ reply_to_message_id: Number(opts.messageId)
14337
+ },
14338
+ 1e4
14339
+ );
14340
+ if (!resp.ok) {
14341
+ process.stderr.write(
14342
+ `telegram-channel(${AGENT_CODE_NAME}): /help reply rejected by Telegram (chat ${redactId(opts.chatId)}): ${resp.description ?? "unknown"}
14343
+ `
14344
+ );
14345
+ }
14346
+ } catch (err) {
14347
+ process.stderr.write(
14348
+ `telegram-channel(${AGENT_CODE_NAME}): /help reply send failed: ${redactAugmentedPaths(err.message)}
14349
+ `
14350
+ );
14351
+ }
14352
+ }
14319
14353
  async function handleRestartCommand(opts) {
14320
14354
  try {
14321
14355
  if (!existsSync(RESTART_FLAGS_DIR)) {
@@ -14405,6 +14439,10 @@ async function resolveBotUsername() {
14405
14439
  return null;
14406
14440
  }
14407
14441
  var RESTART_SYNTAX_RE = /^\/restart(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
14442
+ var HELP_SYNTAX_RE = /^\/help(?:@([A-Za-z0-9_]{1,64}))?(?:\s|$)/;
14443
+ function isHelpSyntax(text) {
14444
+ return HELP_SYNTAX_RE.test(text);
14445
+ }
14408
14446
  function isRestartSyntax(text) {
14409
14447
  return RESTART_SYNTAX_RE.test(text);
14410
14448
  }
@@ -15070,6 +15108,16 @@ async function pollLoop() {
15070
15108
  const chatId = String(msg.chat.id);
15071
15109
  if (ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(chatId)) continue;
15072
15110
  const trimmedContent = content.trim();
15111
+ if (isHelpSyntax(trimmedContent)) {
15112
+ const disposition = await classifyRestartCommand(trimmedContent.replace(/^\/help/, "/restart"));
15113
+ if (disposition === "act") {
15114
+ await handleHelpCommand({
15115
+ chatId,
15116
+ messageId: String(msg.message_id)
15117
+ });
15118
+ }
15119
+ continue;
15120
+ }
15073
15121
  if (isRestartSyntax(trimmedContent)) {
15074
15122
  const disposition = await classifyRestartCommand(trimmedContent);
15075
15123
  if (disposition === "act") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.15.27",
3
+ "version": "0.15.31",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/manager-supervisor.ts"],"sourcesContent":["/**\n * OS-level supervision for the agt manager daemon (ENG-4593).\n *\n * Hands manager lifecycle off to the OS so a missing process is the OS's\n * problem, not ours. Replaces the in-process `agt manager start\n * --supervise` wrapper from ENG-4488.\n *\n * macOS: launchd LaunchAgent (KeepAlive=true + ThrottleInterval=10)\n * Linux: systemd --user unit (Restart=always + RestartSec=5) [Phase 2]\n *\n * Public surface:\n * - installSupervisor(opts) — write the unit, load/enable it, verify\n * - uninstallSupervisor() — unload/disable + remove the unit (idempotent)\n * - supervisorStatus() — quick readout of whether the OS knows about us\n *\n * Each platform implementation lives in its own helper. The dispatcher\n * picks one based on `process.platform` and rejects unsupported OSes\n * with a clear error rather than silently no-oping.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n unlinkSync,\n writeFileSync,\n chmodSync,\n} from 'node:fs';\nimport { homedir, platform } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// macOS LaunchAgent label and on-disk path. Stable so install/uninstall\n// can find each other without a state file.\nconst MACOS_LABEL = 'team.augmented.agt-manager';\nconst MACOS_PLIST = join(homedir(), 'Library', 'LaunchAgents', `${MACOS_LABEL}.plist`);\n\n// Linux systemd --user unit. Lives under the operator's XDG config so it\n// survives reboot only after `loginctl enable-linger` (we surface that as\n// a follow-up step rather than running it ourselves — needs root).\nconst LINUX_UNIT_NAME = 'agt-manager.service';\nconst LINUX_UNIT_PATH = join(homedir(), '.config', 'systemd', 'user', LINUX_UNIT_NAME);\n\nexport interface InstallOpts {\n /** Absolute path to the agt binary the supervisor should launch. */\n agtBin: string;\n /** Poll interval passed to `agt manager start`. Min 5 seconds. */\n intervalSec: number;\n /** Manager config directory (where pid/log/state files live). */\n configDir: string;\n /**\n * Environment variables the supervised manager needs at launch time.\n * AGT_HOST and AGT_API_KEY are required for the manager to call the\n * API; AGT_TEAM is optional but typical.\n */\n env: Record<string, string>;\n}\n\nexport type SupervisorPresence =\n | { kind: 'installed'; pid: number | null; details: string }\n | { kind: 'not-installed' }\n | { kind: 'unsupported-platform'; platform: string };\n\n// ---------------------------------------------------------------------------\n// Public dispatcher\n// ---------------------------------------------------------------------------\n\nexport async function installSupervisor(opts: InstallOpts): Promise<{ ok: true; details: string } | { ok: false; error: string }> {\n switch (platform()) {\n case 'darwin':\n return installMacos(opts);\n case 'linux':\n return installLinux(opts);\n default:\n return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor install is macOS / Linux only.` };\n }\n}\n\nexport async function uninstallSupervisor(): Promise<{ ok: true; details: string } | { ok: false; error: string }> {\n switch (platform()) {\n case 'darwin':\n return uninstallMacos();\n case 'linux':\n return uninstallLinux();\n default:\n return { ok: false, error: `Unsupported platform: ${platform()}. Supervisor uninstall is macOS / Linux only.` };\n }\n}\n\nexport function supervisorStatus(): SupervisorPresence {\n switch (platform()) {\n case 'darwin':\n return statusMacos();\n case 'linux':\n return statusLinux();\n default:\n return { kind: 'unsupported-platform', platform: platform() };\n }\n}\n\n// ---------------------------------------------------------------------------\n// macOS — launchd LaunchAgent\n// ---------------------------------------------------------------------------\n\nfunction installMacos(opts: InstallOpts): { ok: true; details: string } | { ok: false; error: string } {\n if (!existsSync(opts.agtBin)) {\n return { ok: false, error: `agt binary not found at ${opts.agtBin}` };\n }\n if (opts.intervalSec < 5) {\n return { ok: false, error: 'intervalSec must be >= 5' };\n }\n if (!opts.env.AGT_HOST || !opts.env.AGT_API_KEY) {\n return { ok: false, error: 'AGT_HOST and AGT_API_KEY must be set in the calling shell so the supervisor can pass them to the manager. Run `agt setup <token>` first.' };\n }\n\n // If a previous version is already loaded, unload it before rewriting\n // the plist — otherwise launchctl bootstrap will fail with EALREADY.\n bestEffort(() => execFileSync('launchctl', ['unload', '-w', MACOS_PLIST], { stdio: 'ignore' }));\n\n // Ensure the LaunchAgents directory exists. macOS creates it on first\n // login, but freshly-installed admin users may not have it yet.\n mkdirSync(dirname(MACOS_PLIST), { recursive: true });\n // 0o600 — plist embeds AGT_API_KEY in EnvironmentVariables. World-\n // readable would expose the host's tlk_ token to every local user.\n writeFileSync(MACOS_PLIST, renderMacosPlist(opts), { mode: 0o600 });\n chmodSync(MACOS_PLIST, 0o600);\n\n try {\n execFileSync('launchctl', ['load', '-w', MACOS_PLIST], { stdio: 'pipe' });\n } catch (err) {\n return { ok: false, error: `launchctl load failed: ${(err as Error).message}` };\n }\n\n // Verify launchd actually picked it up. `list <label>` returns 0 with\n // a plist of the loaded job's metadata; non-zero if not loaded.\n try {\n const out = execFileSync('launchctl', ['list', MACOS_LABEL], { encoding: 'utf-8' });\n return {\n ok: true,\n details: `Loaded launchd LaunchAgent ${MACOS_LABEL}. Logs: ${join(opts.configDir, 'manager.log')}.\\n${out.trim().split('\\n').slice(0, 10).join('\\n')}`,\n };\n } catch (err) {\n return { ok: false, error: `Plist written but launchctl list reported nothing for ${MACOS_LABEL}: ${(err as Error).message}` };\n }\n}\n\nfunction uninstallMacos(): { ok: true; details: string } | { ok: false; error: string } {\n // Both steps are idempotent — unload reports an error if nothing's\n // loaded, removing the file errors if it doesn't exist. Swallow both;\n // the desired end state is \"no LaunchAgent for this label\".\n bestEffort(() => execFileSync('launchctl', ['unload', '-w', MACOS_PLIST], { stdio: 'ignore' }));\n let removed = false;\n if (existsSync(MACOS_PLIST)) {\n try {\n unlinkSync(MACOS_PLIST);\n removed = true;\n } catch (err) {\n return { ok: false, error: `Failed to remove ${MACOS_PLIST}: ${(err as Error).message}` };\n }\n }\n return {\n ok: true,\n details: removed\n ? `Removed ${MACOS_LABEL} from launchd and deleted ${MACOS_PLIST}.`\n : `${MACOS_LABEL} was already uninstalled (no plist at ${MACOS_PLIST}).`,\n };\n}\n\nfunction statusMacos(): SupervisorPresence {\n if (!existsSync(MACOS_PLIST)) return { kind: 'not-installed' };\n // launchctl list emits a 3-column line: PID Status Label. PID is \"-\"\n // when the job is loaded but not currently running.\n try {\n const out = execFileSync('launchctl', ['list', MACOS_LABEL], { encoding: 'utf-8' });\n const pidMatch = /\"PID\"\\s*=\\s*(\\d+);/.exec(out);\n const pid = pidMatch ? Number(pidMatch[1]) : null;\n return { kind: 'installed', pid, details: `Plist: ${MACOS_PLIST}` };\n } catch {\n // File exists but launchctl can't find the label → user removed it\n // out-of-band. Surface as \"not installed\" so re-install just works.\n return { kind: 'not-installed' };\n }\n}\n\nfunction renderMacosPlist(opts: InstallOpts): string {\n // Load the operator's env into the plist so the manager has the same\n // AGT_HOST / AGT_API_KEY / AGT_TEAM the shell session does. launchd\n // does NOT inherit env from the user's shell — anything we don't bake\n // in here won't be visible to the process.\n const envEntries = Object.entries(opts.env)\n .filter(([k, v]) => k.length > 0 && v != null)\n .map(([k, v]) => ` <key>${escapeXml(k)}</key>\\n <string>${escapeXml(v)}</string>`)\n .join('\\n');\n\n const args = [\n 'manager',\n 'start',\n '--interval',\n String(opts.intervalSec),\n '--config-dir',\n opts.configDir,\n ];\n const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join('\\n');\n\n const logPath = join(opts.configDir, 'manager.log');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>${MACOS_LABEL}</string>\n\n <key>ProgramArguments</key>\n <array>\n <string>${escapeXml(opts.agtBin)}</string>\n${argsXml}\n </array>\n\n <key>RunAtLoad</key>\n <true/>\n\n <key>KeepAlive</key>\n <true/>\n\n <!-- Throttle restarts so a crashloop doesn't peg the CPU. 10 seconds\n between launches is enough room for transient API blips while\n still recovering quickly from a real crash. -->\n <key>ThrottleInterval</key>\n <integer>10</integer>\n\n <!-- Where the manager's stdout / stderr land. The manager's logger\n redirects stderr into manager.log itself, but launchd needs an\n explicit destination at the OS level for any pre-logger output\n (e.g., Node startup errors before the watchdog wires up). -->\n <key>StandardOutPath</key>\n <string>${escapeXml(logPath)}</string>\n <key>StandardErrorPath</key>\n <string>${escapeXml(logPath)}</string>\n\n <key>WorkingDirectory</key>\n <string>${escapeXml(opts.configDir)}</string>\n\n <key>EnvironmentVariables</key>\n <dict>\n${envEntries}\n </dict>\n</dict>\n</plist>\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction bestEffort(fn: () => void): void {\n try { fn(); } catch { /* swallow */ }\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// ---------------------------------------------------------------------------\n// Linux — systemd --user unit\n// ---------------------------------------------------------------------------\n\nfunction installLinux(opts: InstallOpts): { ok: true; details: string } | { ok: false; error: string } {\n if (!existsSync(opts.agtBin)) {\n return { ok: false, error: `agt binary not found at ${opts.agtBin}` };\n }\n if (opts.intervalSec < 5) {\n return { ok: false, error: 'intervalSec must be >= 5' };\n }\n if (!opts.env.AGT_HOST || !opts.env.AGT_API_KEY) {\n return { ok: false, error: 'AGT_HOST and AGT_API_KEY must be set in the calling shell so the supervisor can pass them to the unit. Run `agt setup <token>` first.' };\n }\n\n // Stop any previous instance before rewriting the unit — saves a\n // restart loop while daemon-reload picks up the new file.\n bestEffort(() => execFileSync('systemctl', ['--user', 'stop', LINUX_UNIT_NAME], { stdio: 'ignore' }));\n\n // ~/.config/systemd/user/ is the canonical user-unit path. Create it\n // recursively in case this is a fresh user with no XDG layout yet.\n // 0o700 — the unit body contains AGT_API_KEY in Environment= lines.\n mkdirSync(dirname(LINUX_UNIT_PATH), { recursive: true, mode: 0o700 });\n writeFileSync(LINUX_UNIT_PATH, renderLinuxUnit(opts), { mode: 0o600 });\n chmodSync(LINUX_UNIT_PATH, 0o600);\n\n try {\n execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'pipe' });\n } catch (err) {\n return { ok: false, error: `systemctl --user daemon-reload failed: ${(err as Error).message}` };\n }\n\n // `enable --now` enables the unit at boot AND starts it immediately.\n // Surviving reboot on a headless host additionally requires\n // `loginctl enable-linger <user>` (root-only); we surface that as a\n // post-install hint rather than running it ourselves.\n try {\n execFileSync('systemctl', ['--user', 'enable', '--now', LINUX_UNIT_NAME], { stdio: 'pipe' });\n } catch (err) {\n return { ok: false, error: `systemctl --user enable --now failed: ${(err as Error).message}. (Hint: 'systemctl --user' requires a per-user dbus session; on a fresh SSH login try 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' first.)` };\n }\n\n let isActive = '';\n try {\n isActive = execFileSync('systemctl', ['--user', 'is-active', LINUX_UNIT_NAME], { encoding: 'utf-8' }).trim();\n } catch (err) {\n // is-active exits non-zero for inactive/failed states. Capture its\n // output so the operator sees the real status.\n isActive = ((err as { stdout?: Buffer; stderr?: Buffer }).stdout?.toString() ?? '').trim() || 'unknown';\n }\n\n return {\n ok: true,\n details: `Loaded systemd --user unit ${LINUX_UNIT_NAME} (status: ${isActive}). Logs: ${join(opts.configDir, 'manager.log')}.\\nFor reboot survival on a headless host run as root: 'loginctl enable-linger ${process.env['USER'] ?? '<your-user>'}'`,\n };\n}\n\nfunction uninstallLinux(): { ok: true; details: string } | { ok: false; error: string } {\n // disable --now stops + un-enables in one shot. Both legs are idempotent\n // — disable on a non-existent unit returns 0 with a notice on stderr.\n bestEffort(() => execFileSync('systemctl', ['--user', 'disable', '--now', LINUX_UNIT_NAME], { stdio: 'ignore' }));\n\n let removed = false;\n if (existsSync(LINUX_UNIT_PATH)) {\n try {\n unlinkSync(LINUX_UNIT_PATH);\n removed = true;\n } catch (err) {\n return { ok: false, error: `Failed to remove ${LINUX_UNIT_PATH}: ${(err as Error).message}` };\n }\n }\n\n // Reload so a future install with the same path doesn't reuse the\n // stale unit metadata systemd cached in memory.\n bestEffort(() => execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' }));\n\n return {\n ok: true,\n details: removed\n ? `Disabled systemd --user unit ${LINUX_UNIT_NAME} and deleted ${LINUX_UNIT_PATH}.`\n : `${LINUX_UNIT_NAME} was already uninstalled (no unit file at ${LINUX_UNIT_PATH}).`,\n };\n}\n\nfunction statusLinux(): SupervisorPresence {\n if (!existsSync(LINUX_UNIT_PATH)) return { kind: 'not-installed' };\n let pid: number | null = null;\n let activeState = 'unknown';\n try {\n const out = execFileSync(\n 'systemctl',\n ['--user', 'show', LINUX_UNIT_NAME, '--property=MainPID,ActiveState'],\n { encoding: 'utf-8' },\n );\n const pidMatch = /^MainPID=(\\d+)/m.exec(out);\n if (pidMatch && pidMatch[1] !== '0') pid = Number(pidMatch[1]);\n const stateMatch = /^ActiveState=(\\S+)/m.exec(out);\n if (stateMatch) activeState = stateMatch[1] ?? 'unknown';\n } catch {\n return { kind: 'not-installed' };\n }\n return { kind: 'installed', pid, details: `Unit: ${LINUX_UNIT_PATH} (${activeState})` };\n}\n\nfunction renderLinuxUnit(opts: InstallOpts): string {\n // systemd parses Environment= lines with a peculiar grammar — quotes\n // are optional but values can't contain literal newlines. We trust\n // the env values we forward (AGT_HOST/AGT_API_KEY/PATH etc. are\n // never multi-line) and quote everything to be safe with spaces.\n const envLines = Object.entries(opts.env)\n .filter(([k, v]) => k.length > 0 && v != null)\n .map(([k, v]) => `Environment=\"${k}=${escapeForSystemdEnv(v)}\"`)\n .join('\\n');\n\n // ExecStart cannot use shell metacharacters; pass the args explicitly\n // via systemd's array-of-args style (newline-separated wouldn't\n // parse — systemd splits on whitespace within ExecStart). So we\n // shell-quote each arg and join.\n const execArgs = [\n opts.agtBin,\n 'manager',\n 'start',\n '--interval',\n String(opts.intervalSec),\n '--config-dir',\n opts.configDir,\n ]\n .map(shellQuoteForSystemd)\n .join(' ');\n\n const logPath = join(opts.configDir, 'manager.log');\n\n return `# agt manager supervisor (ENG-4593)\n# Managed by \\`agt manager install\\` — edits will be overwritten.\n\n[Unit]\nDescription=Augmented host manager daemon\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nWorkingDirectory=${opts.configDir}\n${envLines}\nExecStart=${execArgs}\n\n# Restart policy — always come back, with throttling so a crashloop\n# can't peg the CPU. StartLimitBurst/IntervalSec follow systemd's\n# defaults but are stated explicitly so future operators don't have\n# to read the man page to understand the recovery envelope.\nRestart=always\nRestartSec=5\nStartLimitBurst=5\nStartLimitIntervalSec=60\n\n# stdout/stderr → manager.log. journalctl --user -u agt-manager\n# still captures everything; this just gives operators a single file\n# to tail without learning systemd plumbing.\nStandardOutput=append:${logPath}\nStandardError=append:${logPath}\n\n[Install]\nWantedBy=default.target\n`;\n}\n\nfunction escapeForSystemdEnv(value: string): string {\n // Inside an Environment=\"K=V\" line, \" and \\\\ need escaping. Newlines\n // would terminate the directive — reject them upstream.\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nfunction shellQuoteForSystemd(arg: string): string {\n // systemd's ExecStart parser supports double-quoted strings with\n // backslash escapes, similar to a POSIX shell but no command\n // substitution. Wrap any arg containing whitespace or shell metas.\n if (/^[A-Za-z0-9_./:=-]+$/.test(arg)) return arg;\n return `\"${arg.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n// Exposed for unit tests / introspection.\nexport const _internals = {\n MACOS_LABEL,\n MACOS_PLIST,\n LINUX_UNIT_NAME,\n LINUX_UNIT_PATH,\n renderMacosPlist,\n renderLinuxUnit,\n escapeXml,\n escapeForSystemdEnv,\n shellQuoteForSystemd,\n readPlist: () => (existsSync(MACOS_PLIST) ? readFileSync(MACOS_PLIST, 'utf-8') : null),\n readUnit: () => (existsSync(LINUX_UNIT_PATH) ? readFileSync(LINUX_UNIT_PATH, 'utf-8') : null),\n};\n"],"mappings":"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","names":[]}