@phnx-labs/agents-cli 1.20.26 → 1.20.28
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 +29 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +182 -30
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +177 -6
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +15 -23
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +27 -25
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +4 -13
- package/dist/lib/agent-spec.d.ts +36 -0
- package/dist/lib/agent-spec.js +157 -0
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +45 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/doctor-diff.d.ts +19 -0
- package/dist/lib/doctor-diff.js +107 -15
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugin-marketplace.d.ts +18 -0
- package/dist/lib/plugin-marketplace.js +67 -1
- package/dist/lib/plugins.d.ts +33 -1
- package/dist/lib/plugins.js +56 -11
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +23 -0
- package/dist/lib/session/db.js +76 -1
- package/dist/lib/session/discover.d.ts +26 -0
- package/dist/lib/session/discover.js +75 -4
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +61 -0
- package/dist/lib/session/remote.js +221 -0
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/staleness/detectors/commands.js +7 -6
- package/dist/lib/staleness/writers/commands.js +7 -12
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/startup/dev-build.d.ts +22 -0
- package/dist/lib/startup/dev-build.js +41 -0
- package/dist/lib/types.d.ts +28 -0
- package/dist/lib/versions.d.ts +9 -3
- package/dist/lib/versions.js +43 -7
- package/package.json +3 -3
- package/scripts/postinstall.js +62 -0
package/dist/lib/plugins.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
|
-
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir } from './state.js';
|
|
14
|
+
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir, getSystemPluginsDir } from './state.js';
|
|
15
15
|
import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
|
|
16
16
|
import { assertSafeGitTransport } from './git.js';
|
|
17
17
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
18
18
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
19
19
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
20
20
|
import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './command-skills.js';
|
|
21
|
-
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
21
|
+
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
22
22
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
23
23
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
24
24
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
@@ -186,11 +186,20 @@ export function assertPluginTargetContained(targetRoot, pluginsDir) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
|
-
* Get a specific plugin by name.
|
|
189
|
+
* Get a specific plugin by name. On a cross-marketplace name collision the
|
|
190
|
+
* highest-precedence scope wins (project > extra > user > system) — the same
|
|
191
|
+
* resolution the sync writer's Map(last-wins) dedupe and collectPluginScopes()
|
|
192
|
+
* use. discoverPlugins() yields low→high precedence order, so the LAST match is
|
|
193
|
+
* the winner; returning the first match would resolve to the lowest scope (e.g.
|
|
194
|
+
* a system plugin over the user's same-named one), which is exactly backwards.
|
|
190
195
|
*/
|
|
191
196
|
export function getPlugin(name) {
|
|
192
197
|
const plugins = discoverPlugins();
|
|
193
|
-
|
|
198
|
+
for (let i = plugins.length - 1; i >= 0; i--) {
|
|
199
|
+
if (plugins[i].name === name)
|
|
200
|
+
return plugins[i];
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
194
203
|
}
|
|
195
204
|
/**
|
|
196
205
|
* Check if an agent supports a specific plugin.
|
|
@@ -382,13 +391,18 @@ export function checkPluginDependencies(manifest) {
|
|
|
382
391
|
/**
|
|
383
392
|
* Reconstruct a MarketplaceSpec from a marketplace name. The inverse of
|
|
384
393
|
* marketplaceNameFor(): "agents-cli" → user, "agents-project" → project,
|
|
385
|
-
* "agents-<alias>" → extra. The per-version
|
|
386
|
-
* the name (never spec.root), but we
|
|
387
|
-
* spec is honest for any caller that
|
|
394
|
+
* "agents-system" → system, "agents-<alias>" → extra. The per-version
|
|
395
|
+
* marketplace operations only key off the name (never spec.root), but we
|
|
396
|
+
* resolve the real source root anyway so the spec is honest for any caller that
|
|
397
|
+
* inspects it (e.g. descriptionFor, which would otherwise label the system
|
|
398
|
+
* marketplace as an extra repo named "system").
|
|
388
399
|
*/
|
|
389
|
-
function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
400
|
+
export function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
390
401
|
if (!name || name === MARKETPLACE_NAME)
|
|
391
402
|
return { kind: 'user' };
|
|
403
|
+
if (name === SYSTEM_MARKETPLACE_NAME) {
|
|
404
|
+
return { kind: 'system', root: getSystemPluginsDir() };
|
|
405
|
+
}
|
|
392
406
|
if (name === PROJECT_MARKETPLACE_NAME) {
|
|
393
407
|
return { kind: 'project', root: getProjectPluginsDir(cwd) ?? '' };
|
|
394
408
|
}
|
|
@@ -1096,10 +1110,38 @@ export async function installPlugin(spec) {
|
|
|
1096
1110
|
throw new Error(`Installed source has no valid .claude-plugin/plugin.json`);
|
|
1097
1111
|
}
|
|
1098
1112
|
const capabilities = inspectPluginCapabilities(targetRoot);
|
|
1099
|
-
// Persist source for future updates
|
|
1100
|
-
|
|
1113
|
+
// Persist source for future updates. `version` records the manifest version
|
|
1114
|
+
// at pull time — a baseline that lets the heal path tell "central is an
|
|
1115
|
+
// untouched copy of upstream" (safe to fast-forward) from "the user edited
|
|
1116
|
+
// it" (leave alone) without hashing the whole tree.
|
|
1117
|
+
fs.writeFileSync(path.join(targetRoot, SOURCE_FILE), JSON.stringify({ source, isGit: !isLocalPath, version: manifest.version }), 'utf-8');
|
|
1101
1118
|
return { name: manifest.name, root: targetRoot, isNew, capabilities };
|
|
1102
1119
|
}
|
|
1120
|
+
/** Read a plugin's `.source` provenance, or null when absent/unreadable. */
|
|
1121
|
+
export function readPluginSourceInfo(root) {
|
|
1122
|
+
const f = path.join(root, SOURCE_FILE);
|
|
1123
|
+
if (!fs.existsSync(f))
|
|
1124
|
+
return null;
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve the CURRENT upstream manifest version for a local-sourced plugin
|
|
1134
|
+
* (the `.system`/local-path case). Returns null for git sources — reading their
|
|
1135
|
+
* upstream version would need a network fetch, so git plugins are refreshed only
|
|
1136
|
+
* via the explicit `agents plugins update`.
|
|
1137
|
+
*/
|
|
1138
|
+
export function getUpstreamManifestVersion(info) {
|
|
1139
|
+
if (info.isGit)
|
|
1140
|
+
return null;
|
|
1141
|
+
const resolved = info.source.replace(/^~/, homeDir());
|
|
1142
|
+
const m = loadPluginManifest(resolved);
|
|
1143
|
+
return m?.version ?? null;
|
|
1144
|
+
}
|
|
1103
1145
|
/**
|
|
1104
1146
|
* Update an installed plugin by re-pulling from its original source.
|
|
1105
1147
|
* Returns true if the update succeeded.
|
|
@@ -1136,11 +1178,14 @@ export async function updatePlugin(name) {
|
|
|
1136
1178
|
: null;
|
|
1137
1179
|
fs.rmSync(plugin.root, { recursive: true, force: true });
|
|
1138
1180
|
fs.cpSync(resolvedSource, plugin.root, { recursive: true });
|
|
1139
|
-
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify(sourceInfo), 'utf-8');
|
|
1140
1181
|
if (userConfigBackup !== null) {
|
|
1141
1182
|
fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
|
|
1142
1183
|
}
|
|
1143
1184
|
}
|
|
1185
|
+
// Re-stamp .source with the freshly pulled manifest version so the baseline
|
|
1186
|
+
// tracks what's now on disk (keeps the heal "unmodified?" check honest).
|
|
1187
|
+
const freshVersion = loadPluginManifest(plugin.root)?.version;
|
|
1188
|
+
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify({ ...sourceInfo, version: freshVersion }), 'utf-8');
|
|
1144
1189
|
}
|
|
1145
1190
|
catch (err) {
|
|
1146
1191
|
return { success: false, error: err.message };
|
|
@@ -47,6 +47,7 @@ import * as path from 'path';
|
|
|
47
47
|
import { supports } from './capabilities.js';
|
|
48
48
|
import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
|
|
49
49
|
import { getVersionHomePath } from './versions.js';
|
|
50
|
+
import { toPortableKey } from './platform/index.js';
|
|
50
51
|
import { transformSubagentForClaude } from './subagents.js';
|
|
51
52
|
import { compileRulesForProject } from './rules/compile.js';
|
|
52
53
|
import { discoverPluginsInDir, hasPluginExecSurfaces, inspectPluginCapabilities } from './plugins.js';
|
|
@@ -88,15 +89,17 @@ export function runLaunchSync(opts) {
|
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
90
91
|
* Path of the shim's skip-fast sentinel for this (agent, version, cwd) tuple.
|
|
91
|
-
* Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation)
|
|
92
|
-
*
|
|
92
|
+
* Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation),
|
|
93
|
+
* which is the canonical `toPortableKey` mapping: drop the Windows drive colon
|
|
94
|
+
* and fold `\`, `/`, and ` ` → `_`. On POSIX this is byte-identical to the old
|
|
95
|
+
* `/` and ` ` → `_` slug; on Windows it yields a legal filename (no `C:\`).
|
|
93
96
|
*
|
|
94
97
|
* Cache leak note: this dir accumulates one zero-byte file per
|
|
95
98
|
* (agent, version, project) tuple ever launched. Disk impact is negligible
|
|
96
99
|
* (inodes only). A periodic GC belongs in `agents prune` — follow-up.
|
|
97
100
|
*/
|
|
98
101
|
function launchSentinelPath(agent, version, cwd) {
|
|
99
|
-
const slug = cwd
|
|
102
|
+
const slug = toPortableKey(cwd);
|
|
100
103
|
// Prefer $HOME (respects test overrides + matches bash's $HOME expansion in
|
|
101
104
|
// the shim), fall back to os.homedir() so the lookup never resolves to '/'
|
|
102
105
|
// if HOME is somehow unset.
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
13
13
|
import { getRoutinesDir, getUserAgentsDir } from './state.js';
|
|
14
|
+
import { createLink } from './platform/index.js';
|
|
14
15
|
function resolveRealHome() {
|
|
15
16
|
const home = os.homedir();
|
|
16
17
|
try {
|
|
@@ -126,9 +127,11 @@ export function symlinkAllowedDirs(overlayHome, dirs) {
|
|
|
126
127
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
127
128
|
if (!fs.existsSync(symlinkTarget)) {
|
|
128
129
|
try {
|
|
129
|
-
|
|
130
|
+
// createLink uses a junction for directories on Windows (no elevation),
|
|
131
|
+
// a plain symlink on POSIX — allowed dirs are directories.
|
|
132
|
+
createLink(realPath, symlinkTarget);
|
|
130
133
|
}
|
|
131
|
-
catch { /*
|
|
134
|
+
catch { /* link already exists or refused */ }
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
}
|
package/dist/lib/self-update.js
CHANGED
|
@@ -16,6 +16,7 @@ import * as os from 'os';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { spawnSync } from 'child_process';
|
|
18
18
|
import { compareVersions } from './versions.js';
|
|
19
|
+
import { needsWindowsShell } from './platform/index.js';
|
|
19
20
|
export const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
|
|
20
21
|
/**
|
|
21
22
|
* The directory bun installs global packages into:
|
|
@@ -145,7 +146,10 @@ export async function installPackageIntoPrefix(spec, prefix) {
|
|
|
145
146
|
const { execFile } = await import('child_process');
|
|
146
147
|
const { promisify } = await import('util');
|
|
147
148
|
const execFileAsync = promisify(execFile);
|
|
148
|
-
|
|
149
|
+
// On Windows `npm` is `npm.cmd`; execFile cannot run it without a shell (ENOENT).
|
|
150
|
+
await execFileAsync('npm', ['install', '-g', '--prefix', prefix, spec, '--ignore-scripts'], {
|
|
151
|
+
shell: needsWindowsShell('npm'),
|
|
152
|
+
});
|
|
149
153
|
}
|
|
150
154
|
/**
|
|
151
155
|
* Install `spec` into bun's global store with `bun add -g`. bun writes to
|
|
@@ -159,7 +163,8 @@ export async function installPackageWithBun(spec) {
|
|
|
159
163
|
const { execFile } = await import('child_process');
|
|
160
164
|
const { promisify } = await import('util');
|
|
161
165
|
const execFileAsync = promisify(execFile);
|
|
162
|
-
|
|
166
|
+
// On Windows `bun` resolves to `bun.exe`/`bun.cmd`; force shell for the .cmd case.
|
|
167
|
+
await execFileAsync('bun', ['add', '-g', spec], { shell: needsWindowsShell('bun') });
|
|
163
168
|
}
|
|
164
169
|
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
165
170
|
export function readInstalledVersion(packageRoot) {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface QueryOptions {
|
|
|
45
45
|
/** Match any session whose cwd equals this or is a descendant of it. */
|
|
46
46
|
cwdPrefix?: string;
|
|
47
47
|
project?: string;
|
|
48
|
+
/** Match the full session id or short id, case-insensitively (exact). */
|
|
49
|
+
idExact?: string;
|
|
50
|
+
/** Match sessions whose id or short id begins with this (case-insensitive prefix). */
|
|
51
|
+
idPrefix?: string;
|
|
48
52
|
sinceMs?: number;
|
|
49
53
|
untilMs?: number;
|
|
50
54
|
limit?: number;
|
|
@@ -118,6 +122,16 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
118
122
|
* Leaves FTS5 content/topic/project untouched — cheap to call every run.
|
|
119
123
|
*/
|
|
120
124
|
export declare function syncLabels(labelMap: Map<string, string | null>): number;
|
|
125
|
+
/**
|
|
126
|
+
* Sync topics (session titles) for a set of sessions, keyed by id. For agents
|
|
127
|
+
* whose human-readable title lives in a side index that updates independently
|
|
128
|
+
* of the transcript (Codex `session_index.jsonl`), the per-file scan can't see
|
|
129
|
+
* a title that lands later. This applies those titles by id, updating both
|
|
130
|
+
* `sessions.topic` and the FTS5 topic column. Only ever sets a non-empty title
|
|
131
|
+
* and only when it differs from the stored value — cheap to call every run.
|
|
132
|
+
* Returns the number of rows updated.
|
|
133
|
+
*/
|
|
134
|
+
export declare function syncTopics(topicMap: Map<string, string>): number;
|
|
121
135
|
/** Query sessions from the database, applying filters and ordering by timestamp descending. */
|
|
122
136
|
export declare function querySessions(options?: QueryOptions): SessionMeta[];
|
|
123
137
|
/** Count sessions matching the given filter options. */
|
|
@@ -160,6 +174,15 @@ export declare function getAllFilePaths(): Set<string>;
|
|
|
160
174
|
export declare function getSessionsByFilePaths(paths: string[]): Map<string, SessionMeta>;
|
|
161
175
|
/** Look up a single session by its unique ID. */
|
|
162
176
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a full-or-partial session id against the index, exact-first then
|
|
179
|
+
* prefix — the DB-backed equivalent of resolveSessionById() that runs over the
|
|
180
|
+
* SQLite table instead of a pre-loaded array. Matches both the full id and the
|
|
181
|
+
* short id. An exact hit short-circuits so a complete id never also drags in its
|
|
182
|
+
* prefix siblings. `scope` narrows by agent / version / project (cwd) so an
|
|
183
|
+
* ambiguous prefix disambiguates against the caller's context.
|
|
184
|
+
*/
|
|
185
|
+
export declare function findSessionsById(idQuery: string, scope?: Pick<QueryOptions, 'agent' | 'version' | 'cwd' | 'project'>): SessionMeta[];
|
|
163
186
|
/** A single full-text search result with ranking score. */
|
|
164
187
|
export interface FtsHit {
|
|
165
188
|
sessionId: string;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -65,6 +65,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
|
65
65
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
66
66
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
67
67
|
CREATE INDEX IF NOT EXISTS idx_sessions_file_path ON sessions(file_path);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_short_id ON sessions(short_id);
|
|
68
69
|
|
|
69
70
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_text USING fts5(
|
|
70
71
|
session_id UNINDEXED,
|
|
@@ -558,6 +559,48 @@ export function syncLabels(labelMap) {
|
|
|
558
559
|
txn(updates);
|
|
559
560
|
return updates.length;
|
|
560
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Sync topics (session titles) for a set of sessions, keyed by id. For agents
|
|
564
|
+
* whose human-readable title lives in a side index that updates independently
|
|
565
|
+
* of the transcript (Codex `session_index.jsonl`), the per-file scan can't see
|
|
566
|
+
* a title that lands later. This applies those titles by id, updating both
|
|
567
|
+
* `sessions.topic` and the FTS5 topic column. Only ever sets a non-empty title
|
|
568
|
+
* and only when it differs from the stored value — cheap to call every run.
|
|
569
|
+
* Returns the number of rows updated.
|
|
570
|
+
*/
|
|
571
|
+
export function syncTopics(topicMap) {
|
|
572
|
+
if (topicMap.size === 0)
|
|
573
|
+
return 0;
|
|
574
|
+
const db = getDB();
|
|
575
|
+
const ids = [...topicMap.keys()];
|
|
576
|
+
const CHUNK = 500;
|
|
577
|
+
const updates = [];
|
|
578
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
579
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
580
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
581
|
+
const rows = db
|
|
582
|
+
.prepare(`SELECT id, topic FROM sessions WHERE id IN (${placeholders})`)
|
|
583
|
+
.all(...chunk);
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
const live = topicMap.get(row.id) ?? '';
|
|
586
|
+
if (live && live !== (row.topic ?? '')) {
|
|
587
|
+
updates.push({ id: row.id, topic: live });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (updates.length === 0)
|
|
592
|
+
return 0;
|
|
593
|
+
const updSessions = db.prepare(`UPDATE sessions SET topic = ? WHERE id = ?`);
|
|
594
|
+
const updFts = db.prepare(`UPDATE session_text SET topic = ? WHERE session_id = ?`);
|
|
595
|
+
const txn = db.transaction((items) => {
|
|
596
|
+
for (const { id, topic } of items) {
|
|
597
|
+
updSessions.run(topic, id);
|
|
598
|
+
updFts.run(topic, id);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
txn(updates);
|
|
602
|
+
return updates.length;
|
|
603
|
+
}
|
|
561
604
|
/** Convert a raw database row into a SessionMeta object. */
|
|
562
605
|
function rowToMeta(row) {
|
|
563
606
|
return {
|
|
@@ -601,13 +644,28 @@ function buildSessionWhere(options) {
|
|
|
601
644
|
params.push(options.cwd);
|
|
602
645
|
}
|
|
603
646
|
if (options.cwdPrefix) {
|
|
647
|
+
// Stored cwd uses the host path separator (normalizeCwd → path.resolve), so
|
|
648
|
+
// the subdir wildcard must too — a hardcoded '/' never matches a Windows
|
|
649
|
+
// `C:\a\b` subpath and the listing comes back empty.
|
|
604
650
|
where.push('(cwd = ? OR cwd LIKE ?)');
|
|
605
|
-
params.push(options.cwdPrefix, options.cwdPrefix + '
|
|
651
|
+
params.push(options.cwdPrefix, options.cwdPrefix + path.sep + '%');
|
|
606
652
|
}
|
|
607
653
|
if (options.project) {
|
|
608
654
|
where.push('LOWER(IFNULL(project, \'\')) LIKE ?');
|
|
609
655
|
params.push(`%${options.project.toLowerCase()}%`);
|
|
610
656
|
}
|
|
657
|
+
// id lookup. SQLite's LIKE is case-insensitive for ASCII, so a lowercased
|
|
658
|
+
// pattern matches mixed-case ids; the `=` exact compare adds COLLATE NOCASE
|
|
659
|
+
// for the same reason. short_id carries its own index (idx_sessions_short_id);
|
|
660
|
+
// id is the PRIMARY KEY.
|
|
661
|
+
if (options.idExact) {
|
|
662
|
+
where.push('(id = ? COLLATE NOCASE OR short_id = ? COLLATE NOCASE)');
|
|
663
|
+
params.push(options.idExact, options.idExact);
|
|
664
|
+
}
|
|
665
|
+
if (options.idPrefix) {
|
|
666
|
+
where.push('(id LIKE ? OR short_id LIKE ?)');
|
|
667
|
+
params.push(`${options.idPrefix}%`, `${options.idPrefix}%`);
|
|
668
|
+
}
|
|
611
669
|
if (typeof options.sinceMs === 'number') {
|
|
612
670
|
// Compare as strings; ISO 8601 timestamps sort lexicographically.
|
|
613
671
|
where.push('timestamp >= ?');
|
|
@@ -739,6 +797,23 @@ export function getSessionById(id) {
|
|
|
739
797
|
const row = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
|
|
740
798
|
return row ? rowToMeta(row) : null;
|
|
741
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Resolve a full-or-partial session id against the index, exact-first then
|
|
802
|
+
* prefix — the DB-backed equivalent of resolveSessionById() that runs over the
|
|
803
|
+
* SQLite table instead of a pre-loaded array. Matches both the full id and the
|
|
804
|
+
* short id. An exact hit short-circuits so a complete id never also drags in its
|
|
805
|
+
* prefix siblings. `scope` narrows by agent / version / project (cwd) so an
|
|
806
|
+
* ambiguous prefix disambiguates against the caller's context.
|
|
807
|
+
*/
|
|
808
|
+
export function findSessionsById(idQuery, scope = {}) {
|
|
809
|
+
const q = idQuery.trim();
|
|
810
|
+
if (!q)
|
|
811
|
+
return [];
|
|
812
|
+
const exact = querySessions({ ...scope, idExact: q });
|
|
813
|
+
if (exact.length > 0)
|
|
814
|
+
return exact;
|
|
815
|
+
return querySessions({ ...scope, idPrefix: q });
|
|
816
|
+
}
|
|
742
817
|
/**
|
|
743
818
|
* Escape a raw user query into a safe FTS5 MATCH expression.
|
|
744
819
|
* Splits on non-word characters, keeps tokens >= 2 chars, and OR-joins
|
|
@@ -36,6 +36,27 @@ export interface ScanProgress {
|
|
|
36
36
|
parsed: number;
|
|
37
37
|
total: number;
|
|
38
38
|
}
|
|
39
|
+
/** Lightweight metadata extracted from a Claude JSONL file during incremental scan. */
|
|
40
|
+
interface ClaudeSessionScan {
|
|
41
|
+
timestamp?: string;
|
|
42
|
+
cwd?: string;
|
|
43
|
+
gitBranch?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
topic?: string;
|
|
46
|
+
messageCount: number;
|
|
47
|
+
tokenCount?: number;
|
|
48
|
+
/** Total USD cost accumulated from per-(model, direction) token usage. */
|
|
49
|
+
costUsd?: number;
|
|
50
|
+
/** Wall-clock duration in ms between the first and last timestamped event. */
|
|
51
|
+
durationMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Value of the JSONL `entrypoint` field on the first event that carries it.
|
|
54
|
+
* 'cli' for real interactive sessions, 'sdk-cli' for team-spawned ones.
|
|
55
|
+
*/
|
|
56
|
+
entrypoint?: string;
|
|
57
|
+
/** Concatenated user message text, ready to hand to FTS5. */
|
|
58
|
+
contentText?: string;
|
|
59
|
+
}
|
|
39
60
|
/**
|
|
40
61
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
41
62
|
* the last run; everything else is served from the SQLite cache.
|
|
@@ -77,7 +98,12 @@ export declare function getAgentSessionDirs(agent: string, subdir: string): stri
|
|
|
77
98
|
* recent startedAt.
|
|
78
99
|
*/
|
|
79
100
|
export declare function buildClaudeLabelMap(): Map<string, string | null>;
|
|
101
|
+
/** Parse the lines of a Codex `session_index.jsonl` into a session id -> title map. */
|
|
102
|
+
export declare function parseCodexThreadNameIndex(raw: string): Map<string, string>;
|
|
103
|
+
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
104
|
+
export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
|
|
80
105
|
/** Read up to maxLines non-empty lines from the beginning of a file. */
|
|
81
106
|
export declare function readFirstLines(filePath: string, maxLines: number): Promise<string[]>;
|
|
82
107
|
/** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
|
|
83
108
|
export declare function parseTimeFilter(input: string): number;
|
|
109
|
+
export {};
|
|
@@ -21,7 +21,7 @@ import { getConfigSymlinkVersion } from '../shims.js';
|
|
|
21
21
|
import { SESSION_AGENTS } from './types.js';
|
|
22
22
|
import { extractSessionTopic } from './prompt.js';
|
|
23
23
|
import { costOfUsage } from '../pricing/index.js';
|
|
24
|
-
import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
|
|
24
|
+
import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
|
|
25
25
|
const HOME = os.homedir();
|
|
26
26
|
// Versions can live under either repo: the user repo (current canonical
|
|
27
27
|
// location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
|
|
@@ -486,8 +486,14 @@ async function scanCodexIncremental(onProgress) {
|
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
const changed = filterChangedFiles(filePaths);
|
|
489
|
-
|
|
489
|
+
// Codex keeps human-readable titles (`thread_name`) in `session_index.jsonl`,
|
|
490
|
+
// which updates independently of the rollout files — apply them by id on every
|
|
491
|
+
// scan so a title that lands after a session was first indexed still surfaces.
|
|
492
|
+
const titles = readCodexThreadNames();
|
|
493
|
+
if (changed.length === 0) {
|
|
494
|
+
syncTopics(titles);
|
|
490
495
|
return;
|
|
496
|
+
}
|
|
491
497
|
onProgress?.({ agent: 'codex', parsed: 0, total: changed.length });
|
|
492
498
|
const entries = [];
|
|
493
499
|
const touched = [];
|
|
@@ -498,6 +504,10 @@ async function scanCodexIncremental(onProgress) {
|
|
|
498
504
|
const result = await readCodexMeta(filePath, account, currentVersion);
|
|
499
505
|
if (result && !seen.has(result.meta.id)) {
|
|
500
506
|
seen.add(result.meta.id);
|
|
507
|
+
// Prefer the Codex-generated title over the first-prompt fallback.
|
|
508
|
+
const title = titles.get(result.meta.id);
|
|
509
|
+
if (title)
|
|
510
|
+
result.meta.topic = title;
|
|
501
511
|
entries.push({ meta: result.meta, content: result.content, scan });
|
|
502
512
|
}
|
|
503
513
|
else {
|
|
@@ -512,6 +522,48 @@ async function scanCodexIncremental(onProgress) {
|
|
|
512
522
|
}
|
|
513
523
|
upsertSessionsBatch(entries);
|
|
514
524
|
recordScans(touched);
|
|
525
|
+
// Catch sessions whose rollout file was unchanged but gained a title since the
|
|
526
|
+
// last scan (the index changed, the transcript did not).
|
|
527
|
+
syncTopics(titles);
|
|
528
|
+
}
|
|
529
|
+
/** Parse the lines of a Codex `session_index.jsonl` into a session id -> title map. */
|
|
530
|
+
export function parseCodexThreadNameIndex(raw) {
|
|
531
|
+
const titles = new Map();
|
|
532
|
+
for (const line of raw.split('\n')) {
|
|
533
|
+
if (!line.trim())
|
|
534
|
+
continue;
|
|
535
|
+
try {
|
|
536
|
+
const entry = JSON.parse(line);
|
|
537
|
+
const id = typeof entry.id === 'string' ? entry.id : '';
|
|
538
|
+
const name = typeof entry.thread_name === 'string' ? entry.thread_name.trim() : '';
|
|
539
|
+
if (id && name)
|
|
540
|
+
titles.set(id, name);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// skip malformed line
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return titles;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Read Codex session titles across every Codex home (live + versioned). The
|
|
550
|
+
* `session_index.jsonl` file sits beside each `sessions/` rollout tree.
|
|
551
|
+
*/
|
|
552
|
+
function readCodexThreadNames() {
|
|
553
|
+
const titles = new Map();
|
|
554
|
+
for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
|
|
555
|
+
const indexPath = path.join(path.dirname(sessionsDir), 'session_index.jsonl');
|
|
556
|
+
let raw;
|
|
557
|
+
try {
|
|
558
|
+
raw = fs.readFileSync(indexPath, 'utf-8');
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
continue; // no index in this home
|
|
562
|
+
}
|
|
563
|
+
for (const [id, name] of parseCodexThreadNameIndex(raw))
|
|
564
|
+
titles.set(id, name);
|
|
565
|
+
}
|
|
566
|
+
return titles;
|
|
515
567
|
}
|
|
516
568
|
/** Stream-parse a single Codex JSONL file to extract session metadata. */
|
|
517
569
|
async function readCodexMeta(filePath, account, currentVersion) {
|
|
@@ -1240,7 +1292,7 @@ function extractHermesMessageText(content) {
|
|
|
1240
1292
|
.trim();
|
|
1241
1293
|
}
|
|
1242
1294
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
1243
|
-
async function scanClaudeSession(filePath) {
|
|
1295
|
+
export async function scanClaudeSession(filePath) {
|
|
1244
1296
|
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
1245
1297
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1246
1298
|
let timestamp;
|
|
@@ -1248,6 +1300,10 @@ async function scanClaudeSession(filePath) {
|
|
|
1248
1300
|
let gitBranch;
|
|
1249
1301
|
let version;
|
|
1250
1302
|
let topic;
|
|
1303
|
+
// Explicit session titles: `/rename` writes a `custom-title` event; Claude
|
|
1304
|
+
// auto-generates an `ai-title`. Both can repeat across the file — last wins.
|
|
1305
|
+
let customTitle;
|
|
1306
|
+
let aiTitle;
|
|
1251
1307
|
let entrypoint;
|
|
1252
1308
|
let messageCount = 0;
|
|
1253
1309
|
let tokenCount = 0;
|
|
@@ -1291,6 +1347,18 @@ async function scanClaudeSession(filePath) {
|
|
|
1291
1347
|
gitBranch = parsed.gitBranch || undefined;
|
|
1292
1348
|
version = parsed.version || undefined;
|
|
1293
1349
|
}
|
|
1350
|
+
if (parsed.type === 'custom-title') {
|
|
1351
|
+
const t = typeof parsed.customTitle === 'string' ? parsed.customTitle.trim() : '';
|
|
1352
|
+
if (t)
|
|
1353
|
+
customTitle = t;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (parsed.type === 'ai-title') {
|
|
1357
|
+
const t = typeof parsed.aiTitle === 'string' ? parsed.aiTitle.trim() : '';
|
|
1358
|
+
if (t)
|
|
1359
|
+
aiTitle = t;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1294
1362
|
if (parsed.type === 'user') {
|
|
1295
1363
|
const text = extractClaudeUserText(parsed);
|
|
1296
1364
|
if (text) {
|
|
@@ -1344,12 +1412,15 @@ async function scanClaudeSession(filePath) {
|
|
|
1344
1412
|
const durationMs = firstTsMs !== undefined && lastTsMs !== undefined && lastTsMs > firstTsMs
|
|
1345
1413
|
? lastTsMs - firstTsMs
|
|
1346
1414
|
: undefined;
|
|
1415
|
+
// Prefer an explicit session title (user `/rename` > Claude auto-title) over
|
|
1416
|
+
// the first-prompt topic.
|
|
1417
|
+
const resolvedTopic = customTitle || aiTitle || topic;
|
|
1347
1418
|
return {
|
|
1348
1419
|
timestamp,
|
|
1349
1420
|
cwd,
|
|
1350
1421
|
gitBranch,
|
|
1351
1422
|
version,
|
|
1352
|
-
topic,
|
|
1423
|
+
topic: resolvedTopic,
|
|
1353
1424
|
entrypoint,
|
|
1354
1425
|
messageCount,
|
|
1355
1426
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable "time since" formatting for session timestamps. Lives here (not
|
|
3
|
+
* inline in `sessions.ts`) so both the session list renderer and the remote
|
|
4
|
+
* offline-cache banner (`remote.ts`) share one formatter — `sessions.ts` imports
|
|
5
|
+
* `remote.ts`, so a back-import would cycle.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatRelativeTime(isoTimestamp: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable "time since" formatting for session timestamps. Lives here (not
|
|
3
|
+
* inline in `sessions.ts`) so both the session list renderer and the remote
|
|
4
|
+
* offline-cache banner (`remote.ts`) share one formatter — `sessions.ts` imports
|
|
5
|
+
* `remote.ts`, so a back-import would cycle.
|
|
6
|
+
*/
|
|
7
|
+
export function formatRelativeTime(isoTimestamp) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const then = new Date(isoTimestamp).getTime();
|
|
10
|
+
if (isNaN(then))
|
|
11
|
+
return isoTimestamp;
|
|
12
|
+
const diffMs = now - then;
|
|
13
|
+
const diffMin = Math.floor(diffMs / 60_000);
|
|
14
|
+
const diffHrs = Math.floor(diffMs / 3_600_000);
|
|
15
|
+
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
16
|
+
if (diffMin < 1)
|
|
17
|
+
return 'just now';
|
|
18
|
+
if (diffMin < 60)
|
|
19
|
+
return `${diffMin} min ago`;
|
|
20
|
+
if (diffHrs < 24)
|
|
21
|
+
return `${diffHrs} hour${diffHrs === 1 ? '' : 's'} ago`;
|
|
22
|
+
if (diffDays < 7)
|
|
23
|
+
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
|
24
|
+
// Older: show date
|
|
25
|
+
const d = new Date(then);
|
|
26
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
27
|
+
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH target: a bare ssh-config host alias (e.g. `yosemite-s1`) or `user@host`.
|
|
3
|
+
* The strict allowlist blocks shell metacharacters and a leading `-`, so a target
|
|
4
|
+
* can never be smuggled in as an ssh argv flag.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SSH_TARGET_RE: RegExp;
|
|
7
|
+
export declare function assertValidSshTarget(host: string): void;
|
|
8
|
+
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
9
|
+
export declare function shellQuote(s: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Strip the `--host`/`-H` flag (and its value) from a raw `agents sessions` argv,
|
|
12
|
+
* leaving the args to forward to the remote unchanged. The remote runs the same
|
|
13
|
+
* binary, so every other flag (`--since`, `--last`, `--json`, query, …) carries
|
|
14
|
+
* over for free. Handles every form commander accepts: `--host h`, `--host=h`,
|
|
15
|
+
* `-H h`, `-H=h`, and the glued short form `-Hh`.
|
|
16
|
+
*
|
|
17
|
+
* @param argv full process argv; the sessions args begin at index 2
|
|
18
|
+
* (`[runtime, script, 'sessions', ...]`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildForwardedArgs(argv: string[], hosts?: Set<string>): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Build the single remote command string for `ssh <host> <cmd>`. Forwarded args
|
|
23
|
+
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
24
|
+
* quoted again so it survives `bash -lc <...>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildRemoteCommand(forwardedArgs: string[]): string;
|
|
27
|
+
/** The four outcomes of one `ssh <host> agents sessions …` invocation. */
|
|
28
|
+
export type SshOutcome = 'ok' | 'unreachable' | 'query-failed' | 'spawn-error';
|
|
29
|
+
/**
|
|
30
|
+
* Classify an ssh `spawnSync` result. ssh(1) reserves exit 255 for its own
|
|
31
|
+
* connection-layer failures (host down, timeout, refused, auth, changed host
|
|
32
|
+
* key) — distinct from any other non-zero, which is the remote `agents sessions`
|
|
33
|
+
* exit code forwarded back (the query ran but failed). The two must be handled
|
|
34
|
+
* differently: 255 may fall back to cache, a forwarded failure must surface.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifySshFailure(res: {
|
|
37
|
+
error?: Error | null;
|
|
38
|
+
status: number | null;
|
|
39
|
+
}): SshOutcome;
|
|
40
|
+
/**
|
|
41
|
+
* Deterministic cache path for a (host, forwarded-args) pair. The forwarded args
|
|
42
|
+
* are hashed so distinct queries cache independently; the host stays readable in
|
|
43
|
+
* the filename (sanitised so `user@host` and aliases are filesystem-safe).
|
|
44
|
+
*/
|
|
45
|
+
export declare function remoteCachePath(host: string, forwardedArgs: string[]): string;
|
|
46
|
+
/** Banner shown above replayed cache rows when the peer is offline. */
|
|
47
|
+
export declare function formatStaleBanner(host: string, mtimeMs: number): string;
|
|
48
|
+
/** Message shown when a host is unreachable and there is no cache to fall back to. */
|
|
49
|
+
export declare function formatUnreachable(host: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Run the current `agents sessions` invocation on one or more remote machines over
|
|
52
|
+
* SSH, writing each remote's output to the terminal. A successful fetch is cached;
|
|
53
|
+
* an unreachable host falls back to that cache (with a stale banner) when present.
|
|
54
|
+
* Sets `process.exitCode = 1` if any host could not be answered (live or cached).
|
|
55
|
+
* Reads the invocation from `process.argv` (override via `argv` for testing).
|
|
56
|
+
*
|
|
57
|
+
* Output is captured rather than `stdio: 'inherit'`-streamed so it can be cached.
|
|
58
|
+
* Session output is small and the remote returns quickly, so buffering is
|
|
59
|
+
* imperceptible; `maxBuffer` is generous for the rare large `--markdown <id>` dump.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runRemoteSessions(hosts: string[], argv?: string[]): void;
|