@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -10,8 +10,9 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
|
-
import { isDaemonRunning, signalDaemonReload, startDaemon,
|
|
14
|
-
import {
|
|
13
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, readDaemonLog, getDaemonStatus, getDaemonLogPath, } from '../lib/daemon/daemon.js';
|
|
14
|
+
import { isDaemonServiceEnabled, setDaemonServiceEnabled } from '../lib/daemon-services.js';
|
|
15
|
+
import { assertSchedulerEnabled, assertDaemonEnabled, isDaemonEnabled, isSchedulerEnabled, } from '../lib/device-config.js';
|
|
15
16
|
import { parseAgentVersionSpec, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
16
17
|
import { isCustomHarnessName } from '../lib/profiles.js';
|
|
17
18
|
import { RUN_STRATEGIES, normalizeRunStrategy } from '../lib/accounting/rotate.js';
|
|
@@ -313,10 +314,17 @@ function ensureSchedulerRunning(opts = {}) {
|
|
|
313
314
|
console.error(chalk.yellow(err.message));
|
|
314
315
|
return;
|
|
315
316
|
}
|
|
317
|
+
const wasServiceEnabled = isDaemonServiceEnabled('scheduler');
|
|
318
|
+
if (!wasServiceEnabled)
|
|
319
|
+
setDaemonServiceEnabled('scheduler', true);
|
|
316
320
|
if (isDaemonRunning()) {
|
|
317
|
-
signalDaemonReload();
|
|
318
|
-
if (!
|
|
319
|
-
|
|
321
|
+
const reloaded = signalDaemonReload();
|
|
322
|
+
if (!reloaded) {
|
|
323
|
+
console.error(chalk.yellow('Scheduler service enabled, but live reload is unavailable. Restart deliberately with: agents daemon restart'));
|
|
324
|
+
}
|
|
325
|
+
else if (!opts.quiet) {
|
|
326
|
+
log(chalk.gray(wasServiceEnabled ? 'Scheduler reloaded' : 'Scheduler service enabled and reloaded'));
|
|
327
|
+
}
|
|
320
328
|
return;
|
|
321
329
|
}
|
|
322
330
|
const result = startDaemon();
|
|
@@ -2162,7 +2170,7 @@ export function registerRoutinesCommands(program) {
|
|
|
2162
2170
|
// Scheduler lifecycle — usually auto-managed by `routines add`, exposed here for manual control.
|
|
2163
2171
|
routinesCmd
|
|
2164
2172
|
.command('start')
|
|
2165
|
-
.description('
|
|
2173
|
+
.description('Enable and reload the scheduler service. Usually unnecessary — it auto-starts when you add your first routine.')
|
|
2166
2174
|
.action(() => {
|
|
2167
2175
|
try {
|
|
2168
2176
|
// A manual start on a disabled device refuses with the same message the
|
|
@@ -2176,13 +2184,16 @@ export function registerRoutinesCommands(program) {
|
|
|
2176
2184
|
console.error(chalk.red(err.message));
|
|
2177
2185
|
process.exit(1);
|
|
2178
2186
|
}
|
|
2187
|
+
setDaemonServiceEnabled('scheduler', true);
|
|
2179
2188
|
const result = startDaemon();
|
|
2180
2189
|
if (result.method === 'already-running') {
|
|
2181
2190
|
// Signal a reload even here: if the daemon booted while this device had
|
|
2182
2191
|
// scheduler.enabled=false, the reload re-evaluates the gate and boots
|
|
2183
2192
|
// the scheduler — a manual start heals a scheduler-less daemon.
|
|
2184
|
-
signalDaemonReload();
|
|
2185
|
-
console.log(
|
|
2193
|
+
const reloaded = signalDaemonReload();
|
|
2194
|
+
console.log(reloaded
|
|
2195
|
+
? chalk.yellow(`Scheduler service enabled (shared daemon PID: ${result.pid}) — reloaded`)
|
|
2196
|
+
: chalk.yellow(`Scheduler service enabled, but live reload is unavailable. Restart deliberately with: agents daemon restart`));
|
|
2186
2197
|
}
|
|
2187
2198
|
else if (result.pid) {
|
|
2188
2199
|
console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
|
|
@@ -2193,25 +2204,31 @@ export function registerRoutinesCommands(program) {
|
|
|
2193
2204
|
});
|
|
2194
2205
|
routinesCmd
|
|
2195
2206
|
.command('stop')
|
|
2196
|
-
.description('Stop the
|
|
2207
|
+
.description('Stop only the scheduler service. The shared daemon and its browser, secrets, usage, and monitoring services stay running.')
|
|
2197
2208
|
.action(() => {
|
|
2198
|
-
|
|
2199
|
-
|
|
2209
|
+
const daemonRunning = isDaemonRunning();
|
|
2210
|
+
setDaemonServiceEnabled('scheduler', false);
|
|
2211
|
+
if (daemonRunning && !signalDaemonReload()) {
|
|
2212
|
+
console.log(chalk.yellow('Scheduler service disabled, but live reload is unavailable. It will stay off after the next daemon start.'));
|
|
2200
2213
|
return;
|
|
2201
2214
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2215
|
+
console.log(daemonRunning
|
|
2216
|
+
? chalk.green('Scheduler service stopped; shared daemon is still running')
|
|
2217
|
+
: chalk.yellow('Scheduler service disabled; shared daemon was already stopped'));
|
|
2204
2218
|
});
|
|
2205
2219
|
routinesCmd
|
|
2206
2220
|
.command('status')
|
|
2207
|
-
.description('Show scheduler
|
|
2208
|
-
.option('--json', 'Emit machine-readable scheduler + per-routine status (owner device, last fire, last error, in-flight run)')
|
|
2221
|
+
.description('Show scheduler service state, shared-daemon state, enabled routines, and upcoming runs.')
|
|
2222
|
+
.option('--json', 'Emit machine-readable scheduler service + device gate + daemon + per-routine status (owner device, last fire, last error, in-flight run)')
|
|
2209
2223
|
.action((options) => {
|
|
2210
2224
|
try {
|
|
2211
2225
|
monitorRunningJobs();
|
|
2212
2226
|
}
|
|
2213
2227
|
catch { /* best-effort orphan reap */ }
|
|
2214
2228
|
const status = getDaemonStatus();
|
|
2229
|
+
const schedulerServiceEnabled = isDaemonServiceEnabled('scheduler');
|
|
2230
|
+
const schedulerDeviceEnabled = isSchedulerEnabled();
|
|
2231
|
+
const schedulerState = schedulerServiceEnabled && schedulerDeviceEnabled ? status.state : 'stopped';
|
|
2215
2232
|
// The daemon-owned status surface (PHNX-3215): scheduler truth plus, per
|
|
2216
2233
|
// routine, its single owner device, last fire outcome + error, and any
|
|
2217
2234
|
// in-flight spawn — the fields `list --json` (definition-shaped) does not
|
|
@@ -2221,7 +2238,10 @@ export function registerRoutinesCommands(program) {
|
|
|
2221
2238
|
writeJson({
|
|
2222
2239
|
device: machineId(),
|
|
2223
2240
|
scheduler: {
|
|
2224
|
-
state:
|
|
2241
|
+
state: schedulerState,
|
|
2242
|
+
serviceEnabled: schedulerServiceEnabled,
|
|
2243
|
+
deviceEnabled: schedulerDeviceEnabled,
|
|
2244
|
+
daemonState: status.state,
|
|
2225
2245
|
pid: status.pid,
|
|
2226
2246
|
binaryPath: status.binaryPath,
|
|
2227
2247
|
heartbeat: status.heartbeat
|
|
@@ -2238,12 +2258,13 @@ export function registerRoutinesCommands(program) {
|
|
|
2238
2258
|
return;
|
|
2239
2259
|
}
|
|
2240
2260
|
console.log(chalk.bold('Scheduler\n'));
|
|
2241
|
-
const stateLabel =
|
|
2261
|
+
const stateLabel = schedulerState === 'running'
|
|
2242
2262
|
? chalk.green('running')
|
|
2243
|
-
:
|
|
2263
|
+
: schedulerState === 'wedged'
|
|
2244
2264
|
? chalk.red('wedged')
|
|
2245
2265
|
: chalk.gray('stopped');
|
|
2246
2266
|
console.log(` Status: ${stateLabel}`);
|
|
2267
|
+
console.log(` Daemon: ${status.running ? chalk.green('running') : chalk.gray('stopped')}`);
|
|
2247
2268
|
if (status.pid)
|
|
2248
2269
|
console.log(` PID: ${status.pid}`);
|
|
2249
2270
|
if (status.binaryPath)
|
|
@@ -2256,9 +2277,9 @@ export function registerRoutinesCommands(program) {
|
|
|
2256
2277
|
const enabled = jobs.filter((j) => j.enabled);
|
|
2257
2278
|
console.log(` Routines: ${enabled.length} enabled / ${jobs.length} total`);
|
|
2258
2279
|
if (status.state === 'wedged') {
|
|
2259
|
-
console.log(chalk.red('\n The daemon is wedged (heartbeat stale). Restart with: agents
|
|
2280
|
+
console.log(chalk.red('\n The shared daemon is wedged (heartbeat stale). Restart deliberately with: agents daemon restart'));
|
|
2260
2281
|
}
|
|
2261
|
-
if (
|
|
2282
|
+
if (schedulerState === 'running' && enabled.length > 0) {
|
|
2262
2283
|
const scheduler = new JobScheduler(async () => { });
|
|
2263
2284
|
scheduler.loadAll();
|
|
2264
2285
|
const scheduled = scheduler.listScheduled();
|
|
@@ -2269,7 +2290,7 @@ export function registerRoutinesCommands(program) {
|
|
|
2269
2290
|
}
|
|
2270
2291
|
scheduler.stopAll();
|
|
2271
2292
|
}
|
|
2272
|
-
else if (
|
|
2293
|
+
else if (schedulerState !== 'running' && jobs.length > 0) {
|
|
2273
2294
|
console.log(chalk.gray('\n Start the scheduler to begin firing routines: agents routines start'));
|
|
2274
2295
|
}
|
|
2275
2296
|
});
|
package/dist/commands/send.js
CHANGED
|
@@ -2,7 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { die } from '../lib/format.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { readMeta } from '../lib/state.js';
|
|
5
|
-
import { sendMessage } from '../lib/channels/send.js';
|
|
5
|
+
import { sendMessage, isOwnerAlias } from '../lib/channels/send.js';
|
|
6
|
+
import { composeOwnerMessage } from '../lib/owner-message.js';
|
|
7
|
+
import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
|
|
6
8
|
function mergeAttachments(opts) {
|
|
7
9
|
const list = [...(opts.attach ?? []), ...(opts.attachment ?? [])];
|
|
8
10
|
return list.length ? list : undefined;
|
|
@@ -23,7 +25,28 @@ function toInput(positionalText, opts, ownerMode) {
|
|
|
23
25
|
}
|
|
24
26
|
async function runSend(positionalText, opts, ownerMode) {
|
|
25
27
|
const meta = readMeta();
|
|
26
|
-
|
|
28
|
+
let input = toInput(positionalText, opts, ownerMode);
|
|
29
|
+
// An owner-bound ping (`agents notify`, `agents send --to owner`) goes through
|
|
30
|
+
// the SAME composer as an important `feed post` (PHNX-3698): short-shaped body,
|
|
31
|
+
// TEAM-N keys linkified, session crumb as a tappable console URL — instead of a
|
|
32
|
+
// raw dump. A non-owner send (explicit --channel/--to) is delivered verbatim.
|
|
33
|
+
if (ownerMode || isOwnerAlias(opts.to)) {
|
|
34
|
+
const flagged = opts.text?.trim() ?? '';
|
|
35
|
+
const positional = (positionalText ?? '').trim();
|
|
36
|
+
const raw = flagged || positional;
|
|
37
|
+
// When both forms are given and disagree, leave it to sendMessage to fail
|
|
38
|
+
// loud with the "pass the message once" error rather than composing a guess.
|
|
39
|
+
const bothDiffer = flagged !== '' && positional !== '' && flagged !== positional;
|
|
40
|
+
if (raw && !bothDiffer) {
|
|
41
|
+
input = { ...input, text: composeOwnerMessage(raw), positionalText: undefined };
|
|
42
|
+
// The console URL in the composed body only resolves once this session's
|
|
43
|
+
// trace shard is uploaded; fire that now so the tapped link isn't a 404.
|
|
44
|
+
// A --dry-run resolves + composes but MUST NOT act (its documented contract),
|
|
45
|
+
// so it never spawns the sync — it just shows what would be sent.
|
|
46
|
+
fireTraceSyncInBackground({ disabled: Boolean(opts.dryRun) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const out = await sendMessage(input, meta);
|
|
27
50
|
if ('error' in out) {
|
|
28
51
|
die(out.error);
|
|
29
52
|
}
|
|
@@ -127,6 +150,10 @@ export function registerSendCommand(program) {
|
|
|
127
150
|
Set owner.channels + owner.policy.normal in humans.yaml once per fleet.
|
|
128
151
|
Every channel listed in the normal policy receives an owner-addressed send.
|
|
129
152
|
|
|
153
|
+
Owner sends go through the same composer as "feed post": the body is
|
|
154
|
+
short-shaped, any TEAM-N key becomes a Linear URL, and the session crumb
|
|
155
|
+
becomes a tappable https://prix.dev/console/sessions/<id> link.
|
|
156
|
+
|
|
130
157
|
${SHARED_NOTES}
|
|
131
158
|
`,
|
|
132
159
|
});
|
|
@@ -13,5 +13,63 @@
|
|
|
13
13
|
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
|
+
import { type ActiveSession } from '../lib/session/active.js';
|
|
17
|
+
interface InjectOptions {
|
|
18
|
+
pane?: string;
|
|
19
|
+
socket?: string;
|
|
20
|
+
pty?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The remote device. A single `--device box` arrives here as `['box']` because
|
|
23
|
+
* the parent `sessions` command's variadic `-D, --device <target...>` shadows
|
|
24
|
+
* this subcommand's scalar option under `optsWithGlobals()` — normalize it with
|
|
25
|
+
* {@link normalizeInjectDevice} before use (PHNX-3688).
|
|
26
|
+
*/
|
|
27
|
+
device?: string | string[];
|
|
28
|
+
enter?: boolean;
|
|
29
|
+
combined?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
34
|
+
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
35
|
+
* whose full id never resolved (`sessionId` absent) — the `ag-<agent>-<shortid>`
|
|
36
|
+
* tmux name's `shortid` suffix (exact or prefix), the full tmux name, and the
|
|
37
|
+
* pane id. Those are the only selectors an id-less remote tmux row exposes, so
|
|
38
|
+
* without this an operator has no tool-native way to nudge it (PHNX-3688).
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchInjectSelector(session: ActiveSession, token: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The `--device` selector, normalized to a single host string. `optsWithGlobals()`
|
|
43
|
+
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
44
|
+
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
45
|
+
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
46
|
+
* `host.startsWith` (PHNX-3688). Coerce the array to its one element; fail loud on
|
|
47
|
+
* several, since inject delivers to exactly one terminal (a fan-out spelling is a
|
|
48
|
+
* user error, not a first-of-list guess).
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeInjectDevice(value: string | string[] | undefined): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
53
|
+
* there, so resolution must happen there). Every flag rides along EXCEPT
|
|
54
|
+
* `--device`: the command runs on the device, resolving locally. Pure so the
|
|
55
|
+
* forwarded invocation is asserted without an SSH hop (PHNX-3688).
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildRemoteInjectArgv(sessionId: string, text: string, options: InjectOptions): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Resolve `device` (registry alias or `user@host`) to an ssh target and re-run
|
|
60
|
+
* `agents sessions inject` there, so a bare session id + `--device` resolves on
|
|
61
|
+
* the box that actually holds the session's tmux panes. The tool-native form of
|
|
62
|
+
* the `agents ssh <device> "agents sessions inject <id> …"` workaround (PHNX-3688).
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Resolve `--device` to an ssh target. A registered device becomes its
|
|
66
|
+
* `user@dnsName`; a bare unknown name (an ad-hoc `user@host` or ssh_config alias)
|
|
67
|
+
* is handed to ssh verbatim (`resolveHost` returns null for it). A registered
|
|
68
|
+
* device we CANNOT dial — password-auth, addressless — throws its typed error and
|
|
69
|
+
* is NOT degraded to the raw name, which could ssh a coincidentally-matching but
|
|
70
|
+
* unrelated `~/.ssh/config` Host (PHNX-3688 review).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveInjectSshTarget(device: string): Promise<string>;
|
|
16
73
|
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
17
74
|
export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;
|
|
75
|
+
export {};
|
|
@@ -13,14 +13,121 @@
|
|
|
13
13
|
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
-
import { getActiveSessions } from '../lib/session/active.js';
|
|
16
|
+
import { getActiveSessions, shortIdFromName } from '../lib/session/active.js';
|
|
17
17
|
import { injectIntoTerminal, resolveInjectTargetForSession } from '../lib/terminal/index.js';
|
|
18
|
+
import { sshExec, shellQuote } from '../lib/ssh-exec.js';
|
|
19
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
18
21
|
import { setHelpSections } from '../lib/help.js';
|
|
22
|
+
/**
|
|
23
|
+
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
24
|
+
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
25
|
+
* whose full id never resolved (`sessionId` absent) — the `ag-<agent>-<shortid>`
|
|
26
|
+
* tmux name's `shortid` suffix (exact or prefix), the full tmux name, and the
|
|
27
|
+
* pane id. Those are the only selectors an id-less remote tmux row exposes, so
|
|
28
|
+
* without this an operator has no tool-native way to nudge it (PHNX-3688).
|
|
29
|
+
*/
|
|
30
|
+
export function matchInjectSelector(session, token) {
|
|
31
|
+
if (!token)
|
|
32
|
+
return false;
|
|
33
|
+
const sid = session.sessionId;
|
|
34
|
+
if (sid && (sid === token || sid.startsWith(token)))
|
|
35
|
+
return true;
|
|
36
|
+
const short = session.tmuxName ? shortIdFromName(session.tmuxName) : undefined;
|
|
37
|
+
if (short && (short === token || short.startsWith(token)))
|
|
38
|
+
return true;
|
|
39
|
+
if (session.tmuxName && session.tmuxName === token)
|
|
40
|
+
return true;
|
|
41
|
+
if (session.paneId && session.paneId === token)
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `--device` selector, normalized to a single host string. `optsWithGlobals()`
|
|
47
|
+
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
48
|
+
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
49
|
+
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
50
|
+
* `host.startsWith` (PHNX-3688). Coerce the array to its one element; fail loud on
|
|
51
|
+
* several, since inject delivers to exactly one terminal (a fan-out spelling is a
|
|
52
|
+
* user error, not a first-of-list guess).
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeInjectDevice(value) {
|
|
55
|
+
const list = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
56
|
+
const hosts = list.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
57
|
+
if (hosts.length === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (hosts.length > 1) {
|
|
60
|
+
throw new Error(`sessions inject targets a single device, but --device named ${hosts.length}: ${hosts.join(', ')}.`);
|
|
61
|
+
}
|
|
62
|
+
return hosts[0];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
66
|
+
* there, so resolution must happen there). Every flag rides along EXCEPT
|
|
67
|
+
* `--device`: the command runs on the device, resolving locally. Pure so the
|
|
68
|
+
* forwarded invocation is asserted without an SSH hop (PHNX-3688).
|
|
69
|
+
*/
|
|
70
|
+
export function buildRemoteInjectArgv(sessionId, text, options) {
|
|
71
|
+
const argv = ['agents', 'sessions', 'inject', sessionId, text];
|
|
72
|
+
if (options.enter === false)
|
|
73
|
+
argv.push('--no-enter');
|
|
74
|
+
if (options.combined)
|
|
75
|
+
argv.push('--combined');
|
|
76
|
+
if (options.socket)
|
|
77
|
+
argv.push('--socket', options.socket);
|
|
78
|
+
if (options.pty)
|
|
79
|
+
argv.push('--pty', options.pty);
|
|
80
|
+
if (options.pane)
|
|
81
|
+
argv.push('--pane', options.pane);
|
|
82
|
+
if (options.json)
|
|
83
|
+
argv.push('--json');
|
|
84
|
+
return argv;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve `device` (registry alias or `user@host`) to an ssh target and re-run
|
|
88
|
+
* `agents sessions inject` there, so a bare session id + `--device` resolves on
|
|
89
|
+
* the box that actually holds the session's tmux panes. The tool-native form of
|
|
90
|
+
* the `agents ssh <device> "agents sessions inject <id> …"` workaround (PHNX-3688).
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Resolve `--device` to an ssh target. A registered device becomes its
|
|
94
|
+
* `user@dnsName`; a bare unknown name (an ad-hoc `user@host` or ssh_config alias)
|
|
95
|
+
* is handed to ssh verbatim (`resolveHost` returns null for it). A registered
|
|
96
|
+
* device we CANNOT dial — password-auth, addressless — throws its typed error and
|
|
97
|
+
* is NOT degraded to the raw name, which could ssh a coincidentally-matching but
|
|
98
|
+
* unrelated `~/.ssh/config` Host (PHNX-3688 review).
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveInjectSshTarget(device) {
|
|
101
|
+
const host = await resolveHost(device);
|
|
102
|
+
return host ? sshTargetFor(host) : device;
|
|
103
|
+
}
|
|
104
|
+
async function injectOnDevice(sessionId, text, options, device) {
|
|
105
|
+
let target;
|
|
106
|
+
try {
|
|
107
|
+
target = await resolveInjectSshTarget(device);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
if (options.json)
|
|
112
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
113
|
+
else
|
|
114
|
+
console.error(chalk.red(message));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const remoteCmd = buildRemoteInjectArgv(sessionId, text, options).map(shellQuote).join(' ');
|
|
118
|
+
const res = sshExec(target, remoteCmd, { multiplex: true });
|
|
119
|
+
if (res.stdout)
|
|
120
|
+
process.stdout.write(res.stdout);
|
|
121
|
+
if (res.stderr)
|
|
122
|
+
process.stderr.write(res.stderr);
|
|
123
|
+
if (res.code !== 0)
|
|
124
|
+
process.exit(res.code ?? 1);
|
|
125
|
+
}
|
|
19
126
|
/** Resolve a session id (short or full) to an addressable terminal target, via the
|
|
20
127
|
* same resolver the watchdog uses so both agree on what is reachable. */
|
|
21
128
|
async function resolveTarget(sessionId) {
|
|
22
129
|
const sessions = await getActiveSessions();
|
|
23
|
-
const match = sessions.find((s) =>
|
|
130
|
+
const match = sessions.find((s) => matchInjectSelector(s, sessionId));
|
|
24
131
|
if (!match)
|
|
25
132
|
return { target: null, reason: `No active session matches "${sessionId}".` };
|
|
26
133
|
const resolution = resolveInjectTargetForSession(match);
|
|
@@ -35,8 +142,21 @@ async function resolveTarget(sessionId) {
|
|
|
35
142
|
return { target: resolution.target };
|
|
36
143
|
}
|
|
37
144
|
async function runInject(sessionId, text, options) {
|
|
145
|
+
let device;
|
|
146
|
+
try {
|
|
147
|
+
device = normalizeInjectDevice(options.device);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
if (options.json)
|
|
152
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
153
|
+
else
|
|
154
|
+
console.error(chalk.red(message));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
38
157
|
// Direct-target shortcuts skip the session lookup — the watchdog often already
|
|
39
|
-
// holds the pane id or pty session it wants to type into.
|
|
158
|
+
// holds the pane id or pty session it wants to type into. The pane's exact
|
|
159
|
+
// address is known, so it composes with --device (tmux send-keys over SSH).
|
|
40
160
|
let target = null;
|
|
41
161
|
if (options.pty) {
|
|
42
162
|
target = { backend: 'pty', id: options.pty };
|
|
@@ -44,6 +164,12 @@ async function runInject(sessionId, text, options) {
|
|
|
44
164
|
else if (options.pane) {
|
|
45
165
|
target = { backend: 'tmux', pane: options.pane, socket: options.socket };
|
|
46
166
|
}
|
|
167
|
+
else if (device) {
|
|
168
|
+
// A bare session id + a device: the session's tmux panes live ON that device,
|
|
169
|
+
// so getActiveSessions here can't see them. Resolve + deliver THERE by re-running
|
|
170
|
+
// inject over SSH (the same command, minus --device) — PHNX-3688.
|
|
171
|
+
return injectOnDevice(sessionId, text, options, device);
|
|
172
|
+
}
|
|
47
173
|
else {
|
|
48
174
|
const resolved = await resolveTarget(sessionId);
|
|
49
175
|
if (!resolved.target) {
|
|
@@ -60,7 +186,7 @@ async function runInject(sessionId, text, options) {
|
|
|
60
186
|
enter: options.enter !== false,
|
|
61
187
|
combined: options.combined,
|
|
62
188
|
socket: options.socket,
|
|
63
|
-
host:
|
|
189
|
+
host: device,
|
|
64
190
|
});
|
|
65
191
|
if (options.json) {
|
|
66
192
|
console.log(JSON.stringify(res));
|
|
@@ -82,7 +208,7 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
82
208
|
.option('--pane <id>', 'Target a tmux pane id directly (e.g. %3), skipping session lookup')
|
|
83
209
|
.option('--pty <id>', 'Target an agents-pty session id directly, skipping session lookup')
|
|
84
210
|
.option('--socket <path>', 'tmux socket path (defaults to the session/shared socket)')
|
|
85
|
-
.option('--device <target>', 'Deliver on a remote device over SSH
|
|
211
|
+
.option('--device <target>', 'Deliver on a remote device over SSH. With a bare session id, the session is resolved ON that device; with --pane, the pane is addressed there directly.')
|
|
86
212
|
.option('--no-enter', 'Send only the text, without a trailing Enter')
|
|
87
213
|
.option('--combined', 'Fuse text + Enter into ONE write (default: two writes, Ink-TUI safe)')
|
|
88
214
|
.option('--json', 'Output the InjectResult as JSON');
|
|
@@ -96,6 +222,12 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
96
222
|
|
|
97
223
|
# Type into an agents-pty session without submitting
|
|
98
224
|
agents sessions inject _ "ls" --pty $SID --no-enter
|
|
225
|
+
|
|
226
|
+
# Nudge a live session on another box (resolved on the device)
|
|
227
|
+
agents sessions inject 214edaae "continue" --device yosemite-s0
|
|
228
|
+
|
|
229
|
+
# Address a known remote pane directly (skips lookup, sends over SSH)
|
|
230
|
+
agents sessions inject _ "continue" --pane %122 --socket $SOCK --device yosemite-s0
|
|
99
231
|
`,
|
|
100
232
|
notes: `
|
|
101
233
|
- Ink-TUI Enter semantics: by default the text and Enter are two separate
|
|
@@ -103,8 +235,12 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
103
235
|
- A session is addressable by id when it resolves to a precise split —
|
|
104
236
|
tmux, iTerm, a VSCodium/Cursor/VS Code integrated terminal, or a pty
|
|
105
237
|
(resolveInjectTargetForSession). Use --pane/--pty for direct targeting.
|
|
106
|
-
-
|
|
107
|
-
|
|
238
|
+
- The id may be the session id (short or full) OR the '<shortid>' suffix of
|
|
239
|
+
a tmux target (ag-<agent>-<shortid>) — the only selector a live tmux
|
|
240
|
+
session whose id column shows '-' exposes.
|
|
241
|
+
- Built on the Terminal Engine (src/lib/terminal): with --pane, --device
|
|
242
|
+
runs the tmux send-keys spec over SSH; with a bare id, --device re-runs
|
|
243
|
+
the lookup on that box (its tmux panes live there, not here).
|
|
108
244
|
`,
|
|
109
245
|
});
|
|
110
246
|
// The parent `sessions` command also defines --json, so it binds there;
|
|
@@ -35,7 +35,7 @@ export function registerSessionsOptimizeCommand(sessionsCmd) {
|
|
|
35
35
|
notes: `
|
|
36
36
|
- FTS5 appends a segment on every insert and tombstones every delete; the scanner delete+inserts a session's docs on each rescan and never self-merges, so \`tool_call_text_data\` / \`session_text_data\` bloat with unmerged segments — GBs of index for tens of MB of content, and queries slow down.
|
|
37
37
|
- This runs FTS5 \`'optimize'\`: it merges every segment into one and purges tombstones. Non-destructive — no searchable content is lost.
|
|
38
|
-
- Reclaimed space becomes reusable free pages inside the DB file. To return it to the OS,
|
|
38
|
+
- Reclaimed space becomes reusable free pages inside the DB file. To return it to the OS, use the operator lifecycle command \`agents daemon stop\`, run \`VACUUM\` against \`~/.agents/.history/sessions/sessions.db\`, then \`agents daemon start\`. \`agents routines stop\` disables only the scheduler service.
|
|
39
39
|
`,
|
|
40
40
|
});
|
|
41
41
|
}
|
|
@@ -53,7 +53,7 @@ import { itemPicker } from '../lib/picker.js';
|
|
|
53
53
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
54
54
|
import { listInstalledVersions, resolveVersionAliasLoose } from '../lib/installations/versions.js';
|
|
55
55
|
import { getAgentsInvocation } from '../lib/daemon/daemon.js';
|
|
56
|
-
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
56
|
+
import { sessionAgentSupportsResume, sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
57
57
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
58
58
|
import { sessionPicker, buildPreview, loadSessionPreviewDigest, transcriptOnPeerOf, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
59
59
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -4062,6 +4062,8 @@ function versionedAliasIfPresent(agent, version) {
|
|
|
4062
4062
|
return null;
|
|
4063
4063
|
}
|
|
4064
4064
|
export function buildResumeCommand(session) {
|
|
4065
|
+
if (!sessionAgentSupportsResume(session.agent))
|
|
4066
|
+
return null;
|
|
4065
4067
|
switch (session.agent) {
|
|
4066
4068
|
// opencode sessions are shared across versions, so resume is deliberately NOT
|
|
4067
4069
|
// version-pinned — it always goes through the plain launcher.
|
|
@@ -4079,16 +4081,7 @@ export function buildResumeCommand(session) {
|
|
|
4079
4081
|
}
|
|
4080
4082
|
return resumeArgv(session.agent, session.id, cli);
|
|
4081
4083
|
}
|
|
4082
|
-
|
|
4083
|
-
case 'antigravity':
|
|
4084
|
-
case 'openclaw':
|
|
4085
|
-
case 'rush':
|
|
4086
|
-
case 'hermes':
|
|
4087
|
-
case 'grok':
|
|
4088
|
-
case 'kimi':
|
|
4089
|
-
case 'droid':
|
|
4090
|
-
case 'cursor':
|
|
4091
|
-
// Grok (and some others) sessions are captured artifacts, not resumable the same way.
|
|
4084
|
+
default:
|
|
4092
4085
|
return null;
|
|
4093
4086
|
}
|
|
4094
4087
|
}
|
package/dist/commands/share.d.ts
CHANGED
|
@@ -80,7 +80,9 @@ export declare function runShareEdit(target: string, opts: {
|
|
|
80
80
|
removeMeta?: string[];
|
|
81
81
|
/** Change the page's visibility in place (public | unlisted | me | org).
|
|
82
82
|
* A metadata-only rewrite like label/meta — the body is untouched, so no
|
|
83
|
-
* revision is created. me/org require a Phoenix session.
|
|
83
|
+
* revision is created. me/org require a Phoenix session. `private` is
|
|
84
|
+
* rejected here — token-gating needs a fresh viewer key only the publish
|
|
85
|
+
* path mints (PHNX-3654). */
|
|
84
86
|
visibility?: ShareVisibility;
|
|
85
87
|
force?: boolean;
|
|
86
88
|
fetchEdit?: typeof fetch;
|
|
@@ -116,7 +118,7 @@ export declare function runShareList(opts?: {
|
|
|
116
118
|
/** Visibility filter. `public` (default) lists only public gallery pages;
|
|
117
119
|
* `unlisted`/`me`/`org` include that owner's hidden pages; `all` includes
|
|
118
120
|
* every active page. Hidden scopes send the owner's bearer and `scope=mine`. */
|
|
119
|
-
scope?: 'public' | 'unlisted' | 'me' | 'org' | 'all';
|
|
121
|
+
scope?: 'public' | 'unlisted' | 'private' | 'me' | 'org' | 'all';
|
|
120
122
|
}): Promise<ShareListResult>;
|
|
121
123
|
/** Client-side filtering over an already-fetched listing (RUSH-2683) — the
|
|
122
124
|
* Worker has no query surface for this, so it narrows the fetched set instead
|
|
@@ -127,7 +129,7 @@ export declare function applyShareListFilters(result: ShareListResult, filters:
|
|
|
127
129
|
session?: string;
|
|
128
130
|
label?: string;
|
|
129
131
|
meta?: Record<string, string>;
|
|
130
|
-
scope?: 'public' | 'unlisted' | 'me' | 'org' | 'all';
|
|
132
|
+
scope?: 'public' | 'unlisted' | 'private' | 'me' | 'org' | 'all';
|
|
131
133
|
}): ShareListResult;
|
|
132
134
|
export declare function formatShareList(result: ShareListResult, json?: boolean): string;
|
|
133
135
|
/** One retained prior version of a slug, as reported by the Worker's
|