@phnx-labs/agents-cli 1.20.93 → 1.21.1
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 +290 -0
- package/README.md +4 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/feed.js +37 -23
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +13 -0
- package/dist/lib/activity.js +22 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed-broadcast.d.ts +41 -5
- package/dist/lib/feed-broadcast.js +110 -12
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +3 -1
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +61 -1
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +23 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules preset auto-apply at `agents run` launch time.
|
|
3
|
+
*
|
|
4
|
+
* `agents rules switch <agent>@<version> --preset <name>` persists the active
|
|
5
|
+
* preset (`state.ts:setActiveRulesPreset`) and immediately recompiles it into
|
|
6
|
+
* the version home via `syncResourcesToVersion` → the rules writer
|
|
7
|
+
* (`staleness/writers/rules.ts`). Same for `agents add`/`use`. But nothing
|
|
8
|
+
* re-applies the preset on a LATER `agents run` — if `setActiveRulesPreset`
|
|
9
|
+
* is ever called without a follow-up sync (or a subrule file changes after
|
|
10
|
+
* the last sync), the harness launches against a stale rules file until the
|
|
11
|
+
* next explicit `agents rules switch` / `agents sync`.
|
|
12
|
+
*
|
|
13
|
+
* This closes that gap the same way `runLaunchSync` (project-launch.ts)
|
|
14
|
+
* recompiles PROJECT rules on every launch: idempotent, and skip-fast when
|
|
15
|
+
* nothing changed. The skip-fast reuses the staleness checkers' mtime+size
|
|
16
|
+
* fingerprint comparison (`staleness/checkers/rules.ts`,
|
|
17
|
+
* `staleness/fingerprint.ts`) against a small per-(agent,version) sentinel —
|
|
18
|
+
* the same "hash the cheap inputs, compare, skip the write on a match" shape
|
|
19
|
+
* `installScope` (project-launch.ts) uses for plugin marketplaces. The common
|
|
20
|
+
* case (no preset or subrule change since the last run) costs one JSON read
|
|
21
|
+
* plus a handful of `stat()` calls — no recompose, no write.
|
|
22
|
+
*
|
|
23
|
+
* Version-level scope only — the active preset is keyed by (agent, version),
|
|
24
|
+
* matching `getActiveRulesPreset`. Per-model preset scoping is a follow-up.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'fs';
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import { AGENTS } from '../agents.js';
|
|
29
|
+
import { getActiveRulesPreset, getCacheDir } from '../state.js';
|
|
30
|
+
import { getWriter } from '../staleness/registry.js';
|
|
31
|
+
import { buildRules, isRulesStale } from '../staleness/checkers/rules.js';
|
|
32
|
+
/** ~/.agents/.cache/rules-run-sync/ — regenerable; a lost sentinel just costs one extra compose+write. */
|
|
33
|
+
function sentinelPath(agent, version) {
|
|
34
|
+
const key = `${agent}@${version}`.replace(/[^a-zA-Z0-9@._-]/g, '_');
|
|
35
|
+
return path.join(getCacheDir(), 'rules-run-sync', `${key}.json`);
|
|
36
|
+
}
|
|
37
|
+
function loadSentinel(agent, version) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(sentinelPath(agent, version), 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function saveSentinel(agent, version, sentinel) {
|
|
46
|
+
const p = sentinelPath(agent, version);
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
49
|
+
fs.writeFileSync(p, JSON.stringify(sentinel));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Best-effort — a failed write just means the next run redoes the compose+write.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Re-apply the active rules preset for (agent, version) into its version
|
|
57
|
+
* home when the composed source set has drifted since the last time this ran.
|
|
58
|
+
* Returns true when the version-home rules file was (re)written.
|
|
59
|
+
*
|
|
60
|
+
* No-cwd fingerprint on purpose: the version-home rules file never includes
|
|
61
|
+
* the project layer (see `staleness/writers/rules.ts` — project rules are
|
|
62
|
+
* resolved separately, at launch, into the workspace `AGENTS.md` by
|
|
63
|
+
* `compileRulesForProject`). Fingerprinting with a cwd would pull the project
|
|
64
|
+
* layer's subrules into the comparison and trigger a pointless version-home
|
|
65
|
+
* rewrite every time a project's own `.agents/rules/` changes.
|
|
66
|
+
*
|
|
67
|
+
* Silent on any failure (no rules.yaml, unknown preset, unsupported agent) —
|
|
68
|
+
* mirrors `syncResourcesToVersion`'s own catch-and-skip for rules: a bad
|
|
69
|
+
* preset must never block a launch, and this now also runs on the hot path.
|
|
70
|
+
*/
|
|
71
|
+
export function applyActiveRulesPresetAtRun(agent, version, versionHome) {
|
|
72
|
+
const cap = AGENTS[agent].capabilities.rules;
|
|
73
|
+
if (cap === false)
|
|
74
|
+
return false;
|
|
75
|
+
const rulesWriter = getWriter('rules', agent);
|
|
76
|
+
if (!rulesWriter)
|
|
77
|
+
return false;
|
|
78
|
+
const preset = getActiveRulesPreset(agent, version);
|
|
79
|
+
const current = buildRules(agent, version, '');
|
|
80
|
+
const stored = loadSentinel(agent, version);
|
|
81
|
+
if (stored && stored.preset === preset && !isRulesStale(stored.entry, agent, version, '')) {
|
|
82
|
+
return false; // skip-fast: preset unchanged AND composed source set unchanged
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
rulesWriter.write({ version, versionHome, selection: { preset }, cwd: '' });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
saveSentinel(agent, version, { preset, entry: current });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
package/dist/lib/scheduler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* on startup and reloads them on SIGHUP.
|
|
7
7
|
*/
|
|
8
8
|
import { Cron } from 'croner';
|
|
9
|
-
import { listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, } from './routines.js';
|
|
9
|
+
import { listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, hasAmbiguousDevicePin, routineOwnerDevice, } from './routines.js';
|
|
10
10
|
/** In-memory cron scheduler that triggers a callback when jobs fire. */
|
|
11
11
|
export class JobScheduler {
|
|
12
12
|
jobs = new Map();
|
|
@@ -20,7 +20,17 @@ export class JobScheduler {
|
|
|
20
20
|
// Trigger-only jobs (no cron schedule) fire via the webhook receiver,
|
|
21
21
|
// not the cron loop — skip them here. Jobs pinned to another device
|
|
22
22
|
// (routines are fleet-synced) never enter this machine's cron loop.
|
|
23
|
-
if (!config.enabled || !config.schedule
|
|
23
|
+
if (!config.enabled || !config.schedule)
|
|
24
|
+
continue;
|
|
25
|
+
// A multi-device pin is a misconfiguration: it used to fire the routine
|
|
26
|
+
// once per listed device. It now fires only on the owner, but say so —
|
|
27
|
+
// silently reinterpreting someone's config is how this went unnoticed.
|
|
28
|
+
if (hasAmbiguousDevicePin(config)) {
|
|
29
|
+
const owner = routineOwnerDevice(config);
|
|
30
|
+
console.warn(`Job '${config.name}' pins ${config.devices.length} devices; a routine runs on exactly one. ` +
|
|
31
|
+
`Firing only on '${owner}'. Fix with: agents routines devices ${config.name} --set ${owner}`);
|
|
32
|
+
}
|
|
33
|
+
if (!jobRunsOnThisDevice(config))
|
|
24
34
|
continue;
|
|
25
35
|
if (shouldPurgeCompletedOneShotRoutine(config)) {
|
|
26
36
|
deleteJob(config.name);
|
|
Binary file
|
|
Binary file
|
|
@@ -193,11 +193,10 @@ export declare function rekeyStatus(): {
|
|
|
193
193
|
* Throws when the item cannot be reached — on macOS, when the signed helper is
|
|
194
194
|
* unavailable. That is deliberate: this primitive gates destructive writes as
|
|
195
195
|
* well as reads. Through `bundleExists()` it guards the `--force` overwrite
|
|
196
|
-
* checks in `agents secrets create` (`../../commands/secrets.ts`)
|
|
197
|
-
* bundle-rename purge (`./bundles.ts`), the pull-rollback bookkeeping
|
|
198
|
-
* (`./sync.ts`)
|
|
199
|
-
*
|
|
200
|
-
* of them, so an unreachable keychain must fail loudly rather than answer "no".
|
|
196
|
+
* checks in `agents secrets create` (`../../commands/secrets.ts`) and the
|
|
197
|
+
* bundle-rename purge (`./bundles.ts`), and the pull-rollback bookkeeping
|
|
198
|
+
* (`./sync.ts`). A false "absent" silently disarms every one of them, so an
|
|
199
|
+
* unreachable keychain must fail loudly rather than answer "no".
|
|
201
200
|
*
|
|
202
201
|
* Tests needing this path without a helper install a backend via
|
|
203
202
|
* `setKeychainBackendForTest()`, which short-circuits on the next line.
|
|
@@ -680,11 +680,10 @@ export function rekeyStatus() {
|
|
|
680
680
|
* Throws when the item cannot be reached — on macOS, when the signed helper is
|
|
681
681
|
* unavailable. That is deliberate: this primitive gates destructive writes as
|
|
682
682
|
* well as reads. Through `bundleExists()` it guards the `--force` overwrite
|
|
683
|
-
* checks in `agents secrets create` (`../../commands/secrets.ts`)
|
|
684
|
-
* bundle-rename purge (`./bundles.ts`), the pull-rollback bookkeeping
|
|
685
|
-
* (`./sync.ts`)
|
|
686
|
-
*
|
|
687
|
-
* of them, so an unreachable keychain must fail loudly rather than answer "no".
|
|
683
|
+
* checks in `agents secrets create` (`../../commands/secrets.ts`) and the
|
|
684
|
+
* bundle-rename purge (`./bundles.ts`), and the pull-rollback bookkeeping
|
|
685
|
+
* (`./sync.ts`). A false "absent" silently disarms every one of them, so an
|
|
686
|
+
* unreachable keychain must fail loudly rather than answer "no".
|
|
688
687
|
*
|
|
689
688
|
* Tests needing this path without a helper install a backend via
|
|
690
689
|
* `setKeychainBackendForTest()`, which short-circuits on the next line.
|
|
@@ -265,6 +265,14 @@ export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): Ac
|
|
|
265
265
|
export interface ActiveQueryOptions {
|
|
266
266
|
/** Skip the `ps` scan for ad-hoc headless agents. */
|
|
267
267
|
skipHeadless?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* A `--local` query: this machine only. Never dial a remote-host teammate
|
|
270
|
+
* (one dispatched via `agents teams add --device`) over ssh — report its
|
|
271
|
+
* last-persisted meta.json state instead of polling it. RUSH-2118: without
|
|
272
|
+
* this, `agents sessions --active --local` still fired one ssh round-trip
|
|
273
|
+
* per remote-host teammate (finished or not) on every call.
|
|
274
|
+
*/
|
|
275
|
+
localOnly?: boolean;
|
|
268
276
|
}
|
|
269
277
|
/**
|
|
270
278
|
* A live process can only borrow an indexed session file if that transcript
|
|
@@ -421,8 +429,14 @@ export declare function computeLiveSignals(kind: string, sessionFile: string | u
|
|
|
421
429
|
* and truncates. Exported for tests.
|
|
422
430
|
*/
|
|
423
431
|
export declare function summarizeMission(prompt: string | null | undefined): string | undefined;
|
|
424
|
-
/**
|
|
425
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Live teams teammates. Reuses AgentManager which already polls PIDs via
|
|
434
|
+
* `kill -0`. `localOnly` (RUSH-2118) skips the ssh round-trip AgentManager
|
|
435
|
+
* would otherwise issue for every distributed (remote-host) teammate.
|
|
436
|
+
*/
|
|
437
|
+
export declare function listTeamsActive(opts?: {
|
|
438
|
+
localOnly?: boolean;
|
|
439
|
+
}): Promise<ActiveSession[]>;
|
|
426
440
|
/** Live editor-terminal agents across every IDE window. */
|
|
427
441
|
export declare function listTerminalsActive(): Promise<ActiveSession[]>;
|
|
428
442
|
/** Cloud tasks still in a non-terminal state. `tasks.db` may not exist; that's fine. */
|
|
@@ -697,9 +697,13 @@ export function summarizeMission(prompt) {
|
|
|
697
697
|
return undefined;
|
|
698
698
|
return cleaned.length > 80 ? `${cleaned.slice(0, 79)}…` : cleaned;
|
|
699
699
|
}
|
|
700
|
-
/**
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
/**
|
|
701
|
+
* Live teams teammates. Reuses AgentManager which already polls PIDs via
|
|
702
|
+
* `kill -0`. `localOnly` (RUSH-2118) skips the ssh round-trip AgentManager
|
|
703
|
+
* would otherwise issue for every distributed (remote-host) teammate.
|
|
704
|
+
*/
|
|
705
|
+
export async function listTeamsActive(opts = {}) {
|
|
706
|
+
const mgr = new AgentManager(undefined, undefined, undefined, undefined, undefined, opts.localOnly ?? false);
|
|
703
707
|
const running = await mgr.listRunning();
|
|
704
708
|
return running.map((a) => {
|
|
705
709
|
// The teammate's OWN transcript is `remoteSessionId` (captured from its first
|
|
@@ -1438,7 +1442,7 @@ export async function listTmuxAgentSessions() {
|
|
|
1438
1442
|
export async function getActiveSessions(opts = {}) {
|
|
1439
1443
|
const [tmuxAgents, teams, terminals, cloud] = await Promise.all([
|
|
1440
1444
|
listTmuxAgentSessions().catch(() => []),
|
|
1441
|
-
listTeamsActive().catch(() => []),
|
|
1445
|
+
listTeamsActive({ localOnly: opts.localOnly }).catch(() => []),
|
|
1442
1446
|
listTerminalsActive().catch(() => []),
|
|
1443
1447
|
Promise.resolve(listCloudActive()),
|
|
1444
1448
|
]);
|
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import * as fs from 'fs';
|
|
21
21
|
import * as path from 'path';
|
|
22
|
+
import * as crypto from 'crypto';
|
|
22
23
|
import { SYNC_AGENTS, mirrorPath } from './sync/agents.js';
|
|
23
|
-
import { hashContent } from './sync/manifest.js';
|
|
24
24
|
import { redactSecrets } from '../redact.js';
|
|
25
25
|
import { encryptTranscript, decryptTranscriptBody } from './sync/transcript-crypto.js';
|
|
26
26
|
export const BUNDLE_KIND = 'agents-session-bundle';
|
|
27
27
|
export const BUNDLE_VERSION = 1;
|
|
28
|
+
/** SHA-256 of a file body, used for the bundle's dedup/conflict check on import. */
|
|
29
|
+
function hashContent(content) {
|
|
30
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
31
|
+
}
|
|
28
32
|
/** Look up the sync spec for an agent id (undefined → agent not sync-representable). */
|
|
29
33
|
export function specForAgent(agentId) {
|
|
30
34
|
return SYNC_AGENTS.find(s => s.id === agentId);
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { SessionMeta } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* The command run on each peer: answer for itself, as JSON, without recursing.
|
|
4
|
-
* `forwardedArgs` carry the caller's own query/filters (already including the
|
|
5
|
-
* leading `sessions` and a `--json`) so each peer returns a comparable slice.
|
|
6
|
-
* A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
|
|
7
|
-
* there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
|
|
8
|
-
*/
|
|
9
|
-
export declare function remoteListCommand(forwardedArgs: string[], os?: string): string;
|
|
10
2
|
/**
|
|
11
3
|
* Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
|
|
12
4
|
* with `machine`. Defensive against version skew / partial output: non-JSON or
|
|
@@ -17,15 +9,9 @@ export declare function remoteListCommand(forwardedArgs: string[], os?: string):
|
|
|
17
9
|
*/
|
|
18
10
|
export declare function parseRemoteList(stdout: string, machine: string): SessionMeta[];
|
|
19
11
|
export declare function parseRemoteListPayload(stdout: string, machine: string, safeResolver?: boolean): {
|
|
20
|
-
|
|
12
|
+
items: SessionMeta[];
|
|
21
13
|
valid: boolean;
|
|
22
14
|
};
|
|
23
|
-
/** Convert one completed peer process into the exact aggregation result. This
|
|
24
|
-
* is the production parent/peer seam and is exercised with real child output. */
|
|
25
|
-
export declare function remoteListCaptureResult(code: number | null, stdout: string, machine: string, display: string, safeResolver?: boolean): {
|
|
26
|
-
sessions: SessionMeta[];
|
|
27
|
-
unreachable?: string;
|
|
28
|
-
};
|
|
29
15
|
export interface RemoteListResult {
|
|
30
16
|
sessions: SessionMeta[];
|
|
31
17
|
/** How many peer machines we attempted to reach (drives the empty-fleet tip). */
|
|
@@ -17,32 +17,12 @@ import { spawn } from 'child_process';
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
19
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
|
-
import {
|
|
21
|
-
import { loadDevices, isControlDevice, isDialableDevice } from '../devices/registry.js';
|
|
20
|
+
import { loadDevices } from '../devices/registry.js';
|
|
22
21
|
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
23
|
-
import {
|
|
22
|
+
import { gatherRemoteAgentsJson } from '../remote-agents-json.js';
|
|
23
|
+
import { normalizeHost } from './sync/config.js';
|
|
24
24
|
import { NO_FANOUT_ENV } from './remote-active.js';
|
|
25
25
|
import { terminalWidth } from './width.js';
|
|
26
|
-
/** Per-host SSH budget. Slightly above SSH_OPTS' ConnectTimeout=10 so a
|
|
27
|
-
* reachable-but-slow remote still answers before we give up. */
|
|
28
|
-
const REMOTE_TIMEOUT_MS = 12_000;
|
|
29
|
-
/**
|
|
30
|
-
* The command run on each peer: answer for itself, as JSON, without recursing.
|
|
31
|
-
* `forwardedArgs` carry the caller's own query/filters (already including the
|
|
32
|
-
* leading `sessions` and a `--json`) so each peer returns a comparable slice.
|
|
33
|
-
* A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
|
|
34
|
-
* there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
|
|
35
|
-
*/
|
|
36
|
-
export function remoteListCommand(forwardedArgs, os) {
|
|
37
|
-
if (remoteShellFor(os) === 'powershell') {
|
|
38
|
-
return buildWindowsAgentsCommand({
|
|
39
|
-
args: forwardedArgs,
|
|
40
|
-
env: { [NO_FANOUT_ENV]: '1' },
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
const inner = [`${NO_FANOUT_ENV}=1`, 'agents', ...forwardedArgs].map((t, i) => i === 0 ? t : shellQuote(t)).join(' ');
|
|
44
|
-
return `bash -lc ${shellQuote(inner)}`;
|
|
45
|
-
}
|
|
46
26
|
/**
|
|
47
27
|
* Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
|
|
48
28
|
* with `machine`. Defensive against version skew / partial output: non-JSON or
|
|
@@ -83,61 +63,22 @@ export function parseRemoteListPayload(stdout, machine, safeResolver = false) {
|
|
|
83
63
|
parsed = JSON.parse(stdout);
|
|
84
64
|
}
|
|
85
65
|
catch {
|
|
86
|
-
return {
|
|
66
|
+
return { items: [], valid: false };
|
|
87
67
|
}
|
|
88
68
|
if (!Array.isArray(parsed))
|
|
89
|
-
return {
|
|
69
|
+
return { items: [], valid: false };
|
|
90
70
|
const out = [];
|
|
91
71
|
for (const x of parsed) {
|
|
92
72
|
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
93
|
-
return {
|
|
73
|
+
return { items: [], valid: false };
|
|
94
74
|
if (safeResolver && !isSafeResolverRow(x)) {
|
|
95
|
-
return {
|
|
75
|
+
return { items: [], valid: false };
|
|
96
76
|
}
|
|
97
77
|
// `_remote` marks these as living on the peer's disk (not a local mirror),
|
|
98
78
|
// so the picker routes read/resume back over SSH instead of the local FS.
|
|
99
79
|
out.push({ ...x, machine, _remote: true });
|
|
100
80
|
}
|
|
101
|
-
return {
|
|
102
|
-
}
|
|
103
|
-
/** Convert one completed peer process into the exact aggregation result. This
|
|
104
|
-
* is the production parent/peer seam and is exercised with real child output. */
|
|
105
|
-
export function remoteListCaptureResult(code, stdout, machine, display, safeResolver = false) {
|
|
106
|
-
if (code !== 0)
|
|
107
|
-
return { sessions: [], unreachable: display };
|
|
108
|
-
const parsed = parseRemoteListPayload(stdout, machine, safeResolver);
|
|
109
|
-
return parsed.valid ? { sessions: parsed.sessions } : { sessions: [], unreachable: display };
|
|
110
|
-
}
|
|
111
|
-
/** Run one remote `agents sessions … --json` and capture stdout. Resolves
|
|
112
|
-
* `{ code: null }` on spawn error or timeout (host treated as dead). */
|
|
113
|
-
function sshCapture(target, remoteCmd, timeoutMs) {
|
|
114
|
-
assertValidSshTarget(target);
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
117
|
-
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
118
|
-
let stdout = '';
|
|
119
|
-
let settled = false;
|
|
120
|
-
const done = (code) => {
|
|
121
|
-
if (settled)
|
|
122
|
-
return;
|
|
123
|
-
settled = true;
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
resolve({ code, stdout });
|
|
126
|
-
};
|
|
127
|
-
const timer = setTimeout(() => { child.kill('SIGKILL'); done(null); }, timeoutMs);
|
|
128
|
-
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
129
|
-
child.on('error', () => done(null));
|
|
130
|
-
child.on('close', (code) => done(code));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
async function fetchByTarget(target, machine, display, forwardedArgs, os) {
|
|
134
|
-
const { code, stdout } = await sshCapture(target, remoteListCommand(forwardedArgs, os), REMOTE_TIMEOUT_MS);
|
|
135
|
-
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
136
|
-
const result = remoteListCaptureResult(code, stdout, machine, display, safeResolver);
|
|
137
|
-
if (result.unreachable) {
|
|
138
|
-
process.stderr.write(chalk.gray(` ${display}: unreachable or no agents CLI — skipped\n`));
|
|
139
|
-
}
|
|
140
|
-
return result;
|
|
81
|
+
return { items: out, valid: true };
|
|
141
82
|
}
|
|
142
83
|
/**
|
|
143
84
|
* Gather listing sessions from other machines. With an explicit `hosts` list
|
|
@@ -147,51 +88,21 @@ async function fetchByTarget(target, machine, display, forwardedArgs, os) {
|
|
|
147
88
|
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
148
89
|
*/
|
|
149
90
|
export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
let reg;
|
|
159
|
-
try {
|
|
160
|
-
reg = await loadDevices();
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return { sessions: [], deviceCount: 0, unreachable: ['device registry'] };
|
|
164
|
-
}
|
|
165
|
-
for (const d of Object.values(reg)) {
|
|
166
|
-
// Live SSH-probe verdict first, cached tailscale snapshot only as a
|
|
167
|
-
// fallback — see isDialableDevice. A manually-registered device has no
|
|
168
|
-
// tailscale peer entry, so gating on `online` alone hid its sessions.
|
|
169
|
-
if (!isDialableDevice(d))
|
|
170
|
-
continue;
|
|
171
|
-
if (normalizeHost(d.name) === self)
|
|
172
|
-
continue;
|
|
173
|
-
// Control-only devices (a phone/tablet running the cockpit) drive the fleet
|
|
174
|
-
// but never run agents — never dial them, whatever their platform reads as.
|
|
175
|
-
if (isControlDevice(d))
|
|
176
|
-
continue;
|
|
177
|
-
// Only machines that can actually run the CLI. iOS/tablet nodes register as
|
|
178
|
-
// `unknown` platform and can never answer, so skip them rather than burn a
|
|
179
|
-
// full ConnectTimeout on each.
|
|
180
|
-
if (d.platform !== 'windows' && d.platform !== 'linux' && d.platform !== 'macos')
|
|
181
|
-
continue;
|
|
182
|
-
try {
|
|
183
|
-
targets.push({ target: sshTargetFor(d), machine: normalizeHost(d.name), name: d.name, os: d.platform });
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// No address on the profile — nothing to dial; skip silently.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const results = await Promise.all(targets.map((t) => fetchByTarget(t.target, t.machine, t.name, forwardedArgs, t.os)));
|
|
91
|
+
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
92
|
+
const result = await gatherRemoteAgentsJson({
|
|
93
|
+
args: forwardedArgs,
|
|
94
|
+
noFanoutEnv: NO_FANOUT_ENV,
|
|
95
|
+
hosts,
|
|
96
|
+
parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
|
|
97
|
+
});
|
|
191
98
|
return {
|
|
192
|
-
sessions:
|
|
193
|
-
deviceCount:
|
|
194
|
-
unreachable:
|
|
99
|
+
sessions: result.items,
|
|
100
|
+
deviceCount: result.deviceCount,
|
|
101
|
+
unreachable: [
|
|
102
|
+
...(result.discoveryFailed ? ['device registry'] : []),
|
|
103
|
+
...result.skipped,
|
|
104
|
+
...result.parseFailed,
|
|
105
|
+
],
|
|
195
106
|
};
|
|
196
107
|
}
|
|
197
108
|
/** Resolve a peer's SSH target (and OS) from the device registry by its
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* R2 credential resolution + this machine's stable identity. The only field
|
|
3
|
+
* `agents sessions export --encrypt` / `import` actually consume is the
|
|
4
|
+
* shared `R2_SYNC_ENC_KEY` transcript key (`resolveSyncEncKey`); the other R2
|
|
5
|
+
* fields (account/bucket/access keys) are validated here as a presence gate —
|
|
6
|
+
* no `r2.backups` bundle configured means no shared key, so export/import
|
|
7
|
+
* fall back to an ephemeral one. There is no R2 network client in this
|
|
8
|
+
* codebase today (it moved with the rest of the R2/CRDT background sync this
|
|
9
|
+
* bundle used to back — see git history for `./r2.ts`); a future R2-backed
|
|
10
|
+
* feature (an export destination, a fleet cache) would add one where it's
|
|
11
|
+
* actually wired. Credentials come from the `r2.backups` secrets bundle (OS
|
|
12
|
+
* keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
13
|
*/
|
|
6
14
|
/** Secrets bundle holding the R2 credentials. */
|
|
7
15
|
export declare const SYNC_BUNDLE = "r2.backups";
|
|
@@ -16,8 +24,8 @@ export interface R2Config {
|
|
|
16
24
|
* Shared 32-byte key (hex or base64) for client-side transcript encryption,
|
|
17
25
|
* held in the bundle as `R2_SYNC_ENC_KEY`. Optional and deliberately separate
|
|
18
26
|
* from the R2 credentials so rotating the access token never orphans already
|
|
19
|
-
* encrypted
|
|
20
|
-
*
|
|
27
|
+
* encrypted bundles. `agents sessions export --encrypt` prefers this key when
|
|
28
|
+
* present, else mints and prints an ephemeral one — see transcript-crypto.ts.
|
|
21
29
|
*/
|
|
22
30
|
syncEncKey?: string;
|
|
23
31
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* R2 credential resolution + this machine's stable identity. The only field
|
|
3
|
+
* `agents sessions export --encrypt` / `import` actually consume is the
|
|
4
|
+
* shared `R2_SYNC_ENC_KEY` transcript key (`resolveSyncEncKey`); the other R2
|
|
5
|
+
* fields (account/bucket/access keys) are validated here as a presence gate —
|
|
6
|
+
* no `r2.backups` bundle configured means no shared key, so export/import
|
|
7
|
+
* fall back to an ephemeral one. There is no R2 network client in this
|
|
8
|
+
* codebase today (it moved with the rest of the R2/CRDT background sync this
|
|
9
|
+
* bundle used to back — see git history for `./r2.ts`); a future R2-backed
|
|
10
|
+
* feature (an export destination, a fleet cache) would add one where it's
|
|
11
|
+
* actually wired. Credentials come from the `r2.backups` secrets bundle (OS
|
|
12
|
+
* keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
13
|
*/
|
|
6
14
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../../secrets/bundles.js';
|
|
7
15
|
/** Secrets bundle holding the R2 credentials. */
|
|
@@ -12,13 +20,13 @@ export const SYNC_BUNDLE = 'r2.backups';
|
|
|
12
20
|
* without real credentials (no silent fallback).
|
|
13
21
|
*/
|
|
14
22
|
function resolveR2Config() {
|
|
15
|
-
//
|
|
16
|
-
// isHeadlessSecretsContext()
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// and lets any interactive caller of loadR2Config still prompt.
|
|
21
|
-
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: '
|
|
23
|
+
// A headless caller (no TTY — e.g. a routine or SSH-dispatched command) must
|
|
24
|
+
// resolve broker-only: isHeadlessSecretsContext() true means a broker miss
|
|
25
|
+
// can never pop an unattended Touch ID sheet on the user's screen (the same
|
|
26
|
+
// broker-only-when-headless rationale the secrets readers use). Using the
|
|
27
|
+
// shared predicate rather than a literal keeps it consistent with the other
|
|
28
|
+
// callers and lets any interactive caller of loadR2Config still prompt.
|
|
29
|
+
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'session-transport', agentOnly: isHeadlessSecretsContext() });
|
|
22
30
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
23
31
|
const bucket = env.R2_BUCKET_NAME?.trim();
|
|
24
32
|
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
|
|
@@ -31,7 +39,7 @@ function resolveR2Config() {
|
|
|
31
39
|
!secretAccessKey && 'R2_SECRET_ACCESS_KEY',
|
|
32
40
|
].filter(Boolean);
|
|
33
41
|
if (missing.length > 0) {
|
|
34
|
-
throw new Error(`
|
|
42
|
+
throw new Error(`Session R2 transport: bundle '${SYNC_BUNDLE}' is missing ${missing.join(', ')}. ` +
|
|
35
43
|
`Add them with: agents secrets add ${SYNC_BUNDLE} <KEY>`);
|
|
36
44
|
}
|
|
37
45
|
return {
|
|
@@ -40,8 +48,8 @@ function resolveR2Config() {
|
|
|
40
48
|
accessKeyId: accessKeyId,
|
|
41
49
|
secretAccessKey: secretAccessKey,
|
|
42
50
|
// Default to the account's R2 endpoint; an explicit R2_ENDPOINT override
|
|
43
|
-
// points
|
|
44
|
-
//
|
|
51
|
+
// points at any S3-compatible store (MinIO, another provider) — which is
|
|
52
|
+
// also how the feature is verified end-to-end without live R2.
|
|
45
53
|
endpoint: env.R2_ENDPOINT?.trim() || `https://${accountId}.r2.cloudflarestorage.com`,
|
|
46
54
|
syncEncKey,
|
|
47
55
|
};
|
|
@@ -67,7 +67,6 @@ export declare const loadBeta: ModuleLoader;
|
|
|
67
67
|
export declare const loadSync: ModuleLoader;
|
|
68
68
|
export declare const loadLock: ModuleLoader;
|
|
69
69
|
export declare const loadRefreshRules: ModuleLoader;
|
|
70
|
-
export declare const loadDrive: ModuleLoader;
|
|
71
70
|
export declare const loadFactory: ModuleLoader;
|
|
72
71
|
export declare const loadUsage: ModuleLoader;
|
|
73
72
|
export declare const loadCost: ModuleLoader;
|
|
@@ -96,7 +95,6 @@ export declare const loadTeams: ModuleLoader;
|
|
|
96
95
|
export declare const loadCloud: ModuleLoader;
|
|
97
96
|
export declare const loadMessage: ModuleLoader;
|
|
98
97
|
export declare const loadSend: ModuleLoader;
|
|
99
|
-
export declare const loadHq: ModuleLoader;
|
|
100
98
|
export declare const loadFeed: ModuleLoader;
|
|
101
99
|
export declare const loadActivity: ModuleLoader;
|
|
102
100
|
export declare const loadMailboxes: ModuleLoader;
|
|
@@ -45,7 +45,6 @@ export const loadBeta = async () => (await import('../../commands/beta.js')).reg
|
|
|
45
45
|
export const loadSync = async () => (await import('../../commands/sync.js')).registerSyncCommand;
|
|
46
46
|
export const loadLock = async () => (await import('../../commands/lock.js')).registerLockCommand;
|
|
47
47
|
export const loadRefreshRules = async () => (await import('../../commands/refresh-rules.js')).registerRefreshRulesCommand;
|
|
48
|
-
export const loadDrive = async () => (await import('../../commands/drive.js')).registerDriveCommands;
|
|
49
48
|
export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
|
|
50
49
|
export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
|
|
51
50
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
@@ -74,7 +73,6 @@ export const loadTeams = async () => (await import('../../commands/teams.js')).r
|
|
|
74
73
|
export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
|
|
75
74
|
export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
|
|
76
75
|
export const loadSend = async () => (await import('../../commands/send.js')).registerSendCommand;
|
|
77
|
-
export const loadHq = async () => (await import('../../commands/hq.js')).registerHqCommand;
|
|
78
76
|
export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
|
|
79
77
|
export const loadActivity = async () => (await import('../../commands/activity.js')).registerActivityCommand;
|
|
80
78
|
export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
|
|
@@ -90,7 +88,7 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
|
|
|
90
88
|
* inherit the root's custom help formatter rather than getting the per-command
|
|
91
89
|
* recursive pass. Keeping that ordering preserves their `--help` output exactly.
|
|
92
90
|
*/
|
|
93
|
-
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', '
|
|
91
|
+
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
|
|
94
92
|
/**
|
|
95
93
|
* User-typed top-level command name -> ordered list of module loaders to run.
|
|
96
94
|
*
|
|
@@ -161,7 +159,6 @@ export const COMMAND_LOADERS = {
|
|
|
161
159
|
sync: [loadSync],
|
|
162
160
|
lock: [loadLock],
|
|
163
161
|
'refresh-rules': [loadRefreshRules],
|
|
164
|
-
drive: [loadDrive],
|
|
165
162
|
factory: [loadFactory],
|
|
166
163
|
usage: [loadUsage],
|
|
167
164
|
cost: [loadCost],
|
|
@@ -199,7 +196,6 @@ export const COMMAND_LOADERS = {
|
|
|
199
196
|
message: [loadMessage],
|
|
200
197
|
send: [loadSend],
|
|
201
198
|
notify: [loadSend],
|
|
202
|
-
hq: [loadHq],
|
|
203
199
|
feed: [loadFeed],
|
|
204
200
|
activity: [loadActivity],
|
|
205
201
|
mailboxes: [loadMailboxes],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
* and leaves a back-compat symlink at the old path.
|
|
24
24
|
*/
|
|
25
25
|
import type { Meta } from './types.js';
|
|
26
|
+
/**
|
|
27
|
+
* Header prepended to every agents.yaml the CLI writes (central and per-device
|
|
28
|
+
* docs). Carries the yaml-language-server schema hint so editors validate the
|
|
29
|
+
* file against `schema/agents-yaml.schema.json`. Exported so
|
|
30
|
+
* `lib/device-config.ts` writes a sibling device's doc with the same header.
|
|
31
|
+
*/
|
|
32
|
+
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json\n\n";
|
|
26
33
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
27
34
|
export declare function getAgentsDir(): string;
|
|
28
35
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
@@ -194,8 +201,6 @@ export declare function getSystemPluginsDir(): string;
|
|
|
194
201
|
export declare function getExtraPluginsDir(alias: string): string;
|
|
195
202
|
/** Path to a project-scoped plugins directory (<project>/.agents/plugins/), or null when none. */
|
|
196
203
|
export declare function getProjectPluginsDir(cwd?: string): string | null;
|
|
197
|
-
/** Path to synced remote session data (~/.agents/.cache/drive/). */
|
|
198
|
-
export declare function getDriveDir(): string;
|
|
199
204
|
/** Path to soft-deleted resources (~/.agents/.history/trash/). */
|
|
200
205
|
export declare function getTrashDir(): string;
|
|
201
206
|
/** Path to local session indexer storage (~/.agents/.history/sessions/). */
|