@phnx-labs/agents-cli 1.22.44 → 1.22.45
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/bootstrap.js +1 -6
- package/dist/cli/command-registry.d.ts +0 -2
- package/dist/cli/command-registry.js +0 -4
- package/dist/commands/accounts.d.ts +1 -9
- package/dist/commands/accounts.js +12 -90
- package/dist/commands/insights.js +82 -154
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/startup/command-registry.d.ts +2 -1
- package/dist/lib/startup/command-registry.js +5 -2
- package/package.json +1 -1
- package/dist/commands/auth.d.ts +0 -9
- package/dist/commands/auth.js +0 -112
- package/dist/commands/org.d.ts +0 -11
- package/dist/commands/org.js +0 -228
- package/dist/lib/entitlement.d.ts +0 -31
- package/dist/lib/entitlement.js +0 -137
- package/dist/lib/prix-account.d.ts +0 -159
- package/dist/lib/prix-account.js +0 -215
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.45
|
|
4
|
+
|
|
5
|
+
- **BREAKING: the Prix-coupled account layer is removed — `agents auth`, `agents org`, and the plan-tier gates (RUSH-2581).** 1.22.42 shipped `agents auth login/whoami/logout` and `agents org` against the Rush product's backend (`api.prix.dev`), with `entitlement.ts` reading the Rush billing tier to cap accounts at 3-per-harness on free and to gate the `agents insights` friction sections. agents-cli's identity must not ride a separate product's login and database, so all of it is removed: `auth` and `org` are retired top-level names (they fail loudly, no auto-correct), account registration is uncapped again, `agents accounts` output drops the `dormant` field/suffix, and `agents insights` renders the full report with no `plan`/`notice` JSON fields. The Rush *feature* integrations are untouched (`cloud/rush.ts` dispatch, the opt-in secrets sync driver, the owner-notify transport). The replacement — agents-cli's own account backend with Phoenix branding, built for later Phoenix-ID consolidation — is tracked in RUSH-2581. Source: `apps/cli/src/lib/{prix-account,entitlement}.ts` (deleted), `apps/cli/src/commands/{auth,org}.ts` (deleted), `apps/cli/src/commands/{accounts,insights}.ts`, `apps/cli/src/{bootstrap.ts,cli/command-registry.ts,lib/startup/command-registry.ts}`.
|
|
6
|
+
|
|
7
|
+
- **The menubar prepack gate fails closed off-Mac when the bundle has no stapled notarization ticket (RUSH-3026).** 1.22.44 was packed on a Linux box where the gate's codesign/xcrun checks silently no-op'd, so an un-stapled dev bundle shipped and Gatekeeper rejected it on every Mac ("not notarized/valid; skipping launch") — the menu bar died until a rollback to 1.22.43. `stapler staple` writes the ticket as a plain file (`Contents/CodeResources`), so its absence is provable anywhere: the gate now hard-fails the pack when it is missing and no xcrun is available, turning that failure into a pack error instead of a shipped regression. Source: `apps/cli/scripts/verify-menubar-helper.sh`.
|
|
8
|
+
|
|
9
|
+
- **A Linux box can now produce the pretested release tarball — the attestation producer seeds the already-signed helper apps (RUSH-3026).** `release-attestation-produce.sh` runs in a fresh worktree whose `bin/` is empty (the signed `.app` helpers are untracked), so on any non-Mac box `npm pack` died at the prepack gates and attestation production — and therefore every release — stayed chained to a Mac. Off a macOS signing box the producer now seeds `bin/Agents CLI.app` and `bin/MenubarHelper.app` copy-if-absent from the caller checkout; the prepack gates still verify them (keychain sha pin, menubar presence), so a wrong or tampered seed fails the pack exactly as before. A Darwin producer's freshly signed apps are never overwritten. Source: `apps/cli/scripts/release-attestation-produce.sh`.
|
|
10
|
+
|
|
3
11
|
## 1.22.44
|
|
4
12
|
|
|
5
13
|
- **Browser domain-skill discovery is layer-aware (RUSH-2497).** `agents browser start --url <url>` auto-loads a site-specific SKILL.md, but the lookup only ever searched the user layer (`~/.agents/skills/browser/domain-skills`), so a skill shipped in the system layer (`~/.agents/.system/skills/browser/domain-skills`) — or a project's `.agents/` — was silently invisible: the browser just opened without the guide. `resolveDomainSkill` now searches project > user > system > extra repos, first layer with a match wins, mirroring `resolveResource` precedence; `$AGENTS_BROWSER_DOMAIN_SKILLS_DIR` stays a single-root override for tests. The project layer resolves from the calling process's cwd — the shared browser daemon does not yet receive the CLI caller's cwd over IPC, so there the project layer follows the daemon's own cwd until RUSH-2996 threads it through. A miss still never breaks browser start, but now logs the roots searched at debug level (`AGENTS_DEBUG`/`DEBUG`) — the total silence is what hid this. Source: `apps/cli/src/lib/browser/domain-skills.ts`.
|
package/dist/bootstrap.js
CHANGED
|
@@ -264,12 +264,7 @@ Observe (read the fleet — no store merge):
|
|
|
264
264
|
sync status Sync/drift only (not the live fleet snapshot)
|
|
265
265
|
devices snapshot One-process inventory + active sessions poll
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
auth login Sign in via the device-code flow
|
|
269
|
-
auth whoami Show the signed-in Prix user
|
|
270
|
-
auth space Create and manage a space (invite collaborators)
|
|
271
|
-
|
|
272
|
-
Credentials (harness keys, not your Prix user):
|
|
267
|
+
Credentials (harness keys):
|
|
273
268
|
harness Custom (host CLI + model + auth) harnesses; replaces former profiles command
|
|
274
269
|
secrets Keychain-backed env bundles; synced vault: secrets vault unlock|lock
|
|
275
270
|
accounts Provider credentials + native OAuth logout
|
|
@@ -90,8 +90,6 @@ export declare const loadWebhooks: ModuleLoader;
|
|
|
90
90
|
export declare const loadHumans: ModuleLoader;
|
|
91
91
|
export declare const loadAccounts: ModuleLoader;
|
|
92
92
|
export declare const loadDaemon: ModuleLoader;
|
|
93
|
-
export declare const loadAuth: ModuleLoader;
|
|
94
|
-
export declare const loadOrg: ModuleLoader;
|
|
95
93
|
/**
|
|
96
94
|
* Commands whose modules pull in the SQLite-backed session/cloud stack. They are
|
|
97
95
|
* registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
|
|
@@ -93,8 +93,6 @@ export const loadWebhooks = async () => (await import('../commands/webhook.js'))
|
|
|
93
93
|
export const loadHumans = async () => (await import('../commands/humans.js')).registerHumansCommands;
|
|
94
94
|
export const loadAccounts = async () => (await import('../commands/accounts.js')).registerAccountsCommand;
|
|
95
95
|
export const loadDaemon = async () => (await import('../commands/daemon.js')).registerDaemonCommand;
|
|
96
|
-
export const loadAuth = async () => (await import('../commands/auth.js')).registerAuthCommand;
|
|
97
|
-
export const loadOrg = async () => (await import('../commands/org.js')).registerOrgCommand;
|
|
98
96
|
/**
|
|
99
97
|
* Commands whose modules pull in the SQLite-backed session/cloud stack. They are
|
|
100
98
|
* registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
|
|
@@ -126,8 +124,6 @@ export const LAZY_COMMAND_NAMES = new Set([
|
|
|
126
124
|
*/
|
|
127
125
|
export const COMMAND_LOADERS = {
|
|
128
126
|
accounts: [loadAccounts],
|
|
129
|
-
auth: [loadAuth],
|
|
130
|
-
org: [loadOrg],
|
|
131
127
|
view: [loadView],
|
|
132
128
|
inspect: [loadInspect],
|
|
133
129
|
feedback: [loadFeedback],
|
|
@@ -30,16 +30,8 @@ export declare function parseBundleKey(raw: string): {
|
|
|
30
30
|
bundle: string;
|
|
31
31
|
key: string;
|
|
32
32
|
};
|
|
33
|
-
/**
|
|
34
|
-
* Named accounts that `set-default` / `switch` can pin for this harness —
|
|
35
|
-
* capped to the caller's plan tier (RUSH-2424). Downgrading never deletes a
|
|
36
|
-
* credential: an over-cap account simply falls out of this list (see
|
|
37
|
-
* {@link dormantAccountsForHarness}) and stops being a switch/rotation target
|
|
38
|
-
* until the plan is upgraded.
|
|
39
|
-
*/
|
|
33
|
+
/** Named accounts that `set-default` / `switch` can pin for this harness. */
|
|
40
34
|
export declare function listSwitchableAccounts(agent: AgentId): Promise<UnifiedAccount[]>;
|
|
41
|
-
/** Accounts registered for this harness beyond the plan's cap — kept, never deleted, excluded from switch/rotation, reactivated by an upgrade. */
|
|
42
|
-
export declare function dormantAccountsForHarness(agent: AgentId): Promise<UnifiedAccount[]>;
|
|
43
35
|
/**
|
|
44
36
|
* Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
|
|
45
37
|
* Provider accounts must authenticate the harness; native accounts must belong to it.
|
|
@@ -17,7 +17,6 @@ import { discoverNativeAccounts } from '../lib/account-catalog.js';
|
|
|
17
17
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
18
18
|
import { getAccountProvider, listAccountProviders } from '../lib/account-provider-registry.js';
|
|
19
19
|
import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
20
|
-
import { accountCapForTier, getTier } from '../lib/entitlement.js';
|
|
21
20
|
function parseInstallation(raw) {
|
|
22
21
|
const at = raw.lastIndexOf('@');
|
|
23
22
|
if (at < 1 || at === raw.length - 1)
|
|
@@ -74,8 +73,8 @@ function secretFromBundle(raw) {
|
|
|
74
73
|
const { bundle, key } = parseBundleKey(raw);
|
|
75
74
|
return readAndResolveBundleEnv(bundle, { keys: [key], keyMode: 'storage', agentOnly: true, caller: 'accounts import' }).env[key];
|
|
76
75
|
}
|
|
77
|
-
function publicAccount(account
|
|
78
|
-
return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent
|
|
76
|
+
function publicAccount(account) {
|
|
77
|
+
return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent };
|
|
79
78
|
}
|
|
80
79
|
async function printAccounts(json, fleet = false) {
|
|
81
80
|
if (fleet)
|
|
@@ -83,16 +82,12 @@ async function printAccounts(json, fleet = false) {
|
|
|
83
82
|
const records = Object.values(readAccountRegistry().accounts).sort((a, b) => a.name.localeCompare(b.name));
|
|
84
83
|
const discovered = await discoverNativeAccounts();
|
|
85
84
|
const savedNative = listNativeAccounts(readMeta());
|
|
86
|
-
const dormantIds = new Set();
|
|
87
|
-
for (const agent of ALL_AGENT_IDS)
|
|
88
|
-
for (const account of await dormantAccountsForHarness(agent))
|
|
89
|
-
dormantIds.add(account.id);
|
|
90
85
|
const native = discovered.map(row => {
|
|
91
86
|
const saved = savedNative.find(account => account.agent === row.agent && account.identityKey === row.id);
|
|
92
|
-
return { ...row, name: saved?.name, id: saved?.id ?? row.id
|
|
87
|
+
return { ...row, name: saved?.name, id: saved?.id ?? row.id };
|
|
93
88
|
});
|
|
94
89
|
if (json) {
|
|
95
|
-
console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name)
|
|
90
|
+
console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name))), ...native], null, 2));
|
|
96
91
|
return;
|
|
97
92
|
}
|
|
98
93
|
console.log(chalk.bold('Provider account bundles\n'));
|
|
@@ -100,14 +95,13 @@ async function printAccounts(json, fleet = false) {
|
|
|
100
95
|
console.log(chalk.gray(" None. Add one with 'agents accounts add <name> --provider <provider> --auth <type>'."));
|
|
101
96
|
for (const account of records) {
|
|
102
97
|
const present = inspectAccount(account.name).secretPresent ? chalk.green('ready') : chalk.red('missing on this device');
|
|
103
|
-
|
|
104
|
-
console.log(` ${chalk.cyan(account.name)} ${account.provider} ${account.auth} ${present}${dormantSuffix}`);
|
|
98
|
+
console.log(` ${chalk.cyan(account.name)} ${account.provider} ${account.auth} ${present}`);
|
|
105
99
|
}
|
|
106
100
|
console.log(chalk.bold('\nNative harness logins\n'));
|
|
107
101
|
if (!native.length)
|
|
108
102
|
console.log(chalk.gray(' No signed-in native accounts found.'));
|
|
109
103
|
for (const account of native)
|
|
110
|
-
console.log(` ${account.name ? `${chalk.cyan(account.name)} · ` : ''}${account.display} ${account.agent} ${account.versions.join(', ')}
|
|
104
|
+
console.log(` ${account.name ? `${chalk.cyan(account.name)} · ` : ''}${account.display} ${account.agent} ${account.versions.join(', ')}`);
|
|
111
105
|
}
|
|
112
106
|
function parseAuth(raw) {
|
|
113
107
|
if (raw === 'api-key' || raw === 'setup-token' || raw === 'bearer-token')
|
|
@@ -131,7 +125,7 @@ function providerAuthenticatesHarness(provider, auth, agent) {
|
|
|
131
125
|
throw err;
|
|
132
126
|
}
|
|
133
127
|
}
|
|
134
|
-
/** Every account (native + provider) registered/usable for this harness, oldest-first by name
|
|
128
|
+
/** Every account (native + provider) registered/usable for this harness, oldest-first by name. */
|
|
135
129
|
function accountsForHarness(agent) {
|
|
136
130
|
const meta = readMeta();
|
|
137
131
|
const native = listNativeAccounts(meta).filter(account => account.agent === agent);
|
|
@@ -140,52 +134,9 @@ function accountsForHarness(agent) {
|
|
|
140
134
|
.map(account => ({ ...account, kind: 'provider' }));
|
|
141
135
|
return [...native, ...providers].sort((a, b) => a.name.localeCompare(b.name));
|
|
142
136
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Named accounts that `set-default` / `switch` can pin for this harness —
|
|
145
|
-
* capped to the caller's plan tier (RUSH-2424). Downgrading never deletes a
|
|
146
|
-
* credential: an over-cap account simply falls out of this list (see
|
|
147
|
-
* {@link dormantAccountsForHarness}) and stops being a switch/rotation target
|
|
148
|
-
* until the plan is upgraded.
|
|
149
|
-
*/
|
|
137
|
+
/** Named accounts that `set-default` / `switch` can pin for this harness. */
|
|
150
138
|
export async function listSwitchableAccounts(agent) {
|
|
151
|
-
|
|
152
|
-
return accountsForHarness(agent).slice(0, cap);
|
|
153
|
-
}
|
|
154
|
-
/** Accounts registered for this harness beyond the plan's cap — kept, never deleted, excluded from switch/rotation, reactivated by an upgrade. */
|
|
155
|
-
export async function dormantAccountsForHarness(agent) {
|
|
156
|
-
const cap = accountCapForTier(await getTier());
|
|
157
|
-
return accountsForHarness(agent).slice(cap);
|
|
158
|
-
}
|
|
159
|
-
function accountCapRefusalMessage(agent, tier, cap) {
|
|
160
|
-
if (!tier.isPaid)
|
|
161
|
-
return `free plan is capped at 3 ${agent} accounts (3/3). agents upgrade — up to 10 per harness.`;
|
|
162
|
-
return `${tier.tierName} plan is capped at ${cap} ${agent} accounts (${cap}/${cap}).`;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Refuse BEFORE any write when adding a new account would push one of
|
|
166
|
-
* `harnesses` over the caller's plan cap. Returns the tier/cap/pre-add counts
|
|
167
|
-
* so the caller can print the exactly-at-cap notice after a successful write.
|
|
168
|
-
*/
|
|
169
|
-
async function assertAccountCapacityFor(harnesses) {
|
|
170
|
-
const tier = await getTier();
|
|
171
|
-
const cap = accountCapForTier(tier);
|
|
172
|
-
const existing = new Map();
|
|
173
|
-
for (const harness of harnesses)
|
|
174
|
-
existing.set(harness, accountsForHarness(harness).length);
|
|
175
|
-
const capped = harnesses.find(harness => (existing.get(harness) ?? 0) >= cap);
|
|
176
|
-
if (capped)
|
|
177
|
-
throw new Error(accountCapRefusalMessage(capped, tier, cap));
|
|
178
|
-
return { tier, cap, existing };
|
|
179
|
-
}
|
|
180
|
-
/** One-line, non-blocking notice for every harness a free-tier add just brought to exactly its cap. */
|
|
181
|
-
function printCapNoticesFor(harnesses, tier, cap, existing) {
|
|
182
|
-
if (tier.isPaid)
|
|
183
|
-
return;
|
|
184
|
-
for (const harness of harnesses) {
|
|
185
|
-
if ((existing.get(harness) ?? 0) + 1 === cap) {
|
|
186
|
-
console.log(chalk.yellow(`${cap}/${cap} ${harness} accounts on the free plan. agents upgrade — up to 10 per harness.`));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
139
|
+
return accountsForHarness(agent);
|
|
189
140
|
}
|
|
190
141
|
/**
|
|
191
142
|
* Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
|
|
@@ -279,16 +230,10 @@ export function registerAccountsCommand(program) {
|
|
|
279
230
|
const provider = getAccountProvider(o.provider);
|
|
280
231
|
if (!provider.authKinds.includes(auth))
|
|
281
232
|
throw new Error(`Provider '${provider.provider}' does not support ${auth}. Supported: ${provider.authKinds.join(', ')}.`);
|
|
282
|
-
// A provider account may authenticate more than one harness (e.g. openrouter);
|
|
283
|
-
// it counts toward every harness it can authenticate, so refuse before any
|
|
284
|
-
// write if adding it would push ANY of those harnesses over the plan cap.
|
|
285
|
-
const harnesses = ALL_AGENT_IDS.filter(h => providerAuthenticatesHarness(provider.provider, auth, h));
|
|
286
|
-
const { tier, cap, existing } = await assertAccountCapacityFor(harnesses);
|
|
287
233
|
const secret = o.fromSecrets ? secretFromBundle(o.fromSecrets) : await password({ message: `Enter ${provider.provider} ${auth} for '${name}':` });
|
|
288
234
|
const account = addAccount(name, provider.provider, auth, secret, undefined, { baseUrl: o.baseUrl });
|
|
289
235
|
console.log(chalk.green(`Added ${account.provider} ${account.auth} account '${account.name}'.`));
|
|
290
236
|
console.log(chalk.gray(`Secret bundle '${account.name}' is the account and uses policy never, so agent launches never request Touch ID.`));
|
|
291
|
-
printCapNoticesFor(harnesses, tier, cap, existing);
|
|
292
237
|
});
|
|
293
238
|
accounts.command('set-key <name>')
|
|
294
239
|
.description('Rotate an account credential without changing its identity')
|
|
@@ -306,22 +251,12 @@ export function registerAccountsCommand(program) {
|
|
|
306
251
|
const unified = findUnifiedAccount(name, meta);
|
|
307
252
|
if (!unified)
|
|
308
253
|
throw new Error(`Unknown account '${name}'.`);
|
|
309
|
-
const harnesses = unified.kind === 'provider'
|
|
310
|
-
? ALL_AGENT_IDS.filter(h => providerAuthenticatesHarness(unified.provider, unified.auth, h))
|
|
311
|
-
: [unified.agent];
|
|
312
|
-
let dormant = false;
|
|
313
|
-
for (const harness of harnesses) {
|
|
314
|
-
if ((await dormantAccountsForHarness(harness)).some(a => a.id === unified.id)) {
|
|
315
|
-
dormant = true;
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
254
|
const account = unified.kind === 'provider'
|
|
320
|
-
? { ...publicAccount(inspectAccount(unified.name)
|
|
321
|
-
: { ...unified,
|
|
255
|
+
? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
|
|
256
|
+
: { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
|
|
322
257
|
if (o.json || command.optsWithGlobals().json)
|
|
323
258
|
return console.log(JSON.stringify(account, null, 2));
|
|
324
|
-
console.log(chalk.bold(account.name)
|
|
259
|
+
console.log(chalk.bold(account.name));
|
|
325
260
|
console.log(` kind: ${account.kind}`);
|
|
326
261
|
console.log(` id: ${account.id}`);
|
|
327
262
|
console.log(` custody: ${account.custody}`);
|
|
@@ -340,12 +275,10 @@ export function registerAccountsCommand(program) {
|
|
|
340
275
|
.description('Name a signed-in native installation without copying its OAuth credentials')
|
|
341
276
|
.action(async (source, name) => {
|
|
342
277
|
const identity = await nativeIdentityFromSource(source);
|
|
343
|
-
const { tier, cap, existing } = await assertAccountCapacityFor([identity.agent]);
|
|
344
278
|
const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
|
|
345
279
|
console.log(chalk.green(`Named ${source} as ${account.name}.`));
|
|
346
280
|
if (account.scope === 'device')
|
|
347
281
|
console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
|
|
348
|
-
printCapNoticesFor([identity.agent], tier, cap, existing);
|
|
349
282
|
});
|
|
350
283
|
accounts.command('attach <account> <target>')
|
|
351
284
|
.description('Attach a named account to a native installation or custom harness')
|
|
@@ -383,17 +316,6 @@ export function registerAccountsCommand(program) {
|
|
|
383
316
|
// Provider account: it must be able to authenticate the target's harness.
|
|
384
317
|
getAccountProvider(account.provider).envFor(targetAgent, account.auth);
|
|
385
318
|
}
|
|
386
|
-
// Defense-in-depth cap check: by the point we get here `account` already
|
|
387
|
-
// authenticates/belongs to `targetAgent` (validated above), so it is
|
|
388
|
-
// already counted in accountsForHarness(targetAgent) from its `add`/`name`
|
|
389
|
-
// time — this can only refuse if that invariant is ever violated, never a
|
|
390
|
-
// normal re-attach of an account the harness is already at cap with.
|
|
391
|
-
const tier = await getTier();
|
|
392
|
-
const cap = accountCapForTier(tier);
|
|
393
|
-
const alreadyCounted = accountsForHarness(targetAgent).some(existing => existing.id === account.id);
|
|
394
|
-
const otherCount = accountsForHarness(targetAgent).length - (alreadyCounted ? 1 : 0);
|
|
395
|
-
if (otherCount >= cap)
|
|
396
|
-
throw new Error(accountCapRefusalMessage(targetAgent, tier, cap));
|
|
397
319
|
bindAccount(name, target);
|
|
398
320
|
console.log(chalk.green(`Attached ${account.name} to ${target}.`));
|
|
399
321
|
});
|
|
@@ -49,24 +49,7 @@ import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/
|
|
|
49
49
|
import { registerMixCommands } from '../lib/analytics/mix-commands.js';
|
|
50
50
|
import { registerCostCommand } from './cost.js';
|
|
51
51
|
import { registerOutputCommand } from './output.js';
|
|
52
|
-
import { getTier } from '../lib/entitlement.js';
|
|
53
52
|
const execFileAsync = promisify(execFile);
|
|
54
|
-
/**
|
|
55
|
-
* Plan-tier gate for the behavioural report (RUSH-2424). Free keeps top-line
|
|
56
|
-
* counts and the harness mix (and `insights mix` / `agents perf`, which never
|
|
57
|
-
* enter this file's gating since they're separate command trees); the
|
|
58
|
-
* friction/correction-signal sections, grouping `--by account`, and
|
|
59
|
-
* `--narrative` are paid. `insights mix`/`cost`/`output` are unaffected — this
|
|
60
|
-
* gate applies only to the default behavioural report.
|
|
61
|
-
*/
|
|
62
|
-
const PAID_PLAN_NOTICE = 'Friction and account-split analysis are on the paid plan.';
|
|
63
|
-
function resolveInsightsGate(tier, dim) {
|
|
64
|
-
return {
|
|
65
|
-
tier,
|
|
66
|
-
groupGated: !tier.isPaid && dim === 'account',
|
|
67
|
-
frictionGated: !tier.isPaid,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
53
|
function collectAgent(value, previous) {
|
|
71
54
|
return [...previous, value];
|
|
72
55
|
}
|
|
@@ -225,7 +208,7 @@ function renderHours(hours, out) {
|
|
|
225
208
|
out.push(` ${chalk.cyan(spark)}`);
|
|
226
209
|
out.push(` ${chalk.gray('0h'.padEnd(6))}${chalk.gray('6h'.padEnd(6))}${chalk.gray('12h'.padEnd(6))}${chalk.gray('18h'.padEnd(5))}${chalk.gray('23h')}`);
|
|
227
210
|
}
|
|
228
|
-
function renderReport(groups, dim, meta, actions, harnesses
|
|
211
|
+
function renderReport(groups, dim, meta, actions, harnesses) {
|
|
229
212
|
const out = [];
|
|
230
213
|
const scope = meta.since ? `last ${meta.since}` : 'all time';
|
|
231
214
|
out.push(chalk.bold('Insights') + chalk.gray(` ${scope} · ${meta.analyzed} of ${meta.scanned} sessions`));
|
|
@@ -235,44 +218,28 @@ function renderReport(groups, dim, meta, actions, harnesses, gate) {
|
|
|
235
218
|
console.log(out.join('\n'));
|
|
236
219
|
return;
|
|
237
220
|
}
|
|
238
|
-
let noticePrinted = false;
|
|
239
|
-
const printPlanNotice = () => {
|
|
240
|
-
if (noticePrinted)
|
|
241
|
-
return;
|
|
242
|
-
noticePrinted = true;
|
|
243
|
-
out.push('');
|
|
244
|
-
out.push(chalk.gray(` ${PAID_PLAN_NOTICE}`));
|
|
245
|
-
};
|
|
246
221
|
// Per-group table — the headline, and the thing no sibling command produces.
|
|
247
222
|
// Includes silent-stall counts so harness/account laziness is visible without --json.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
out.push(
|
|
265
|
-
`${
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const dur = g.durationMs > 0 ? formatDuration(g.durationMs) : '—';
|
|
269
|
-
const stalls = gate.frictionGated ? '—' : String(stallOf(g));
|
|
270
|
-
const resumes = gate.frictionGated ? '' : String(resumeOf(g));
|
|
271
|
-
out.push(` ${padToWidth(truncateToWidth(g.label, labelW), labelW)} ` +
|
|
272
|
-
`${chalk.gray(String(g.sessions).padStart(sessW))} ${chalk.gray('sess')} ` +
|
|
273
|
-
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(padToWidth(dur, 8))} ` +
|
|
274
|
-
`${chalk.cyan(stalls.padStart(stallW))} ${chalk.cyan(resumes)}`);
|
|
275
|
-
}
|
|
223
|
+
out.push('');
|
|
224
|
+
out.push(chalk.bold(`By ${dim}`));
|
|
225
|
+
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(16, terminalWidth() - 58));
|
|
226
|
+
const sessW = Math.max(...groups.map((g) => String(g.sessions).length), 3);
|
|
227
|
+
const stallOf = (g) => Object.entries(g.facets.frictionSignals)
|
|
228
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
229
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
230
|
+
const resumeOf = (g) => g.facets.correctionSignals['resume after silent stall'] ?? 0;
|
|
231
|
+
const stallW = Math.max(...groups.map((g) => String(stallOf(g)).length), 5);
|
|
232
|
+
out.push(chalk.gray(` ${padToWidth('', labelW)} ${''.padStart(sessW)} ` +
|
|
233
|
+
`${''.padStart(9)} ${''.padStart(8)} ${'stalls'.padStart(stallW)} resume`));
|
|
234
|
+
for (const g of groups) {
|
|
235
|
+
const cost = g.costUsd > 0 ? formatUsd(g.costUsd) : '—';
|
|
236
|
+
const dur = g.durationMs > 0 ? formatDuration(g.durationMs) : '—';
|
|
237
|
+
const stalls = String(stallOf(g));
|
|
238
|
+
const resumes = String(resumeOf(g));
|
|
239
|
+
out.push(` ${padToWidth(truncateToWidth(g.label, labelW), labelW)} ` +
|
|
240
|
+
`${chalk.gray(String(g.sessions).padStart(sessW))} ${chalk.gray('sess')} ` +
|
|
241
|
+
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(padToWidth(dur, 8))} ` +
|
|
242
|
+
`${chalk.cyan(stalls.padStart(stallW))} ${chalk.cyan(resumes)}`);
|
|
276
243
|
}
|
|
277
244
|
// Everything below is the whole scope folded together; per-group detail is in --json.
|
|
278
245
|
const all = newFacetAccumulator();
|
|
@@ -281,64 +248,54 @@ function renderReport(groups, dim, meta, actions, harnesses, gate) {
|
|
|
281
248
|
renderCounts('Top tools', topEntries(all.toolCounts, 8), out);
|
|
282
249
|
renderCounts('Languages', topEntries(all.languages, 6), out);
|
|
283
250
|
renderCounts('Models', topEntries(all.models, 6), out);
|
|
284
|
-
// Friction — the section that earns the command.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
251
|
+
// Friction — the section that earns the command.
|
|
252
|
+
renderCounts('Silent stalls by model', topEntries(all.silentStallsByModel ?? {}, 8), out);
|
|
253
|
+
const gaps = all.responseGaps;
|
|
254
|
+
const silentStalls = Object.entries(all.frictionSignals)
|
|
255
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
256
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
257
|
+
const resumeNudges = all.correctionSignals['resume after silent stall'] ?? 0;
|
|
258
|
+
out.push('');
|
|
259
|
+
out.push(chalk.bold('Friction'));
|
|
260
|
+
out.push(` ${padToWidth('interruptions', 18)} ${chalk.cyan(String(all.interruptions))}` +
|
|
261
|
+
chalk.gray(' turns you cut short'));
|
|
262
|
+
out.push(` ${padToWidth('tool errors', 18)} ${chalk.cyan(String(all.errorCount))}`);
|
|
263
|
+
if (gaps.length > 0) {
|
|
264
|
+
// Same timestamps as silent stalls; this line is the distribution. Silent
|
|
265
|
+
// stalls (below) are the agent-attributed long gaps after the model stopped.
|
|
266
|
+
out.push(` ${padToWidth('gap until next msg', 18)} ` +
|
|
267
|
+
chalk.cyan(`p50 ${Math.round(percentile(gaps, 50))}s`) + chalk.gray(` · p90 ${Math.round(percentile(gaps, 90))}s`) +
|
|
268
|
+
chalk.gray(' after assistant last spoke'));
|
|
288
269
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (gaps.length > 0) {
|
|
302
|
-
// Same timestamps as silent stalls; this line is the distribution. Silent
|
|
303
|
-
// stalls (below) are the agent-attributed long gaps after the model stopped.
|
|
304
|
-
out.push(` ${padToWidth('gap until next msg', 18)} ` +
|
|
305
|
-
chalk.cyan(`p50 ${Math.round(percentile(gaps, 50))}s`) + chalk.gray(` · p90 ${Math.round(percentile(gaps, 90))}s`) +
|
|
306
|
-
chalk.gray(' after assistant last spoke'));
|
|
307
|
-
}
|
|
308
|
-
if (silentStalls > 0) {
|
|
309
|
-
out.push(` ${padToWidth('silent stalls', 18)} ${chalk.cyan(String(silentStalls))}` +
|
|
310
|
-
chalk.gray(' agent idle ≥5m until you resumed (also in By ' + dim + ' table)'));
|
|
311
|
-
}
|
|
312
|
-
if (resumeNudges > 0) {
|
|
313
|
-
out.push(` ${padToWidth('resume nudges', 18)} ${chalk.cyan(String(resumeNudges))}` +
|
|
314
|
-
chalk.gray(' "continue"/"keep going" after a silent stall'));
|
|
315
|
-
}
|
|
316
|
-
const errs = topEntries(all.errorCategories, 6);
|
|
317
|
-
if (errs.length > 0) {
|
|
318
|
-
for (const e of errs)
|
|
319
|
-
out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
|
|
320
|
-
}
|
|
321
|
-
renderCounts('Friction / thrash', topEntries(all.frictionSignals, 10), out);
|
|
322
|
-
renderCounts('Dissatisfaction / corrections', topEntries(all.correctionSignals, 10), out);
|
|
270
|
+
if (silentStalls > 0) {
|
|
271
|
+
out.push(` ${padToWidth('silent stalls', 18)} ${chalk.cyan(String(silentStalls))}` +
|
|
272
|
+
chalk.gray(' agent idle ≥5m until you resumed (also in By ' + dim + ' table)'));
|
|
273
|
+
}
|
|
274
|
+
if (resumeNudges > 0) {
|
|
275
|
+
out.push(` ${padToWidth('resume nudges', 18)} ${chalk.cyan(String(resumeNudges))}` +
|
|
276
|
+
chalk.gray(' "continue"/"keep going" after a silent stall'));
|
|
277
|
+
}
|
|
278
|
+
const errs = topEntries(all.errorCategories, 6);
|
|
279
|
+
if (errs.length > 0) {
|
|
280
|
+
for (const e of errs)
|
|
281
|
+
out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
|
|
323
282
|
}
|
|
283
|
+
renderCounts('Friction / thrash', topEntries(all.frictionSignals, 10), out);
|
|
284
|
+
renderCounts('Dissatisfaction / corrections', topEntries(all.correctionSignals, 10), out);
|
|
324
285
|
renderCounts('Automatable repeats', topEntries(all.automationSignals, 10), out);
|
|
325
286
|
renderCounts('Harness split', harnesses, out);
|
|
326
287
|
// Actions are built from frictionSignals/correctionSignals/automationSignals
|
|
327
|
-
// together (buildInsightActions)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
out.push('');
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
for (const action of actions.slice(0, 12)) {
|
|
339
|
-
out.push(` ${padToWidth(action.priority, 7)} ${padToWidth(action.category, 11)} ` +
|
|
340
|
-
`${String(action.evidenceCount).padStart(8)} ${padToWidth(action.sampleSessionIds.join(', '), 25)} ${action.action}`);
|
|
341
|
-
}
|
|
288
|
+
// together (buildInsightActions).
|
|
289
|
+
out.push('');
|
|
290
|
+
out.push(chalk.bold('Actions'));
|
|
291
|
+
if (actions.length === 0) {
|
|
292
|
+
out.push(chalk.gray(' No repeated action pattern met the evidence threshold in this window.'));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
out.push(chalk.gray(' pri category evidence sample sessions action'));
|
|
296
|
+
for (const action of actions.slice(0, 12)) {
|
|
297
|
+
out.push(` ${padToWidth(action.priority, 7)} ${padToWidth(action.category, 11)} ` +
|
|
298
|
+
`${String(action.evidenceCount).padStart(8)} ${padToWidth(action.sampleSessionIds.join(', '), 25)} ${action.action}`);
|
|
342
299
|
}
|
|
343
300
|
}
|
|
344
301
|
// Output
|
|
@@ -394,9 +351,7 @@ function renderReport(groups, dim, meta, actions, harnesses, gate) {
|
|
|
394
351
|
out.push('');
|
|
395
352
|
out.push(chalk.yellow(` ${meta.unreadable} transcripts could not be read; their behaviour is missing from these totals.`));
|
|
396
353
|
}
|
|
397
|
-
|
|
398
|
-
// "silent stall" outright — must not render on the free plan.
|
|
399
|
-
if (!gate.frictionGated && all.gapsOverCeiling > 0) {
|
|
354
|
+
if (all.gapsOverCeiling > 0) {
|
|
400
355
|
out.push(chalk.gray(` ${all.gapsOverCeiling} gaps over an hour excluded from p50/p90 (still counted as silent stall: 1h+ when the assistant last spoke).`));
|
|
401
356
|
}
|
|
402
357
|
out.push('');
|
|
@@ -451,23 +406,8 @@ async function renderNarrative(payload) {
|
|
|
451
406
|
process.exitCode = 1;
|
|
452
407
|
}
|
|
453
408
|
}
|
|
454
|
-
/** Facet keys that belong to the paid friction/correction sections — stripped from `--json` on free (RUSH-2424). */
|
|
455
|
-
const PAID_FACET_KEYS = new Set([
|
|
456
|
-
'frictionSignals', 'correctionSignals', 'silentStallsByModel',
|
|
457
|
-
'interruptions', 'errorCount', 'errorCategories', 'responseGaps', 'gapsOverCeiling',
|
|
458
|
-
]);
|
|
459
|
-
function freeFacetSubset(facets) {
|
|
460
|
-
const out = {};
|
|
461
|
-
for (const [key, value] of Object.entries(facets)) {
|
|
462
|
-
if (!PAID_FACET_KEYS.has(key))
|
|
463
|
-
out[key] = value;
|
|
464
|
-
}
|
|
465
|
-
return out;
|
|
466
|
-
}
|
|
467
409
|
async function insightsAction(options) {
|
|
468
410
|
const dim = resolveGroup(options.by);
|
|
469
|
-
const tier = await getTier();
|
|
470
|
-
const gate = resolveInsightsGate(tier, dim);
|
|
471
411
|
const minMessages = Number.parseInt(options.minMessages ?? '2', 10);
|
|
472
412
|
if (!Number.isFinite(minMessages) || minMessages < 0) {
|
|
473
413
|
console.error(chalk.red('error: --min-messages must be a non-negative integer'));
|
|
@@ -533,37 +473,31 @@ async function insightsAction(options) {
|
|
|
533
473
|
unreadable,
|
|
534
474
|
minMessages,
|
|
535
475
|
by: dim,
|
|
536
|
-
plan: { tierName: tier.tierName, isPaid: tier.isPaid },
|
|
537
|
-
...(gate.groupGated || gate.frictionGated ? { notice: PAID_PLAN_NOTICE } : {}),
|
|
538
476
|
overlap,
|
|
539
477
|
// Built from frictionSignals/correctionSignals/automationSignals together
|
|
540
478
|
// (buildInsightActions) — same paid friction/correction data as above.
|
|
541
|
-
actions
|
|
479
|
+
actions,
|
|
542
480
|
harnesses,
|
|
543
|
-
groups:
|
|
481
|
+
groups: groups.map((g) => ({
|
|
544
482
|
key: g.key,
|
|
545
483
|
label: g.label,
|
|
546
484
|
sessions: g.sessions,
|
|
547
485
|
costUsd: g.costUsd,
|
|
548
486
|
durationMs: g.durationMs,
|
|
549
487
|
outputTokens: g.outputTokens,
|
|
550
|
-
...
|
|
488
|
+
...{
|
|
551
489
|
...g.facets,
|
|
552
490
|
responseGapP50: Math.round(percentile(g.facets.responseGaps, 50)),
|
|
553
491
|
responseGapP90: Math.round(percentile(g.facets.responseGaps, 90)),
|
|
554
492
|
responseGapBuckets: bucketGaps(g.facets.responseGaps),
|
|
555
493
|
// The raw sample is large and uninteresting once bucketed.
|
|
556
494
|
responseGaps: undefined,
|
|
557
|
-
}
|
|
495
|
+
},
|
|
558
496
|
})),
|
|
559
497
|
};
|
|
560
498
|
console.log(JSON.stringify(payload, null, 2));
|
|
561
|
-
if (options.narrative)
|
|
562
|
-
|
|
563
|
-
console.error(chalk.gray(` ${PAID_PLAN_NOTICE}`));
|
|
564
|
-
else
|
|
565
|
-
await renderNarrative(payload);
|
|
566
|
-
}
|
|
499
|
+
if (options.narrative)
|
|
500
|
+
await renderNarrative(payload);
|
|
567
501
|
return;
|
|
568
502
|
}
|
|
569
503
|
renderReport(groups, dim, {
|
|
@@ -574,24 +508,18 @@ async function insightsAction(options) {
|
|
|
574
508
|
unreadable,
|
|
575
509
|
minMessages,
|
|
576
510
|
overlap,
|
|
577
|
-
}, actions, harnesses
|
|
511
|
+
}, actions, harnesses);
|
|
578
512
|
if (options.narrative) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
interruptions: g.facets.interruptions,
|
|
590
|
-
linesTouchedAfter: g.facets.linesTouchedAfter, linesTouchedBefore: g.facets.linesTouchedBefore,
|
|
591
|
-
gitCommits: g.facets.gitCommits,
|
|
592
|
-
replyP50s: Math.round(percentile(g.facets.responseGaps, 50)),
|
|
593
|
-
})));
|
|
594
|
-
}
|
|
513
|
+
await renderNarrative(groups.map((g) => ({
|
|
514
|
+
account: g.label, sessions: g.sessions, costUsd: g.costUsd,
|
|
515
|
+
topTools: topEntries(g.facets.toolCounts, 8),
|
|
516
|
+
languages: topEntries(g.facets.languages, 6),
|
|
517
|
+
errorCategories: topEntries(g.facets.errorCategories, 6),
|
|
518
|
+
interruptions: g.facets.interruptions,
|
|
519
|
+
linesTouchedAfter: g.facets.linesTouchedAfter, linesTouchedBefore: g.facets.linesTouchedBefore,
|
|
520
|
+
gitCommits: g.facets.gitCommits,
|
|
521
|
+
replyP50s: Math.round(percentile(g.facets.responseGaps, 50)),
|
|
522
|
+
})));
|
|
595
523
|
}
|
|
596
524
|
}
|
|
597
525
|
function configureInsightsCommand(cmd) {
|
|
Binary file
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
<key>CFBundleIdentifier</key>
|
|
8
8
|
<string>com.phnx-labs.agents-menubar</string>
|
|
9
9
|
<key>CFBundleName</key>
|
|
10
|
-
<string>
|
|
10
|
+
<string>AGI Menu</string>
|
|
11
|
+
<key>CFBundleDisplayName</key>
|
|
12
|
+
<string>AGI Menu</string>
|
|
13
|
+
<key>CFBundleIconFile</key>
|
|
14
|
+
<string>AppIcon</string>
|
|
11
15
|
<key>CFBundlePackageType</key>
|
|
12
16
|
<string>APPL</string>
|
|
13
17
|
<key>CFBundleShortVersionString</key>
|
|
Binary file
|