@phnx-labs/agents-cli 1.20.34 → 1.20.35
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/CHANGELOG.md +22 -0
- package/README.md +5 -1
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +59 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/secrets.js +40 -16
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +58 -31
- package/dist/lib/agents.js +30 -9
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +76 -16
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.js +22 -0
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +11 -3
- package/dist/lib/shims.js +54 -30
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -51,9 +51,11 @@ if (IS_DEV_BUILD) {
|
|
|
51
51
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
52
52
|
// first byte of output), the registry maps a command name to a thunk that
|
|
53
53
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
54
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
54
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
55
55
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
56
|
+
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
57
|
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
58
|
+
import { emit, redactArgs } from './lib/events.js';
|
|
57
59
|
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
58
60
|
// `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
|
|
59
61
|
// parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
|
|
@@ -75,6 +77,56 @@ program
|
|
|
75
77
|
.version(VERSION)
|
|
76
78
|
.helpOption('-h, --help', 'Show help')
|
|
77
79
|
.addHelpCommand(false);
|
|
80
|
+
// ─── Audit backbone ────────────────────────────────────────────────────────────
|
|
81
|
+
// One choke point logs every `agents <module> <cmd>` invocation to the structured
|
|
82
|
+
// event log — so team create/disband, agent run, secrets access, and everything
|
|
83
|
+
// else is captured generically (with SSH/remote-user attribution added in emit()),
|
|
84
|
+
// no per-command wiring. `agents events` reads it back. Attached to the root
|
|
85
|
+
// program, so it's inherited by every subcommand regardless of lazy registration.
|
|
86
|
+
/** Command path from the acting command up to (but excluding) the `agents` root. */
|
|
87
|
+
function auditCommandPath(cmd) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let c = cmd;
|
|
90
|
+
while (c && c.name() && c.name() !== 'agents') {
|
|
91
|
+
parts.unshift(c.name());
|
|
92
|
+
c = c.parent;
|
|
93
|
+
}
|
|
94
|
+
return parts;
|
|
95
|
+
}
|
|
96
|
+
const auditStarts = new WeakMap();
|
|
97
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
98
|
+
try {
|
|
99
|
+
const parts = auditCommandPath(actionCommand);
|
|
100
|
+
if (parts.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
auditStarts.set(actionCommand, Date.now());
|
|
103
|
+
emit('command.start', {
|
|
104
|
+
module: parts[0],
|
|
105
|
+
command: parts.join(' '),
|
|
106
|
+
args: redactArgs(actionCommand.args),
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Audit logging must never break command dispatch.
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
program.hook('postAction', (_thisCommand, actionCommand) => {
|
|
115
|
+
try {
|
|
116
|
+
const parts = auditCommandPath(actionCommand);
|
|
117
|
+
if (parts.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
const started = auditStarts.get(actionCommand);
|
|
120
|
+
emit('command.end', {
|
|
121
|
+
module: parts[0],
|
|
122
|
+
command: parts.join(' '),
|
|
123
|
+
...(started !== undefined ? { durationMs: Date.now() - started } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Best-effort completion record; the start line is the durable audit fact.
|
|
128
|
+
}
|
|
129
|
+
});
|
|
78
130
|
// Custom help for the main program only
|
|
79
131
|
const originalHelpInformation = program.helpInformation.bind(program);
|
|
80
132
|
program.helpInformation = function () {
|
|
@@ -121,6 +173,7 @@ Run and dispatch:
|
|
|
121
173
|
teams Coordinate multiple agents on shared work
|
|
122
174
|
routines Run agents on a cron schedule (scheduler auto-starts)
|
|
123
175
|
sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
|
|
176
|
+
logs [id] Show a run's log — host-dispatch task or session; -f to follow
|
|
124
177
|
browser Automate a browser — navigate, click, screenshot, console, network
|
|
125
178
|
pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
|
|
126
179
|
|
|
@@ -175,41 +228,13 @@ async function showWhatsNew(fromVersion, toVersion) {
|
|
|
175
228
|
const response = await fetch(`https://unpkg.com/@phnx-labs/agents-cli@${toVersion}/CHANGELOG.md`);
|
|
176
229
|
if (!response.ok)
|
|
177
230
|
return;
|
|
178
|
-
const
|
|
179
|
-
const lines = changelog.split('\n');
|
|
180
|
-
const relevantChanges = [];
|
|
181
|
-
let inRelevantSection = false;
|
|
182
|
-
let currentVersion = '';
|
|
183
|
-
for (const line of lines) {
|
|
184
|
-
const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
|
|
185
|
-
if (versionMatch) {
|
|
186
|
-
currentVersion = versionMatch[1];
|
|
187
|
-
// Only the range the user actually moved through: (fromVersion, toVersion].
|
|
188
|
-
// Bounding the top end matters when upgrading to a specific older
|
|
189
|
-
// version, and guards against a changelog that lists unreleased entries.
|
|
190
|
-
const inRange = compareVersions(currentVersion, fromVersion) > 0 &&
|
|
191
|
-
compareVersions(currentVersion, toVersion) <= 0;
|
|
192
|
-
inRelevantSection = inRange;
|
|
193
|
-
if (inRelevantSection) {
|
|
194
|
-
relevantChanges.push('');
|
|
195
|
-
relevantChanges.push(chalk.bold(`v${currentVersion}`));
|
|
196
|
-
}
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (inRelevantSection && line.trim()) {
|
|
200
|
-
if (line.startsWith('**') && line.endsWith('**')) {
|
|
201
|
-
relevantChanges.push(chalk.cyan(line.replace(/\*\*/g, '')));
|
|
202
|
-
}
|
|
203
|
-
else if (line.startsWith('- ')) {
|
|
204
|
-
relevantChanges.push(chalk.gray(` ${line}`));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
231
|
+
const relevantChanges = renderWhatsNew(await response.text(), fromVersion, toVersion);
|
|
208
232
|
if (relevantChanges.length > 0) {
|
|
209
233
|
console.log(chalk.bold("\nWhat's new:\n"));
|
|
210
234
|
for (const line of relevantChanges) {
|
|
211
235
|
console.log(line);
|
|
212
236
|
}
|
|
237
|
+
console.log(chalk.gray('\nFull notes: https://github.com/phnx-labs/agents-cli/blob/main/CHANGELOG.md'));
|
|
213
238
|
console.log();
|
|
214
239
|
}
|
|
215
240
|
}
|
|
@@ -785,6 +810,8 @@ async function registerAllEagerCommands() {
|
|
|
785
810
|
await reg(loadBrowser);
|
|
786
811
|
await reg(loadComputer);
|
|
787
812
|
await reg(loadHosts);
|
|
813
|
+
await reg(loadLogs);
|
|
814
|
+
await reg(loadEvents);
|
|
788
815
|
await reg(loadSsh);
|
|
789
816
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
790
817
|
registerJobsCronAliasCommand(program, 'cron');
|
package/dist/lib/agents.js
CHANGED
|
@@ -327,11 +327,16 @@ export const AGENTS = {
|
|
|
327
327
|
commandsSubdir: 'commands',
|
|
328
328
|
skillsDir: path.join(HOME, '.copilot', 'skills'),
|
|
329
329
|
hooksDir: 'hooks',
|
|
330
|
+
// Copilot reads a plugin's manifest from the plugin ROOT (plugin.json),
|
|
331
|
+
// not `.claude-plugin/plugin.json`. Mirror it there. Verified against the
|
|
332
|
+
// GitHub Copilot CLI (1.0.56): `copilot plugin install` produces an
|
|
333
|
+
// installed plugin dir whose manifest sits at the root.
|
|
334
|
+
pluginManifestDir: '.',
|
|
330
335
|
instructionsFile: 'AGENTS.md',
|
|
331
336
|
format: 'markdown',
|
|
332
337
|
variableSyntax: '$ARGUMENTS',
|
|
333
338
|
supportsHooks: false,
|
|
334
|
-
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins:
|
|
339
|
+
capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
|
|
335
340
|
},
|
|
336
341
|
amp: {
|
|
337
342
|
id: 'amp',
|
|
@@ -491,11 +496,26 @@ export const AGENTS = {
|
|
|
491
496
|
// Install: `curl -fsSL https://app.factory.ai/cli | sh` (no npm package).
|
|
492
497
|
// Binary is NOT in node_modules/.bin — the shim resolves the fixed install
|
|
493
498
|
// path ~/.local/bin/droid directly (see the droid branch in shims.ts).
|
|
494
|
-
// Config: `~/.factory/` (settings.json, mcp.json, droids/, commands
|
|
495
|
-
// Memory: native AGENTS.md. Subagents = custom droids (top-level
|
|
496
|
-
// in ~/.factory/droids/). Config isolation rides the ~/.factory
|
|
497
|
-
// switch (no FACTORY_HOME env var exists). Headless:
|
|
498
|
-
// with --auto low|medium|high, -o stream-json,
|
|
499
|
+
// Config: `~/.factory/` (settings.json, mcp.json, droids/, commands/, hooks/,
|
|
500
|
+
// plugins/). Memory: native AGENTS.md. Subagents = custom droids (top-level
|
|
501
|
+
// .md files in ~/.factory/droids/). Config isolation rides the ~/.factory
|
|
502
|
+
// symlink switch (no FACTORY_HOME env var exists). Headless:
|
|
503
|
+
// `droid exec "<prompt>"` with --auto low|medium|high, -o stream-json,
|
|
504
|
+
// -m <model>, -r <effort>.
|
|
505
|
+
//
|
|
506
|
+
// Hooks: Claude-shaped. settings.json carries a top-level `hooks` object keyed
|
|
507
|
+
// by event (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart,
|
|
508
|
+
// SessionEnd, Stop, SubagentStop, Notification, PreCompact), each an array of
|
|
509
|
+
// `{ matcher?, hooks: [{ type: "command", command, timeout? }] }` matcher
|
|
510
|
+
// groups — verified against the droid binary's zod schema. So the Claude
|
|
511
|
+
// registrar is reused verbatim, just targeting `.factory/settings.json`.
|
|
512
|
+
//
|
|
513
|
+
// Plugins: native `droid plugin` command group + ~/.factory/plugins/ with the
|
|
514
|
+
// same marketplace layout Claude uses (known_marketplaces.json +
|
|
515
|
+
// marketplaces/<name>/plugins/<plugin>/). Droid's plugin manifest dir is
|
|
516
|
+
// `.factory-plugin/` (it also reads `.claude-plugin/` for compatibility) — set
|
|
517
|
+
// pluginManifestDir so syncPluginToVersion mirrors the manifest into it, the
|
|
518
|
+
// same pattern codex uses with `.codex-plugin`.
|
|
499
519
|
droid: {
|
|
500
520
|
id: 'droid',
|
|
501
521
|
name: 'Droid',
|
|
@@ -509,20 +529,21 @@ export const AGENTS = {
|
|
|
509
529
|
commandsSubdir: 'commands',
|
|
510
530
|
skillsDir: '', // no skills concept
|
|
511
531
|
hooksDir: 'hooks',
|
|
532
|
+
pluginManifestDir: '.factory-plugin',
|
|
512
533
|
instructionsFile: 'AGENTS.md',
|
|
513
534
|
format: 'markdown',
|
|
514
535
|
variableSyntax: '$ARGUMENTS',
|
|
515
|
-
supportsHooks:
|
|
536
|
+
supportsHooks: true,
|
|
516
537
|
// Factory Droid Computers (cloud VMs) reached via `droid computer ssh` +
|
|
517
538
|
// remote headless `droid exec`.
|
|
518
539
|
cloudProvider: 'factory',
|
|
519
540
|
capabilities: {
|
|
520
|
-
hooks:
|
|
541
|
+
hooks: true,
|
|
521
542
|
mcp: true,
|
|
522
543
|
allowlist: false,
|
|
523
544
|
skills: false,
|
|
524
545
|
commands: true,
|
|
525
|
-
plugins:
|
|
546
|
+
plugins: true,
|
|
526
547
|
subagents: true,
|
|
527
548
|
rules: { file: 'AGENTS.md' },
|
|
528
549
|
workflows: false,
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -219,7 +219,11 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
|
|
|
219
219
|
// to a Comet instance doesn't trip a false "identity mismatch".
|
|
220
220
|
comet: ['comet', 'chrome'],
|
|
221
221
|
brave: ['brave', 'brave-browser', 'chrome'],
|
|
222
|
-
|
|
222
|
+
// Windows/Chromium Edge reports its /json/version "Browser" field as
|
|
223
|
+
// "Edg/<version>" (the `Edg` token, not `Edge`), which normalizeBrowserName
|
|
224
|
+
// reduces to "edg". Accept it so attaching to a real Edge doesn't trip a
|
|
225
|
+
// false identity mismatch (the profile enum only allows "edge").
|
|
226
|
+
edge: ['edge', 'edg', 'microsoft-edge', 'msedge', 'chrome'],
|
|
223
227
|
};
|
|
224
228
|
const accepted = matches[expected] || [expected];
|
|
225
229
|
if (accepted.includes(reported))
|
|
@@ -53,7 +53,15 @@ export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string,
|
|
|
53
53
|
* (Get-NetTCPConnection → Stop-Process).
|
|
54
54
|
*/
|
|
55
55
|
export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
|
|
56
|
+
export declare function ensureRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
56
57
|
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
60
|
+
* WMI-spawned (Windows) / detached (posix) browser process is not orphaned when
|
|
61
|
+
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
62
|
+
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
63
|
+
*/
|
|
64
|
+
export declare function killRemoteBrowser(user: string, host: string, remoteOs: RemoteOs, port: number): Promise<void>;
|
|
57
65
|
/**
|
|
58
66
|
* Identify whether a pid listening on our target local port is an SSH
|
|
59
67
|
* tunnel WE would have spawned for `host:remotePort`. Used so that two
|
|
@@ -48,12 +48,12 @@ export async function connectSSH(endpoint, profile) {
|
|
|
48
48
|
`is already in use by ${occupant.command} (pid ${occupant.pid}). ` +
|
|
49
49
|
`Either kill that process (\`kill ${occupant.pid}\`) or change the profile's port.`);
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// A non-zero remote exit (missing exe, auth failure, bad launch) now rejects
|
|
52
|
+
// with the captured stderr and propagates. An already-running browser exits 0
|
|
53
|
+
// (the launch backgrounds/WMI-spawns regardless), so this does not spuriously
|
|
54
|
+
// fail the reconnect path — only genuine launch failures reach the caller
|
|
55
|
+
// instead of being swallowed and mis-reported later as a tunnel timeout.
|
|
56
|
+
await ensureRemoteBrowser(user, host, profile.browser, remotePort, remoteOs, profile.binary);
|
|
57
57
|
let tunnel;
|
|
58
58
|
if (occupant) {
|
|
59
59
|
// Reuse the existing tunnel rather than spawning a duplicate.
|
|
@@ -102,6 +102,11 @@ export async function connectSSH(endpoint, profile) {
|
|
|
102
102
|
pid: tunnelPid,
|
|
103
103
|
cleanup: () => {
|
|
104
104
|
cdp.close();
|
|
105
|
+
// Kill the remote browser BEFORE tearing down the tunnel. It runs on a
|
|
106
|
+
// separate ssh connection (independent of the tunnel we're about to
|
|
107
|
+
// kill), so tunnel teardown can't cut it off. Fire-and-forget — cleanup
|
|
108
|
+
// stays synchronous, and killRemoteBrowser never rejects.
|
|
109
|
+
killRemoteBrowser(user, host, remoteOs, remotePort).catch(() => { });
|
|
105
110
|
tunnel.kill();
|
|
106
111
|
clearProfileRuntime(profile.name);
|
|
107
112
|
},
|
|
@@ -177,9 +182,9 @@ export function encodePowerShell(script) {
|
|
|
177
182
|
* registry at runtime rather than relying on a bare `msedge` name.
|
|
178
183
|
*/
|
|
179
184
|
export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
180
|
-
let
|
|
185
|
+
let exeStmts;
|
|
181
186
|
if (customBinary) {
|
|
182
|
-
|
|
187
|
+
exeStmts = [`$exe = ${psSingleQuote(customBinary)}`];
|
|
183
188
|
}
|
|
184
189
|
else if (browserType === 'custom') {
|
|
185
190
|
throw new Error('browser: custom requires a binary path in the profile');
|
|
@@ -188,13 +193,24 @@ export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
|
188
193
|
const exeKey = WIN_BROWSER_APPPATH[browserType];
|
|
189
194
|
if (!exeKey)
|
|
190
195
|
throw new Error(`Unknown browser type for windows remote: ${browserType}`);
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
const appPath = `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeKey}`;
|
|
197
|
+
// Per-user installs — the DEFAULT for Edge and Chrome — register App Paths
|
|
198
|
+
// under HKCU, not HKLM, so an HKLM-only lookup silently missed them and the
|
|
199
|
+
// launch failed. Resolve from HKLM first, then fall through to HKCU.
|
|
200
|
+
// Windows PowerShell 5.1 (what `powershell.exe` is) has no `??`, so the
|
|
201
|
+
// fallback is an explicit `if`. A missing exe `throw`s so the failure rides
|
|
202
|
+
// out as a non-zero ssh exit (surfaced by ensureRemoteBrowser) instead of a
|
|
203
|
+
// silent empty launch.
|
|
204
|
+
exeStmts = [
|
|
205
|
+
`$exe = (Get-ItemProperty 'HKLM:\\${appPath}' -EA SilentlyContinue).'(default)'`,
|
|
206
|
+
`if (-not $exe) { $exe = (Get-ItemProperty 'HKCU:\\${appPath}' -EA SilentlyContinue).'(default)' }`,
|
|
207
|
+
`if (-not $exe) { throw 'browser exe not found in HKLM/HKCU App Paths: ${exeKey}' }`,
|
|
208
|
+
];
|
|
193
209
|
}
|
|
194
210
|
// Keep the `--remote-allow-origins=http://127.0.0.1:${port}` literal in
|
|
195
211
|
// source — a test asserts CDP is never opened to `*`.
|
|
196
212
|
return [
|
|
197
|
-
|
|
213
|
+
...exeStmts,
|
|
198
214
|
`$cl = '"' + $exe + '" --remote-debugging-port=${port}` +
|
|
199
215
|
` --remote-allow-origins=http://127.0.0.1:${port}` +
|
|
200
216
|
` --disable-background-timer-throttling --user-data-dir="' + $env:TEMP + '\\agents-browser-${port}"'`,
|
|
@@ -242,21 +258,56 @@ export function buildKillCmd(remoteOs, port) {
|
|
|
242
258
|
}
|
|
243
259
|
return `pids=$(lsof -ti ${shellQuote(`:${port}`)} 2>/dev/null); [ -z "$pids" ] || kill -9 $pids 2>/dev/null || true`;
|
|
244
260
|
}
|
|
245
|
-
async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
261
|
+
export async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
246
262
|
const remoteCmd = buildLaunchCmd(remoteOs, browserType, port, customBinary);
|
|
247
263
|
return new Promise((resolve, reject) => {
|
|
264
|
+
// Capture stderr so a real launch failure (missing exe, auth denied, bad
|
|
265
|
+
// CreateProcess) surfaces here as a clear error. Previously stdio was
|
|
266
|
+
// ignored and this resolved on close regardless of exit code, so failures
|
|
267
|
+
// were swallowed and only re-emerged 8s later as a generic
|
|
268
|
+
// "SSH tunnel failed to establish".
|
|
248
269
|
const child = spawn('ssh', [
|
|
249
270
|
`${user}@${host}`,
|
|
250
271
|
'-o',
|
|
251
272
|
'BatchMode=yes',
|
|
252
273
|
remoteCmd,
|
|
253
|
-
], { stdio: 'ignore' });
|
|
254
|
-
|
|
255
|
-
child.on('
|
|
256
|
-
|
|
274
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
275
|
+
let stderr = '';
|
|
276
|
+
child.stderr?.on('data', (d) => {
|
|
277
|
+
stderr += d.toString();
|
|
278
|
+
});
|
|
279
|
+
let settled = false;
|
|
280
|
+
const timer = setTimeout(() => {
|
|
281
|
+
if (settled)
|
|
282
|
+
return;
|
|
283
|
+
settled = true;
|
|
284
|
+
// ssh is still running the backgrounding launch command past our window.
|
|
285
|
+
// Killing the local ssh client does NOT reap the WMI/detached remote
|
|
286
|
+
// browser, so treat this as launched and let waitForPort confirm.
|
|
257
287
|
child.kill();
|
|
258
288
|
resolve();
|
|
259
289
|
}, 2000);
|
|
290
|
+
child.on('close', (code) => {
|
|
291
|
+
if (settled)
|
|
292
|
+
return;
|
|
293
|
+
settled = true;
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
if (code && code !== 0) {
|
|
296
|
+
const detail = stderr.trim();
|
|
297
|
+
reject(new Error(`Remote browser launch failed on ${host} (ssh exit ${code})` +
|
|
298
|
+
(detail ? `: ${detail}` : '')));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
resolve();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
child.on('error', (err) => {
|
|
305
|
+
if (settled)
|
|
306
|
+
return;
|
|
307
|
+
settled = true;
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
reject(err);
|
|
310
|
+
});
|
|
260
311
|
});
|
|
261
312
|
}
|
|
262
313
|
export async function restartRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
@@ -266,6 +317,15 @@ export async function restartRemoteBrowser(user, host, browserType, port, remote
|
|
|
266
317
|
await ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary);
|
|
267
318
|
await sleep(1500);
|
|
268
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
322
|
+
* WMI-spawned (Windows) / detached (posix) browser process is not orphaned when
|
|
323
|
+
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
324
|
+
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
325
|
+
*/
|
|
326
|
+
export function killRemoteBrowser(user, host, remoteOs, port) {
|
|
327
|
+
return runSSHCommand(user, host, buildKillCmd(remoteOs, port));
|
|
328
|
+
}
|
|
269
329
|
function runSSHCommand(user, host, cmd) {
|
|
270
330
|
return new Promise((resolve) => {
|
|
271
331
|
const child = spawn('ssh', [`${user}@${host}`, '-o', 'BatchMode=yes', cmd], {
|
|
@@ -8,6 +8,9 @@ export declare class BrowserDaemonNotRunningError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function formatBrowserDaemonNotRunningError(): string;
|
|
10
10
|
export declare function getSocketPath(): string;
|
|
11
|
+
/** Is the daemon reachable? A real connect probe on every platform — a socket
|
|
12
|
+
* file existing on disk is not proof a daemon is listening on it. */
|
|
13
|
+
export declare function isDaemonReachable(): Promise<boolean>;
|
|
11
14
|
export declare class BrowserIPCServer {
|
|
12
15
|
private server;
|
|
13
16
|
private service;
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -32,8 +32,12 @@ export function getSocketPath() {
|
|
|
32
32
|
function getIpcEndpoint() {
|
|
33
33
|
return ipcEndpoint(getSocketPath());
|
|
34
34
|
}
|
|
35
|
-
/** Can we open a connection to the daemon right now?
|
|
36
|
-
*
|
|
35
|
+
/** Can we open a connection to the daemon right now? Resolves false on any
|
|
36
|
+
* error. This is the authoritative liveness check on every platform: a live
|
|
37
|
+
* daemon accepts the connection, while a stale POSIX socket file left behind by
|
|
38
|
+
* a crashed daemon (or one that "appears and is immediately destroyed", #556)
|
|
39
|
+
* rejects with ECONNREFUSED and is correctly reported as not reachable —
|
|
40
|
+
* something fs.existsSync can't distinguish. */
|
|
37
41
|
function probeDaemon(endpoint, timeoutMs = 500) {
|
|
38
42
|
return new Promise((resolve) => {
|
|
39
43
|
const sock = net.createConnection(endpoint);
|
|
@@ -45,16 +49,16 @@ function probeDaemon(endpoint, timeoutMs = 500) {
|
|
|
45
49
|
sock.on('error', () => { clearTimeout(timer); finish(false); });
|
|
46
50
|
});
|
|
47
51
|
}
|
|
48
|
-
/** Is the daemon reachable?
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return fs.existsSync(getSocketPath());
|
|
52
|
+
/** Is the daemon reachable? A real connect probe on every platform — a socket
|
|
53
|
+
* file existing on disk is not proof a daemon is listening on it. */
|
|
54
|
+
export async function isDaemonReachable() {
|
|
55
|
+
return probeDaemon(getIpcEndpoint());
|
|
53
56
|
}
|
|
54
|
-
async function waitForSocket(
|
|
57
|
+
async function waitForSocket(_socketPath, timeoutMs) {
|
|
58
|
+
const endpoint = getIpcEndpoint();
|
|
55
59
|
const deadline = Date.now() + timeoutMs;
|
|
56
60
|
while (Date.now() < deadline) {
|
|
57
|
-
if (
|
|
61
|
+
if (await probeDaemon(endpoint))
|
|
58
62
|
return;
|
|
59
63
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
60
64
|
}
|
|
@@ -412,12 +412,23 @@ export class BrowserService {
|
|
|
412
412
|
return this.stop(taskName);
|
|
413
413
|
}
|
|
414
414
|
async stopProfile(profileName) {
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
// Connections are keyed by the composite `<profile>@<endpoint>` (see start()),
|
|
416
|
+
// but callers pass the bare profile name (or, occasionally, an exact composite).
|
|
417
|
+
// A plain `connections.get(profileName)` therefore missed every real remote
|
|
418
|
+
// connection, so `cleanup()` never ran — leaving the SSH tunnel and, on
|
|
419
|
+
// Windows, the WMI-spawned browser orphaned on the remote host after every
|
|
420
|
+
// `browser stop --profile` (#559). Match the exact key AND every
|
|
421
|
+
// `<profileName>@<endpoint>` composite so all of a profile's live connections
|
|
422
|
+
// are torn down.
|
|
423
|
+
const keys = [...this.connections.keys()].filter((k) => k === profileName || k.startsWith(`${profileName}@`));
|
|
424
|
+
for (const key of keys) {
|
|
425
|
+
const conn = this.connections.get(key);
|
|
426
|
+
if (!conn)
|
|
427
|
+
continue;
|
|
417
428
|
conn.cdp.close();
|
|
418
429
|
killChrome(conn.pid);
|
|
419
430
|
conn.cleanup?.();
|
|
420
|
-
this.connections.delete(
|
|
431
|
+
this.connections.delete(key);
|
|
421
432
|
}
|
|
422
433
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
423
434
|
const pidFile = path.join(runtimeDir, 'pid');
|
|
@@ -693,14 +704,22 @@ export class BrowserService {
|
|
|
693
704
|
const outputPath = path.join(recordingsDir, `${Date.now()}.webm`);
|
|
694
705
|
// Resolve ffmpeg lazily so non-recording paths don't pay the import cost.
|
|
695
706
|
const { spawn } = await import('child_process');
|
|
707
|
+
// CDP `Page.startScreencast` delivers frames at a VARIABLE cadence (paints
|
|
708
|
+
// are event-driven, not clocked). Feeding those into a fixed `-framerate`
|
|
709
|
+
// input made ffmpeg assume every frame was 1/fps apart, so a page that
|
|
710
|
+
// painted slower/faster than `fps` played back at the wrong speed. Instead
|
|
711
|
+
// stamp each frame with its wall-clock arrival time
|
|
712
|
+
// (`-use_wallclock_as_timestamps 1`) and keep those timings on output
|
|
713
|
+
// (`-vsync vfr`), so playback duration matches the real capture.
|
|
696
714
|
const ffmpeg = spawn('ffmpeg', [
|
|
697
715
|
'-loglevel', 'error',
|
|
698
716
|
'-f', 'image2pipe',
|
|
699
|
-
'-
|
|
717
|
+
'-use_wallclock_as_timestamps', '1',
|
|
700
718
|
'-i', '-',
|
|
701
719
|
'-c:v', 'libvpx-vp9',
|
|
702
720
|
'-b:v', '1M',
|
|
703
721
|
'-pix_fmt', 'yuv420p',
|
|
722
|
+
'-vsync', 'vfr',
|
|
704
723
|
'-y',
|
|
705
724
|
outputPath,
|
|
706
725
|
], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
@@ -724,9 +743,16 @@ export class BrowserService {
|
|
|
724
743
|
ffmpeg.once('error', onError);
|
|
725
744
|
ffmpeg.once('spawn', onSpawn);
|
|
726
745
|
});
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
ffmpeg
|
|
746
|
+
// Capture ffmpeg's own diagnostics (encoder error, bad codec, etc.) so a
|
|
747
|
+
// failing encode is DIAGNOSABLE at recordStop instead of being discarded.
|
|
748
|
+
// Cap the buffer so a chatty ffmpeg can't grow it unbounded.
|
|
749
|
+
let stderrBuf = '';
|
|
750
|
+
ffmpeg.stderr?.on('data', (d) => {
|
|
751
|
+
stderrBuf += d.toString();
|
|
752
|
+
if (stderrBuf.length > 64 * 1024)
|
|
753
|
+
stderrBuf = stderrBuf.slice(-64 * 1024);
|
|
754
|
+
});
|
|
755
|
+
const ffmpegStderr = () => stderrBuf;
|
|
730
756
|
ffmpeg.on('error', () => { });
|
|
731
757
|
// 30 fps is CDP's screencast cap; everyNthFrame = round(30/fps).
|
|
732
758
|
const everyNthFrame = Math.max(1, Math.round(30 / fps));
|
|
@@ -752,6 +778,7 @@ export class BrowserService {
|
|
|
752
778
|
durationMs,
|
|
753
779
|
maxBytes,
|
|
754
780
|
ffmpeg,
|
|
781
|
+
ffmpegStderr,
|
|
755
782
|
sessionId,
|
|
756
783
|
conn,
|
|
757
784
|
frameHandler,
|
|
@@ -795,27 +822,56 @@ export class BrowserService {
|
|
|
795
822
|
catch {
|
|
796
823
|
// session may already be gone
|
|
797
824
|
}
|
|
798
|
-
// Close ffmpeg stdin so it flushes the output file cleanly
|
|
799
|
-
|
|
800
|
-
|
|
825
|
+
// Close ffmpeg stdin so it flushes the output file cleanly, and observe how
|
|
826
|
+
// it exits. A non-zero exit means the encode failed — the recording must
|
|
827
|
+
// NOT be reported as success. If ffmpeg hangs, KILL it (don't just abandon
|
|
828
|
+
// the promise, which leaked the process) and treat the recording as failed.
|
|
829
|
+
const finalize = await new Promise((resolve) => {
|
|
830
|
+
let done = false;
|
|
831
|
+
const settle = (r) => {
|
|
832
|
+
if (done)
|
|
833
|
+
return;
|
|
834
|
+
done = true;
|
|
835
|
+
clearTimeout(timer);
|
|
836
|
+
resolve(r);
|
|
837
|
+
};
|
|
838
|
+
const timer = setTimeout(() => {
|
|
839
|
+
try {
|
|
840
|
+
rec.ffmpeg.kill('SIGKILL');
|
|
841
|
+
}
|
|
842
|
+
catch { /* already gone */ }
|
|
843
|
+
settle({ code: null, timedOut: true });
|
|
844
|
+
}, 5000);
|
|
845
|
+
rec.ffmpeg.once('exit', (code) => settle({ code, timedOut: false }));
|
|
801
846
|
try {
|
|
802
847
|
rec.ffmpeg.stdin?.end();
|
|
803
848
|
}
|
|
804
849
|
catch {
|
|
805
|
-
|
|
850
|
+
// stdin already closed; wait for exit or the hard timeout above.
|
|
806
851
|
}
|
|
807
|
-
setTimeout(resolve, 5000); // Hard timeout if ffmpeg hangs
|
|
808
852
|
});
|
|
853
|
+
const durationMs = Date.now() - rec.startedAt;
|
|
854
|
+
// Drop the recording from the map before any throw, so a failed stop
|
|
855
|
+
// doesn't wedge the task in a permanent "already recording" state.
|
|
856
|
+
this.recordings.delete(taskId);
|
|
857
|
+
if (finalize.timedOut) {
|
|
858
|
+
throw new Error(`ffmpeg did not exit within 5s while finalizing the recording; killed it. ` +
|
|
859
|
+
`The recording at ${rec.outputPath} is incomplete.`);
|
|
860
|
+
}
|
|
861
|
+
if (finalize.code !== 0) {
|
|
862
|
+
const err = rec.ffmpegStderr().trim();
|
|
863
|
+
throw new Error(`ffmpeg exited abnormally (code ${finalize.code}) while finalizing the recording at ` +
|
|
864
|
+
`${rec.outputPath}; the file is likely corrupt or empty.` +
|
|
865
|
+
(err ? ` ffmpeg: ${err.slice(-800)}` : ''));
|
|
866
|
+
}
|
|
809
867
|
let bytes = 0;
|
|
810
868
|
try {
|
|
811
869
|
const st = await fs.promises.stat(rec.outputPath);
|
|
812
870
|
bytes = st.size;
|
|
813
871
|
}
|
|
814
872
|
catch {
|
|
815
|
-
// ffmpeg
|
|
873
|
+
// ffmpeg exited 0 but the file is missing — still a failed recording.
|
|
816
874
|
}
|
|
817
|
-
const durationMs = Date.now() - rec.startedAt;
|
|
818
|
-
this.recordings.delete(taskId);
|
|
819
875
|
return { path: rec.outputPath, bytes, durationMs, reason };
|
|
820
876
|
}
|
|
821
877
|
async recordStatus(taskId) {
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -46,6 +46,40 @@ export declare function startDaemon(): {
|
|
|
46
46
|
* value (e.g. inherited from launchd) is left untouched.
|
|
47
47
|
*/
|
|
48
48
|
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve how to launch the daemon: `node <entry> daemon _run`, matching the
|
|
51
|
+
* exact form that works under a direct `daemon _run`.
|
|
52
|
+
*
|
|
53
|
+
* We spawn the Node runtime (`process.execPath`) with the CLI entry as an
|
|
54
|
+
* argument rather than executing the entry path directly. Executing the `.js`
|
|
55
|
+
* path relies on its shebang on POSIX, and on Windows CreateProcess can't run a
|
|
56
|
+
* `.js`/shim directly at all — it gets launched through a transient
|
|
57
|
+
* console-owning wrapper (cmd.exe / the npm shim). When that wrapper exits it
|
|
58
|
+
* closes its console, and the detached daemon sharing that console receives a
|
|
59
|
+
* console-close event that trips its shutdown handler — the daemon comes up,
|
|
60
|
+
* binds the browser IPC socket, then tears itself down ~36ms later (#556).
|
|
61
|
+
* Going through `process.execPath` means a real PE/binary is spawned with
|
|
62
|
+
* `detached: true` and no console, so nothing signals the daemon after launch.
|
|
63
|
+
*
|
|
64
|
+
* When the entry isn't a JS file (e.g. a native launcher resolved via
|
|
65
|
+
* `which agents`), run it directly — it owns its own runtime resolution.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getDaemonLaunch(agentsBin?: string): {
|
|
68
|
+
command: string;
|
|
69
|
+
args: string[];
|
|
70
|
+
};
|
|
71
|
+
interface StartDetachedOptions {
|
|
72
|
+
/** CLI entry to launch (defaults to the running binary). Injectable for tests. */
|
|
73
|
+
agentsBin?: string;
|
|
74
|
+
/** Log file the daemon's stdio is redirected to (defaults to the daemon log). */
|
|
75
|
+
logPath?: string;
|
|
76
|
+
/** Environment for the child (defaults to the OAuth-augmented detached env). */
|
|
77
|
+
env?: NodeJS.ProcessEnv;
|
|
78
|
+
}
|
|
79
|
+
export declare function startDetached(opts?: StartDetachedOptions): {
|
|
80
|
+
pid: number | null;
|
|
81
|
+
method: string;
|
|
82
|
+
};
|
|
49
83
|
/** Stop the daemon, unloading it from launchd/systemd if applicable. */
|
|
50
84
|
export declare function stopDaemon(): boolean;
|
|
51
85
|
/** Get current daemon status including running state, PID, and enabled job count. */
|
|
@@ -59,3 +93,4 @@ export declare function getDaemonStatus(): {
|
|
|
59
93
|
export declare function readDaemonLog(lines?: number): string;
|
|
60
94
|
/** Send SIGHUP to the daemon to trigger a job reload. */
|
|
61
95
|
export declare function signalDaemonReload(): boolean;
|
|
96
|
+
export {};
|