@phnx-labs/agents-cli 1.20.73 → 1.20.74
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 +96 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +13 -1
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/setup.js +6 -1
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +63 -1
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/shims.d.ts +26 -0
- package/dist/lib/shims.js +68 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.js +14 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/versions.d.ts +13 -0
- package/dist/lib/versions.js +61 -2
- package/package.json +1 -1
package/dist/commands/view.js
CHANGED
|
@@ -4,13 +4,13 @@ import { visibleWidth, termLink } from '../lib/format.js';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanagedCliState, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
8
|
import { loginHint } from '../lib/signin-badge.js';
|
|
9
9
|
import { machineId } from '../lib/machine-id.js';
|
|
10
10
|
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
12
12
|
import { readManifest } from '../lib/manifest.js';
|
|
13
|
-
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
13
|
+
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
|
|
14
14
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
15
15
|
import { getAgentResources } from '../lib/resources.js';
|
|
16
16
|
import { resolveVersionFilter, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
@@ -243,9 +243,27 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
243
243
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
244
244
|
: 'Checking installed agents...';
|
|
245
245
|
const spinner = ora({ text: spinnerText, isSilent: !process.stdout.isTTY }).start();
|
|
246
|
-
const cliStates = await getAllCliStates();
|
|
247
|
-
spinner.stop();
|
|
248
246
|
const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
|
|
247
|
+
// A globally-installed CLI is superseded only by a NORMAL managed version — that
|
|
248
|
+
// is when agents-cli owns the launcher and a "global" row would just be our own
|
|
249
|
+
// shim reported back. `--isolated` promises the opposite: no default, no bare
|
|
250
|
+
// shim, no adopted launcher, the user's own `~/.<agent>` untouched. So an
|
|
251
|
+
// isolated-only install must not make that still-live global CLI disappear from
|
|
252
|
+
// `agents view` — the two are genuinely separate installs and both get listed.
|
|
253
|
+
const hasNonIsolatedVersion = (agentId) => listInstalledVersions(agentId).some((v) => !isVersionIsolated(agentId, v));
|
|
254
|
+
// Every `cliStates` read in this function feeds the "Not Managed by Agents CLI"
|
|
255
|
+
// block, so resolve it the way that block means it: the user's own CLI on PATH.
|
|
256
|
+
// `getCliState` would answer with a version-dir install — including an isolated
|
|
257
|
+
// copy that is deliberately absent from PATH — and print it as "(global)".
|
|
258
|
+
//
|
|
259
|
+
// Resolved only for agents that can actually reach that block. `getCliState`
|
|
260
|
+
// deliberately avoids subprocesses for a version-managed agent, and PATH
|
|
261
|
+
// resolution costs a `<cli> --version` spawn on a cold cache — so probing an
|
|
262
|
+
// agent whose global row is suppressed anyway would be pure added latency.
|
|
263
|
+
const cliStates = Object.fromEntries(await Promise.all(agentsToShow
|
|
264
|
+
.filter((agentId) => !hasNonIsolatedVersion(agentId))
|
|
265
|
+
.map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
|
|
266
|
+
spinner.stop();
|
|
249
267
|
const showPaths = !!filterAgentId;
|
|
250
268
|
const profilesByAgent = getProfilesByAgent(filterAgentId);
|
|
251
269
|
const profileSummaries = [...profilesByAgent.values()].flat();
|
|
@@ -276,19 +294,18 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
276
294
|
const infoFetches = [];
|
|
277
295
|
const globalInfoFetches = [];
|
|
278
296
|
for (const agentId of agentsToShow) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
info,
|
|
288
|
-
})));
|
|
289
|
-
}
|
|
297
|
+
for (const ver of listInstalledVersions(agentId)) {
|
|
298
|
+
const home = getVersionHomePath(agentId, ver);
|
|
299
|
+
infoFetches.push(getAccountInfo(agentId, home).then((info) => ({
|
|
300
|
+
agentId,
|
|
301
|
+
version: ver,
|
|
302
|
+
home,
|
|
303
|
+
info,
|
|
304
|
+
})));
|
|
290
305
|
}
|
|
291
|
-
|
|
306
|
+
// Mirrors the classification below: fetch the global account whenever the
|
|
307
|
+
// global install will still be rendered (no versions at all, or isolated-only).
|
|
308
|
+
if (!hasNonIsolatedVersion(agentId)) {
|
|
292
309
|
globalInfoFetches.push(getAccountInfo(agentId).then((info) => ({
|
|
293
310
|
agentId,
|
|
294
311
|
cliVersion: cliStates[agentId]?.version || null,
|
|
@@ -356,10 +373,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
356
373
|
if (versions.length > 0) {
|
|
357
374
|
versionManaged.push(agentId);
|
|
358
375
|
}
|
|
359
|
-
|
|
360
|
-
|
|
376
|
+
if (cliState?.installed) {
|
|
377
|
+
// Isolated-only installs sit alongside the global CLI rather than replacing it.
|
|
378
|
+
if (!hasNonIsolatedVersion(agentId))
|
|
379
|
+
globallyInstalled.push(agentId);
|
|
361
380
|
}
|
|
362
|
-
else if (hasProfiles) {
|
|
381
|
+
else if (versions.length === 0 && hasProfiles) {
|
|
363
382
|
profileOnly.push(agentId);
|
|
364
383
|
}
|
|
365
384
|
}
|
|
@@ -376,6 +395,22 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
376
395
|
liveVersionByAgent.set(agentId, live);
|
|
377
396
|
}));
|
|
378
397
|
const displayVersion = (agentId, dirVersion) => liveVersionByAgent.get(agentId) ?? dirVersion;
|
|
398
|
+
// Uncolored row label, shared by the width pass and the render so padding lines
|
|
399
|
+
// up. An isolated copy is never the global default (installing one deliberately
|
|
400
|
+
// records no default), so the two tags can't collide.
|
|
401
|
+
const versionRowLabel = (agentId, version, globalDefault) => {
|
|
402
|
+
const shown = displayVersion(agentId, version);
|
|
403
|
+
if (version === globalDefault)
|
|
404
|
+
return `${shown} (default)`;
|
|
405
|
+
if (isVersionIsolated(agentId, version)) {
|
|
406
|
+
// The isolated default is what a bare `agents run <agent>` reaches, so it is
|
|
407
|
+
// worth distinguishing from the other isolated copies sitting beside it.
|
|
408
|
+
return getIsolatedDefault(agentId) === version
|
|
409
|
+
? `${shown} (isolated default)`
|
|
410
|
+
: `${shown} (isolated)`;
|
|
411
|
+
}
|
|
412
|
+
return shown;
|
|
413
|
+
};
|
|
379
414
|
// Show version-managed agents
|
|
380
415
|
if (versionManaged.length > 0) {
|
|
381
416
|
// Calculate column widths across all agents for alignment
|
|
@@ -388,9 +423,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
388
423
|
const versions = listInstalledVersions(agentId);
|
|
389
424
|
const globalDefault = getGlobalDefault(agentId);
|
|
390
425
|
for (const v of versions) {
|
|
391
|
-
|
|
392
|
-
const label = v === globalDefault ? `${shown} (default)` : shown;
|
|
393
|
-
maxVerLabel = Math.max(maxVerLabel, label.length);
|
|
426
|
+
maxVerLabel = Math.max(maxVerLabel, versionRowLabel(agentId, v, globalDefault).length);
|
|
394
427
|
const rawInfo = infoMap.get(`${agentId}:${v}`);
|
|
395
428
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
396
429
|
const accountLabel = accountColumnLabel(info);
|
|
@@ -431,7 +464,14 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
431
464
|
const globalDefault = getGlobalDefault(agentId);
|
|
432
465
|
const runStrategy = getConfiguredRunStrategy(agentId);
|
|
433
466
|
const strategyLabel = chalk.gray(` (${runStrategy})`);
|
|
434
|
-
|
|
467
|
+
// `(no default)` is a nudge to go set one. It would read as a contradiction
|
|
468
|
+
// directly above a row tagged `(isolated default)`, and it would be bad
|
|
469
|
+
// advice besides: for an isolated-only agent the pointer below IS how a
|
|
470
|
+
// bare `agents run <agent>` resolves, and setting a global default is
|
|
471
|
+
// precisely what `--isolated` exists to avoid.
|
|
472
|
+
const noDefaultLabel = !globalDefault && !getIsolatedDefault(agentId)
|
|
473
|
+
? chalk.yellow(' (no default)')
|
|
474
|
+
: '';
|
|
435
475
|
console.log(` ${chalk.bold(agentLabel(agentId))}${strategyLabel}${noDefaultLabel}`);
|
|
436
476
|
// Sort versions with default first, then by semver descending
|
|
437
477
|
const sortedVersions = [...versions].sort((a, b) => {
|
|
@@ -443,10 +483,16 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
443
483
|
});
|
|
444
484
|
for (const version of sortedVersions) {
|
|
445
485
|
const isDefault = version === globalDefault;
|
|
486
|
+
const isolated = !isDefault && isVersionIsolated(agentId, version);
|
|
487
|
+
const isolatedTag = getIsolatedDefault(agentId) === version ? ' (isolated default)' : ' (isolated)';
|
|
446
488
|
const shown = displayVersion(agentId, version);
|
|
447
|
-
const base =
|
|
448
|
-
const
|
|
449
|
-
const label = isDefault
|
|
489
|
+
const base = versionRowLabel(agentId, version, globalDefault);
|
|
490
|
+
const tagPad = ' '.repeat(maxVerLabel - base.length);
|
|
491
|
+
const label = isDefault
|
|
492
|
+
? `${shown}${chalk.green(' (default)')}${tagPad}`
|
|
493
|
+
: isolated
|
|
494
|
+
? `${shown}${chalk.gray(isolatedTag)}${tagPad}`
|
|
495
|
+
: base.padEnd(maxVerLabel);
|
|
450
496
|
const rawInfo = infoMap.get(`${agentId}:${version}`);
|
|
451
497
|
const vInfo = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
452
498
|
const usageKey = getUsageLookupKey(vInfo);
|
|
@@ -580,7 +626,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
580
626
|
// Profile rows under a globally-installed harness. Use a simpler
|
|
581
627
|
// alignment here since this section doesn't share column state with
|
|
582
628
|
// the version-managed block.
|
|
583
|
-
|
|
629
|
+
// An isolated-only agent now appears in BOTH blocks; its profiles already
|
|
630
|
+
// rendered under the version-managed one, so don't print them twice.
|
|
631
|
+
const profilesHere = versionManaged.includes(agentId) ? [] : (profilesByAgent.get(agentId) ?? []);
|
|
584
632
|
if (profilesHere.length > 0) {
|
|
585
633
|
const nameWidth = Math.max(globalMaxVerLabel, ...profilesHere.map((p) => p.name.length));
|
|
586
634
|
const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ if (IS_DEV_BUILD) {
|
|
|
55
55
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
56
56
|
// first byte of output), the registry maps a command name to a thunk that
|
|
57
57
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
58
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadResources, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadHq, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
58
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadResources, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadHq, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
59
59
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
60
60
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
61
61
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -730,6 +730,7 @@ async function registerAllEagerCommands() {
|
|
|
730
730
|
await reg(loadWorktree);
|
|
731
731
|
await reg(loadVersions);
|
|
732
732
|
await reg(loadImport);
|
|
733
|
+
await reg(loadExport);
|
|
733
734
|
await reg(loadPackages);
|
|
734
735
|
await reg(loadRoutines);
|
|
735
736
|
await reg(loadMonitors);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -71,6 +71,17 @@ export declare function getCliPath(agentId: AgentId): Promise<string | null>;
|
|
|
71
71
|
* back to a plain PATH lookup.
|
|
72
72
|
*/
|
|
73
73
|
export declare function getCliState(agentId: AgentId): Promise<CliState>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the agent's OWN install — the one agents-cli does not manage — by plain
|
|
76
|
+
* PATH lookup, ignoring the version dirs entirely.
|
|
77
|
+
*
|
|
78
|
+
* Callers that specifically mean "the user's own globally-installed CLI" must use
|
|
79
|
+
* this rather than `getCliState`, whose managed fast path reports an installed
|
|
80
|
+
* version (any version dir, in readdir order) and would therefore hand back an
|
|
81
|
+
* isolated copy — a copy that is deliberately unreachable from PATH — labelled as
|
|
82
|
+
* the global install.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getUnmanagedCliState(agentId: AgentId): Promise<CliState>;
|
|
74
85
|
/** Resolve CLI state for all registered agents in parallel. */
|
|
75
86
|
export declare function getAllCliStates(): Promise<Partial<Record<AgentId, CliState>>>;
|
|
76
87
|
/** Info about an existing unmanaged agent installation. */
|
|
@@ -230,6 +241,30 @@ export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
|
|
|
230
241
|
} | null;
|
|
231
242
|
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
232
243
|
export declare function __resetAntigravityKeychainCacheForTest(): void;
|
|
244
|
+
/**
|
|
245
|
+
* Whether a Claude version home's credential file is present but carries no
|
|
246
|
+
* token — the "must have a real credential" floor (see
|
|
247
|
+
* `isValidOpenCodeCredential`) applied to claude.
|
|
248
|
+
*
|
|
249
|
+
* A FAILED OAuth refresh leaves exactly this state behind: Claude Code rewrites
|
|
250
|
+
* `.claude/.credentials.json` with `accessToken: ""`, `refreshToken: ""` and
|
|
251
|
+
* `expiresAt: 0`, keeping only the descriptive fields (`subscriptionType`,
|
|
252
|
+
* `rateLimitTier`, `refreshTokenExpiresAt`). Everything we derive from
|
|
253
|
+
* `.claude.json` — email, plan — still looks healthy, so the install reported
|
|
254
|
+
* `signedIn: true`, `agents view` drew usage bars for it, and balanced rotation
|
|
255
|
+
* (whose `authValid` is just "email present") kept picking it — every pick dying
|
|
256
|
+
* at spawn on "OAuth session expired and could not be refreshed".
|
|
257
|
+
*
|
|
258
|
+
* Only decidable off macOS: there the login Keychain is the canonical store and
|
|
259
|
+
* this file is not authoritative, and probing the Keychain would raise an
|
|
260
|
+
* authorization sheet per installed version on every `agents run` — the reason
|
|
261
|
+
* rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
|
|
262
|
+
* only store, so a token-less file is proof of signed-out. `platform` is a
|
|
263
|
+
* parameter so both branches are testable on any host.
|
|
264
|
+
*
|
|
265
|
+
* Sync, no Keychain, no network — safe on the `agents run` hot path.
|
|
266
|
+
*/
|
|
267
|
+
export declare function isClaudeCredentialFileBlank(base: string, platform?: NodeJS.Platform): boolean;
|
|
233
268
|
export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
|
|
234
269
|
/**
|
|
235
270
|
* Determine when the agent was last used by checking session file mtimes,
|
package/dist/lib/agents.js
CHANGED
|
@@ -860,7 +860,20 @@ export async function getCliState(agentId) {
|
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
|
-
|
|
863
|
+
return getUnmanagedCliState(agentId);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Resolve the agent's OWN install — the one agents-cli does not manage — by plain
|
|
867
|
+
* PATH lookup, ignoring the version dirs entirely.
|
|
868
|
+
*
|
|
869
|
+
* Callers that specifically mean "the user's own globally-installed CLI" must use
|
|
870
|
+
* this rather than `getCliState`, whose managed fast path reports an installed
|
|
871
|
+
* version (any version dir, in readdir order) and would therefore hand back an
|
|
872
|
+
* isolated copy — a copy that is deliberately unreachable from PATH — labelled as
|
|
873
|
+
* the global install.
|
|
874
|
+
*/
|
|
875
|
+
export async function getUnmanagedCliState(agentId) {
|
|
876
|
+
const agent = AGENTS[agentId];
|
|
864
877
|
// Special case for grok: it manages its own binaries in ~/.grok/downloads/
|
|
865
878
|
if (agentId === 'grok') {
|
|
866
879
|
const grokBin = resolveGrokBinary();
|
|
@@ -1322,6 +1335,48 @@ function isValidOpenCodeCredential(value) {
|
|
|
1322
1335
|
default: return false;
|
|
1323
1336
|
}
|
|
1324
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Whether a Claude version home's credential file is present but carries no
|
|
1340
|
+
* token — the "must have a real credential" floor (see
|
|
1341
|
+
* `isValidOpenCodeCredential`) applied to claude.
|
|
1342
|
+
*
|
|
1343
|
+
* A FAILED OAuth refresh leaves exactly this state behind: Claude Code rewrites
|
|
1344
|
+
* `.claude/.credentials.json` with `accessToken: ""`, `refreshToken: ""` and
|
|
1345
|
+
* `expiresAt: 0`, keeping only the descriptive fields (`subscriptionType`,
|
|
1346
|
+
* `rateLimitTier`, `refreshTokenExpiresAt`). Everything we derive from
|
|
1347
|
+
* `.claude.json` — email, plan — still looks healthy, so the install reported
|
|
1348
|
+
* `signedIn: true`, `agents view` drew usage bars for it, and balanced rotation
|
|
1349
|
+
* (whose `authValid` is just "email present") kept picking it — every pick dying
|
|
1350
|
+
* at spawn on "OAuth session expired and could not be refreshed".
|
|
1351
|
+
*
|
|
1352
|
+
* Only decidable off macOS: there the login Keychain is the canonical store and
|
|
1353
|
+
* this file is not authoritative, and probing the Keychain would raise an
|
|
1354
|
+
* authorization sheet per installed version on every `agents run` — the reason
|
|
1355
|
+
* rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
|
|
1356
|
+
* only store, so a token-less file is proof of signed-out. `platform` is a
|
|
1357
|
+
* parameter so both branches are testable on any host.
|
|
1358
|
+
*
|
|
1359
|
+
* Sync, no Keychain, no network — safe on the `agents run` hot path.
|
|
1360
|
+
*/
|
|
1361
|
+
export function isClaudeCredentialFileBlank(base, platform = process.platform) {
|
|
1362
|
+
if (platform === 'darwin')
|
|
1363
|
+
return false;
|
|
1364
|
+
try {
|
|
1365
|
+
const raw = fs.readFileSync(path.join(base, '.claude', '.credentials.json'), 'utf-8');
|
|
1366
|
+
const oauth = JSON.parse(raw).claudeAiOauth;
|
|
1367
|
+
if (!oauth)
|
|
1368
|
+
return false;
|
|
1369
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
1370
|
+
return !nonEmpty(oauth.accessToken) && !nonEmpty(oauth.refreshToken);
|
|
1371
|
+
}
|
|
1372
|
+
catch {
|
|
1373
|
+
// No file (a Keychain-backed home, or never logged in here) or an
|
|
1374
|
+
// unreadable/corrupt one: not positive evidence of a blank credential, so
|
|
1375
|
+
// leave the existing signal alone rather than declaring a working install
|
|
1376
|
+
// signed out.
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1325
1380
|
export async function getAccountInfo(agentId, home) {
|
|
1326
1381
|
const base = home || os.homedir();
|
|
1327
1382
|
const empty = {
|
|
@@ -1358,6 +1413,13 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1358
1413
|
const accountId = normalizeIdentityPart(oa?.accountUuid);
|
|
1359
1414
|
const organizationId = normalizeIdentityPart(oa?.organizationUuid);
|
|
1360
1415
|
const email = oa?.emailAddress || null;
|
|
1416
|
+
// Credential floor: a blanked credential file means this home cannot
|
|
1417
|
+
// authenticate, whatever `.claude.json` still says. Report it signed out
|
|
1418
|
+
// so `agents view` prompts a re-login and rotation routes around it,
|
|
1419
|
+
// instead of handing runs to an install that dies at spawn.
|
|
1420
|
+
if (email && isClaudeCredentialFileBlank(base)) {
|
|
1421
|
+
return { ...empty, lastActive };
|
|
1422
|
+
}
|
|
1361
1423
|
const accountKey = buildIdentityKey(agentId, [
|
|
1362
1424
|
['account', accountId],
|
|
1363
1425
|
['org', organizationId],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move `source` onto `dest` across possibly-different volumes. `renameSync` is
|
|
3
|
+
* atomic but throws EXDEV when `~/.agents` lives on a different filesystem than
|
|
4
|
+
* `$HOME`; fall back to copy-then-remove so the restore still completes. The
|
|
5
|
+
* source is removed only after the copy succeeds, so a mid-copy failure never
|
|
6
|
+
* destroys the sole surviving copy.
|
|
7
|
+
*/
|
|
8
|
+
export declare function moveDirCrossDevice(source: string, dest: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Copy `source` to `dest`, dropping any symlink whose target resolves back into
|
|
11
|
+
* `~/.agents`. Managed resources (skills/commands) are synced into a version home
|
|
12
|
+
* as symlinks into `~/.agents`; copying them verbatim would leave the result full
|
|
13
|
+
* of links that dangle the moment `~/.agents` is disposed. Stripping them yields a
|
|
14
|
+
* clean, self-contained copy — which is the entire point of an export.
|
|
15
|
+
*/
|
|
16
|
+
export declare function copyDirStrippingAgentsSymlinks(source: string, dest: string, agentsDir: string): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Primitives for moving an agent config directory across the agents-cli boundary.
|
|
3
|
+
*
|
|
4
|
+
* Both helpers were part of {@link ./uninstall.ts} and are unchanged — teardown was
|
|
5
|
+
* simply the first caller. `agents export` needs the same two operations (relocate a
|
|
6
|
+
* directory that may sit on another volume; copy one without dragging `~/.agents`
|
|
7
|
+
* symlinks along), so they live here rather than being duplicated or imported out of
|
|
8
|
+
* a module named for teardown.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
/**
|
|
13
|
+
* Move `source` onto `dest` across possibly-different volumes. `renameSync` is
|
|
14
|
+
* atomic but throws EXDEV when `~/.agents` lives on a different filesystem than
|
|
15
|
+
* `$HOME`; fall back to copy-then-remove so the restore still completes. The
|
|
16
|
+
* source is removed only after the copy succeeds, so a mid-copy failure never
|
|
17
|
+
* destroys the sole surviving copy.
|
|
18
|
+
*/
|
|
19
|
+
export function moveDirCrossDevice(source, dest) {
|
|
20
|
+
try {
|
|
21
|
+
fs.renameSync(source, dest);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code !== 'EXDEV')
|
|
25
|
+
throw err;
|
|
26
|
+
fs.cpSync(source, dest, { recursive: true });
|
|
27
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Copy `source` to `dest`, dropping any symlink whose target resolves back into
|
|
32
|
+
* `~/.agents`. Managed resources (skills/commands) are synced into a version home
|
|
33
|
+
* as symlinks into `~/.agents`; copying them verbatim would leave the result full
|
|
34
|
+
* of links that dangle the moment `~/.agents` is disposed. Stripping them yields a
|
|
35
|
+
* clean, self-contained copy — which is the entire point of an export.
|
|
36
|
+
*/
|
|
37
|
+
export function copyDirStrippingAgentsSymlinks(source, dest, agentsDir) {
|
|
38
|
+
const inside = agentsDir + path.sep;
|
|
39
|
+
fs.cpSync(source, dest, {
|
|
40
|
+
recursive: true,
|
|
41
|
+
filter: (src) => {
|
|
42
|
+
try {
|
|
43
|
+
const st = fs.lstatSync(src);
|
|
44
|
+
if (st.isSymbolicLink()) {
|
|
45
|
+
const tgt = path.resolve(path.dirname(src), fs.readlinkSync(src));
|
|
46
|
+
if (tgt === agentsDir || tgt.startsWith(inside))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* unreadable entry — let cpSync surface it on the real copy */
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AgentId } from './types.js';
|
|
2
|
+
export type ExportMode = 'merge' | 'replace' | 'staged';
|
|
3
|
+
/** Filename of the provenance receipt written into the agent's config dir. */
|
|
4
|
+
export declare const RECEIPT_NAME = ".agents-cli-export.json";
|
|
5
|
+
/** Suffix for the incoming copy of a file the user already has. */
|
|
6
|
+
export declare const CONFLICT_SUFFIX = ".from-agents-cli";
|
|
7
|
+
/** Why an export cannot proceed. Each maps to a distinct user-facing remedy. */
|
|
8
|
+
export type ExportBlocker = {
|
|
9
|
+
kind: 'not-installed';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'not-isolated';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'no-config';
|
|
14
|
+
source: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'dest-adopted';
|
|
17
|
+
realPath: string;
|
|
18
|
+
adoptedVersion: string;
|
|
19
|
+
};
|
|
20
|
+
/** What the destination `~/.<agent>` currently is. */
|
|
21
|
+
export type DestKind = 'absent' | 'real-dir' | 'foreign-symlink';
|
|
22
|
+
/** One file the export would place, relative to the config dir root. */
|
|
23
|
+
export interface ExportEntry {
|
|
24
|
+
/** Path relative to the config dir, e.g. `prompts/review.md`. */
|
|
25
|
+
rel: string;
|
|
26
|
+
/** Absolute source file inside the isolated home. */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Absolute path this file will be written to. */
|
|
29
|
+
target: string;
|
|
30
|
+
/**
|
|
31
|
+
* Set when the user already has `rel`. `target` then points at the
|
|
32
|
+
* `.from-agents-cli` sibling, and `existing` is the untouched original.
|
|
33
|
+
*/
|
|
34
|
+
existing?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ExportPlan {
|
|
37
|
+
agent: AgentId;
|
|
38
|
+
version: string;
|
|
39
|
+
mode: ExportMode;
|
|
40
|
+
/** The isolated config dir, e.g. `<versionDir>/home/.codex`. */
|
|
41
|
+
source: string;
|
|
42
|
+
/** The user's real config dir, e.g. `~/.codex`. */
|
|
43
|
+
dest: string;
|
|
44
|
+
destKind: DestKind;
|
|
45
|
+
/** Files with no counterpart in `dest` — written in place. */
|
|
46
|
+
writes: ExportEntry[];
|
|
47
|
+
/** Files the user already has — written alongside, never over. */
|
|
48
|
+
conflicts: ExportEntry[];
|
|
49
|
+
/** replace mode only: where `dest` gets moved first. */
|
|
50
|
+
backupPath: string | null;
|
|
51
|
+
/** staged mode only: the subdirectory receiving the whole tree. */
|
|
52
|
+
stagedPath: string | null;
|
|
53
|
+
receiptPath: string;
|
|
54
|
+
blocker: ExportBlocker | null;
|
|
55
|
+
}
|
|
56
|
+
export interface ExportResult {
|
|
57
|
+
exported: boolean;
|
|
58
|
+
dest: string;
|
|
59
|
+
written: string[];
|
|
60
|
+
conflicts: Array<{
|
|
61
|
+
path: string;
|
|
62
|
+
theirs: string;
|
|
63
|
+
}>;
|
|
64
|
+
backupPath: string | null;
|
|
65
|
+
stagedPath: string | null;
|
|
66
|
+
receiptPath: string | null;
|
|
67
|
+
errors: string[];
|
|
68
|
+
}
|
|
69
|
+
/** Build a read-only plan. Performs no mutations. */
|
|
70
|
+
export declare function planExport(agent: AgentId, version: string, timestamp: number, mode?: ExportMode): ExportPlan;
|
|
71
|
+
/** Execute a plan from {@link planExport}. */
|
|
72
|
+
export declare function executeExport(plan: ExportPlan, timestamp: number): ExportResult;
|