@phnx-labs/agents-cli 1.22.82 → 1.22.83
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 +8 -0
- package/dist/commands/accounts.js +33 -5
- package/dist/commands/view.js +3 -6
- package/dist/lib/account-registry.d.ts +40 -3
- package/dist/lib/account-registry.js +89 -18
- package/dist/lib/claude-statusline.d.ts +13 -5
- package/dist/lib/claude-statusline.js +19 -6
- package/dist/lib/devices/fleet-inventory.js +3 -4
- package/dist/lib/devices/harness-inventory.js +4 -5
- package/dist/lib/feed/activity.js +95 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.83
|
|
4
|
+
|
|
5
|
+
- **The Claude status line shows the registered account NAME (PHNX-3987).** A login named with `agents accounts` (`work`, `dev`, …) now renders that name between the host and the model — `yosemite · work · Fable 5.1 · …` — which is what tells several same-harness logins apart at a glance; an unnamed login keeps the email label. The lookup is the one `agents view` and the device inventories already use, lifted into `findNativeAccountByIdentity` so the three copies share it. Source: `cli/src/lib/claude-statusline.ts`, `cli/src/lib/account-registry.ts`.
|
|
6
|
+
|
|
7
|
+
- **`agents accounts rename` / `remove` / `view` honor per-harness account names (PHNX-3988).** Native labels are unique per harness (PHNX-3887), but rename/remove still ran a fleet-wide uniqueness check and resolved a bare name to whichever harness row the store ordered first — so renaming Codex's `cxicloud` to `icloud` failed because Claude already owned `icloud`. Rename now scopes uniqueness to the target row's harness, so one harness can take a name another already uses; a duplicate within the same harness is still refused. Rename, remove, and view accept `<harness>#<name>` and refuse an ambiguous bare name rather than guessing. Source: `cli/src/lib/account-registry.ts`, `cli/src/commands/accounts.ts`.
|
|
8
|
+
|
|
9
|
+
- **Feed activity polling reuses unchanged log tails (PHNX-3939).** The activity reader caches file-version-validated tails and timestamp summaries within a bounded process-local budget. Cursor polls skip unchanged historical events without parsing them again; appends, rewrites, replacements and tail-budget changes invalidate the cached snapshot. Returned events remain independently mutable, with timestamp, filter and limit behavior preserved.
|
|
10
|
+
|
|
3
11
|
## 1.22.82
|
|
4
12
|
|
|
5
13
|
- **The Claude status line names the signed-in account before the model (PHNX-3987).** The managed `agents __claude-statusline` render now reads `host · account · model · 5h n% · 7d n% · ◆ reminder`, where the account is the email the running Claude is signed into (plus the org name for a Team/Enterprise seat, the same label `agents view` renders), read from the `.claude.json` of the home Claude is actually running with — the version home under the launch shim, `$HOME` otherwise. A never-signed-in home omits the part. Source: `cli/src/lib/claude-statusline.ts`.
|
|
@@ -21,7 +21,7 @@ import { connectRefusal, connectSupported, runConnect } from '../lib/accounts/co
|
|
|
21
21
|
import { acquireAuthOperationLock } from '../lib/accounts/auth-operation-lock.js';
|
|
22
22
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
23
23
|
import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
|
|
24
|
-
import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, nativeAccountHome, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
24
|
+
import { accountBindings, addAccount, addNativeAccount, assertUnambiguousNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, nativeAccountHome, parseAccountSelector, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
25
25
|
import { registerMintCommand } from './auth-mint.js';
|
|
26
26
|
/** Comma-joined list of harnesses `accounts connect` can drive today. */
|
|
27
27
|
function connectSupportedList() {
|
|
@@ -580,12 +580,21 @@ agents run claude --account work`,
|
|
|
580
580
|
console.log(chalk.green(`Updated credential for account '${name}'.`));
|
|
581
581
|
});
|
|
582
582
|
});
|
|
583
|
-
accounts.command('view <name>')
|
|
583
|
+
const viewCmd = accounts.command('view <name>')
|
|
584
|
+
.alias('inspect')
|
|
585
|
+
.description('Show safe account metadata, custody, and attachments. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
586
|
+
.option('--json', 'Machine-readable output')
|
|
587
|
+
.action(async (name, o, command) => {
|
|
584
588
|
await runAccountsAction(command, async () => {
|
|
585
589
|
const meta = readMeta();
|
|
586
|
-
const
|
|
590
|
+
const selector = parseAccountSelector(name);
|
|
591
|
+
assertUnambiguousNativeAccount(meta, selector.name, selector.agent);
|
|
592
|
+
const unified = findUnifiedAccount(selector.name, meta, undefined, selector.agent);
|
|
587
593
|
if (!unified)
|
|
588
594
|
throw new Error(`Unknown account '${name}'.`);
|
|
595
|
+
if (selector.agent && unified.kind === 'native' && unified.agent !== selector.agent) {
|
|
596
|
+
throw new Error(`Unknown ${selector.agent} account '${selector.name}'.`);
|
|
597
|
+
}
|
|
589
598
|
const account = unified.kind === 'provider'
|
|
590
599
|
? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
|
|
591
600
|
: { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
|
|
@@ -607,6 +616,11 @@ agents run claude --account work`,
|
|
|
607
616
|
console.log(` attached: ${account.attached.length ? account.attached.join(', ') : 'none'}`);
|
|
608
617
|
});
|
|
609
618
|
});
|
|
619
|
+
setHelpSections(viewCmd, {
|
|
620
|
+
examples: `agents accounts view work --json
|
|
621
|
+
agents accounts view codex#icloud`,
|
|
622
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
623
|
+
});
|
|
610
624
|
accounts.command('name <source> <name>')
|
|
611
625
|
.description('Name a signed-in native installation without copying its OAuth credentials')
|
|
612
626
|
.action(async (source, name, _o, command) => {
|
|
@@ -723,12 +737,25 @@ agents run codex#work`,
|
|
|
723
737
|
console.log(chalk.green(`Detached ${name} from ${target}.`));
|
|
724
738
|
});
|
|
725
739
|
});
|
|
726
|
-
accounts.command('rename <old> <new>')
|
|
740
|
+
const renameCmd = accounts.command('rename <old> <new>')
|
|
741
|
+
.description('Rename an account without changing its stable id. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
742
|
+
.action(async (oldName, newName, _o, command) => {
|
|
727
743
|
await runAccountsAction(command, () => { renameAccount(oldName, newName); });
|
|
728
744
|
});
|
|
729
|
-
|
|
745
|
+
setHelpSections(renameCmd, {
|
|
746
|
+
examples: `agents accounts rename cxicloud icloud
|
|
747
|
+
agents accounts rename codex#icloud cloud`,
|
|
748
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
749
|
+
});
|
|
750
|
+
const removeCmd = accounts.command('remove <name>')
|
|
751
|
+
.description('Remove an account and its device-local credential. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
752
|
+
.action(async (name, _o, command) => {
|
|
730
753
|
await runAccountsAction(command, () => { removeAccount(name); });
|
|
731
754
|
});
|
|
755
|
+
setHelpSections(removeCmd, {
|
|
756
|
+
examples: `agents accounts remove claude#icloud`,
|
|
757
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
758
|
+
});
|
|
732
759
|
accounts.command('set-default <agent> <name>')
|
|
733
760
|
.description('Use this account for a harness when --account is omitted')
|
|
734
761
|
.action(async (agentRaw, name, _o, command) => {
|
|
@@ -849,6 +876,7 @@ agents accounts name claude@2.1.220 work
|
|
|
849
876
|
agents accounts label codex@0.146.0 personal
|
|
850
877
|
agents accounts attach work claude@2.1.225
|
|
851
878
|
agents accounts view work --json
|
|
879
|
+
agents accounts view codex#icloud
|
|
852
880
|
agents accounts switch claude
|
|
853
881
|
agents accounts switch claude work
|
|
854
882
|
agents accounts set-default claude work
|
package/dist/commands/view.js
CHANGED
|
@@ -20,7 +20,7 @@ import { listCliStatus } from '../lib/cli-resources.js';
|
|
|
20
20
|
import { isCapable } from '../lib/capabilities.js';
|
|
21
21
|
import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins/plugins.js';
|
|
22
22
|
import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts, readMeta } from '../lib/state.js';
|
|
23
|
-
import {
|
|
23
|
+
import { findNativeAccountByIdentity } from '../lib/account-registry.js';
|
|
24
24
|
import { buildNativeCatalog } from '../lib/account-catalog.js';
|
|
25
25
|
import { connectSupported } from '../lib/accounts/connect.js';
|
|
26
26
|
import { readInstallation } from '../lib/installations/store.js';
|
|
@@ -67,11 +67,8 @@ export const accountColumnLabel = accountDisplayLabel;
|
|
|
67
67
|
*/
|
|
68
68
|
export function namedAccountColumnLabel(agentId, info) {
|
|
69
69
|
const display = accountColumnLabel(info);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
return display;
|
|
73
|
-
const saved = listNativeAccounts(readMeta()).find(item => item.agent === agentId && item.identityKey === identityKey);
|
|
74
|
-
return saved ? `${saved.name} · ${display || saved.identityLabel || identityKey}` : display;
|
|
70
|
+
const saved = findNativeAccountByIdentity(readMeta(), agentId, info);
|
|
71
|
+
return saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display;
|
|
75
72
|
}
|
|
76
73
|
/** Human account identity; release labels belong only in installation diagnostics. */
|
|
77
74
|
export function nativeAccountViewLabel(row) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentId, type Meta } from './types.js';
|
|
2
2
|
import { type AccountAuthKind } from './account-provider-registry.js';
|
|
3
3
|
export interface CredentialAccount {
|
|
4
4
|
id: string;
|
|
@@ -43,6 +43,18 @@ export declare function findAccount(name: string, doc?: AccountRegistryDocument)
|
|
|
43
43
|
* accumulate). On an id collision the device slice wins.
|
|
44
44
|
*/
|
|
45
45
|
export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): NativeAccount[];
|
|
46
|
+
/**
|
|
47
|
+
* The native account registered for one harness login, or null when that login
|
|
48
|
+
* is unnamed. `info` is the login's identity as `getAccountInfo` /
|
|
49
|
+
* `readClaudeHomeConfig` report it; the match key is the same value every
|
|
50
|
+
* catalog and inventory groups a home on — the stable `accountKey`, else the
|
|
51
|
+
* lowercased email. The one lookup behind `agents view`, the device
|
|
52
|
+
* inventories, and the Claude status line.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findNativeAccountByIdentity(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agent: AgentId, info: {
|
|
55
|
+
accountKey?: string | null;
|
|
56
|
+
email?: string | null;
|
|
57
|
+
} | null | undefined): NativeAccount | null;
|
|
46
58
|
/**
|
|
47
59
|
* Resolve one account by name or id across both stores, native first.
|
|
48
60
|
*
|
|
@@ -55,6 +67,23 @@ export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'device
|
|
|
55
67
|
* able to omit it.
|
|
56
68
|
*/
|
|
57
69
|
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument, preferAgent?: AgentId): UnifiedAccount | null;
|
|
70
|
+
/**
|
|
71
|
+
* Split a management selector into its harness and name. `<harness>#<name>`
|
|
72
|
+
* pins the harness; a bare name (or row id) carries none. Native names are
|
|
73
|
+
* unique per harness (PHNX-3887), so a bare name alone can legitimately match a
|
|
74
|
+
* claude row AND a codex row — the selector is how rename/remove say which.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseAccountSelector(input: string): {
|
|
77
|
+
agent?: AgentId;
|
|
78
|
+
name: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Refuse a bare name that native rows in several harnesses share. A
|
|
82
|
+
* harness-qualified selector (`agent` set) never hits this — uniqueness is
|
|
83
|
+
* per harness. Ids are unique so they never collide either. Shared by
|
|
84
|
+
* rename/remove/view so the message stays one string.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertUnambiguousNativeAccount(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, name: string, agent?: AgentId): void;
|
|
58
87
|
/**
|
|
59
88
|
* Validate a native account NAME (charset + per-harness uniqueness) WITHOUT a
|
|
60
89
|
* known identity — the pre-flight `agents accounts connect` runs before it
|
|
@@ -120,8 +149,16 @@ export interface AddAccountOptions {
|
|
|
120
149
|
}
|
|
121
150
|
export declare function addAccount(name: string, provider: string, auth: AccountAuthKind, secret: string, base?: string, opts?: AddAccountOptions): CredentialAccount;
|
|
122
151
|
export declare function setAccountSecret(name: string, secret: string, base?: string): void;
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
|
|
154
|
+
* carries its harness, so the new name only has to be free within THAT harness
|
|
155
|
+
* (PHNX-3887 / PHNX-3988): renaming codex's `cxicloud` to `icloud` is fine
|
|
156
|
+
* while claude's `icloud` stays untouched. Provider accounts have no harness
|
|
157
|
+
* and stay globally unique.
|
|
158
|
+
*/
|
|
159
|
+
export declare function renameAccount(oldSelector: string, newName: string, base?: string): void;
|
|
160
|
+
/** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
|
|
161
|
+
export declare function removeAccount(selector: string, base?: string): void;
|
|
125
162
|
export declare function inspectAccount(name: string, base?: string): CredentialAccount & {
|
|
126
163
|
secretPresent: boolean;
|
|
127
164
|
policy: 'never';
|
|
@@ -23,6 +23,7 @@ import * as yaml from 'yaml';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
25
25
|
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
26
|
+
import { isAgentId } from './types.js';
|
|
26
27
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken } from './secrets/index.js';
|
|
27
28
|
import { bundleExists, deleteBundle, listBundles, readAndResolveBundleEnv, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
|
|
28
29
|
import { getAccountProvider } from './account-provider-registry.js';
|
|
@@ -148,6 +149,20 @@ export function listNativeAccounts(meta) {
|
|
|
148
149
|
const merged = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
|
|
149
150
|
return Object.values(merged).map(account => ({ ...account, kind: 'native' }));
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The native account registered for one harness login, or null when that login
|
|
154
|
+
* is unnamed. `info` is the login's identity as `getAccountInfo` /
|
|
155
|
+
* `readClaudeHomeConfig` report it; the match key is the same value every
|
|
156
|
+
* catalog and inventory groups a home on — the stable `accountKey`, else the
|
|
157
|
+
* lowercased email. The one lookup behind `agents view`, the device
|
|
158
|
+
* inventories, and the Claude status line.
|
|
159
|
+
*/
|
|
160
|
+
export function findNativeAccountByIdentity(meta, agent, info) {
|
|
161
|
+
const identityKey = info?.accountKey ?? info?.email?.toLowerCase();
|
|
162
|
+
if (!identityKey)
|
|
163
|
+
return null;
|
|
164
|
+
return listNativeAccounts(meta).find(account => account.agent === agent && account.identityKey === identityKey) ?? null;
|
|
165
|
+
}
|
|
151
166
|
/**
|
|
152
167
|
* Resolve one account by name or id across both stores, native first.
|
|
153
168
|
*
|
|
@@ -168,8 +183,9 @@ export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
|
168
183
|
// merged store happened to order first, so `agents run claude#<email>` died with
|
|
169
184
|
// "Account 'personal' is a codex login and cannot authenticate the claude harness"
|
|
170
185
|
// while that identity's own claude login sat right there. Prefer the harness being
|
|
171
|
-
// launched; fall back to the first match
|
|
172
|
-
//
|
|
186
|
+
// launched; fall back to the first match when the caller has no harness in hand.
|
|
187
|
+
// rename/remove/view refuse an ambiguous *name* via assertUnambiguousNativeAccount
|
|
188
|
+
// before they get here — this fallback is for identityLabel collisions.
|
|
173
189
|
const native = matches.find(account => account.agent === preferAgent) ?? matches[0];
|
|
174
190
|
if (native)
|
|
175
191
|
return native;
|
|
@@ -179,10 +195,46 @@ export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
|
179
195
|
function nativeIdentityRows(meta, agent, identityKey) {
|
|
180
196
|
return listNativeAccounts(meta).filter(account => account.agent === agent && account.identityKey === identityKey);
|
|
181
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Split a management selector into its harness and name. `<harness>#<name>`
|
|
200
|
+
* pins the harness; a bare name (or row id) carries none. Native names are
|
|
201
|
+
* unique per harness (PHNX-3887), so a bare name alone can legitimately match a
|
|
202
|
+
* claude row AND a codex row — the selector is how rename/remove say which.
|
|
203
|
+
*/
|
|
204
|
+
export function parseAccountSelector(input) {
|
|
205
|
+
const hash = input.indexOf('#');
|
|
206
|
+
if (hash < 0)
|
|
207
|
+
return { name: input };
|
|
208
|
+
const agentRaw = input.slice(0, hash);
|
|
209
|
+
const name = input.slice(hash + 1).trim();
|
|
210
|
+
if (!isAgentId(agentRaw))
|
|
211
|
+
throw new Error(`Unknown agent '${agentRaw}'.`);
|
|
212
|
+
if (!name)
|
|
213
|
+
throw new Error('Select an account after #.');
|
|
214
|
+
return { agent: agentRaw, name };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Refuse a bare name that native rows in several harnesses share. A
|
|
218
|
+
* harness-qualified selector (`agent` set) never hits this — uniqueness is
|
|
219
|
+
* per harness. Ids are unique so they never collide either. Shared by
|
|
220
|
+
* rename/remove/view so the message stays one string.
|
|
221
|
+
*/
|
|
222
|
+
export function assertUnambiguousNativeAccount(meta, name, agent) {
|
|
223
|
+
const matches = listNativeAccounts(meta).filter(account => (agent === undefined || account.agent === agent) && (account.id === name || account.name === name));
|
|
224
|
+
const harnesses = [...new Set(matches.map(account => account.agent))].sort();
|
|
225
|
+
if (harnesses.length > 1) {
|
|
226
|
+
throw new Error(`Account '${name}' exists for several harnesses (${harnesses.join(', ')}). `
|
|
227
|
+
+ `Pick one with <harness>#${name}, e.g. ${harnesses[0]}#${name}.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
182
230
|
/** Every row (central + this box's device store) for the identity that `name`
|
|
183
|
-
* (id or account name) resolves to.
|
|
184
|
-
|
|
185
|
-
|
|
231
|
+
* (id or account name) resolves to. With `agent` set only that harness's rows
|
|
232
|
+
* are considered; without it, a name owned by rows in several harnesses is
|
|
233
|
+
* refused rather than resolved to whichever the store ordered first. */
|
|
234
|
+
function nativeRowsForNameOrId(meta, name, agent) {
|
|
235
|
+
assertUnambiguousNativeAccount(meta, name, agent);
|
|
236
|
+
const matches = listNativeAccounts(meta).filter(account => (agent === undefined || account.agent === agent) && (account.id === name || account.name === name));
|
|
237
|
+
const found = matches[0];
|
|
186
238
|
if (!found)
|
|
187
239
|
return [];
|
|
188
240
|
return nativeIdentityRows(meta, found.agent, found.identityKey);
|
|
@@ -197,9 +249,9 @@ function nativeRowsForNameOrId(meta, name) {
|
|
|
197
249
|
* actually ambiguous at the point of use: the selector is `<harness>#<label>`,
|
|
198
250
|
* and `findUnifiedAccount` already disambiguates via `preferAgent`.
|
|
199
251
|
*
|
|
200
|
-
* Pass `agent` to scope the check to that harness.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
252
|
+
* Pass `agent` to scope the check to that harness. Every native path has one
|
|
253
|
+
* in hand — connect/label from the caller, rename from the row being renamed —
|
|
254
|
+
* so the un-scoped form is only for provider accounts.
|
|
203
255
|
*
|
|
204
256
|
* Provider (non-native) accounts stay globally unique — they are selected by
|
|
205
257
|
* bare name via `--account`, with no harness to scope them by.
|
|
@@ -493,21 +545,32 @@ export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
|
|
|
493
545
|
bundle.created_at = readBundle(account.name).created_at; // rotate the secret, keep the bundle's birth time
|
|
494
546
|
writeBundleWithItems(bundle, items);
|
|
495
547
|
}
|
|
496
|
-
|
|
548
|
+
/**
|
|
549
|
+
* `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
|
|
550
|
+
* carries its harness, so the new name only has to be free within THAT harness
|
|
551
|
+
* (PHNX-3887 / PHNX-3988): renaming codex's `cxicloud` to `icloud` is fine
|
|
552
|
+
* while claude's `icloud` stays untouched. Provider accounts have no harness
|
|
553
|
+
* and stay globally unique.
|
|
554
|
+
*/
|
|
555
|
+
export function renameAccount(oldSelector, newName, base = getUserAgentsDir()) {
|
|
497
556
|
assertName(newName);
|
|
498
|
-
const doc = readAccountRegistry(base);
|
|
499
557
|
const meta = readMeta();
|
|
500
|
-
const
|
|
558
|
+
const selector = parseAccountSelector(oldSelector);
|
|
559
|
+
const rows = nativeRowsForNameOrId(meta, selector.name, selector.agent);
|
|
501
560
|
if (rows.length) {
|
|
502
|
-
assertUniqueUnifiedName(newName, meta,
|
|
561
|
+
assertUniqueUnifiedName(newName, meta, undefined, new Set(rows.map(account => account.id)), rows[0].agent);
|
|
503
562
|
// Sweep every row for the identity (PHNX-3206) in its owning store (PHNX-3315)
|
|
504
563
|
// and any per-harness default that points to the old name or row ids.
|
|
505
564
|
const rowScope = rows[0].scope;
|
|
506
|
-
const
|
|
565
|
+
const renamedAgent = rows[0].agent;
|
|
566
|
+
const renamedIds = new Set(rows.map(row => row.id));
|
|
507
567
|
updateMeta(current => {
|
|
508
568
|
const defaults = { ...current.accounts?.defaults };
|
|
509
569
|
for (const [agent, value] of Object.entries(defaults)) {
|
|
510
|
-
|
|
570
|
+
// Ids are unique so an id match may rewrite any harness's default. A
|
|
571
|
+
// bare-name match must only rewrite THIS harness — two harnesses may
|
|
572
|
+
// legitimately share the same native name (PHNX-3988).
|
|
573
|
+
if (renamedIds.has(value) || (agent === renamedAgent && value === selector.name))
|
|
511
574
|
defaults[agent] = newName;
|
|
512
575
|
}
|
|
513
576
|
const next = { ...current, accounts: { ...current.accounts, defaults } };
|
|
@@ -527,9 +590,12 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
527
590
|
});
|
|
528
591
|
return;
|
|
529
592
|
}
|
|
530
|
-
|
|
593
|
+
if (selector.agent)
|
|
594
|
+
throw new Error(`Unknown ${selector.agent} account '${selector.name}'.`);
|
|
595
|
+
const doc = readAccountRegistry(base);
|
|
596
|
+
const account = findAccount(selector.name, doc);
|
|
531
597
|
if (!account)
|
|
532
|
-
throw new Error(`Unknown account '${
|
|
598
|
+
throw new Error(`Unknown account '${selector.name}'.`);
|
|
533
599
|
assertUniqueUnifiedName(newName, meta, doc);
|
|
534
600
|
const idsToRename = new Set([account.name, account.id]);
|
|
535
601
|
updateMeta(current => {
|
|
@@ -543,9 +609,11 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
543
609
|
renameBundle(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
|
|
544
610
|
renameProfileConsumers(account.name, newName, base);
|
|
545
611
|
}
|
|
546
|
-
|
|
612
|
+
/** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
|
|
613
|
+
export function removeAccount(selector, base = getUserAgentsDir()) {
|
|
547
614
|
const meta = readMeta();
|
|
548
|
-
const
|
|
615
|
+
const parsed = parseAccountSelector(selector);
|
|
616
|
+
const rows = nativeRowsForNameOrId(meta, parsed.name, parsed.agent);
|
|
549
617
|
if (rows.length) {
|
|
550
618
|
const bindings = [...new Set(rows.flatMap(row => accountBindings(row.id, meta)))].sort();
|
|
551
619
|
if (bindings.length)
|
|
@@ -571,6 +639,9 @@ export function removeAccount(name, base = getUserAgentsDir()) {
|
|
|
571
639
|
});
|
|
572
640
|
return;
|
|
573
641
|
}
|
|
642
|
+
if (parsed.agent)
|
|
643
|
+
throw new Error(`Unknown ${parsed.agent} account '${parsed.name}'.`);
|
|
644
|
+
const name = parsed.name;
|
|
574
645
|
const account = findAccount(name, readAccountRegistry(base));
|
|
575
646
|
if (!account)
|
|
576
647
|
throw new Error(`Unknown account '${name}'.`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClaudeHomeIdentity } from './agent-spec/agents.js';
|
|
2
|
+
import { type NativeAccount } from './account-registry.js';
|
|
2
3
|
export declare const CLAUDE_STATUSLINE_COMMAND = "agents __claude-statusline";
|
|
3
4
|
/**
|
|
4
5
|
* True when `command` re-invokes THIS status-line producer (our private
|
|
@@ -45,12 +46,19 @@ export declare function claudeHomeFromEnv(env: NodeJS.ProcessEnv): string;
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function readClaudeIdentity(claudeHome: string): ClaudeHomeIdentity | null;
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* accounts`) uses — the email, plus the org name for a multi-seat Team/Enterprise
|
|
51
|
-
* seat.
|
|
49
|
+
* The registered native account for the running Claude's identity (`agents
|
|
50
|
+
* accounts` — `work`, `dev`, …), or null when that login is unnamed.
|
|
52
51
|
*/
|
|
53
|
-
export declare function
|
|
52
|
+
export declare function resolveNativeAccount(identity: ClaudeHomeIdentity | null): NativeAccount | null;
|
|
53
|
+
/**
|
|
54
|
+
* Format the signed-in account as a statusline part ('' when never signed in).
|
|
55
|
+
* A named login renders its registered NAME — the short handle the owner chose
|
|
56
|
+
* (`work`, `dev`), which is what tells eight same-harness logins apart at a
|
|
57
|
+
* glance. An unnamed login renders the label every other account-aware surface
|
|
58
|
+
* (`agents view`, `agents accounts`) uses: the email, plus the org name for a
|
|
59
|
+
* multi-seat Team/Enterprise seat.
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatAccountPart(identity: ClaudeHomeIdentity | null, account: NativeAccount | null): string;
|
|
54
62
|
export declare function renderDelegate(payload: string, versionHome: string): string;
|
|
55
63
|
/**
|
|
56
64
|
* Format a reminder as a dimmed statusline part (empty string when none). The
|
|
@@ -3,9 +3,11 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import { accountDisplayLabel, readClaudeHomeConfig, } from './agent-spec/agents.js';
|
|
6
|
+
import { findNativeAccountByIdentity } from './account-registry.js';
|
|
6
7
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
7
8
|
import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
8
9
|
import { loadReminders, pickReminderForSession } from './reminders.js';
|
|
10
|
+
import { readMeta } from './state.js';
|
|
9
11
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
10
12
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
11
13
|
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
@@ -97,14 +99,25 @@ export function readClaudeIdentity(claudeHome) {
|
|
|
97
99
|
return readClaudeHomeConfig(claudeHome)?.identity ?? null;
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* accounts`) uses — the email, plus the org name for a multi-seat Team/Enterprise
|
|
103
|
-
* seat.
|
|
102
|
+
* The registered native account for the running Claude's identity (`agents
|
|
103
|
+
* accounts` — `work`, `dev`, …), or null when that login is unnamed.
|
|
104
104
|
*/
|
|
105
|
-
export function
|
|
105
|
+
export function resolveNativeAccount(identity) {
|
|
106
|
+
return identity ? findNativeAccountByIdentity(readMeta(), 'claude', identity) : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format the signed-in account as a statusline part ('' when never signed in).
|
|
110
|
+
* A named login renders its registered NAME — the short handle the owner chose
|
|
111
|
+
* (`work`, `dev`), which is what tells eight same-harness logins apart at a
|
|
112
|
+
* glance. An unnamed login renders the label every other account-aware surface
|
|
113
|
+
* (`agents view`, `agents accounts`) uses: the email, plus the org name for a
|
|
114
|
+
* multi-seat Team/Enterprise seat.
|
|
115
|
+
*/
|
|
116
|
+
export function formatAccountPart(identity, account) {
|
|
106
117
|
if (!identity)
|
|
107
118
|
return '';
|
|
119
|
+
if (account)
|
|
120
|
+
return account.name;
|
|
108
121
|
return accountDisplayLabel({ ...identity, signedIn: true });
|
|
109
122
|
}
|
|
110
123
|
function delegatePath(versionHome) {
|
|
@@ -194,7 +207,7 @@ export async function runClaudeStatusLine() {
|
|
|
194
207
|
const identity = readClaudeIdentity(claudeHomeFromEnv(process.env));
|
|
195
208
|
if (versionHome)
|
|
196
209
|
ingestClaudeStatusLineUsage(payload, identity);
|
|
197
|
-
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id), formatAccountPart(identity)));
|
|
210
|
+
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id), formatAccountPart(identity, resolveNativeAccount(identity))));
|
|
198
211
|
return 0;
|
|
199
212
|
}
|
|
200
213
|
export function installClaudeStatusLine(versionHome) {
|
|
@@ -12,7 +12,7 @@ import { getAvailableResources, getVersionHomePath, isVersionIsolated, listInsta
|
|
|
12
12
|
import { supports } from '../capabilities.js';
|
|
13
13
|
import { checkVersionHookWiring } from '../hooks/install.js';
|
|
14
14
|
import { getUserAgentsDir, getSystemAgentsDir, readMeta } from '../state.js';
|
|
15
|
-
import {
|
|
15
|
+
import { findNativeAccountByIdentity } from '../account-registry.js';
|
|
16
16
|
import { readRepoState } from '../git.js';
|
|
17
17
|
import { ALL_AGENT_IDS, accountDisplayLabel, credentialPresence, getAccountInfo, supportsAccountInspection, } from '../agents.js';
|
|
18
18
|
import { FLEET_RESOURCE_KINDS, } from './fleet-divergence.js';
|
|
@@ -46,9 +46,8 @@ export async function collectLocalFleetSignIn() {
|
|
|
46
46
|
signedIn = info.signedIn;
|
|
47
47
|
// Prefix the durable account name when this identity has been named.
|
|
48
48
|
const display = accountDisplayLabel(info);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
account = (saved ? `${saved.name} · ${display || saved.identityLabel || identityKey}` : display) || null;
|
|
49
|
+
const saved = findNativeAccountByIdentity(readMeta(), agent, info);
|
|
50
|
+
account = (saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display) || null;
|
|
52
51
|
}
|
|
53
52
|
catch {
|
|
54
53
|
/* advisory only — treat as logged out, provability decided below */
|
|
@@ -18,7 +18,7 @@ import { ALL_AGENT_IDS, accountDisplayLabel, getAccountInfo, } from '../agents.j
|
|
|
18
18
|
import { deriveUsageStatusFromSnapshot, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
|
|
19
19
|
import { getVersionHomePath, listInstalledVersions } from '../installations/store.js';
|
|
20
20
|
import { readMeta } from '../state.js';
|
|
21
|
-
import {
|
|
21
|
+
import { findNativeAccountByIdentity } from '../account-registry.js';
|
|
22
22
|
/**
|
|
23
23
|
* Roll one usage snapshot into a {@link QuotaSummary}. Mirrors the blocking-window
|
|
24
24
|
* selection of {@link deriveUsageStatusFromSnapshot} (the model-specific
|
|
@@ -118,7 +118,7 @@ export async function collectLocalHarnessInventory(opts) {
|
|
|
118
118
|
const usageByKey = usageInputs.length
|
|
119
119
|
? (await getUsageInfoByIdentity(usageInputs, opts?.refresh ? { forceRefresh: true } : undefined)).usageByKey
|
|
120
120
|
: new Map();
|
|
121
|
-
const
|
|
121
|
+
const meta = readMeta();
|
|
122
122
|
return pending.map(({ agent, version, info }) => {
|
|
123
123
|
const key = getUsageLookupKey(info);
|
|
124
124
|
const usage = key ? usageByKey.get(key) : undefined;
|
|
@@ -130,9 +130,8 @@ export async function collectLocalHarnessInventory(opts) {
|
|
|
130
130
|
const signedIn = !!info?.signedIn;
|
|
131
131
|
const { ready, reason } = computeReady(signedIn, quota);
|
|
132
132
|
const display = info ? accountDisplayLabel(info) || null : null;
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const account = saved ? `${saved.name} · ${display || saved.identityLabel || identityKey}` : display;
|
|
133
|
+
const saved = findNativeAccountByIdentity(meta, agent, info);
|
|
134
|
+
const account = saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display;
|
|
136
135
|
return { agent, version, account, signedIn, quota, ready, reason };
|
|
137
136
|
});
|
|
138
137
|
}
|
|
@@ -181,24 +181,105 @@ function readTail(file, maxBytes) {
|
|
|
181
181
|
return text;
|
|
182
182
|
}
|
|
183
183
|
catch {
|
|
184
|
-
return
|
|
184
|
+
return undefined;
|
|
185
185
|
}
|
|
186
186
|
finally {
|
|
187
187
|
if (fd !== undefined)
|
|
188
188
|
fs.closeSync(fd);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
+
// Process-local tail cache: bounded independently of the number of historical
|
|
192
|
+
// sessions or directories a long-lived consumer visits. Never expose cached
|
|
193
|
+
// objects: callers enrich and mutate returned events (including nested values).
|
|
194
|
+
const ACTIVITY_CACHE_BYTES = 32 * 1024 * 1024;
|
|
195
|
+
const ACTIVITY_CACHE_FILES = 1024;
|
|
196
|
+
const activityTails = new Map();
|
|
197
|
+
let activityTailBytes = 0;
|
|
198
|
+
function forgetActivityTail(key) {
|
|
199
|
+
const old = activityTails.get(key);
|
|
200
|
+
if (old)
|
|
201
|
+
activityTailBytes -= old.weight;
|
|
202
|
+
activityTails.delete(key);
|
|
203
|
+
}
|
|
204
|
+
function activityStamp(file) {
|
|
205
|
+
// ctime catches same-size rewrites even when a producer restores mtime;
|
|
206
|
+
// device/inode catches atomic replacement. Nanoseconds avoid rounding away
|
|
207
|
+
// changes made between consecutive polls on high-resolution filesystems.
|
|
208
|
+
const st = fs.statSync(file, { bigint: true });
|
|
209
|
+
return `${st.dev}:${st.ino}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
210
|
+
}
|
|
211
|
+
function readActivityTail(file, maxBytes, sinceMs = -Infinity) {
|
|
212
|
+
const key = `${path.resolve(file)}\0${maxBytes}`;
|
|
213
|
+
let stamp;
|
|
214
|
+
try {
|
|
215
|
+
stamp = activityStamp(file);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
forgetActivityTail(key);
|
|
219
|
+
return { stamp: '', events: [], newestMs: -Infinity, weight: 0 };
|
|
220
|
+
}
|
|
221
|
+
const cached = activityTails.get(key);
|
|
222
|
+
if (cached?.stamp === stamp && (cached.events || cached.newestMs < sinceMs)) {
|
|
223
|
+
activityTails.delete(key);
|
|
224
|
+
activityTails.set(key, cached);
|
|
225
|
+
return cached;
|
|
226
|
+
}
|
|
227
|
+
forgetActivityTail(key);
|
|
228
|
+
const text = readTail(file, maxBytes);
|
|
229
|
+
// Transient I/O failure is not an empty file snapshot; retry next read.
|
|
230
|
+
if (text === undefined)
|
|
231
|
+
return { stamp, events: [], newestMs: -Infinity, weight: 0 };
|
|
232
|
+
const events = [];
|
|
233
|
+
let newestMs = -Infinity;
|
|
234
|
+
for (const line of text.split('\n')) {
|
|
235
|
+
const event = parseLine(line);
|
|
236
|
+
if (!event)
|
|
237
|
+
continue;
|
|
238
|
+
events.push(event);
|
|
239
|
+
try {
|
|
240
|
+
const ms = Date.parse(event.ts);
|
|
241
|
+
if (Number.isFinite(ms))
|
|
242
|
+
newestMs = Math.max(newestMs, ms);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// The tolerant parser historically accepts truthy non-string values.
|
|
246
|
+
// Do not introduce a conversion error before the caller's event filters
|
|
247
|
+
// (or into session reads, which never converted timestamps at all).
|
|
248
|
+
newestMs = Infinity;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Bound retained source-derived strings, objects, and entry overhead. Large
|
|
252
|
+
// one-off tails still read normally, but cannot displace the entire cache.
|
|
253
|
+
const weight = text.length * 2 + events.length * 1024 + key.length * 2 + 256;
|
|
254
|
+
const result = { stamp, events, newestMs, weight };
|
|
255
|
+
try {
|
|
256
|
+
// A writer racing the read must not pin a mixed snapshot as unchanged.
|
|
257
|
+
if (activityStamp(file) === stamp) {
|
|
258
|
+
if (activityTails.size >= ACTIVITY_CACHE_FILES)
|
|
259
|
+
forgetActivityTail(activityTails.keys().next().value);
|
|
260
|
+
const summaryWeight = key.length * 2 + 256;
|
|
261
|
+
const retained = weight <= ACTIVITY_CACHE_BYTES / 2 ? { ...result } : { ...result, events: undefined, weight: summaryWeight };
|
|
262
|
+
// Retain timestamp summaries even when parsed tails exceed the budget:
|
|
263
|
+
// otherwise a large historical corpus thrashes on every cursor poll.
|
|
264
|
+
for (const [oldKey, old] of activityTails) {
|
|
265
|
+
if (activityTailBytes + retained.weight <= ACTIVITY_CACHE_BYTES)
|
|
266
|
+
break;
|
|
267
|
+
const summary = oldKey.length * 2 + 256;
|
|
268
|
+
activityTailBytes -= old.weight - summary;
|
|
269
|
+
old.events = undefined;
|
|
270
|
+
old.weight = summary;
|
|
271
|
+
}
|
|
272
|
+
activityTails.set(key, retained);
|
|
273
|
+
activityTailBytes += retained.weight;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch { /* Deleted during the read: return this snapshot without caching. */ }
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
191
279
|
/** Read all events for one session (bounded tail). */
|
|
192
280
|
export function readSessionActivity(sessionId, root, maxBytes = 256 * 1024) {
|
|
193
281
|
const dir = root ?? getActivityDir();
|
|
194
|
-
|
|
195
|
-
const out = [];
|
|
196
|
-
for (const line of text.split('\n')) {
|
|
197
|
-
const ev = parseLine(line);
|
|
198
|
-
if (ev)
|
|
199
|
-
out.push(ev);
|
|
200
|
-
}
|
|
201
|
-
return out;
|
|
282
|
+
return structuredClone(readActivityTail(activityPath(dir, sessionId), maxBytes).events ?? []);
|
|
202
283
|
}
|
|
203
284
|
/** List session ids that have an activity log. */
|
|
204
285
|
export function listActivitySessions(root) {
|
|
@@ -220,7 +301,10 @@ export function readRecentActivity(opts = {}) {
|
|
|
220
301
|
const wanted = opts.events && opts.events.length > 0 ? new Set(opts.events) : null;
|
|
221
302
|
const all = [];
|
|
222
303
|
for (const sessionId of listActivitySessions(dir)) {
|
|
223
|
-
|
|
304
|
+
const tail = readActivityTail(activityPath(dir, sessionId), opts.maxBytesPerSession ?? 256 * 1024, sinceMs);
|
|
305
|
+
if (tail.newestMs < sinceMs)
|
|
306
|
+
continue;
|
|
307
|
+
for (const ev of tail.events ?? []) {
|
|
224
308
|
if (wanted && !wanted.has(ev.event))
|
|
225
309
|
continue;
|
|
226
310
|
if (opts.tier && ev.tier !== opts.tier)
|
|
@@ -231,7 +315,7 @@ export function readRecentActivity(opts = {}) {
|
|
|
231
315
|
}
|
|
232
316
|
}
|
|
233
317
|
all.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
234
|
-
return typeof opts.limit === 'number' ? all.slice(0, opts.limit) : all;
|
|
318
|
+
return structuredClone(typeof opts.limit === 'number' ? all.slice(0, opts.limit) : all);
|
|
235
319
|
}
|
|
236
320
|
/**
|
|
237
321
|
* Split a chronological event list into individual milestones plus a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.83",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|